Skip to content

Commit

Permalink
enhance tests
Browse files Browse the repository at this point in the history
  • Loading branch information
88d52bdba0366127fffca9dfa93895 committed May 1, 2023
1 parent 8c3c1bd commit a6e6865
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 372 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
- name: Test with pytest
run: |
pip install pytest pytest-cov
pytest ./tests --doctest-modules --cov-report=html
pytest -s ./tests --doctest-modules --cov-report=html
2 changes: 1 addition & 1 deletion pypfopt/base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def __init__(
for portfolios with shorting.
:type weight_bounds: tuple OR tuple list, optional
:param solver: name of solver. list available solvers with: ``cvxpy.installed_solvers()``
:type solver: str, optional. Defaults to "ECOS"
:type solver: str, optional.
:param verbose: whether performance and debugging info should be printed, defaults to False
:type verbose: bool, optional
:param solver_options: parameters for the given solver
Expand Down
76 changes: 22 additions & 54 deletions tests/test_base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import os
import tempfile

import cvxpy as cp
import numpy as np
import pandas as pd
import pytest
import cvxpy as cp

from pypfopt import EfficientFrontier, objective_functions
from pypfopt import exceptions
from pypfopt.base_optimizer import portfolio_performance, BaseOptimizer
from pypfopt import EfficientFrontier, exceptions, objective_functions
from pypfopt.base_optimizer import BaseOptimizer, portfolio_performance
from tests.utilities_for_tests import get_data, setup_efficient_frontier


Expand All @@ -23,9 +22,7 @@ def test_base_optimizer():


def test_custom_bounds():
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(0.02, 0.13)
)
ef = setup_efficient_frontier(weight_bounds=(0.02, 0.13))
ef.min_volatility()
np.testing.assert_allclose(ef._lower_bounds, np.array([0.02] * ef.n_assets))
np.testing.assert_allclose(ef._upper_bounds, np.array([0.13] * ef.n_assets))
Expand All @@ -37,97 +34,71 @@ def test_custom_bounds():

def test_custom_bounds_different_values():
bounds = [(0.01, 0.13), (0.02, 0.11)] * 10
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=bounds
)
ef = setup_efficient_frontier(weight_bounds=bounds)
ef.min_volatility()
assert (0.01 <= ef.weights[::2]).all() and (ef.weights[::2] <= 0.13).all()
assert (0.02 <= ef.weights[1::2]).all() and (ef.weights[1::2] <= 0.11).all()
np.testing.assert_almost_equal(ef.weights.sum(), 1)

bounds = ((0.01, 0.13), (0.02, 0.11)) * 10
assert EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=bounds
)
assert setup_efficient_frontier(weight_bounds=bounds)


def test_weight_bounds_minus_one_to_one():
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
)
ef = setup_efficient_frontier(weight_bounds=(-1, 1))
assert ef.max_sharpe()
ef2 = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
)
ef2 = setup_efficient_frontier(weight_bounds=(-1, 1))
assert ef2.min_volatility()


def test_none_bounds():
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(None, 0.3)
)
ef = setup_efficient_frontier(weight_bounds=(None, 0.3))
ef.min_volatility()
w1 = ef.weights

ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 0.3)
)
ef = setup_efficient_frontier(weight_bounds=(-1, 0.3))
ef.min_volatility()
w2 = ef.weights

np.testing.assert_array_almost_equal(w1, w2)


def test_bound_input_types():
bounds = [0.01, 0.13]
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=bounds
)
ef = setup_efficient_frontier(weight_bounds=bounds)
assert ef
np.testing.assert_allclose(ef._lower_bounds, np.array([0.01] * ef.n_assets))
np.testing.assert_allclose(ef._upper_bounds, np.array([0.13] * ef.n_assets))

lb = np.array([0.01, 0.02] * 10)
ub = np.array([0.07, 0.2] * 10)
assert EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(lb, ub)
)
assert setup_efficient_frontier(weight_bounds=(lb, ub))

bounds = ((0.01, 0.13), (0.02, 0.11)) * 10
assert EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=bounds
)
assert setup_efficient_frontier(weight_bounds=bounds)


def test_bound_failure():
# Ensure optimization fails when lower bound is too high or upper bound is too low
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(0.06, 0.13)
)
ef = setup_efficient_frontier(weight_bounds=(0.06, 0.13))
with pytest.raises(exceptions.OptimizationError):
ef.min_volatility()

ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(0, 0.04)
)
ef = setup_efficient_frontier(weight_bounds=(0, 0.04))
with pytest.raises(exceptions.OptimizationError):
ef.min_volatility()


def test_bounds_errors():
assert EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(0, 1)
)
assert setup_efficient_frontier(weight_bounds=(0, 1))

with pytest.raises(TypeError):
EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(0.06, 1, 3)
)
setup_efficient_frontier(weight_bounds=(0.06, 1, 3))

with pytest.raises(TypeError):
# Not enough bounds
bounds = [(0.01, 0.13), (0.02, 0.11)] * 5
EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=bounds
)
setup_efficient_frontier(weight_bounds=bounds)


def test_clean_weights():
Expand All @@ -138,16 +109,13 @@ def test_clean_weights():
cleaned_weights = cleaned.values()
clean_number_tiny_weights = sum(i < 1e-4 for i in cleaned_weights)
assert clean_number_tiny_weights == number_tiny_weights
#  Check rounding
# Check rounding
cleaned_weights_str_length = [len(str(i)) for i in cleaned_weights]
assert all([length == 7 or length == 3 for length in cleaned_weights_str_length])


def test_clean_weights_short():
ef = setup_efficient_frontier()
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(-1, 1)
)
ef = setup_efficient_frontier(weight_bounds=(-1, 1))
ef.min_volatility()
# In practice we would never use such a high cutoff
number_tiny_weights = sum(np.abs(ef.weights) < 0.05)
Expand Down
39 changes: 21 additions & 18 deletions tests/test_black_litterman.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import pandas as pd
import pytest

from pypfopt import black_litterman
from pypfopt.black_litterman import BlackLittermanModel
from pypfopt import risk_models, expected_returns
from pypfopt.black_litterman import (
BlackLittermanModel,
market_implied_risk_aversion,
market_implied_prior_returns,
)
from tests.utilities_for_tests import get_data, get_market_caps, resource


Expand Down Expand Up @@ -241,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 = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)
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 = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)
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 = black_litterman.market_implied_risk_aversion(list_invalid)
delta = market_implied_risk_aversion(list_invalid)


def test_bl_weights():
Expand All @@ -266,7 +269,7 @@ def test_bl_weights():
resource("spy_prices.csv"), parse_dates=True, index_col=0
).squeeze("columns")

delta = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)
bl.bl_weights(delta)
w = bl.clean_weights()
assert abs(sum(w.values()) - 1) < 1e-5
Expand Down Expand Up @@ -318,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 = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)

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

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

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


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

delta = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)

mcaps = get_market_caps()

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

prior = black_litterman.market_implied_prior_returns(mcaps, delta, S)
prior = market_implied_prior_returns(mcaps, delta, S)

viewdict = {"GOOG": 0.40, "AAPL": -0.30, "FB": 0.30, "BABA": 0}
bl = BlackLittermanModel(S, pi=prior, absolute_views=viewdict)
Expand Down Expand Up @@ -419,7 +422,7 @@ def test_bl_market_automatic():
rets = bl.bl_returns()

# Compare with explicit
prior = black_litterman.market_implied_prior_returns(mcaps, 1, S, 0)
prior = market_implied_prior_returns(mcaps, 1, S, 0)
bl2 = BlackLittermanModel(S, pi=prior, absolute_views=viewdict)
rets2 = bl2.bl_returns()
pd.testing.assert_series_equal(rets, rets2)
Expand All @@ -432,8 +435,8 @@ def test_bl_market_automatic():
# mcaps2 = {k: v for k, v in list(mcaps.items())[::-1]}
# # mcaps = pd.Series(mcaps)

# market_prior1 = black_litterman.market_implied_prior_returns(mcaps, 2, S.values, 0)
# market_prior2 = black_litterman.market_implied_prior_returns(mcaps2, 2, S.values, 0)
# market_prior1 = market_implied_prior_returns(mcaps, 2, S.values, 0)
# market_prior2 = market_implied_prior_returns(mcaps2, 2, S.values, 0)
# market_prior1 == market_prior2

# mcaps = pd.Series(mcaps)
Expand Down Expand Up @@ -471,10 +474,10 @@ def test_bl_tau():
resource("spy_prices.csv"), parse_dates=True, index_col=0
).squeeze("columns")

delta = black_litterman.market_implied_risk_aversion(prices)
delta = market_implied_risk_aversion(prices)

mcaps = get_market_caps()
prior = black_litterman.market_implied_prior_returns(mcaps, delta, S)
prior = market_implied_prior_returns(mcaps, delta, S)

viewdict = {"GOOG": 0.40, "AAPL": -0.30, "FB": 0.30, "BABA": 0}

Expand Down
11 changes: 6 additions & 5 deletions tests/test_cla.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import numpy as np
import pytest
from tests.utilities_for_tests import get_data, setup_cla

from pypfopt import risk_models
from pypfopt.cla import CLA
from tests.utilities_for_tests import get_data, setup_cla


def test_portfolio_performance():
Expand Down Expand Up @@ -33,7 +34,7 @@ def test_cla_max_sharpe_long_only():


def test_cla_max_sharpe_short():
cla = CLA(*setup_cla(data_only=True), weight_bounds=(-1, 1))
cla = setup_cla(weight_bounds=(-1, 1))
w = cla.max_sharpe()
assert isinstance(w, dict)
assert set(w.keys()) == set(cla.tickers)
Expand All @@ -53,7 +54,7 @@ def test_cla_max_sharpe_short():

def test_cla_custom_bounds():
bounds = [(0.01, 0.13), (0.02, 0.11)] * 10
cla = CLA(*setup_cla(data_only=True), weight_bounds=bounds)
cla = setup_cla(weight_bounds=bounds)
df = get_data()
cla.cov_matrix = risk_models.exp_cov(df).values
w = cla.min_volatility()
Expand All @@ -64,7 +65,7 @@ def test_cla_custom_bounds():
assert (0.02 <= cla.weights[1::2]).all() and (cla.weights[1::2] <= 0.11).all()
# Test polymorphism of the weight_bounds param.
bounds2 = ([bounds[0][0], bounds[1][0]] * 10, [bounds[0][1], bounds[1][1]] * 10)
cla2 = CLA(*setup_cla(data_only=True), weight_bounds=bounds2)
cla2 = setup_cla(weight_bounds=bounds2)
cla2.cov_matrix = risk_models.exp_cov(df).values
w2 = cla2.min_volatility()
assert dict(w2) == dict(w)
Expand Down Expand Up @@ -126,7 +127,7 @@ def test_cla_max_sharpe_exp_cov():


def test_cla_min_volatility_exp_cov_short():
cla = CLA(*setup_cla(data_only=True), weight_bounds=(-1, 1))
cla = setup_cla(weight_bounds=(-1, 1))
df = get_data()
cla.cov_matrix = risk_models.exp_cov(df).values
w = cla.min_volatility()
Expand Down

0 comments on commit a6e6865

Please sign in to comment.