Skip to content

Commit dc7437f

Browse files
committed
test: update tests for plot_forecast_monte_carlo
and move dcf test to a separate folder
1 parent 89a0b91 commit dc7437f

File tree

4 files changed

+131
-0
lines changed

4 files changed

+131
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pytest
2+
import okama as ok
3+
4+
5+
@pytest.fixture()
6+
def pf_single_monthly(synthetic_env):
7+
"""Single-asset portfolio with monthly rebalancing and mocked data."""
8+
return ok.Portfolio(["A.US"], ccy="USD", inflation=False, rebalancing_strategy=ok.Rebalance(period="month"))
9+
10+
11+
def test_vds_frequency_is_year_and_setter_raises(pf_single_monthly):
12+
vds = ok.VanguardDynamicSpending(pf_single_monthly)
13+
assert vds.frequency == "year"
14+
with pytest.raises(AttributeError, match=r"frequency.*year"):
15+
vds.frequency = "month"
16+
17+
18+
def test_vds_calculate_withdrawal_size_limits(pf_single_monthly):
19+
vds = ok.VanguardDynamicSpending(
20+
pf_single_monthly,
21+
percentage=-0.10,
22+
min_max_annual_withdrawal=(500.0, 900.0),
23+
adjust_min_max=False,
24+
floor_ceiling=(-0.10, 0.10),
25+
adjust_floor_ceiling=False,
26+
indexation=0.0,
27+
)
28+
# Cap by ceiling when percentage-based withdrawal is too high.
29+
withdrawal = vds._calculate_withdrawal_size(last_withdrawal=-800.0, balance=10_000.0, number_of_periods=0)
30+
assert withdrawal == pytest.approx(-880.0)
31+
# Enforce floor when percentage-based withdrawal is too low.
32+
withdrawal_low = vds._calculate_withdrawal_size(last_withdrawal=-800.0, balance=4_000.0, number_of_periods=0)
33+
assert withdrawal_low == pytest.approx(-720.0)
34+
35+
36+
def test_vds_cash_flow_ts_yearly_entries(pf_single_monthly):
37+
vds = ok.VanguardDynamicSpending(
38+
pf_single_monthly,
39+
percentage=-0.06,
40+
min_max_annual_withdrawal=(200.0, 2_000.0),
41+
floor_ceiling=(-0.10, 0.10),
42+
adjust_min_max=False,
43+
adjust_floor_ceiling=False,
44+
indexation=0.0,
45+
)
46+
pf_single_monthly.dcf.cashflow_parameters = vds
47+
cfts = pf_single_monthly.dcf.cash_flow_ts(discounting="fv", remove_if_wealth_index_negative=False)
48+
non_zero = cfts[cfts != 0]
49+
# With 24 months of data and yearly frequency, there should be two cash flow entries.
50+
assert len(non_zero) == 2
51+
assert (non_zero <= 0).all()
52+
53+
54+
def test_cwid_calculate_withdrawal_size_reduction(pf_single_monthly):
55+
cwid = ok.CutWithdrawalsIfDrawdown(
56+
pf_single_monthly,
57+
amount=-1000.0,
58+
indexation=0.0,
59+
crash_threshold_reduction=[(0.20, 0.40), (0.50, 1.0)],
60+
)
61+
assert cwid._calculate_withdrawal_size(drawdown=-0.25, withdrawal_without_drawdowns=-1000.0) == pytest.approx(-600.0)
62+
assert cwid._calculate_withdrawal_size(drawdown=-0.55, withdrawal_without_drawdowns=-1000.0) == pytest.approx(0.0)
63+
64+
65+
def test_cwid_crash_threshold_reduction_validation(pf_single_monthly):
66+
cwid = ok.CutWithdrawalsIfDrawdown(pf_single_monthly, amount=-1000.0, indexation=0.0)
67+
with pytest.raises(ValueError, match=r"threshold"):
68+
cwid.crash_threshold_reduction = [(0.0, 0.4)]
69+
with pytest.raises(ValueError, match=r"reductiuon"):
70+
cwid.crash_threshold_reduction = [(0.2, 1.1)]
71+
72+
73+
def test_cwid_cash_flow_ts_yearly_entries(pf_single_monthly):
74+
cwid = ok.CutWithdrawalsIfDrawdown(
75+
pf_single_monthly,
76+
amount=-1000.0,
77+
indexation=0.0,
78+
crash_threshold_reduction=[(0.10, 0.30)],
79+
)
80+
pf_single_monthly.dcf.cashflow_parameters = cwid
81+
cfts = pf_single_monthly.dcf.cash_flow_ts(discounting="fv", remove_if_wealth_index_negative=False)
82+
non_zero = cfts[cfts != 0]
83+
assert len(non_zero) == 2
84+
assert (non_zero <= 0).all()
85+
assert non_zero.abs().max() <= 1000.0
File renamed without changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import matplotlib
2+
3+
# Ensure non-interactive backend for headless test runs
4+
matplotlib.use("Agg")
5+
6+
import matplotlib.pyplot as plt
7+
import pytest
8+
9+
import okama as ok
10+
from okama.settings import DEFAULT_DISCOUNT_RATE
11+
12+
13+
@pytest.fixture()
14+
def pf_single_monthly(synthetic_env):
15+
"""Single-asset Portfolio with monthly rebalancing and no inflation (mocked data)."""
16+
return ok.Portfolio(["A.US"], ccy="USD", inflation=False, rebalancing_strategy=ok.Rebalance(period="month"))
17+
18+
19+
def _configure_dcf_for_plot(pf):
20+
ind = ok.IndexationStrategy(pf)
21+
ind.initial_investment = 10_000
22+
ind.frequency = "year"
23+
ind.amount = -500
24+
ind.indexation = DEFAULT_DISCOUNT_RATE
25+
pf.dcf.cashflow_parameters = ind
26+
pf.dcf.mc.period = 1
27+
pf.dcf.mc.number = 3
28+
return pf.dcf
29+
30+
31+
def test_plot_forecast_monte_carlo_returns_axes_backtest(pf_single_monthly):
32+
dcf = _configure_dcf_for_plot(pf_single_monthly)
33+
ax = dcf.plot_forecast_monte_carlo(backtest=True, figsize=(4, 3))
34+
assert ax is not None
35+
assert ax.figure is not None
36+
assert len(ax.lines) >= 1
37+
plt.close("all")
38+
39+
40+
def test_plot_forecast_monte_carlo_returns_axes_no_backtest(pf_single_monthly):
41+
dcf = _configure_dcf_for_plot(pf_single_monthly)
42+
ax = dcf.plot_forecast_monte_carlo(backtest=False, figsize=(4, 3))
43+
assert ax is not None
44+
assert ax.figure is not None
45+
assert len(ax.lines) == dcf.mc.number
46+
plt.close("all")

0 commit comments

Comments
 (0)