Skip to content

Commit

Permalink
Introducing the R-squared coefficient and Treynor Ratio for a financi…
Browse files Browse the repository at this point in the history
…al portfolio (#134)

The R-squared coefficient measures how closely the portfolio's returns
track the benchmark market index's returns. On the other hand, the
Treynor Ratio is a metric used to evaluate an investment portfolio's
risk-adjusted returns.

They have been incorporated in the same PR to simplify the merging, and
because such parameters both refer to the market index.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Milthaler <fmilthaler@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 3, 2023
1 parent 2ca498c commit 55112a9
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 20 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<img src="https://img.shields.io/github/stars/fmilthaler/FinQuant.svg?style=social&label=Star" alt='pypi'>
</a>
<a href="https://pypi.org/project/FinQuant">
<img src="https://img.shields.io/badge/pypi-v0.6.2-brightgreen.svg?style=popout" alt='pypi'>
<img src="https://img.shields.io/badge/pypi-v0.7.0-brightgreen.svg?style=popout" alt='pypi'>
</a>
<a href="https://github.com/fmilthaler/FinQuant">
<img src="https://github.com/fmilthaler/finquant/actions/workflows/pytest.yml/badge.svg?branch=master" alt='GitHub Actions'>
Expand Down Expand Up @@ -169,6 +169,7 @@ As it is common for open-source projects, there are several ways to get hold of
- quandl>=3.4.5
- yfinance>=0.1.43
- scipy>=1.2.0
- scikit-learn>=1.3.0

### From PyPI
*FinQuant* can be obtained from PyPI
Expand Down Expand Up @@ -253,7 +254,9 @@ look at the examples provided in `./example`.
- Value at Risk,
- Sharpe Ratio,
- Sortino Ratio,
- Beta parameter.
- Treynor Ratio,
- Beta parameter,
- R squared coefficient.

It also shows how to extract individual stocks from the given portfolio. Moreover it shows how to compute and visualise:
- the different Returns provided by the module `finquant.returns`,
Expand Down
7 changes: 5 additions & 2 deletions README.tex.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<img src="https://img.shields.io/github/stars/fmilthaler/FinQuant.svg?style=social&label=Star" alt='pypi'>
</a>
<a href="https://pypi.org/project/FinQuant">
<img src="https://img.shields.io/badge/pypi-v0.6.2-brightgreen.svg?style=popout" alt='pypi'>
<img src="https://img.shields.io/badge/pypi-v0.7.0-brightgreen.svg?style=popout" alt='pypi'>
</a>
<a href="https://github.com/fmilthaler/FinQuant">
<img src="https://github.com/fmilthaler/finquant/actions/workflows/pytest.yml/badge.svg?branch=master" alt='GitHub Actions'>
Expand Down Expand Up @@ -169,6 +169,7 @@ As it is common for open-source projects, there are several ways to get hold of
- quandl>=3.4.5
- yfinance>=0.1.43
- scipy>=1.2.0
- scikit-learn>=1.3.0

### From PyPI
*FinQuant* can be obtained from PyPI
Expand Down Expand Up @@ -253,7 +254,9 @@ look at the examples provided in `./example`.
- Value at Risk,
- Sharpe Ratio,
- Sortino Ratio,
- Beta parameter.
- Treynor Ratio,
- Beta parameter,
- R squared coefficient.

It also shows how to extract individual stocks from the given portfolio. Moreover it shows how to compute and visualise:
- the different Returns provided by the module `finquant.returns`,
Expand Down
3 changes: 2 additions & 1 deletion example/Example-Build-Portfolio-from-web.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
# * `build_portfolio(names=names, data_api="yfinance")`
#
# In the below example we are using `yfinance` to download stock data. We specify the start and end date of the stock prices to be downloaded.
# We also provide the optional parameter `market_index` to download the historical data of a market index. `FinQuant` can use them to calculate the beta parameter, measuring the portfolio's daily volatility compared to the market.
# We also provide the optional parameter `market_index` to download the historical data of a market index.
# `FinQuant` can use them to calculate the Treynor Ratio, beta parameter, and R squared coefficient, measuring the portfolio's daily volatility compared to the market.

# <codecell>

Expand Down
74 changes: 68 additions & 6 deletions finquant/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
- Value at Risk,
- Sharpe Ratio,
- Sortino Ratio,
- Treynor Ratio (optional),
- Beta parameter (optional),
- R squared coefficient (optional),
- skewness of the portfolio's stocks,
- Kurtosis of the portfolio's stocks,
- the portfolio's covariance matrix.
Expand Down Expand Up @@ -84,6 +86,7 @@
downside_risk,
sharpe_ratio,
sortino_ratio,
treynor_ratio,
value_at_risk,
weighted_mean,
weighted_std,
Expand Down Expand Up @@ -116,6 +119,7 @@ class Portfolio:
var: FLOAT
sharpe: FLOAT
sortino: FLOAT
treynor: Optional[FLOAT]
skew: pd.Series
kurtosis: pd.Series
__totalinvestment: NUMERIC
Expand All @@ -127,6 +131,8 @@ class Portfolio:
__market_index: Optional[Market]
beta_stocks: pd.DataFrame
beta: Optional[FLOAT]
rsquared_stocks: pd.DataFrame
rsquared: Optional[FLOAT]

def __init__(self) -> None:
"""Initiates ``Portfolio``."""
Expand All @@ -142,9 +148,14 @@ def __init__(self) -> None:
self.mc = None
# instance variable for Market class
self.__market_index = None
# dataframe containing beta values of stocks
# Treynor Ratio of the portfolio
self.treynor = None
# dataframe containing beta parameters of stocks
self.beta_stocks = pd.DataFrame(index=["beta"])
self.beta = None
# dataframe containing rsquared coefficients of stocks
self.rsquared_stocks = pd.DataFrame(index=["rsquared"])
self.rsquared = None

@property
def totalinvestment(self) -> NUMERIC:
Expand Down Expand Up @@ -234,6 +245,13 @@ def add_stock(self, stock: Stock, defer_update: bool = False) -> None:
- ``skew``: Skewness of the portfolio's stocks
- ``kurtosis``: Kurtosis of the portfolio's stocks
If argument ``defer_update`` is ``True`` and ``__market_index`` is not ``None``,
the following instance variables are (re-)computed as well:
- ``beta``: Beta parameter of the portfolio
- ``rsquared``: R squared coefficient of the portfolio
- ``treynor``: Treynor Ratio of the portfolio
:param stock: An instance of the class ``Stock``.
:param defer_update: bool, if True instance variables are not (re-)computed at the end of this method.
"""
Expand Down Expand Up @@ -267,6 +285,10 @@ def _add_stock_data(self, stock: Stock) -> None:
beta_stock = stock.comp_beta(self.market_index.daily_returns)
# add beta of stock to portfolio's betas dataframe
self.beta_stocks[stock.name] = [beta_stock]
# compute R squared coefficient of stock
rsquared_stock = stock.comp_rsquared(self.market_index.daily_returns)
# add rsquared of stock to portfolio's R squared dataframe
self.rsquared_stocks[stock.name] = [rsquared_stock]

def _update(self) -> None:
# sanity check (only update values if none of the below is empty):
Expand All @@ -282,6 +304,8 @@ def _update(self) -> None:
self.kurtosis = self._comp_kurtosis()
if self.market_index is not None:
self.beta = self.comp_beta()
self.rsquared = self.comp_rsquared()
self.treynor = self.comp_treynor()

def get_stock(self, name: str) -> Stock:
"""Returns the instance of ``Stock`` with name ``name``.
Expand Down Expand Up @@ -458,6 +482,25 @@ def comp_beta(self) -> Optional[FLOAT]:
else:
return None

def comp_rsquared(self) -> Optional[FLOAT]:
"""Compute and return the R squared coefficient of the portfolio.
:rtype: :py:data:`~.finquant.data_types.FLOAT`
:return: R squared coefficient of the portfolio
"""

# compute the R squared coefficient of the portfolio
weights: pd.Series = self.comp_weights()
if weights.size == self.beta_stocks.size:
rsquared: FLOAT = weighted_mean(
self.rsquared_stocks.transpose()["rsquared"].values, weights
)

self.rsquared = rsquared
return rsquared
else:
return None

def comp_sortino(self) -> FLOAT:
"""Compute and return the Sortino Ratio of the portfolio
Expand All @@ -469,6 +512,19 @@ def comp_sortino(self) -> FLOAT:
self.expected_return, self.downside_risk, self.risk_free_rate
)

def comp_treynor(self) -> Optional[FLOAT]:
"""Compute and return the Treynor Ratio of the portfolio.
:rtype: :py:data:`~.finquant.data_types.FLOAT`
:return: The Treynor Ratio of the portfolio.
"""
# compute the Treynor Ratio of the portfolio
treynor: Optional[FLOAT] = treynor_ratio(
self.expected_return, self.beta, self.risk_free_rate
)
self.treynor = treynor
return treynor

def _comp_skew(self) -> pd.Series:
"""Computes and returns the skewness of the stocks in the portfolio."""
return self.data.skew()
Expand Down Expand Up @@ -731,7 +787,9 @@ def properties(self) -> None:
- Confidence level of VaR,
- Sharpe Ratio,
- Sortino Ratio,
- Treynor Ratio (optional),
- Beta (optional),
- R squared (optional),
- skewness,
- Kurtosis
Expand All @@ -755,8 +813,12 @@ def properties(self) -> None:
string += f"{self.var_confidence_level * 100:0.2f} %"
string += f"\nPortfolio Sharpe Ratio: {self.sharpe:0.3f}"
string += f"\nPortfolio Sortino Ratio: {self.sortino:0.3f}"
if self.treynor is not None:
string += f"\nPortfolio Treynor Ratio: {self.treynor:0.3f}"
if self.beta is not None:
string += f"\nPortfolio Beta: {self.beta:0.3f}"
if self.rsquared is not None:
string += f"\nPortfolio R squared: {self.rsquared:0.3f}"
string += "\n\nSkewness:"
string += "\n" + str(self.skew.to_frame().transpose())
string += "\n\nKurtosis:"
Expand Down Expand Up @@ -999,8 +1061,8 @@ def _build_portfolio_from_api(
if data is not provided by the user. Valid values:
- ``quandl`` (Python package/API to `Quandl`)
- ``yfinance`` (Python package formerly known as ``fix-yahoo-finance``)
:param market_index: (optional) A string which determines the market index to be used for the
computation of the beta parameter of the stocks, default: ``None``
:param market_index: (optional, default: ``None``) A string which determines the market index
to be used for the computation of the Trenor Ratio, beta parameter and the R squared of the portfolio.
:return: Instance of Portfolio which contains all the information requested by the user.
"""
Expand Down Expand Up @@ -1227,9 +1289,9 @@ def build_portfolio(**kwargs: Dict[str, Any]) -> Portfolio:
- ``quandl`` (Python package/API to `Quandl`)
- ``yfinance`` (Python package formerly known as ``fix-yahoo-finance``)
:param market_index: (optional) string which determines the
market index to be used for the computation of the beta parameter of the stocks,
default: ``None``.
:param market_index: (optional) A string (default: ``None``) which determines the
market index to be used for the computation of the Treynor ratio, beta parameter
and the R squared coefficient of the portflio.
:return: Instance of ``Portfolio`` which contains all the information requested by the user.
Expand Down
33 changes: 32 additions & 1 deletion finquant/quants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""


from typing import Tuple
from typing import Optional, Tuple

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -115,6 +115,37 @@ def sortino_ratio(
return (exp_return - risk_free_rate) / float(downs_risk)


def treynor_ratio(
exp_return: FLOAT, beta: Optional[FLOAT], risk_free_rate: FLOAT = 0.005
) -> Optional[FLOAT]:
"""Computes the Treynor Ratio.
:param exp_return: Expected Return of a portfolio
:type exp_return: :py:data:`~.finquant.data_types.FLOAT`
:param beta: Beta parameter of a portfolio
:type beta: :py:data:`~.finquant.data_types.FLOAT`
:param risk_free_rate: Risk free rate
:type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005
:rtype: :py:data:`~.finquant.data_types.FLOAT`
:return: Treynor Ratio as a floating point number:
``(exp_return - risk_free_rate) / beta``
"""
# Type validations:
type_validation(
expected_return=exp_return,
beta_parameter=beta,
risk_free_rate=risk_free_rate,
)
if beta is None:
return None
else:
res_treynor_ratio: FLOAT = (exp_return - risk_free_rate) / beta
return res_treynor_ratio


def downside_risk(
data: pd.DataFrame, weights: ARRAY_OR_SERIES[FLOAT], risk_free_rate: FLOAT = 0.005
) -> FLOAT:
Expand Down
36 changes: 31 additions & 5 deletions finquant/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
The ``Stock`` class computes various quantities related to the stock or fund, such as expected return,
volatility, skewness, and kurtosis. It also provides functionality to calculate the beta parameter
of the stock using the CAPM (Capital Asset Pricing Model).
using the CAPM (Capital Asset Pricing Model) and the R squared value of the stock .
The ``Stock`` class inherits from the ``Asset`` class in ``finquant.asset``, which provides common
functionality and attributes for financial assets.
Expand All @@ -27,6 +27,7 @@

import numpy as np
import pandas as pd
from sklearn.metrics import r2_score

from finquant.asset import Asset
from finquant.data_types import FLOAT
Expand All @@ -44,14 +45,15 @@ class Stock(Asset):
It requires investment information and historical price data for the stock to initialize an instance.
In addition to the attributes inherited from the ``Asset`` class, the ``Stock`` class provides
a method to compute the beta parameter specific to stocks in a portfolio when compared to
the market index.
a method to compute the beta parameter and one to compute the R squared coefficient
specific to stocks in a portfolio when compared to the market index.
"""

# Attributes:
investmentinfo: pd.DataFrame
beta: Optional[FLOAT]
rsquared: Optional[FLOAT]

def __init__(self, investmentinfo: pd.DataFrame, data: pd.Series) -> None:
"""
Expand All @@ -63,6 +65,8 @@ def __init__(self, investmentinfo: pd.DataFrame, data: pd.Series) -> None:
super().__init__(data, self.name, asset_type="Stock")
# beta parameter of stock (CAPM)
self.beta = None
# R squared coefficient of stock
self.rsquared = None

def comp_beta(self, market_daily_returns: pd.Series) -> FLOAT:
"""Computes and returns the Beta parameter of the stock.
Expand All @@ -83,10 +87,30 @@ def comp_beta(self, market_daily_returns: pd.Series) -> FLOAT:
self.beta = beta
return beta

def comp_rsquared(self, market_daily_returns: pd.Series) -> FLOAT:
"""Computes and returns the R squared coefficient of the stock.
:param market_daily_returns: Daily returns of the market index.
:rtype: :py:data:`~.finquant.data_types.FLOAT`
:return: R squared coefficient of the stock
"""
# Type validations:
type_validation(market_daily_returns=market_daily_returns)

rsquared = float(
r2_score(
market_daily_returns.to_frame()[market_daily_returns.name],
self.comp_daily_returns(),
)
)
self.rsquared = rsquared
return rsquared

def properties(self) -> None:
"""Nicely prints out the properties of the stock: Expected Return,
Volatility, Beta (optional), Skewness, Kurtosis as well as the ``Allocation`` (and other
information provided in investmentinfo.)
Volatility, Beta (optional), R squared (optional), Skewness, Kurtosis as well as the ``Allocation``
(and other information provided in investmentinfo.)
"""
# nicely printing out information and quantities of the stock
string = "-" * 50
Expand All @@ -95,6 +119,8 @@ def properties(self) -> None:
string += f"\nVolatility: {self.volatility:0.3f}"
if self.beta is not None:
string += f"\n{self.asset_type} Beta: {self.beta:0.3f}"
if self.rsquared is not None:
string += f"\n{self.asset_type} R squared: {self.rsquared:0.3f}"
string += f"\nSkewness: {self.skew:0.5f}"
string += f"\nKurtosis: {self.kurtosis:0.5f}"
string += "\nInformation:"
Expand Down
1 change: 1 addition & 0 deletions finquant/type_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def _check_empty_data(arg_name: str, arg_values: Any) -> None:
"mu": ((float, np.floating), None),
"sigma": ((float, np.floating), None),
"conf_level": ((float, np.floating), None),
"beta_parameter": ((float, np.floating), None),
# INTs:
"freq": ((int, np.integer), None),
"span": ((int, np.integer), None),
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ scipy>=1.2.0
pandas>=2.0
matplotlib>=3.0
quandl>=3.4.5
yfinance>=0.1.43
yfinance>=0.1.43
scikit-learn>=1.3.0
2 changes: 2 additions & 0 deletions tests/test_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ def test_Market():
assert isinstance(pf.market_index, Market)
assert pf.market_index.name == "^GSPC"
assert pf.beta is not None
assert pf.rsquared is not None
assert pf.treynor is not None

0 comments on commit 55112a9

Please sign in to comment.