diff --git a/.DS_Store b/.DS_Store index 36728efc..55899226 100644 Binary files a/.DS_Store and b/.DS_Store differ 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)