Skip to content

Commit d46d64d

Browse files
committed
test: add tests for MC methods
and move some tests from prtofolio to mc
1 parent 0b66a73 commit d46d64d

File tree

2 files changed

+155
-61
lines changed

2 files changed

+155
-61
lines changed

tests/portfolio/mc/test_mc.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# tests/test_mc.py
2+
import re
3+
4+
import pytest
5+
from pytest import approx
6+
import pandas as pd
7+
8+
9+
def test_optimize_df_for_students_valid_case(mc_students):
10+
result = mc_students.optimize_df_for_students(var_level=5)
11+
assert isinstance(result, float), "The returned optimized degrees of freedom should be a float."
12+
assert result == approx(3.04, rel=1e-2)
13+
14+
15+
def test_optimize_df_for_students_invalid_var_level_high(mc_students):
16+
with pytest.raises(ValueError, match=re.escape("var_level must be in [1, 99]")):
17+
mc_students.optimize_df_for_students(var_level=100)
18+
19+
20+
# moved from core
21+
def test_monte_carlo_returns_ts(mc_normal_small):
22+
df = mc_normal_small.monte_carlo_returns_ts
23+
assert df.shape == (12, 10)
24+
assert df.iloc[-1, :].mean() == approx(0.0156, abs=1e-1)
25+
26+
27+
def test_forecast_monte_carlo_cagr(mc_students):
28+
dic = mc_students.percentile_distribution_cagr(percentiles=[50])
29+
assert dic[50] == approx(0.2275, abs=1e-1)
30+
31+
32+
def test_skewness(mc_normal_small):
33+
assert mc_normal_small.skewness.iloc[-1] == approx(-0.6448, abs=1e-2)
34+
35+
36+
def test_rolling_skewness(mc_normal_small):
37+
assert mc_normal_small.skewness_rolling(window=24).iloc[-1] == approx(0.1449, abs=1e-1)
38+
39+
40+
def test_kurtosis(mc_normal_small):
41+
assert mc_normal_small.kurtosis.iloc[-1] == approx(2.7960, rel=1e-2)
42+
43+
44+
def test_kurtosis_rolling(mc_normal_small):
45+
assert mc_normal_small.kurtosis_rolling(window=24).iloc[-1] == approx(-0.1149, rel=1e-1)
46+
47+
48+
def test_jarque_bera(mc_normal_small):
49+
assert mc_normal_small.jarque_bera["statistic"] == approx(66.765, rel=1e-1)
50+
51+
52+
# New tests to extend coverage of MonteCarlo
53+
54+
def test_percentile_inverse_cagr_range(mc_students):
55+
# Should return a percentile between 0 and 100
56+
p = mc_students.percentile_inverse_cagr(score=0)
57+
assert isinstance(p, float)
58+
assert 0.0 <= p <= 100.0
59+
60+
61+
def test_kstest_structure(mc_students):
62+
res = mc_students.kstest
63+
assert set(res.keys()) == {"statistic", "p-value"}
64+
assert isinstance(res["statistic"], float)
65+
assert isinstance(res["p-value"], float)
66+
assert 0.0 <= res["p-value"] <= 1.0
67+
68+
69+
def test_kstest_for_all_distributions(mc_students):
70+
df = mc_students.kstest_for_all_distributions
71+
assert isinstance(df, pd.DataFrame)
72+
# Expect rows for all configured distributions
73+
assert len(df.index) >= 3
74+
for col in ("statistic", "p-value"):
75+
assert col in df.columns
76+
77+
78+
def test_model_risk_structure(mc_students):
79+
res = mc_students.model_risk(var_level=5)
80+
assert set(res.keys()) == {"delta_arithmetic_mean", "delta_var", "delta_cvar"}
81+
for k in res:
82+
assert isinstance(res[k], float)
83+
84+
85+
# Tests for get_parameters_for_distribution
86+
87+
def test_get_parameters_for_distribution_norm_defaults(mc_normal_small):
88+
# With None parameters, should use historical mean and std
89+
mc_normal_small.distribution_parameters = (None, None)
90+
mu, sigma = mc_normal_small.get_parameters_for_distribution()
91+
assert isinstance(mu, float) and isinstance(sigma, float)
92+
assert mu == approx(float(mc_normal_small.ror.mean()), rel=1e-12, abs=0)
93+
assert sigma == approx(float(mc_normal_small.ror.std()), rel=1e-12, abs=0)
94+
95+
96+
def test_get_parameters_for_distribution_norm_partial_override(mc_normal_small):
97+
# Override only mu, keep sigma from data
98+
mc_normal_small.distribution_parameters = (0.01, None)
99+
mu, sigma = mc_normal_small.get_parameters_for_distribution()
100+
assert mu == approx(0.01, rel=0, abs=0)
101+
assert sigma == approx(float(mc_normal_small.ror.std()), rel=1e-12, abs=0)
102+
103+
104+
def test_get_parameters_for_distribution_norm_full_override(mc_normal_small):
105+
# Full pass-through when both params provided
106+
mc_normal_small.distribution_parameters = (0.02, 0.05)
107+
mu, sigma = mc_normal_small.get_parameters_for_distribution()
108+
assert mu == approx(0.02)
109+
assert sigma == approx(0.05)
110+
111+
112+
def test_get_parameters_for_distribution_lognorm_defaults(mc_lognormal_small):
113+
# With None parameters, should fit with loc fixed at -1.0
114+
mc_lognormal_small.distribution_parameters = (None, None, None)
115+
shape, loc, scale = mc_lognormal_small.get_parameters_for_distribution()
116+
assert isinstance(shape, float) and isinstance(loc, float) and isinstance(scale, float)
117+
assert loc == approx(-1.0, rel=0, abs=0)
118+
assert shape == approx(0.07, abs=1e-02)
119+
assert scale == approx(1.012, abs=1e-02)
120+
121+
122+
def test_get_parameters_for_distribution_lognorm_full_override(mc_lognormal_small):
123+
# Full pass-through for lognormal; returned loc must be preserved
124+
mc_lognormal_small.distribution_parameters = (0.4, -1.0, 0.1)
125+
shape, loc, scale = mc_lognormal_small.get_parameters_for_distribution()
126+
assert shape == approx(0.4)
127+
assert loc == approx(-1.0, rel=0, abs=0)
128+
assert scale == approx(0.1)
129+
130+
131+
def test_get_parameters_for_distribution_t_defaults(mc_students):
132+
# With None parameters, should fit t distribution
133+
mc_students.distribution_parameters = (None, None, None)
134+
df, loc, scale = mc_students.get_parameters_for_distribution()
135+
assert isinstance(df, float) and isinstance(loc, float) and isinstance(scale, float)
136+
assert df > 2 # df must be > 2 for finite variance
137+
assert scale > 0
138+
139+
140+
def test_get_parameters_for_distribution_t_full_override(mc_students):
141+
mc_students.distribution_parameters = (5.0, 0.0, 0.02)
142+
df, loc, scale = mc_students.get_parameters_for_distribution()
143+
assert df == approx(5.0)
144+
assert loc == approx(0.0)
145+
assert scale == approx(0.02)
146+
147+
def test_get_parameters_for_distribution_lognormal_defaults(mc_lognormal_small):
148+
mc_lognormal_small.distribution_parameters = (None, None, None)
149+
shape, loc, scale = mc_lognormal_small.get_parameters_for_distribution()

tests/test_portfolio.py renamed to tests/portfolio/test_portfolio.py

Lines changed: 6 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import numpy as np
22
import pandas as pd
33
import pytest
4-
from pytest import approx
5-
from pytest import mark
4+
from pytest import approx, mark
65
from numpy.testing import assert_array_equal, assert_allclose
76
from pandas.testing import assert_series_equal, assert_frame_equal
87

@@ -247,26 +246,18 @@ def test_describe_no_inflation(portfolio_no_inflation):
247246
description_sample = pd.read_pickle(conftest.data_folder / "portfolio_description_no_inflation.pkl")
248247
assert_frame_equal(description, description_sample, check_dtype=False, check_column_type=False, atol=1e-2)
249248

249+
def test_percentile_inverse_cagr(portfolio_rebalanced_month):
250+
assert portfolio_rebalanced_month.percentile_inverse_cagr(years=1, score=0) == approx(0, abs=1e-2)
250251

251252
def test_percentile_from_history(portfolio_rebalanced_month, portfolio_no_inflation, portfolio_short_history):
252-
assert portfolio_rebalanced_month.percentile_history_cagr(years=1).iloc[0, 1] == approx(0.173181, abs=1e-2)
253-
assert portfolio_no_inflation.percentile_history_cagr(years=1).iloc[0, 1] == approx(0.17318, abs=1e-2)
253+
assert portfolio_rebalanced_month.percentile_cagr(years=1).iloc[0, 1] == approx(0.173181, abs=1e-2)
254+
assert portfolio_no_inflation.percentile_cagr(years=1).iloc[0, 1] == approx(0.17318, abs=1e-2)
254255
with pytest.raises(
255256
ValueError,
256257
match="Time series does not have enough history to forecast. "
257258
"Period length is 0.90 years. At least 2 years are required.",
258259
):
259-
portfolio_short_history.percentile_history_cagr(years=1)
260-
261-
262-
@mark.parametrize(
263-
"distribution, expected",
264-
[("hist", 0), ("norm", 0.9), ("lognorm", 0.7), ("t", 1.4)],
265-
)
266-
def test_percentile_inverse_cagr(portfolio_rebalanced_month, distribution, expected):
267-
assert portfolio_rebalanced_month.percentile_inverse_cagr(distr=distribution, years=1, score=0, n=5000) == approx(
268-
expected, abs=1e-0
269-
)
260+
portfolio_short_history.percentile_cagr(years=1)
270261

271262

272263
def test_table(portfolio_rebalanced_month):
@@ -304,52 +295,6 @@ def test_get_rolling_cagr_failing_no_inflation(portfolio_no_inflation):
304295
portfolio_no_inflation.get_rolling_cagr(real=True)
305296

306297

307-
def test_monte_carlo_wealth(portfolio_rebalanced_month):
308-
df = portfolio_rebalanced_month.monte_carlo_wealth(distr="norm", years=1, n=1000)
309-
assert df.shape == (13, 1000)
310-
assert df.iloc[-1, :].mean() == approx(2915.55, rel=1e-1)
311-
312-
313-
def test_monte_carlo_returns_ts(portfolio_rebalanced_month):
314-
df = portfolio_rebalanced_month.monte_carlo_returns_ts(distr="lognorm", years=1, n=1000)
315-
assert df.shape == (12, 1000)
316-
assert df.iloc[-1, :].mean() == approx(0.0156, abs=1e-1)
317-
318-
319-
@mark.parametrize(
320-
"distribution, expected",
321-
[("hist", 2897.72), ("norm", 2940.70), ("lognorm", 2932.56), ("t", 2900)],
322-
)
323-
def test_percentile_wealth(portfolio_rebalanced_month, distribution, expected):
324-
dic = portfolio_rebalanced_month.percentile_wealth(distr=distribution, years=1, n=100, percentiles=[50])
325-
assert dic[50] == approx(expected, rel=1e-1)
326-
327-
328-
def test_forecast_monte_carlo_cagr(portfolio_rebalanced_month):
329-
dic = portfolio_rebalanced_month.percentile_distribution_cagr(years=2, distr="lognorm", n=100, percentiles=[50])
330-
assert dic[50] == approx(0.1905, abs=5e-2)
331-
332-
333-
def test_skewness(portfolio_rebalanced_month):
334-
assert portfolio_rebalanced_month.skewness.iloc[-1] == approx(0.4980, abs=1e-1)
335-
336-
337-
def test_rolling_skewness(portfolio_rebalanced_month):
338-
assert portfolio_rebalanced_month.skewness_rolling(window=24).iloc[-1] == approx(0.4498, abs=1e-1)
339-
340-
341-
def test_kurtosis(portfolio_rebalanced_month):
342-
assert portfolio_rebalanced_month.kurtosis.iloc[-1] == approx(1.46, rel=1e-2)
343-
344-
345-
def test_kurtosis_rolling(portfolio_rebalanced_month):
346-
assert portfolio_rebalanced_month.kurtosis_rolling(window=24).iloc[-1] == approx(-0.2498, rel=1e-1)
347-
348-
349-
def test_jarque_bera(portfolio_rebalanced_month):
350-
assert portfolio_rebalanced_month.jarque_bera["statistic"] == approx(6.3657, rel=1e-1)
351-
352-
353298
def test_get_sharpe_ratio(portfolio_no_inflation):
354299
assert portfolio_no_inflation.get_sharpe_ratio(rf_return=0.05) == approx(1.6457, abs=1e-1)
355300

0 commit comments

Comments
 (0)