Files
fxhnt/tests/integration/test_paper_sim_web.py
jgrusewski f6f1c9082b chore(cockpit): strip jargon text blocks + hard-coded values from the pages
Following the /paper blurb removal, sweep the remaining pages:
- Backtest (sim.html): drop the paragraph-long verdict banner; keep a one-line
  plain subtitle ("a what-if on capital & period at real trading cost").
- Measured-cost caption (_sim_result.html): visible text now plain
  ("real trading cost, per coin — fee + spread · avg drag X%"); the technical
  detail (L1 quoted spread / current-snapshot caveat / "not a slider") moves to
  the hover tooltip. IBKR book caption likewise plain.
- Hard-coded "vol-targeted @ 20%" label (Overview, paper_crypto, strategy) ->
  "each shown on its own" (drops the hard-coded 20% + the jargon).

Tests updated to the new plain copy; technical strings that stay in tooltips
(real L1 quoted spread, the snapshot caveat) are still asserted there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:46:51 +02:00

199 lines
10 KiB
Python

"""Web smoke for the simplified cockpit Backtest: /paper/sim renders a tiny book/capital/period form and
lazy-loads the precomputed real-cost (MEASURED) curve from /paper/sim/run. The configurable-cost SANDBOX is
GONE — no cost (bps) slider, no measured/flat cost-mode select, no sleeve-composition presets/checkboxes, no
Kelly slider, no controller select. The cost is a measured FACT, not a knob (drag it to 0 and you'd resurrect
the +7097% mirage). When the measured artifact has not been precomputed the page degrades to a graceful
'precomputing' note (never the flat slider, never a 500). Bybit is the cockpit's only venue (Task 7a removed
the Binance venue toggle + the binance-vs-bybit "compare" view — see test_cockpit_bybit_only.py).
The measured-curve behaviour (capital-scale, period-window, the measured marker, cache-absent note) is
covered in test_backtest_measured_single_book.py; this file is the page-skin / form / routing smoke.
NO network — in-memory SQLite repos, seeded directly.
"""
from __future__ import annotations
import datetime as dt
import re
from fastapi.testclient import TestClient
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.web.app import create_app
def _seeded_repo() -> PaperRepo:
"""A repo with ~120 days of precomputed BYBIT per-sleeve returns. No measured cache, so the single-book
Backtest shows the graceful 'precomputing' note — the honest curve is read, never recomputed on the
request path."""
repo = PaperRepo("sqlite://")
repo.migrate()
at = dt.datetime(2026, 6, 22, tzinfo=dt.UTC)
base = dt.date(2026, 1, 1)
sleeves = {"crypto_tstrend": 0.004, "unlock": 0.0025, "positioning": 0.0015,
"xsfunding": 0.0008}
for i in range(120):
d = (base + dt.timedelta(days=i)).isoformat()
for sleeve, amp in sleeves.items():
ret = amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8)
repo.upsert_bybit_sleeve_ret(d, sleeve, ret, at=at)
return repo
def _client(repo: PaperRepo) -> TestClient:
fwd = ForwardNavRepo("sqlite://")
fwd.migrate()
return TestClient(create_app(fwd, paper_repo=repo))
def test_sim_page_renders_book_capital_period_form_and_lazy_loads_result() -> None:
"""The page paints the tiny book/capital/period form WITHOUT running anything — the curve is fetched
lazily by HTMX from /paper/sim/run on load. The configurable-cost sandbox controls are absent."""
c = _client(_seeded_repo())
r = c.get("/paper/sim")
assert r.status_code == 200
assert 'id="sim-form"' in r.text # the (tiny) config panel
# the honest knobs: book/capital/period only.
assert 'name="capital"' in r.text and 'name="start"' in r.text and 'name="end"' in r.text
# the sandbox knobs are GONE.
assert 'type="range"' not in r.text # no Kelly / cost sliders
assert 'name="cost_bps"' not in r.text and 'name="cost_mode"' not in r.text
assert 'name="sleeve"' not in r.text and 'name="kelly"' not in r.text
assert 'name="controller"' not in r.text and "sim-preset" not in r.text
# the result is lazy-loaded, not computed inline.
assert 'id="sim-result-wrap"' in r.text
assert 'hx-get="/paper/sim/run' in r.text and 'hx-trigger="load"' in r.text
# the form refreshes via HTMX-NATIVE wiring on change (NOT a fragile inline <script> — the live bug was the
# IIFE never executing in this template's content path, so adjusting capital/period did nothing).
assert 'hx-trigger="submit, change' in r.text # Enter/submit AND field change re-fetch (htmx catches Enter → no native page reload)
assert 'keyup changed delay' in r.text # capital updates live as you TYPE (debounced), not just on blur
assert 'type="hidden" name="book"' in r.text # book travels with the htmx GET
assert "<svg" not in r.text and "Sharpe" not in r.text
def test_sim_run_cache_absent_shows_pending_note_not_flat() -> None:
"""The default lazy-load fragment, with no measured artifact seeded, is the graceful 'precomputing' note —
NOT a flat what-if slider, NOT a 500."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "precomputing" in r.text.lower() and "nightly" in r.text.lower()
assert 'data-cost-mode="flat"' not in r.text
def test_sim_page_works_with_empty_repo() -> None:
repo = PaperRepo("sqlite://")
repo.migrate()
c = _client(repo)
r = c.get("/paper/sim")
assert r.status_code == 200
assert 'id="sim-form"' in r.text
def test_sim_run_capital_field_is_lenient_never_422() -> None:
"""Regression: a cleared / comma-formatted / odd capital must NOT 422 the backtest — the run endpoint
parses capital leniently (empty/garbage -> default)."""
c = _client(_seeded_repo())
for cap in ["", "100,000", "250000", "abc", " 50000 "]:
r = c.get("/paper/sim/run", params={"capital": cap, "book": "bybit_4edge"})
assert r.status_code == 200, f"capital={cap!r} should render, not 422"
def test_sim_page_result_url_capital_is_plain_decimal_not_scientific() -> None:
"""The lazy-load hx-get must format capital as a plain decimal (NOT :g scientific like 1e+06, whose '+'
decodes to a space in the query string and 422s)."""
c = _client(_seeded_repo())
html = c.get("/paper/sim").text
m = re.search(r'/paper/sim/run\?capital=([^&"]+)', html)
assert m, "result_url with capital not found"
cap = m.group(1)
assert "e" not in cap.lower() and "+" not in cap, f"capital must be plain decimal, got {cap!r}"
def test_sim_defaults_to_bybit_deploy_book() -> None:
"""The Backtest must DEFAULT to the Bybit 4-edge deploy book. So a bare /paper/sim leads with the honest
deployable curve: the form data-book + the lazy-load result_url are bybit."""
c = _client(_seeded_repo())
html = c.get("/paper/sim").text
assert 'data-book="bybit_4edge"' in html # the form runs the bybit book by default
m = re.search(r'hx-get="(/paper/sim/run\?[^"]+)"', html)
assert m, "lazy-load result URL not found"
result_url = m.group(1)
assert "book=bybit_4edge" in result_url
def test_sim_page_title_is_backtest_not_paper_sim() -> None:
"""The page title is 'Backtest' (consistent with the nav), not the old 'Paper sim'."""
c = _client(_seeded_repo())
html = c.get("/paper/sim").text
assert "<title>Backtest · fxhnt</title>" in html
assert "Paper sim" not in html
def test_sim_page_caption_is_honest_real_cost_not_configurable_cost() -> None:
"""The subtitle reflects the honest real-cost framing in PLAIN language (no jargon block): a what-if on
capital & period at real trading cost. The old 'choose ... cost' / sleeves sandbox prose stays gone."""
c = _client(_seeded_repo())
html = c.get("/paper/sim").text
assert "a what-if on capital" in html # the what-if framing (plain subtitle)
assert "real trading cost" in html # honest-cost framing, plain (no "taker fee"/"L1")
assert "choose book / capital / sleeves / period / cost" not in html # the old sandbox prose is gone
assert "configurable what-if" not in html
def test_caption_band_aid_is_gone() -> None:
"""The old special-case "≈ -36% / see Compare A/B" caption band-aid is REMOVED (both the Binance venue
and the Compare A/B view are gone as of Task 7a). With no measured artifact the single-book view shows
the graceful pending note (not the old caption, not the flat slider)."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "real measured costs" not in r.text and "-36%" not in r.text
assert "Compare A/B" not in r.text
assert "precomputing" in r.text.lower()
assert 'data-cost-mode="flat"' not in r.text
def test_capital_input_does_not_reject_round_numbers() -> None:
"""Regression: min=1 step=1000 made the browser reject 100000 (not 1+n·1000) with 'enter a valid number'
and block submit. The capital input must use step='any' so any amount is accepted client-side."""
c = _client(_seeded_repo())
html = c.get("/paper/sim").text
m = re.search(r'<input[^>]*name="capital"[^>]*>', html)
assert m, "capital input not found"
tag = m.group(0)
assert 'step="any"' in tag and 'step="1000"' not in tag, tag
def test_sim_book_switch_routes_all_200() -> None:
"""The 2-way book switch (Bybit deploy / Bybit levered shadow) renders; a stale binance_combined or
compare book param (the removed Task-7a surfaces) degrades gracefully to the default rather than 500ing."""
c = _client(_seeded_repo())
for book in ("bybit_4edge", "bybit_4edge_levered"):
assert c.get("/paper/sim", params={"book": book}).status_code == 200
for stale_book in ("binance_combined", "compare"):
r = c.get("/paper/sim", params={"book": stale_book})
assert r.status_code == 200
assert 'data-book="bybit_4edge"' in r.text # degraded to the default deploy book
def test_sim_shows_the_levered_shadow_pill_and_runs_gracefully() -> None:
"""The Backtest exposes the levered shadow as a 4th book pill, and its lazy /run renders gracefully (the
measured pending note when the levered cache is absent — never a 500, never a flat slider)."""
c = _client(_seeded_repo())
page = c.get("/paper/sim", params={"book": "bybit_4edge_levered"})
assert page.status_code == 200
assert "Bybit 4-edge levered" in page.text and "shadow" in page.text
assert 'data-book="bybit_4edge_levered"' in page.text or "book=bybit_4edge_levered" in page.text
run = c.get("/paper/sim/run", params={"book": "bybit_4edge_levered", "capital": 100000})
assert run.status_code == 200
assert "precomputing" in run.text.lower() and 'data-cost-mode="flat"' not in run.text
_SIM_ABS_URL = re.compile(r"https?://(?!www\.w3\.org/)[^\s\"'<>]+")
def test_sim_page_is_leak_clean() -> None:
hits = _SIM_ABS_URL.findall(_client(_seeded_repo()).get("/paper/sim").text)
assert not hits, f"leak-clean violation: {hits}"