Skip to content

Commit 597e18c

Browse files
enarjordclaude
andcommitted
Remove double-correction of objectives in pareto_store script
The ratio-based objective correction in pareto_store.py main() would double-inflate objectives for entries produced by the fixed optimizer (which already writes correct aggregated values into flat_stats). Removed the correction block — objectives are now passed through unchanged. The optimize.py fix ensures new entries have correct objectives at creation time. Limit filtering (-l) still correctly uses aggregated values from suite_metrics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20eb3ea commit 597e18c

File tree

2 files changed

+32
-119
lines changed

2 files changed

+32
-119
lines changed

src/pareto_store.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -609,25 +609,6 @@ def parse_limit_expr(expr: str) -> LimitSpec:
609609
)
610610
stats_flat.update(stats_flat_suite)
611611
aggregated_values.update(aggregated_values_suite)
612-
# Correct objectives for scoring metrics whose aggregate method
613-
# is not "mean". The stored w_i was computed as metric_mean * weight;
614-
# the correct value is metric_agg * weight. We apply a ratio
615-
# correction (agg / mean) so we don't need the scoring weights.
616-
constraint_violation = metrics_block.get("constraint_violation", 0.0)
617-
if aggregate_cfg and not constraint_violation:
618-
scoring_keys = entry.get("optimize", {}).get("scoring", [])
619-
for idx, sk in enumerate(scoring_keys):
620-
mode = _resolve_aggregate_mode(sk, aggregate_cfg)
621-
if mode == "mean":
622-
continue
623-
w_key = f"w_{idx}"
624-
stored = objectives.get(w_key)
625-
if stored is None:
626-
continue
627-
agg_val = aggregated_values.get(sk)
628-
mean_val = stats_flat.get(f"{sk}_mean")
629-
if agg_val is not None and mean_val and mean_val != 0.0:
630-
objectives[w_key] = stored * (agg_val / mean_val)
631612
if not w_keys:
632613
all_w_keys = sorted(k for k in objectives if k.startswith("w_"))
633614

tests/test_aggregate_methods.py

Lines changed: 32 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)