backtest: explain the regime-gated empty window + toggle to view raw-criteria trades

Owner saw a blank /backtest — it was CORRECT (the regime gate sat out every
recent session) but unexplained. Now: ?regime=0|1 override on /api/backtest,
an 'apply regime gate (as deployed)' checkbox, and an empty-state message
saying WHY there are no trades and what to click. Gated view = what the bot
would do; ungated = the raw simple criteria.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-17 22:40:03 -05:00
1 parent 3ce8594605
commit 053c927cf7
3 files changed
+43 -3

No files matched your search

+8 -2
View File
@@ -511,7 +511,7 @@ class DashboardState:
return rows
def backtest(self, days: int | None = 10, since: str | None = None,
until: str | None = None) -> dict:
until: str | None = None, regime_override: bool | None = None) -> dict:
"""Theoretical trades under CURRENT live settings (plan 015) —
assembly only, the simulation lives in research.experiments.replay.
@@ -530,6 +530,10 @@ class DashboardState:
pop = PopulationConfig.from_env()
regime_on = os.environ.get("TRADING_REGIME_GATE_ENABLED", "0").strip().lower() in {
"1", "true", "yes", "on"}
if regime_override is not None:
# /backtest UX: let the page show RAW-criteria trades when the gate
# sits out the whole window (owner: 'no data in backtest').
regime_on = regime_override
fidelity = {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1}
settings = {
"population": {"price_min": pop.price_min, "price_max": pop.price_max,
@@ -805,8 +809,10 @@ class DashboardHandler(BaseHTTPRequestHandler):
since = (q.get("since") or [""])[0] or None
until = (q.get("until") or [""])[0] or None
days = (q.get("days") or [""])[0]
rg = (q.get("regime") or [""])[0]
self._send_json(self.state.backtest(
days=int(days) if days else 10, since=since, until=until))
days=int(days) if days else 10, since=since, until=until,
regime_override=(rg == "1") if rg in ("0", "1") else None))
elif route == "/api/attribution":
# per-component stage ratings (research/attribution.py); computed on
# demand — a handful of indexed event queries, fine per click
+14 -1
View File
@@ -141,6 +141,9 @@
<input type="date" id="since-input">
<input type="date" id="until-input">
<button class="run" id="run-custom">Run range</button>
<label style="font: 600 11px var(--mono); color: var(--muted); display:flex; align-items:center; gap:5px; margin-left:8px">
<input type="checkbox" id="regime-toggle" checked> apply regime gate (as deployed)
</label>
</div>
</header>
@@ -209,6 +212,9 @@ function setLoading(msg) {
async function runBacktest(params, activeBtn) {
document.querySelectorAll(".picker button[data-days]").forEach(b => b.classList.toggle("active", b === activeBtn));
state.lastParams = params; state.lastBtn = activeBtn;
// the gate toggle rides every request: unchecked = raw-criteria trades
params = { ...params, regime: $("regime-toggle").checked ? "1" : "0" };
const n = params.days || "the requested";
setLoading("replaying " + n + " session" + (n === 1 ? "" : "s") + "… (can take up to 30s)");
try {
@@ -229,6 +235,7 @@ async function runBacktest(params, activeBtn) {
document.querySelectorAll(".picker button[data-days]").forEach(btn => {
btn.onclick = () => runBacktest({ days: btn.dataset.days }, btn);
});
$("regime-toggle").onchange = () => runBacktest(state.lastParams || { days: 10 }, state.lastBtn || null);
$("run-custom").onclick = () => {
const since = $("since-input").value, until = $("until-input").value;
if (!since || !until) return;
@@ -285,7 +292,13 @@ function renderSummary(sum, skipped, screened) {
// (cum_pnl -> cum_r, day_pnl -> sum_r); click a day filters the trade table.
function renderDaily(days) {
const host = $("daily-curve");
if (!days || !days.length) { host.innerHTML = '<span class="empty">No trades in this window.</span>'; return; }
if (!days || !days.length) {
const reg = ((state.data || {}).screened || {}).regime || 0;
host.innerHTML = '<span class="empty">' + (reg > 0
? 'No trades: the regime gate sat out every replayed session in this window — the bot would not have traded these days at all. Uncheck \u201capply regime gate\u201d above to see what the raw criteria would have done.'
: 'No trades in this window (no qualifying setups).') + '</span>';
return;
}
const n = days.length;
const W = Math.max(host.clientWidth || 680, n * 48);
const H = 184, padT = 16, padB = 30, padX = 12, plotH = H - padT - padB;
+21
View File
@@ -244,3 +244,24 @@ def test_replay_window_replays_live_track_only():
from research.experiments.replay import replay_window
out = replay_window(store, str(SESS1), str(SESS2))
assert len(out["trades"]) == 4 # the 4 v1 fires only; liq1 excluded
def test_backtest_regime_override_bypasses_gate(monkeypatch):
"""/backtest UX fix (owner: 'no data in backtest'): when the regime gate
zeroes a whole window the page must be able to show RAW-criteria trades
via ?regime=0, and the response must echo which mode ran."""
import api.main as m
store = _seed_store()
monkeypatch.setenv("TRADING_REGIME_GATE_ENABLED", "1")
# unfavorable scores for BOTH sessions -> gated run has zero trades
from research import regime as rg
rg.apply_schema(store.con)
for sess in (SESS1, SESS2):
store.con.execute(
"INSERT INTO regime_scores (model_version, session_date, p_profit, favorable, computed_at) "
"VALUES ('meta_v1', ?, 0.1, false, now()) ON CONFLICT DO NOTHING", [sess])
state = m.DashboardState("momentum")
gated = state.backtest(days=10)
raw = state.backtest(days=10, regime_override=False)
assert gated["summary"]["n"] == 0 and gated["settings"]["regime_gate"] is True
assert raw["summary"]["n"] == 4 and raw["settings"]["regime_gate"] is False