Skip to content

Commit 6508dac

Browse files
committed
test: update EfficientFrontierReb tests
1 parent e533503 commit 6508dac

File tree

1 file changed

+135
-15
lines changed

1 file changed

+135
-15
lines changed

tests/test_frontier_reb.py

Lines changed: 135 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
@pytest.fixture()
1010
def ef_reb_ab(synthetic_env):
11-
"""EfficientFrontierReb with two mocked assets A.US and B.US in USD.
11+
"""
12+
EfficientFrontierReb with two mocked assets A.US and B.US in USD.
1213
1314
Uses synthetic_env to patch asset loading and currency, so no API is called.
1415
"""
@@ -19,7 +20,9 @@ def ef_reb_ab(synthetic_env):
1920

2021
@pytest.fixture()
2122
def ef_reb_three(synthetic_env):
22-
"""EfficientFrontierReb with three mocked assets IDX.US, A.US and B.US."""
23+
"""
24+
EfficientFrontierReb with three mocked assets IDX.US, A.US and B.US.
25+
"""
2326
return ok.EfficientFrontierReb(
2427
["IDX.US", "A.US", "B.US"], ccy="USD", inflation=False, n_points=12, rebalancing_strategy=ok.Rebalance(period="year")
2528
)
@@ -109,19 +112,6 @@ def test_plot_pair_ef_returns_axes(ef_reb_three):
109112
ax = ef_reb_three.plot_pair_ef(tickers="tickers")
110113
assert hasattr(ax, "lines") and len(ax.lines) >= 1
111114

112-
# 2
113-
114-
115-
@pytest.fixture()
116-
def ef_reb_ab(synthetic_env):
117-
"""EfficientFrontierReb with two mocked assets A.US and B.US in USD.
118-
119-
Uses synthetic_env to patch asset loading and currency, so no API is called.
120-
"""
121-
return ok.EfficientFrontierReb(
122-
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10, rebalancing_strategy=ok.Rebalance(period="year")
123-
)
124-
125115

126116
def test_gmv_monthly_weights_basic(ef_reb_ab):
127117
w = ef_reb_ab.gmv_monthly_weights
@@ -222,3 +212,133 @@ def test_max_annual_risk_asset_structure(ef_reb_ab):
222212
assert 0 <= info["list_position"] < len(ef_reb_ab.symbols)
223213
assert ef_reb_ab.symbols[info["list_position"]] == info["ticker_with_largest_risk"]
224214

215+
216+
def test_verbose_property_setter(ef_reb_ab):
217+
"""Test verbose property getter and setter."""
218+
# Default value from fixture
219+
assert isinstance(ef_reb_ab.verbose, bool)
220+
# Set new value and verify cache is cleared
221+
_ = ef_reb_ab.ef_points
222+
assert not ef_reb_ab._ef_points.empty
223+
ef_reb_ab.verbose = True
224+
assert ef_reb_ab.verbose is True
225+
assert ef_reb_ab._ef_points.empty
226+
# Test validation
227+
with pytest.raises(ValueError, match=r"verbose should be True or False"):
228+
ef_reb_ab.verbose = "true" # type: ignore
229+
230+
231+
def test_full_frontier_parameter(synthetic_env):
232+
"""Test that full_frontier parameter is properly set."""
233+
ef_full = ok.EfficientFrontierReb(
234+
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10,
235+
rebalancing_strategy=ok.Rebalance(period="year"), full_frontier=True
236+
)
237+
assert ef_full.full_frontier is True
238+
239+
ef_partial = ok.EfficientFrontierReb(
240+
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10,
241+
rebalancing_strategy=ok.Rebalance(period="year"), full_frontier=False
242+
)
243+
assert ef_partial.full_frontier is False
244+
245+
246+
def test_max_cagr_asset_structure(ef_reb_ab):
247+
"""Test _max_cagr_asset property returns correct structure."""
248+
info = ef_reb_ab._max_cagr_asset
249+
assert set(info.keys()) == {"max_asset_cagr", "ticker_with_largest_cagr", "list_position"}
250+
assert info["ticker_with_largest_cagr"] in ef_reb_ab.symbols
251+
assert 0 <= info["list_position"] < len(ef_reb_ab.symbols)
252+
assert ef_reb_ab.symbols[info["list_position"]] == info["ticker_with_largest_cagr"]
253+
assert isinstance(info["max_asset_cagr"], (float, np.floating))
254+
255+
256+
def test_min_ratio_asset_structure(ef_reb_ab):
257+
"""Test _min_ratio_asset property returns correct structure."""
258+
info = ef_reb_ab._min_ratio_asset
259+
assert set(info.keys()) == {"min_asset_cagr", "ticker_with_smallest_ratio", "list_position"}
260+
assert info["ticker_with_smallest_ratio"] in ef_reb_ab.symbols
261+
assert 0 <= info["list_position"] < len(ef_reb_ab.symbols)
262+
assert ef_reb_ab.symbols[info["list_position"]] == info["ticker_with_smallest_ratio"]
263+
264+
265+
def test_max_ratio_asset_right_to_max_cagr(ef_reb_ab):
266+
"""Test _max_ratio_asset_right_to_max_cagr property returns correct structure or None."""
267+
info = ef_reb_ab._max_ratio_asset_right_to_max_cagr
268+
if info is not None:
269+
assert set(info.keys()) == {"max_asset_cagr", "ticker_with_largest_cagr", "list_position"}
270+
assert info["ticker_with_largest_cagr"] in ef_reb_ab.symbols
271+
assert 0 <= info["list_position"] < len(ef_reb_ab.symbols)
272+
273+
274+
def test_target_cagr_range_left_properties(ef_reb_ab):
275+
"""Test _target_cagr_range_left returns proper array."""
276+
r = ef_reb_ab._target_cagr_range_left
277+
assert isinstance(r, np.ndarray)
278+
assert len(r) == ef_reb_ab.n_points
279+
# Should be non-decreasing
280+
assert np.all(np.diff(r) >= -1e-12)
281+
282+
283+
def test_bounds_validation(synthetic_env):
284+
"""Test bounds validation raises error for incorrect number of bounds."""
285+
ef = ok.EfficientFrontierReb(
286+
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10,
287+
rebalancing_strategy=ok.Rebalance(period="year")
288+
)
289+
# Try to set bounds with wrong length
290+
with pytest.raises(ValueError, match=r"The number of symbols .* and the length of bounds .* should be equal"):
291+
ef.bounds = ((0.0, 0.5),) # Only one bound for two assets
292+
293+
294+
def test_rebalancing_strategy_validation(synthetic_env):
295+
"""Test that rebalancing_strategy setter validates input type."""
296+
ef = ok.EfficientFrontierReb(
297+
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10,
298+
rebalancing_strategy=ok.Rebalance(period="year")
299+
)
300+
with pytest.raises(ValueError, match=r"rebalancing_strategy must be of type Rebalance"):
301+
ef.rebalancing_strategy = "year" # type: ignore
302+
303+
304+
def test_ticker_names_validation(ef_reb_ab):
305+
"""Test ticker_names property validation."""
306+
assert isinstance(ef_reb_ab.ticker_names, bool)
307+
with pytest.raises(ValueError, match=r"tickers should be True or False"):
308+
ef_reb_ab.ticker_names = "yes" # type: ignore
309+
310+
311+
def test_get_monte_carlo_with_bounds(synthetic_env):
312+
"""Test get_monte_carlo respects bounds constraints."""
313+
ef = ok.EfficientFrontierReb(
314+
["A.US", "B.US"], ccy="USD", inflation=False, n_points=10,
315+
rebalancing_strategy=ok.Rebalance(period="year"),
316+
bounds=((0.3, 0.7), (0.3, 0.7))
317+
)
318+
np.random.seed(42)
319+
mc = ef.get_monte_carlo(n=5)
320+
assert len(mc) == 5
321+
assert "Risk" in mc.columns
322+
assert "CAGR" in mc.columns
323+
324+
325+
def test_ef_points_caching(ef_reb_ab):
326+
"""Test that ef_points results are cached properly."""
327+
# First call computes
328+
pts1 = ef_reb_ab.ef_points
329+
# Second call returns cached result
330+
pts2 = ef_reb_ab.ef_points
331+
assert pts1 is pts2 # Same object reference
332+
pd.testing.assert_frame_equal(pts1, pts2)
333+
334+
335+
def test_minimize_risk_with_extreme_target(ef_reb_ab):
336+
"""Test minimize_risk with target at boundaries."""
337+
# Test with target near GMV CAGR
338+
_, gmv_cagr = ef_reb_ab.gmv_annual_values
339+
result = ef_reb_ab.minimize_risk(gmv_cagr)
340+
assert "CAGR" in result
341+
assert "Risk" in result
342+
assert "Weights" in result
343+
assert result["CAGR"] == pytest.approx(gmv_cagr, rel=1e-2, abs=1e-3)
344+

0 commit comments

Comments
 (0)