Design (frontend-design skill, following the existing /paper/sim + fleet-table pattern instead of the ad-hoc fieldset markup): - selection TABLE with an explicit TYPE column (semantic chips: Crypto / Equities & macro) in the fleet skin, so it reflows to stacked cards on mobile - verdict banner (the cockpit trust-anchor), .card/.stat metric tiles, token-themed Plotly curve + heatmap, marginal table with pos/neg colouring - live htmx update on change (no Run button), matching Backtest SSOT consolidation (was 3 name sources + no type resolver): - display_names.py is now THE resolver: absorbs SLEEVE_DISPLAY_NAMES so display_name() covers sleeves AND strategies, and adds asset_class() (the Type column) derived from the registry field via a prefix rule — no hand-kept per-edge map - both registered as Jinja globals -> every page reads names/types consistently - bybit_book_eval._SLEEVE_DISPLAY_NAMES re-exports from the SSOT (no duplicate data) - registry display_names made concise; the rename propagates to Overview/Paper/detail (crypto_tstrend had no name at all and rendered as a raw id) Removed Manual weights: the Book allocator COMPUTES the weights (inverse-vol -> cap -> vol-target + Kelly). Empty inputs the user must fill implied they set the sizing, which is backwards — a control that misleads is worse than no control. Drops the explicit/ combine_returns branch, w_<edge> parsing, and the method param. 62 tests green (portfolio, SSOT guard, dashboard-service, web, crypto book eval). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
4.8 KiB
Python
98 lines
4.8 KiB
Python
"""Web smoke: routes return 200 and render the seeded fleet + a strategy detail + healthz."""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
|
|
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
|
|
|
|
|
|
def _client() -> TestClient:
|
|
repo = ForwardNavRepo("sqlite://")
|
|
repo.migrate()
|
|
at = dt.datetime(2026, 6, 14, 23, 30)
|
|
repo.upsert_rows([Row("unlock", "2026-06-05", 0.01, 1.01),
|
|
Row("unlock", "2026-06-06", 0.0099, 1.0201)], at)
|
|
repo.upsert_summary(ForwardSummary("unlock", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01),
|
|
"WAIT", "2/20 forward days", at)
|
|
repo.upsert_backtest_summary(BacktestSummary(
|
|
"xsfunding", "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))
|
|
|
|
|
|
def test_healthz() -> None:
|
|
assert _client().get("/healthz").status_code == 200
|
|
|
|
|
|
def test_cockpit_lists_fleet() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
assert "Token-unlock shorts" in r.text and "WAIT" in r.text
|
|
|
|
|
|
def test_cockpit_shows_record_and_forward_columns() -> None:
|
|
# Redesigned compact edges table: edge · record · forward · nav (the standalone backtest reference is
|
|
# folded into the "record" column; the gate status into "forward"). The internal sleeve-id "class"
|
|
# column is intentionally NOT shown — it's dev info, not user info.
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
for header in (">edge<", ">record<", ">forward<", ">nav<"):
|
|
assert header in r.text, f"column header {header!r} missing"
|
|
assert ">class<" not in r.text # the internal sleeve-id dev column is removed
|
|
assert "badge WAIT" in r.text # a gate status badge rendered in the forward column
|
|
|
|
|
|
def test_cockpit_has_responsive_markup() -> None:
|
|
r = _client().get("/")
|
|
assert r.status_code == 200
|
|
assert 'class="fleet"' in r.text # fleet table marked for card reflow
|
|
assert 'data-label="forward"' in r.text # per-cell labels for mobile card view
|
|
|
|
|
|
def test_strategy_detail_renders_backtest_verdict() -> None:
|
|
# Redesigned: the old standalone "backtest verdict" <h3> is gone; the backtest now renders inside the
|
|
# "Backtested edge" stat card (one of the three top-of-page cards) — Sharpe, CAGR, and the PASS/FAIL
|
|
# badge all still render there.
|
|
r = _client().get("/strategy/xsfunding")
|
|
assert r.status_code == 200
|
|
assert "Backtested edge" in r.text # the new verdict card header present
|
|
assert "badge PASS" in r.text # PASS badge rendered
|
|
assert "Sharpe 0.8" in r.text # backtest Sharpe rendered
|
|
assert "+12%/yr" in r.text # backtest CAGR rendered
|
|
|
|
|
|
def test_failed_backtest_verdict_is_honest_not_a_green_pass() -> None:
|
|
# Regression (found verifying the IBKR /strategy/multistrat page): a backtest that FAILED but has a high
|
|
# Sharpe (multistrat: Sharpe 1.7, gate FAIL) used to render a GREEN "Validated edge ... backtest passed"
|
|
# banner directly above the FAIL badge — the Sharpe>=1.0 fallback overrode the explicit FAIL. The verdict
|
|
# must respect the badge: an explicit FAIL is amber ("didn't clear the gate"), never a green "passed".
|
|
repo = ForwardNavRepo("sqlite://")
|
|
repo.migrate()
|
|
at = dt.datetime(2026, 6, 14, 23, 30)
|
|
repo.upsert_backtest_summary(BacktestSummary(
|
|
"positioning", "2026-05-30", cagr=0.15, ann_vol=0.20, sharpe=1.7, max_drawdown=-0.05,
|
|
passed=False, dsr=0.4, is_sharpe=1.7, oos_sharpe=1.2, pvalue=0.2), at)
|
|
r = TestClient(create_app(repo)).get("/strategy/positioning")
|
|
assert r.status_code == 200
|
|
assert "badge FAIL" in r.text # the FAIL badge still renders in the card
|
|
assert "didn't clear the gate" in r.text # honest amber verdict
|
|
assert "verdict warn" in r.text # amber banner, not the green ".verdict ok"
|
|
assert "backtest passed" not in r.text # never claim "passed" over a FAIL
|
|
assert "Validated edge" not in r.text # nor "validated"
|
|
|
|
|
|
def test_strategy_detail_renders() -> None:
|
|
r = _client().get("/strategy/unlock")
|
|
assert r.status_code == 200 and "2026-06-06" in r.text
|
|
assert "weekly" in r.text and "rolling mean" in r.text # period-aware section present
|
|
|
|
|
|
def test_strategy_detail_unknown_404() -> None:
|
|
assert _client().get("/strategy/nope").status_code == 404
|