From 350d88f03573ab4a81b87514bbc9a826e5e99093 Mon Sep 17 00:00:00 2001 From: Dimitris Papakyriakopoulos Date: Sat, 18 Jun 2022 13:02:21 +0300 Subject: [PATCH] Implement Sortino ratio for portfolio --- .DS_Store | Bin 8196 -> 6148 bytes pypfopt/expected_returns.py | 34 +++++++++++++++++++++++++++++++++ pypfopt/objective_functions.py | 31 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/.DS_Store b/.DS_Store index 36728efc61c6541180655d1dce6a6b7b8e5f0144..55899226633c86a372187cdb11a99ac19f39d68d 100644 GIT binary patch delta 108 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$SAxqU^g?P@MazXQKrrQLN^#E uHt;QG=im@z2C4!A0d64S3evE#@H_Klei=`Yb_OPhQ6SS9HplbKVFm!z84)u8 delta 131 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD6%nNH}hr%jz7$c**Q2SHn1>? zZ02DRWnwO1D45*GF2=_&9caLs|4^`5itRq*W_6zD%v=)OKyzF{Mr{`4_|80;U&M2= RKMw~7Bg7Vl&G9^Qm;wI+AejIF diff --git a/pypfopt/expected_returns.py b/pypfopt/expected_returns.py index 42a90ca4..59d24080 100644 --- a/pypfopt/expected_returns.py +++ b/pypfopt/expected_returns.py @@ -23,6 +23,7 @@ import warnings import pandas as pd import numpy as np +import math def returns_from_prices(prices, log_returns=False): @@ -256,3 +257,36 @@ def capm_return( # CAPM formula return risk_free_rate + betas * (mkt_mean_ret - risk_free_rate) + + +def calculate_downside_diviation( historical_returns, mar): + """ + Calculate the downside diviation of all assets in a portfolio + + :param historical_returns: historical returns of assets in dataframe. + :type historical_returns: np.ndarray + :param mar: Minimum Acceptable Return. Preffered practices include either US 13-week T-bill or zero. + CAUTION: mar must be in the same period as the returns. If you give daily returns then you need to convert mar to daily value. + :type mar: float + :param return_Max: an option to return the Max sortino ratio of the portfolio instead of a list of all sortino ratios for all stocks. Defaults to False + :type return_Max: Boolean + :return: Sortino ratio + :rtype: float + """ + downsideDiviations = [] + linesOfData = len(historical_returns.iloc[:, [0]]) + for stock in range(len(historical_returns.columns)): + noDataLines = 0 # counts NaN lines in dataset if any + negativeReturns = [] # stores the negative daily returns + for row in range(linesOfData): + if ( math.isnan(historical_returns.iloc[row, [stock]][0]) ): + noDataLines += 1 + continue + if ( (historical_returns.iloc[row, [stock]][0] - mar) < 0 ): + negativeReturns.append(historical_returns.iloc[row, [stock][0]] - mar) + period = linesOfData - noDataLines # number of actual observations + squaredReturns = [r ** 2 for r in negativeReturns] + ts = sum(squaredReturns) + dd = math.sqrt(ts / period) * math.sqrt(period) + downsideDiviations.append(dd) + return downsideDiviations \ No newline at end of file diff --git a/pypfopt/objective_functions.py b/pypfopt/objective_functions.py index 32c73661..835e05f1 100644 --- a/pypfopt/objective_functions.py +++ b/pypfopt/objective_functions.py @@ -115,6 +115,8 @@ def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative= return _objective_value(w, sign * sharpe) + + def L2_reg(w, gamma=1): r""" L2 regularisation, i.e :math:`\gamma ||w||^2`, to increase the number of nonzero weights. @@ -224,3 +226,32 @@ def ex_post_tracking_error(w, historic_returns, benchmark_returns): mean = cp.sum(x_i) / len(benchmark_returns) tracking_error = cp.sum_squares(x_i - mean) return _objective_value(w, tracking_error) + + +def sortino_ratio(w, expected_returns, downside_diviations, risk_free_rate = 0.02): + """ + Calculate the Sortino ratio of a portfolio + + :param w: asset weights in the portfolio + :type w: np.ndarray OR cp.Variable + :param expected_returns: expected return of each asset + :type expected_returns: np.ndarray + :param downside_diviations: The downside diviation of each asset + :type downside_diviations: np.ndarray + :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02. + The period of the risk-free rate should correspond to the + frequency of expected returns. + :type risk_free_rate: float, optional + :return: Sortino ratio + :rtype: float + """ + mu = w @ expected_returns + if isinstance(downside_diviations, list): + x = np.array(downside_diviations) + dd = w * x + sortino = (mu - risk_free_rate) / dd + return sum((w * sortino)) + else: + dd = w @ downside_diviations + sortino = (mu - risk_free_rate) / dd + return _objective_value(w, sortino)