fix: carry action/risk_level onto backtest candidates for the gate ablation
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 57s
Deploy / deploy (push) Successful in 2m24s

_window_setups computed them but _replay_ticker dropped them, so the
ablation's NEUTRAL/tightener checks saw None for every candidate and the
'without confidence floor' / 'without R:R floor' rows collapsed to 0
setups (impossible — removing a floor can only add setups). Regression
test now goes through the real _replay_ticker path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 08:07:27 +02:00
parent 29b1a9a28c
commit 8750aac6d9
2 changed files with 31 additions and 0 deletions
+4
View File
@@ -394,6 +394,10 @@ def _replay_ticker(symbol: str, records: list, config: dict, activation: dict) -
"best_prob": s["best_prob"],
"momentum": s["momentum"],
"meets_core": s["meets_core"],
# Gate fields the ablation recomputes floors from — without them
# every candidate looks NEUTRAL and the ablation rows collapse.
"action": s["action"],
"risk_level": s["risk_level"],
"outcome": outcome,
"target_hit": target_hit,
"realized_r": realized_r,
+27
View File
@@ -313,6 +313,33 @@ def test_window_setups_too_short_returns_empty():
assert bt._window_setups([], {}, {}) == []
def test_replay_ticker_candidates_carry_gate_fields():
"""The ablation recomputes floors from candidate fields — a candidate missing
action/risk_level silently zeroes the ablation rows (July 2026 regression)."""
from app.services.admin_service import ACTIVATION_DEFAULTS
from app.services.recommendation_service import DEFAULT_RECOMMENDATION_CONFIG
base = date(2025, 1, 1)
bars = []
for i in range(160):
close = 100.0 + 8.0 * math.sin(i / 6.0)
bars.append(SimpleNamespace(
date=base + timedelta(days=i),
open=close,
high=close + 1.5,
low=close - 1.5,
close=close,
volume=1_000_000 + (i % 5) * 1000,
))
cands = bt._replay_ticker(
"OSC", bars, dict(DEFAULT_RECOMMENDATION_CONFIG), dict(ACTIVATION_DEFAULTS)
)
assert cands, "expected the oscillating series to produce candidates"
for c in cands:
assert c.get("action") is not None
assert "risk_level" in c
async def _seed_oscillating_ticker(session, symbol: str, n: int = 160) -> None:
t = Ticker(symbol=symbol)
session.add(t)