fix(backtest): address the second review of the breakeven and book-config work
Deploy / lint (push) Successful in 14s
Deploy / test (push) Successful in 2m0s
Deploy / deploy (push) Successful in 48s

- The breakeven study grades at the shadow book's capacity AND risk per
  trade, and labels each with where it came from. A missing setting row
  now reads "shadow_book default" instead of crediting the setting; the
  committed report said shadow_book_capacity though the snapshot has no
  such row.
- _production_book_config reads strategies through the same
  _portfolio_monitor_strategies() hook as the monitor.
- breakeven_gate_reset is rejected when there is no post-stop episode or
  cooldown for it to start, and the study skips its diagnostic arm for a
  book without one, so a production config change cannot abort the run.
- identical_to_control is renamed ties_control_on_graded_metrics and needs
  real values: be_2r ties on every graded metric but still changed 2 trades.
- _BookConfig drops an unused field and hashes by identity; the min-R:R
  sweep swaps its gate in with dataclasses.replace instead of passing it.
- New tests cover the parity guard's success path, the no-config fallback
  and the gate-reset check.

The breakeven report is regenerated: every row, diagnostic row and the
parity check are identical; only the labels, the risk fields and the
renamed flag changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dennisthiessenandClaude Opus 5.5 committed 2026-10-10 21:41:44 +02:00
1 parent 7a9b3ad38b
commit 231372fb3a
5 files changed
+168 -88

No files matched your search

+63 -23
View File
@@ -1005,6 +1005,8 @@ class TestSimulatePortfolio:
("atr_trail3", {"breakeven_r": float("nan")}),
("hold", {"breakeven_r": 1.0}),
("atr_trail3", {"breakeven_gate_reset": True}),
# Gated, but nothing to gate: no post-stop episode and no cooldown.
("atr_trail3", {"breakeven_r": 1.0, "breakeven_gate_reset": True}),
],
)
def test_breakeven_rejects_settings_it_cannot_honor(self, policy, kwargs):
@@ -1013,47 +1015,85 @@ class TestSimulatePortfolio:
with pytest.raises(ValueError):
bt._simulate_portfolio([cand], prices, None, policy, 30, **kwargs)
def test_breakeven_study_uses_live_capacity_and_flags_ties(self, monkeypatch):
# One trade per window: AAA before the split, BBB after it. be_2r never
# triggers (+2R needs a 110 close), so it ties control in both windows.
LIVE_EXIT = {"mode": "atr_trailing", "hold_days": 30, "atr_multiplier": 3.0}
def _split_book(self, monkeypatch, per_window: int = 1) -> tuple[list[dict], dict]:
"""``per_window`` trades on each side of the split, one symbol each, 50
days apart. be_2r never triggers (+2R needs a 110 close), so it ties
control in both windows. Three per window spans the >0.25y CAGR needs."""
closes = [100.0, 103.0, 106.0, 104.0, 101.0, 99.0, 97.0, 96.0]
prices = {
"AAA": _sim_prices(self.ORD, closes),
"BBB": _sim_prices(self.ORD + 10, closes),
}
cands = [
_sim_cand("AAA", self.ORD, entry=100.0, stop=95.0, target=130.0),
_sim_cand("BBB", self.ORD + 10, entry=100.0, stop=95.0, target=130.0),
]
monkeypatch.setattr(bt, "BREAKEVEN_STUDY_SPLIT", date.fromordinal(self.ORD + 10))
capacities: list[int] = []
split = self.ORD + 50 * per_window
prices, cands = {}, []
for side, first in (("A", self.ORD), ("B", split)):
for k in range(per_window):
sym, day = f"{side}{k}", first + 50 * k
prices[sym] = _sim_prices(day, closes)
cands.append(_sim_cand(sym, day, entry=100.0, stop=95.0, target=130.0))
monkeypatch.setattr(bt, "BREAKEVEN_STUDY_SPLIT", date.fromordinal(split))
return cands, prices
def test_breakeven_study_grades_the_live_book_and_flags_ties(self, monkeypatch):
cands, prices = self._split_book(monkeypatch, per_window=3)
sizes: list[tuple[int, float]] = []
real_simulate = bt._simulate_portfolio
def spy(*args, **kwargs):
capacities.append(kwargs["max_positions"])
sizes.append((kwargs["max_positions"], kwargs["risk_per_trade"]))
return real_simulate(*args, **kwargs)
monkeypatch.setattr(bt, "_simulate_portfolio", spy)
report = bt._breakeven_exit_study(
cands, prices, None, 30,
live_exit_policy={"mode": "atr_trailing", "hold_days": 30, "atr_multiplier": 3.0},
live_capacity=3,
live_exit_policy=self.LIVE_EXIT,
live_book={
"capacity": 3, "risk_per_trade": 0.02,
"capacity_source": "shadow_book_capacity",
"risk_source": "shadow_book default",
},
)
assert report["capacity"] == 3
assert (report["capacity"], report["risk_per_trade"]) == (3, 0.02)
assert report["capacity_source"] == "shadow_book_capacity"
assert report["verdicts"]["be_2r"]["identical_to_control"] is True
assert report["verdicts"]["be_1r"]["identical_to_control"] is False
assert report["risk_source"] == "shadow_book default"
assert report["verdicts"]["be_2r"]["ties_control_on_graded_metrics"] is True
assert report["verdicts"]["be_1r"]["ties_control_on_graded_metrics"] is False
# No monitor row to match, so nothing can pass.
assert report["parity"]["ok"] is False
assert not any(v["passes"] for v in report["verdicts"].values())
assert {r["arm"] for r in report["diagnostic_rows"]} == {"be_1r_gated"}
assert all(r["breakeven_gate_reset"] for r in report["diagnostic_rows"])
# 3 graded arms + 1 diagnostic arm x 3 windows at capacity 3, then the
# parity row at the monitor's capacity.
assert capacities[:12] == [3] * 12
assert capacities[12:] == [report["parity"]["capacity"]]
# 3 graded arms + 1 diagnostic arm x 3 windows on the live book, then the
# parity row on the monitor's.
assert sizes[:12] == [(3, 0.02)] * 12
assert sizes[12:] == [
(report["parity"]["capacity"], report["parity"]["risk_per_trade"])
]
def test_breakeven_study_without_live_config_says_so(self, monkeypatch):
cands, prices = self._split_book(monkeypatch)
report = bt._breakeven_exit_study(cands, prices, None, 30, live_exit_policy=self.LIVE_EXIT)
assert (report["capacity"], report["capacity_source"]) == (
bt.SIM_MAX_POSITIONS, "SIM_MAX_POSITIONS",
)
assert report["risk_source"] == "entry variant"
def test_breakeven_parity_finds_the_monitor_production_row(self, monkeypatch):
# The guard's success path: the study's control at the monitor's capacity
# must match the monitor's production "all" row, and then a tie can pass.
cands, prices = self._split_book(monkeypatch, per_window=3)
monitor = bt._portfolio_monitor(cands, prices, None, 30, live_exit_policy=self.LIVE_EXIT)
report = bt._breakeven_exit_study(
cands, prices, None, 30, live_exit_policy=self.LIVE_EXIT, monitor_report=monitor,
)
parity = report["parity"]
assert parity["monitor_production_all"] is not None
assert parity["study_control"] == parity["monitor_production_all"]
assert parity["ok"] is True
assert report["verdicts"]["be_2r"]["passes"] is True
assert report["verdicts"]["be_2r"]["ties_control_on_graded_metrics"] is True
def test_breakeven_checks_fail_on_missing_metrics(self):
control = {"sharpe": 1.5, "cagr_pct": 40.0, "max_drawdown_pct": 20.0}