Skip to content

Commit

Permalink
default risk free rate to 0.0, fixes robertmartin8#594
Browse files Browse the repository at this point in the history
  • Loading branch information
robertmartin8 authored and codez0mb1e committed Dec 22, 2024
1 parent 7ac2bbe commit b2845d0
Show file tree
Hide file tree
Showing 20 changed files with 240 additions and 190 deletions.
7 changes: 5 additions & 2 deletions pypfopt/base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def _make_output_weights(self, weights=None):
if weights is None:
weights = self.weights

# Convert numpy float64 to plain Python float
weights = [float(w) for w in weights]

return collections.OrderedDict(zip(self.tickers, weights))

def set_weights(self, input_weights):
Expand Down Expand Up @@ -510,7 +513,7 @@ def nonconvex_objective(


def portfolio_performance(
weights, expected_returns, cov_matrix, verbose=False, risk_free_rate=0.02
weights, expected_returns, cov_matrix, verbose=False, risk_free_rate=0.0
):
"""
After optimising, calculate (and optionally print) the performance of the optimal
Expand All @@ -525,7 +528,7 @@ def portfolio_performance(
:type weights: list, np.array or dict, optional
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0
:type risk_free_rate: float, optional
:raises ValueError: if weights have not been calculated yet
:return: expected return, volatility, Sharpe ratio.
Expand Down
22 changes: 11 additions & 11 deletions pypfopt/black_litterman.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


def market_implied_prior_returns(
market_caps, risk_aversion, cov_matrix, risk_free_rate=0.02
market_caps, risk_aversion, cov_matrix, risk_free_rate=0.0
):
r"""
Compute the prior estimate of returns implied by the market weights.
Expand All @@ -34,7 +34,7 @@ def market_implied_prior_returns(
:type risk_aversion: positive float
:param cov_matrix: covariance matrix of asset returns
:type cov_matrix: pd.DataFrame
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
You should use the appropriate time period, corresponding
to the covariance matrix.
:type risk_free_rate: float, optional
Expand All @@ -52,7 +52,7 @@ def market_implied_prior_returns(
return risk_aversion * cov_matrix.dot(mkt_weights) + risk_free_rate


def market_implied_risk_aversion(market_prices, frequency=252, risk_free_rate=0.02):
def market_implied_risk_aversion(market_prices, frequency=252, risk_free_rate=0.0):
r"""
Calculate the market-implied risk-aversion parameter (i.e market price of risk)
based on market prices. For example, if the market has excess returns of 10% a year
Expand All @@ -68,7 +68,7 @@ def market_implied_risk_aversion(market_prices, frequency=252, risk_free_rate=0.
:param frequency: number of time periods in a year, defaults to 252 (the number
of trading days in a year)
:type frequency: int, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down Expand Up @@ -168,7 +168,7 @@ def __init__(
:param market_caps: (kwarg) market caps for the assets, required if pi="market"
:type market_caps: np.ndarray, pd.Series, optional
:param risk_free_rate: (kwarg) risk_free_rate is needed in some methods
:type risk_free_rate: float, defaults to 0.02
:type risk_free_rate: float, defaults to 0.0
"""
if sys.version_info[1] == 5: # pragma: no cover
warnings.warn(
Expand Down Expand Up @@ -268,7 +268,7 @@ def _set_pi(self, pi, **kwargs):
"Please pass a series/array of market caps via the market_caps keyword argument"
)
market_caps = kwargs.get("market_caps")
risk_free_rate = kwargs.get("risk_free_rate", 0)
risk_free_rate = kwargs.get("risk_free_rate", 0.0)

market_prior = market_implied_prior_returns(
market_caps, self.risk_aversion, self._raw_cov_matrix, risk_free_rate
Expand Down Expand Up @@ -406,7 +406,7 @@ def bl_returns(self):
try:
solution = np.linalg.solve(self._A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
if "Singular matrix" in str(e):
solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
Expand All @@ -432,7 +432,7 @@ def bl_cov(self):
try:
M_solution = np.linalg.solve(self._A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
if "Singular matrix" in str(e):
M_solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
Expand Down Expand Up @@ -465,7 +465,7 @@ def bl_weights(self, risk_aversion=None):
try:
weight_solution = np.linalg.solve(A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
if "Singular matrix" in str(e):
weight_solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
Expand All @@ -477,15 +477,15 @@ def optimize(self, risk_aversion=None):
"""Alias for bl_weights for consistency with other methods."""
return self.bl_weights(risk_aversion)

def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, verbose=False, risk_free_rate=0.0):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
This method uses the BL posterior returns and covariance matrix.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down
4 changes: 2 additions & 2 deletions pypfopt/cla.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,14 +443,14 @@ def set_weights(self, _):
# Overrides parent method since set_weights does nothing.
raise NotImplementedError("set_weights does nothing for CLA")

def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, verbose=False, risk_free_rate=0.0):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0
:type risk_free_rate: float, optional
:raises ValueError: if weights have not been calculated yet
:return: expected return, volatility, Sharpe ratio.
Expand Down
2 changes: 1 addition & 1 deletion pypfopt/efficient_frontier/efficient_cdar.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _validate_beta(beta):
def min_volatility(self):
raise NotImplementedError("Please use min_cdar instead.")

def max_sharpe(self, risk_free_rate=0.02):
def max_sharpe(self, risk_free_rate=0.0):
raise NotImplementedError("Method not available in EfficientCDaR.")

def max_quadratic_utility(self, risk_aversion=1, market_neutral=False):
Expand Down
2 changes: 1 addition & 1 deletion pypfopt/efficient_frontier/efficient_cvar.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _validate_beta(beta):
def min_volatility(self):
raise NotImplementedError("Please use min_cvar instead.")

def max_sharpe(self, risk_free_rate=0.02):
def max_sharpe(self, risk_free_rate=0.0):
raise NotImplementedError("Method not available in EfficientCVaR.")

def max_quadratic_utility(self, risk_aversion=1, market_neutral=False):
Expand Down
8 changes: 4 additions & 4 deletions pypfopt/efficient_frontier/efficient_frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,15 @@ def _max_return(self, return_value=True):
else:
return res

def max_sharpe(self, risk_free_rate=0.02):
def max_sharpe(self, risk_free_rate=0.0):
"""
Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio,
as it is the portfolio for which the capital market line is tangent to the efficient frontier.
This is a convex optimization problem after making a certain variable substitution. See
`Cornuejols and Tutuncu (2006) <http://web.math.ku.dk/~rolf/CT_FinOpt.pdf>`_ for more.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down Expand Up @@ -419,14 +419,14 @@ def efficient_return(self, target_return, market_neutral=False):
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()

def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, verbose=False, risk_free_rate=0.0):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down
6 changes: 3 additions & 3 deletions pypfopt/efficient_frontier/efficient_semivariance.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def __init__(
def min_volatility(self):
raise NotImplementedError("Please use min_semivariance instead.")

def max_sharpe(self, risk_free_rate=0.02):
def max_sharpe(self, risk_free_rate=0.0):
raise NotImplementedError("Method not available in EfficientSemivariance")

def min_semivariance(self, market_neutral=False):
Expand Down Expand Up @@ -246,14 +246,14 @@ def efficient_return(self, target_return, market_neutral=False):
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()

def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, verbose=False, risk_free_rate=0.0):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio, specifically: expected return, semideviation, Sortino ratio.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down
4 changes: 2 additions & 2 deletions pypfopt/expected_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def capm_return(
prices,
market_prices=None,
returns_data=False,
risk_free_rate=0.02,
risk_free_rate=0.0,
compounding=True,
frequency=252,
log_returns=False,
Expand All @@ -221,7 +221,7 @@ def capm_return(
:type market_prices: pd.DataFrame, optional
:param returns_data: if true, the first arguments are returns instead of prices.
:type returns_data: bool, defaults to False.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
You should use the appropriate time period, corresponding
to the frequency parameter.
:type risk_free_rate: float, optional
Expand Down
4 changes: 2 additions & 2 deletions pypfopt/hierarchical_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ def optimize(self, linkage_method="single"):
self.set_weights(weights)
return weights

def portfolio_performance(self, verbose=False, risk_free_rate=0.02, frequency=252):
def portfolio_performance(self, verbose=False, risk_free_rate=0.0, frequency=252):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio
assuming returns are daily
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down
4 changes: 2 additions & 2 deletions pypfopt/objective_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def portfolio_return(w, expected_returns, negative=True):
return _objective_value(w, sign * mu)


def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative=True):
def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.0, negative=True):
"""
Calculate the (negative) Sharpe ratio of a portfolio
Expand All @@ -99,7 +99,7 @@ def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative=
:type expected_returns: np.ndarray
:param cov_matrix: covariance matrix
:type cov_matrix: np.ndarray
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
Expand Down
29 changes: 15 additions & 14 deletions tests/test_black_litterman.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def test_bl_equal_prior():

bl.bl_weights()
np.testing.assert_allclose(
bl.portfolio_performance(),
bl.portfolio_performance(risk_free_rate=0.02),
(0.1877432247395778, 0.3246889329226965, 0.5166274785827545),
)

Expand Down Expand Up @@ -244,18 +244,18 @@ def test_market_risk_aversion():
prices = pd.read_csv(
resource("spy_prices.csv"), parse_dates=True, index_col=0
).squeeze("columns")
delta = market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices, risk_free_rate=0.02)
assert np.round(delta, 5) == 2.68549

# check it works for df
prices = pd.read_csv(resource("spy_prices.csv"), parse_dates=True, index_col=0)
delta = market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices, risk_free_rate=0.02)
assert np.round(delta.iloc[0], 5) == 2.68549

# Check it raises for other types.
list_invalid = [100.0, 110.0, 120.0, 130.0]
with pytest.raises(TypeError):
delta = market_implied_risk_aversion(list_invalid)
delta = market_implied_risk_aversion(list_invalid, risk_free_rate=0.02)


def test_bl_weights():
Expand Down Expand Up @@ -321,10 +321,10 @@ def test_market_implied_prior():
prices = pd.read_csv(
resource("spy_prices.csv"), parse_dates=True, index_col=0
).squeeze("columns")
delta = market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices, risk_free_rate=0.02)

mcaps = get_market_caps()
pi = market_implied_prior_returns(mcaps, delta, S)
pi = market_implied_prior_returns(mcaps, delta, S, risk_free_rate=0.02)
assert isinstance(pi, pd.Series)
assert list(pi.index) == list(df.columns)
assert pi.notnull().all()
Expand Down Expand Up @@ -358,7 +358,7 @@ def test_market_implied_prior():
)

mcaps = pd.Series(mcaps)
pi2 = market_implied_prior_returns(mcaps, delta, S)
pi2 = market_implied_prior_returns(mcaps, delta, S, risk_free_rate=0.02)
pd.testing.assert_series_equal(pi, pi2, check_exact=False)

# Test alternate syntax
Expand All @@ -368,8 +368,9 @@ def test_market_implied_prior():
market_caps=mcaps,
absolute_views={"AAPL": 0.1},
risk_aversion=delta,
risk_free_rate=0.02,
)
pi = market_implied_prior_returns(mcaps, delta, S, risk_free_rate=0)
pi = market_implied_prior_returns(mcaps, delta, S, risk_free_rate=0.02)
np.testing.assert_array_almost_equal(bl.pi, pi.values.reshape(-1, 1))


Expand All @@ -381,14 +382,14 @@ def test_bl_market_prior():
resource("spy_prices.csv"), parse_dates=True, index_col=0
).squeeze("columns")

delta = market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices, risk_free_rate=0.02)

mcaps = get_market_caps()

with pytest.warns(RuntimeWarning):
market_implied_prior_returns(mcaps, delta, S.values)

prior = market_implied_prior_returns(mcaps, delta, S)
prior = market_implied_prior_returns(mcaps, delta, S, risk_free_rate=0.02)

viewdict = {"GOOG": 0.40, "AAPL": -0.30, "FB": 0.30, "BABA": 0}
bl = BlackLittermanModel(S, pi=prior, absolute_views=viewdict)
Expand All @@ -401,11 +402,11 @@ def test_bl_market_prior():
)

with pytest.raises(ValueError):
bl.portfolio_performance()
bl.portfolio_performance(risk_free_rate=0.02)

bl.bl_weights(delta)
np.testing.assert_allclose(
bl.portfolio_performance(),
bl.portfolio_performance(risk_free_rate=0.02),
(0.2580693114409672, 0.265445955488424, 0.8968654692926723),
)
# Check that bl.cov() has been called and used
Expand Down Expand Up @@ -657,11 +658,11 @@ def test_idzorek_with_priors():
np.testing.assert_almost_equal(rets["AAPL"], -0.3)

with pytest.raises(ValueError):
bl.portfolio_performance()
bl.portfolio_performance(risk_free_rate=0.02)

bl.bl_weights()
np.testing.assert_allclose(
bl.portfolio_performance(),
bl.portfolio_performance(risk_free_rate=0.02),
(0.943431295405105, 0.5361412623208567, 1.722365653051476),
)
# Check that bl.cov() has been called and used
Expand Down
Loading

0 comments on commit b2845d0

Please sign in to comment.