88
99@pytest .fixture ()
1010def 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 ()
2122def 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
126116def 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