fix(backtest): one window, and stop calling a rejected exit "recommended"
Two ways the recommendation still disagreed with the page it sits on. It preferred the "all" monitor row while the UI defaulted its selector to "3y", so a default page load showed one set of returns in the tiles and a different set in the recommendation. The row it used is now published as basis_lookback and the page defaults to it, so the two cannot open on different windows. The test fixture gains a second monitor row with different numbers — with only an "all" row present, a lookback mix-up could not fail. Robustness picked its basis between "the recommended Nd hold" and "the S/R target exit", naming an exit the production book replaced as recommended. There is no ATR-trail ex-top-5% figure in the report, so it now always reports the gate-level grading and says that is what it is, rather than dressing a legacy number as a verdict on the production book. time_exit_sweep is no longer read here at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3944,14 +3944,11 @@ def _build_recommendation(report: dict) -> dict:
|
|||||||
})
|
})
|
||||||
|
|
||||||
q = report.get("overall_qualified") or {}
|
q = report.get("overall_qualified") or {}
|
||||||
target_net = q.get("net_avg_r")
|
|
||||||
|
|
||||||
# The best fixed hold, kept ONLY to pick the basis for the robustness check
|
# Nothing here reads time_exit_sweep any more. The hold-vs-target comparison
|
||||||
# below. The hold-vs-target comparison itself is deliberately not reported:
|
# is not reported (both are exits the production book replaced, so choosing
|
||||||
# both are legacy exits the production book replaced with the ATR trail, so
|
# between them cannot lead to an action), and the robustness check below no
|
||||||
# a recommendation between them could not lead to an action.
|
# longer picks its basis from them either.
|
||||||
time_rows = [r for r in report.get("time_exit_sweep") or [] if r.get("net_avg_r") is not None]
|
|
||||||
best_hold = max(time_rows, key=lambda r: r["net_avg_r"], default=None)
|
|
||||||
|
|
||||||
# Gate floors, judged under the hold exit (the ablation's Hold column).
|
# Gate floors, judged under the hold exit (the ablation's Hold column).
|
||||||
ablation = {r["variant"]: r for r in report.get("gate_ablation") or []}
|
ablation = {r["variant"]: r for r in report.get("gate_ablation") or []}
|
||||||
@@ -4016,19 +4013,15 @@ def _build_recommendation(report: dict) -> dict:
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Robustness: does the edge survive without the biggest winners? Judged on
|
# Robustness: does the edge survive without the biggest winners?
|
||||||
# the RECOMMENDED exit — outlier dependence under an exit we'd abandon
|
#
|
||||||
# would be the wrong warning.
|
# There is no ATR-trail equivalent of this number in the report — the only
|
||||||
hold_recommended = (
|
# ex-top-5% figure is the gate-level target/stop grading. So it is reported
|
||||||
best_hold is not None and target_net is not None
|
# on that basis and SAYS SO, rather than being dressed up as a verdict on the
|
||||||
and best_hold["net_avg_r"] > target_net + _EXIT_SWITCH_THRESHOLD
|
# production book. It used to pick between "the recommended Nd hold" and "the
|
||||||
)
|
# S/R target exit", naming a rejected exit as recommended.
|
||||||
if hold_recommended and best_hold.get("net_avg_r_ex_top5") is not None:
|
|
||||||
trimmed = best_hold["net_avg_r_ex_top5"]
|
|
||||||
basis = f"under the recommended {best_hold['hold_days']}d hold"
|
|
||||||
else:
|
|
||||||
trimmed = q.get("net_avg_r_ex_top5")
|
trimmed = q.get("net_avg_r_ex_top5")
|
||||||
basis = "under the S/R target exit"
|
basis = "gate-level grading, not the production ATR-trail book"
|
||||||
if trimmed is not None:
|
if trimmed is not None:
|
||||||
if trimmed > 0:
|
if trimmed > 0:
|
||||||
items.append({
|
items.append({
|
||||||
@@ -4057,6 +4050,12 @@ def _build_recommendation(report: dict) -> dict:
|
|||||||
return {
|
return {
|
||||||
"headline": headline,
|
"headline": headline,
|
||||||
"items": items,
|
"items": items,
|
||||||
|
# Which monitor row every production/benchmark figure above was read
|
||||||
|
# from. The page defaults its lookback selector to this, so the tiles and
|
||||||
|
# the recommendation cannot open on different windows — they used to,
|
||||||
|
# because this preferred "all" while the UI defaulted to "3y".
|
||||||
|
"basis_lookback": (production_row or {}).get("lookback"),
|
||||||
|
"basis_lookback_label": (production_row or {}).get("lookback_label"),
|
||||||
"note": "Derived from this report's numbers on every run — the advice flips if the data does.",
|
"note": "Derived from this report's numbers on every run — the advice flips if the data does.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1287,6 +1287,12 @@ def test_build_recommendation_reads_the_report():
|
|||||||
"strategy": "prod", "lookback": "all", "lookback_label": "All history",
|
"strategy": "prod", "lookback": "all", "lookback_label": "All history",
|
||||||
"cagr_pct": 40.0, "sharpe": 1.72, "max_drawdown_pct": 17.7,
|
"cagr_pct": 40.0, "sharpe": 1.72, "max_drawdown_pct": 17.7,
|
||||||
"total_return_pct": 297.8, "spy_return_pct": 101.9,
|
"total_return_pct": 297.8, "spy_return_pct": 101.9,
|
||||||
|
}, {
|
||||||
|
# A second window with DIFFERENT numbers. Without it the "all"
|
||||||
|
# preference is untested and a lookback mix-up cannot fail.
|
||||||
|
"strategy": "prod", "lookback": "3y", "lookback_label": "3y",
|
||||||
|
"cagr_pct": 47.9, "sharpe": 1.96, "max_drawdown_pct": 17.3,
|
||||||
|
"total_return_pct": 220.9, "spy_return_pct": 71.5,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1308,12 +1314,21 @@ def test_build_recommendation_reads_the_report():
|
|||||||
assert "keep the NEUTRAL exclusion" in gate_texts
|
assert "keep the NEUTRAL exclusion" in gate_texts
|
||||||
assert "80" in by_topic["cutoff"][0]
|
assert "80" in by_topic["cutoff"][0]
|
||||||
assert "beats" in by_topic["benchmark"][0]
|
assert "beats" in by_topic["benchmark"][0]
|
||||||
# robustness is judged under the RECOMMENDED exit (the 30d hold), not the
|
|
||||||
# target model the recommendation advises abandoning
|
# Every production figure comes from ONE window, and the report says which,
|
||||||
assert any(
|
# so the page can default its selector to the same one.
|
||||||
"not a handful of outliers" in t and "under the recommended 30d hold" in t
|
assert rec["basis_lookback"] == "all"
|
||||||
for t in by_topic["robustness"]
|
assert rec["basis_lookback_label"] == "All history"
|
||||||
)
|
assert "+40.0%" in by_topic["production"][0]
|
||||||
|
assert "47.9" not in by_topic["production"][0] # the 3y row must not leak in
|
||||||
|
assert "220.9" not in by_topic["benchmark"][0]
|
||||||
|
|
||||||
|
# Robustness names its real basis. It used to claim "under the recommended
|
||||||
|
# 30d hold" — nothing recommends that exit; production is the ATR trail.
|
||||||
|
robustness = by_topic["robustness"][0]
|
||||||
|
assert "not a handful of outliers" in robustness
|
||||||
|
assert "gate-level grading" in robustness
|
||||||
|
assert "recommended" not in robustness
|
||||||
|
|
||||||
|
|
||||||
def test_build_recommendation_flags_outlier_dependence():
|
def test_build_recommendation_flags_outlier_dependence():
|
||||||
|
|||||||
Reference in New Issue
Block a user