feat(b3b): surface backtest verdict (PASS/FAIL + Sharpe/DSR) in cockpit fleet view

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-18 18:44:17 +02:00
parent b42177e853
commit 82fad9c21e
7 changed files with 94 additions and 1 deletions

View File

@@ -16,6 +16,7 @@
th:first-child, td:first-child { text-align:left; } a { color:#58a6ff; text-decoration:none; }
.badge { padding:2px 8px; border-radius:10px; font-size:12px; }
.WAIT { background:#21262d; color:#8b949e; } .GO { background:#193b1e; color:#3fb950; } .NO_GO { background:#3b1919; color:#f85149; }
.PASS { background:#193b1e; color:#3fb950; } .FAIL { background:#3b1919; color:#f85149; }
.pos { color:#3fb950; } .neg { color:#f85149; } .muted { color:#8b949e; }
</style>
</head>

View File

@@ -3,7 +3,7 @@
{% block body %}
<table hx-get="/" hx-trigger="every 60s" hx-select="table" hx-swap="outerHTML">
<thead><tr><th>strategy</th><th>sleeve</th><th>days</th><th>ret%</th><th>Sharpe</th>
<th>maxDD</th><th>nav</th><th>gate</th></tr></thead>
<th>maxDD</th><th>nav</th><th>gate</th><th>backtest</th></tr></thead>
<tbody>
{% for f in fleet %}
<tr>
@@ -15,6 +15,14 @@
<td class="neg">{{ '%.1f'|format(100*f.maxdd) }}%</td>
<td>{{ spark(f.strategy_id)|safe }}</td>
<td><span class="badge {{ f.gate_status }}">{{ f.gate_status }}</span></td>
<td>
{% if f.bt_status %}
<span class="badge {{ f.bt_status }}">{{ f.bt_status }}</span>
<span class="muted" style="font-size:11px">Sh {{ '%.2f'|format(f.bt_sharpe) }} / DSR {{ '%.2f'|format(f.bt_dsr) }}</span>
{% else %}
<span class="muted"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -9,6 +9,15 @@
· Sharpe {{ '%.2f'|format(d.sharpe) }} · maxDD {{ '%.1f'|format(100*d.maxdd) }}%</p>
<div>{{ curve|safe }}</div>
{% if d.bt_status %}
<h3>backtest verdict <span class="badge {{ d.bt_status }}">{{ d.bt_status }}</span>
<span class="muted">as of {{ d.bt_as_of or '—' }}</span></h3>
<p><span class="{{ 'pos' if d.bt_cagr >= 0 else 'neg' }}">CAGR {{ '%+.1f'|format(100*d.bt_cagr) }}%</span>
· vol {{ '%.1f'|format(100*d.bt_ann_vol) }}% · Sharpe {{ '%.2f'|format(d.bt_sharpe) }}
· maxDD {{ '%.1f'|format(100*d.bt_maxdd) }}% · DSR {{ '%.2f'|format(d.bt_dsr) }}
· OOS Sharpe {{ '%.2f'|format(d.bt_oos_sharpe) }}</p>
{% endif %}
<h3>per-period returns &amp; rolling mean <span class="muted">(raw — no normalization)</span></h3>
{% for period in ["daily", "weekly", "monthly"] %}
<details {% if period == "weekly" %}open{% endif %}>

View File

@@ -14,9 +14,11 @@ class DashboardService:
def fleet(self) -> list[FleetRow]:
summaries = {r.strategy_id: r for r in self._repo.all_summaries()}
backtests = {r.strategy_id: r for r in self._repo.all_backtest_summaries()}
out: list[FleetRow] = []
for reg in self._repo.registry():
s = summaries.get(reg.strategy_id)
b = backtests.get(reg.strategy_id) # most strategies have no backtest verdict -> None
out.append(FleetRow(
strategy_id=reg.strategy_id, display_name=reg.display_name, sleeve=reg.sleeve,
days=s.days if s else 0,
@@ -25,6 +27,12 @@ class DashboardService:
gate_status=s.gate_status if s else "WAIT",
gate_reason=s.gate_reason if s else "no data yet",
as_of=s.as_of if s else "",
bt_status=("PASS" if b.passed else "FAIL") if b else "",
bt_sharpe=b.sharpe if b else None,
bt_dsr=b.dsr if b else None,
bt_cagr=b.cagr if b else None,
bt_maxdd=b.max_drawdown if b else None,
bt_as_of=b.as_of if b else "",
))
return out
@@ -33,6 +41,7 @@ class DashboardService:
if reg is None:
return None
s = {r.strategy_id: r for r in self._repo.all_summaries()}.get(strategy_id)
b = {r.strategy_id: r for r in self._repo.all_backtest_summaries()}.get(strategy_id)
history = self._repo.nav_history(strategy_id)
dates = [p.date for p in history]
rets = [p.ret for p in history]
@@ -44,4 +53,9 @@ class DashboardService:
gate_status=s.gate_status if s else "WAIT",
gate_reason=s.gate_reason if s else "no data yet", as_of=s.as_of if s else "",
history=history, periods=periods,
bt_status=("PASS" if b.passed else "FAIL") if b else "",
bt_cagr=b.cagr if b else None, bt_ann_vol=b.ann_vol if b else None,
bt_sharpe=b.sharpe if b else None, bt_maxdd=b.max_drawdown if b else None,
bt_dsr=b.dsr if b else None, bt_oos_sharpe=b.oos_sharpe if b else None,
bt_as_of=b.as_of if b else "",
)

View File

@@ -21,6 +21,14 @@ class FleetRow:
gate_status: str
gate_reason: str
as_of: str
# historical-backtest verdict; only the equity-factor constructions have one. "no backtest" renders
# as bt_status="" with bt_* numerics None (template shows an em-dash).
bt_status: str = "" # "PASS" | "FAIL" | "" (none)
bt_sharpe: float | None = None
bt_dsr: float | None = None
bt_cagr: float | None = None
bt_maxdd: float | None = None
bt_as_of: str = ""
@dataclass(frozen=True, slots=True)
@@ -38,6 +46,15 @@ class StrategyDetail:
as_of: str
history: list[ForwardNavRow]
periods: dict[str, list[PeriodPoint]] # raw per-period rolling means: keys "daily","weekly","monthly"
# historical-backtest verdict; None when the strategy has no backtest row (most do not).
bt_status: str = "" # "PASS" | "FAIL" | "" (none)
bt_cagr: float | None = None
bt_ann_vol: float | None = None
bt_sharpe: float | None = None
bt_maxdd: float | None = None
bt_dsr: float | None = None
bt_oos_sharpe: float | None = None
bt_as_of: str = ""
class DashboardReadModel(Protocol):

View File

@@ -6,6 +6,7 @@ import datetime as dt
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.dashboard_service import DashboardService
from fxhnt.application.forward_models import BacktestSummary
from fxhnt.application.forward_models import ForwardNavRow as Row
from fxhnt.application.forward_models import ForwardSummary
@@ -18,6 +19,13 @@ def _seeded() -> ForwardNavRepo:
Row("combined", "2026-06-06", 0.0099, 1.0201)], at)
repo.upsert_summary(ForwardSummary("combined", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01),
"WAIT", "2/20 forward days", at)
# backtest verdicts exist only for the equity-factor constructions
repo.upsert_backtest_summary(BacktestSummary(
"eqfactor_long", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22,
passed=True, dsr=0.61, is_sharpe=0.9, oos_sharpe=0.7, pvalue=0.03), at)
repo.upsert_backtest_summary(BacktestSummary(
"eqfactor_ls", "2026-05-30", cagr=-0.01, ann_vol=0.11, sharpe=-0.05, max_drawdown=-0.31,
passed=False, dsr=-0.2, is_sharpe=0.1, oos_sharpe=-0.3, pvalue=0.6), at)
return repo
@@ -57,3 +65,27 @@ def test_detail_includes_raw_period_aggregations() -> None:
def test_detail_unknown_strategy_is_none() -> None:
svc = DashboardService(_seeded())
assert svc.detail("does-not-exist") is None
def test_fleet_surfaces_backtest_verdict() -> None:
fleet = DashboardService(_seeded()).fleet()
el = next(f for f in fleet if f.strategy_id == "eqfactor_long")
assert el.bt_status == "PASS"
assert abs(el.bt_sharpe - 0.85) < 1e-9
assert abs(el.bt_dsr - 0.61) < 1e-9
assert abs(el.bt_cagr - 0.12) < 1e-9
assert abs(el.bt_maxdd - (-0.22)) < 1e-9
assert el.bt_as_of == "2026-05-30"
ls = next(f for f in fleet if f.strategy_id == "eqfactor_ls")
assert ls.bt_status == "FAIL"
assert abs(ls.bt_sharpe - (-0.05)) < 1e-9
def test_fleet_no_backtest_renders_blank() -> None:
fleet = DashboardService(_seeded()).fleet()
combined = next(f for f in fleet if f.strategy_id == "combined")
assert combined.bt_status == ""
assert combined.bt_sharpe is None and combined.bt_dsr is None
assert combined.bt_cagr is None and combined.bt_maxdd is None
assert combined.bt_as_of == ""

View File

@@ -7,6 +7,7 @@ from fastapi.testclient import TestClient
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.web.app import create_app
from fxhnt.application.forward_models import BacktestSummary
from fxhnt.application.forward_models import ForwardNavRow as Row
from fxhnt.application.forward_models import ForwardSummary
@@ -19,6 +20,9 @@ def _client() -> TestClient:
Row("combined", "2026-06-06", 0.0099, 1.0201)], at)
repo.upsert_summary(ForwardSummary("combined", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01),
"WAIT", "2/20 forward days", at)
repo.upsert_backtest_summary(BacktestSummary(
"eqfactor_long", "2026-05-30", cagr=0.12, ann_vol=0.18, sharpe=0.85, max_drawdown=-0.22,
passed=True, dsr=0.61, is_sharpe=0.9, oos_sharpe=0.7, pvalue=0.03), at)
return TestClient(create_app(repo))
@@ -32,6 +36,14 @@ def test_cockpit_lists_fleet() -> None:
assert "Combined book" in r.text and "WAIT" in r.text
def test_cockpit_shows_backtest_verdict_column() -> None:
r = _client().get("/")
assert r.status_code == 200
assert "backtest" in r.text # column header present
assert "badge PASS" in r.text # eqfactor_long verdict rendered
assert "DSR" in r.text
def test_strategy_detail_renders() -> None:
r = _client().get("/strategy/combined")
assert r.status_code == 200 and "2026-06-06" in r.text