From 55112a9b3d5182cb291225ea530fce5b403e49e6 Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Sun, 3 Sep 2023 21:16:54 +0200 Subject: [PATCH] Introducing the R-squared coefficient and Treynor Ratio for a financial 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] Co-authored-by: Frank Milthaler --- README.md | 7 +- README.tex.md | 7 +- example/Example-Build-Portfolio-from-web.py | 3 +- finquant/portfolio.py | 74 +++++++++++++++++++-- finquant/quants.py | 33 ++++++++- finquant/stock.py | 36 ++++++++-- finquant/type_utilities.py | 1 + requirements.txt | 3 +- tests/test_market.py | 2 + tests/test_quants.py | 6 ++ version | 4 +- 11 files changed, 156 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9ae1244d..0f1b8cc1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ pypi - pypi + pypi GitHub Actions @@ -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 @@ -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`, diff --git a/README.tex.md b/README.tex.md index cdf10d64..6dd741fd 100644 --- a/README.tex.md +++ b/README.tex.md @@ -7,7 +7,7 @@ pypi - pypi + pypi GitHub Actions @@ -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 @@ -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`, diff --git a/example/Example-Build-Portfolio-from-web.py b/example/Example-Build-Portfolio-from-web.py index f2406c24..d0abe52f 100644 --- a/example/Example-Build-Portfolio-from-web.py +++ b/example/Example-Build-Portfolio-from-web.py @@ -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. # diff --git a/finquant/portfolio.py b/finquant/portfolio.py index ad840b72..9a570f53 100644 --- a/finquant/portfolio.py +++ b/finquant/portfolio.py @@ -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. @@ -84,6 +86,7 @@ downside_risk, sharpe_ratio, sortino_ratio, + treynor_ratio, value_at_risk, weighted_mean, weighted_std, @@ -116,6 +119,7 @@ class Portfolio: var: FLOAT sharpe: FLOAT sortino: FLOAT + treynor: Optional[FLOAT] skew: pd.Series kurtosis: pd.Series __totalinvestment: NUMERIC @@ -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``.""" @@ -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: @@ -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. """ @@ -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): @@ -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``. @@ -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 @@ -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() @@ -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 @@ -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:" @@ -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. """ @@ -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. diff --git a/finquant/quants.py b/finquant/quants.py index 99bddaaf..30727a1e 100644 --- a/finquant/quants.py +++ b/finquant/quants.py @@ -4,7 +4,7 @@ """ -from typing import Tuple +from typing import Optional, Tuple import numpy as np import pandas as pd @@ -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: diff --git a/finquant/stock.py b/finquant/stock.py index bd3113c7..b512a835 100644 --- a/finquant/stock.py +++ b/finquant/stock.py @@ -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. @@ -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 @@ -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: """ @@ -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. @@ -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 @@ -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:" diff --git a/finquant/type_utilities.py b/finquant/type_utilities.py index 313b4c32..25d86fd7 100644 --- a/finquant/type_utilities.py +++ b/finquant/type_utilities.py @@ -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), diff --git a/requirements.txt b/requirements.txt index 58c18e68..3216f4da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ scipy>=1.2.0 pandas>=2.0 matplotlib>=3.0 quandl>=3.4.5 -yfinance>=0.1.43 \ No newline at end of file +yfinance>=0.1.43 +scikit-learn>=1.3.0 \ No newline at end of file diff --git a/tests/test_market.py b/tests/test_market.py index b58c5578..cccb2c40 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -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 diff --git a/tests/test_quants.py b/tests/test_quants.py index 1d39763c..f7916941 100644 --- a/tests/test_quants.py +++ b/tests/test_quants.py @@ -9,6 +9,7 @@ downside_risk, sharpe_ratio, sortino_ratio, + treynor_ratio, value_at_risk, weighted_mean, weighted_std, @@ -44,6 +45,11 @@ def test_sortino_ratio(): assert sortino_ratio(0.005, 8.5, 0.005) == 0.0 +def test_treynor_ratio(): + assert treynor_ratio(0.2, 0.9, 0.002) == 0.22 + assert treynor_ratio(0.005, 0.92, 0.005) == 0.0 + + def test_value_at_risk(): assert abs(value_at_risk(1e2, 0.5, 0.25, 0.95) - 91.12) <= 1e-1 assert abs(value_at_risk(1e3, 0.8, 0.5, 0.99) - 1963.17) <= 1e-1 diff --git a/version b/version index d4576a1d..7ad36c63 100644 --- a/version +++ b/version @@ -1,2 +1,2 @@ -version=0.6.2 -release=0.6.2 +version=0.7.0 +release=0.7.0