fix: carry action/risk_level onto backtest candidates for the gate ablation
_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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user