@@ -324,13 +324,14 @@ def test_override_changes_objective(self):
324324
325325
326326# ---------------------------------------------------------------------------
327- # pareto_store.main() objective correction via ratio
327+ # pareto_store.main() does NOT mutate stored objectives
328328# ---------------------------------------------------------------------------
329329
330330
331- class TestObjectiveCorrectionRatio :
332- """Test the ratio-based correction applied in pareto_store main() for
333- scoring metrics with non-default aggregate methods."""
331+ class TestObjectivesNotDoubleCorrected :
332+ """The optimizer (optimize.py) now writes correct objectives at creation
333+ time. pareto_store.py must NOT apply a second correction on top, which
334+ would inflate values for entries produced by the fixed optimizer."""
334335
335336 AGGREGATE_CFG = {
336337 "default" : "mean" ,
@@ -339,16 +340,11 @@ class TestObjectiveCorrectionRatio:
339340 "position_held_hours_max" : "max" ,
340341 }
341342
342- def _make_pareto_entry (self , scoring_keys , mean_values , max_values , weights = None ):
343- """Build a pareto entry with suite_metrics in the 'metrics' format."""
344- if weights is None :
345- weights = {k : 1.0 for k in scoring_keys }
346- objectives = {}
343+ def _make_pareto_entry (self , scoring_keys , objective_values , mean_values , max_values ):
344+ """Build a pareto entry as the fixed optimizer would produce it."""
345+ objectives = {f"w_{ i } " : v for i , v in enumerate (objective_values )}
347346 suite_metric_payloads = {}
348- for i , sk in enumerate (scoring_keys ):
349- w = weights .get (sk , 1.0 )
350- # Stored objectives were computed with _mean (the bug)
351- objectives [f"w_{ i } " ] = mean_values [sk ] * w
347+ for sk in scoring_keys :
352348 suite_metric_payloads [sk ] = {
353349 "stats" : {
354350 "mean" : mean_values [sk ],
@@ -372,102 +368,38 @@ def _make_pareto_entry(self, scoring_keys, mean_values, max_values, weights=None
372368 "suite_metrics" : {"metrics" : suite_metric_payloads },
373369 }
374370
375- def test_correction_applied_for_max_aggregate (self ):
371+ def test_new_entry_objectives_unchanged (self ):
372+ """Objectives produced by the fixed optimizer must pass through
373+ pareto_store untouched — no ratio correction applied."""
376374 scoring = ["adg_pnl" , "high_exposure_hours_max_long" ]
377375 means = {"adg_pnl" : 0.001 , "high_exposure_hours_max_long" : 150.0 }
378376 maxes = {"adg_pnl" : 0.001 , "high_exposure_hours_max_long" : 300.0 }
379- entry = self ._make_pareto_entry (scoring , means , maxes )
377+ # Fixed optimizer: adg_pnl uses mean (0.001), exposure uses max (300)
378+ entry = self ._make_pareto_entry (scoring , [0.001 , 300.0 ], means , maxes )
380379
381- # Extract and correct (same logic as main())
380+ # Reproduce pareto_store. main() logic
382381 metrics_block = entry ["metrics" ]
383- objectives = dict (metrics_block ["objectives" ])
384- aggregate_cfg = entry ["backtest" ]["aggregate" ]
385- stats_flat_suite , aggregated_values = _suite_metrics_to_stats (
386- entry , aggregate_cfg = aggregate_cfg ,
387- )
388-
389- constraint_violation = metrics_block .get ("constraint_violation" , 0.0 )
390- assert not constraint_violation
391-
392- for idx , sk in enumerate (scoring ):
393- mode = _resolve_aggregate_mode (sk , aggregate_cfg )
394- if mode == "mean" :
395- continue
396- w_key = f"w_{ idx } "
397- stored = objectives .get (w_key )
398- agg_val = aggregated_values .get (sk )
399- mean_val = stats_flat_suite .get (f"{ sk } _mean" )
400- if agg_val is not None and mean_val and mean_val != 0.0 :
401- objectives [w_key ] = stored * (agg_val / mean_val )
402-
403- # adg_pnl (mean aggregate) should be unchanged
404- assert objectives ["w_0" ] == pytest .approx (0.001 )
405- # high_exposure_hours_max_long: stored=150*1.0=150, corrected=150*(300/150)=300
406- assert objectives ["w_1" ] == pytest .approx (300.0 )
407-
408- def test_correction_preserves_weight_sign (self ):
409- """Ratio correction preserves the scoring weight direction."""
410- scoring = ["peak_recovery_hours_pnl" ]
411- means = {"peak_recovery_hours_pnl" : 200.0 }
412- maxes = {"peak_recovery_hours_pnl" : 500.0 }
413- weights = {"peak_recovery_hours_pnl" : 1.0 }
414- entry = self ._make_pareto_entry (scoring , means , maxes , weights )
415-
416- metrics_block = entry ["metrics" ]
417- objectives = dict (metrics_block ["objectives" ])
418- aggregate_cfg = entry ["backtest" ]["aggregate" ]
419- stats_flat_suite , aggregated_values = _suite_metrics_to_stats (
420- entry , aggregate_cfg = aggregate_cfg ,
421- )
422-
423- stored = objectives ["w_0" ]
424- assert stored == pytest .approx (200.0 ) # mean * weight(1.0)
425-
426- agg_val = aggregated_values ["peak_recovery_hours_pnl" ]
427- mean_val = stats_flat_suite ["peak_recovery_hours_pnl_mean" ]
428- objectives ["w_0" ] = stored * (agg_val / mean_val )
429-
430- assert objectives ["w_0" ] == pytest .approx (500.0 ) # max * weight(1.0)
431-
432- def test_no_correction_with_constraint_violation (self ):
433- scoring = ["high_exposure_hours_max_long" ]
434- means = {"high_exposure_hours_max_long" : 150.0 }
435- maxes = {"high_exposure_hours_max_long" : 300.0 }
436- entry = self ._make_pareto_entry (scoring , means , maxes )
437- entry ["metrics" ]["constraint_violation" ] = 5000.0
438-
439- metrics_block = entry ["metrics" ]
440- objectives = dict (metrics_block ["objectives" ])
441- aggregate_cfg = entry ["backtest" ]["aggregate" ]
442-
443- constraint_violation = metrics_block .get ("constraint_violation" , 0.0 )
444- # Should skip correction
445- assert constraint_violation
446- # Objective remains at the stored (mean-based) value
447- assert objectives ["w_0" ] == pytest .approx (150.0 )
448-
449- def test_no_correction_for_mean_aggregate_metric (self ):
450- scoring = ["adg_pnl" ]
451- means = {"adg_pnl" : 0.001 }
452- maxes = {"adg_pnl" : 0.0015 }
453- entry = self ._make_pareto_entry (scoring , means , maxes )
454-
455- metrics_block = entry ["metrics" ]
456- objectives = dict (metrics_block ["objectives" ])
457- aggregate_cfg = entry ["backtest" ]["aggregate" ]
458-
459- mode = _resolve_aggregate_mode ("adg_pnl" , aggregate_cfg )
460- assert mode == "mean"
461- # No correction needed
382+ objectives = dict (metrics_block .get ("objectives" , metrics_block ))
383+ aggregate_cfg = entry .get ("backtest" , {}).get ("aggregate" )
384+ if "suite_metrics" in entry :
385+ stats_flat_suite , aggregated_values = _suite_metrics_to_stats (
386+ entry , aggregate_cfg = aggregate_cfg ,
387+ )
388+ # main() does NOT modify objectives — verify they are unchanged
462389 assert objectives ["w_0" ] == pytest .approx (0.001 )
390+ assert objectives ["w_1" ] == pytest .approx (300.0 ) # NOT 600
463391
464- def test_no_correction_without_aggregate_cfg (self ):
392+ def test_limit_filtering_uses_aggregated_values (self ):
393+ """Even without objective correction, -l limit filtering still uses
394+ the correctly aggregated values from suite_metrics."""
465395 scoring = ["high_exposure_hours_max_long" ]
466396 means = {"high_exposure_hours_max_long" : 150.0 }
467397 maxes = {"high_exposure_hours_max_long" : 300.0 }
468- entry = self ._make_pareto_entry (scoring , means , maxes )
469- del entry ["backtest" ]["aggregate" ]
398+ entry = self ._make_pareto_entry (scoring , [300.0 ], means , maxes )
470399
471400 aggregate_cfg = entry .get ("backtest" , {}).get ("aggregate" )
472- assert aggregate_cfg is None
473- # Without cfg, no correction is attempted
401+ _ , aggregated_values = _suite_metrics_to_stats (
402+ entry , aggregate_cfg = aggregate_cfg ,
403+ )
404+ # Limit filtering sees the correct max value (300), not the mean (150)
405+ assert aggregated_values ["high_exposure_hours_max_long" ] == 300.0
0 commit comments