Files
fxhnt/tests/integration/test_sim_compare_web.py
jgrusewski 456e9a9102 feat(cockpit): roll SSOT to Replay + Backtest; Backtest defaults to Bybit deploy book (was the Binance mirage)
Roll the Overview/Paper SSOT skin to the last two inconsistent pages so all
five cockpit pages read as one product, and fix the Backtest's misleading
default book.

Replay (/paper/replay):
- reuse the section wrapper + exec_badge('paper') macro (it's the paper record)
- cut the wall-of-text intro to a one-line caption + title= tooltip
- keep the unique scrubber / Play + as-of positions/trades book
- title "Paper replay" -> "Replay" (matches the nav)

Backtest (/paper/sim):
- DEFAULT book binance_combined -> bybit_4edge (the deploy fund). The Binance
  combined book is a flat-cost MIRAGE (~162% gross vs ~-36% on real measured
  costs) and must never lead the Backtest unqualified.
- book switch reordered: Bybit 4-edge (default) / Binance combined / Compare A/B
- honest one-line caption on the Binance-combined view (flat-cost mirage vs
  ~-36% measured; see Compare A/B)
- title "Paper sim" -> "Backtest"; intro paragraph cut to one line
- section wrapper + exec_badge('paper'); all functionality kept (config form,
  3-way switch, curve, scrubber, Compare A/B measured-cost mode)

Presentation + default-book switch only — no book data/returns, gate logic, or
sim computation changed. Tests updated to pin book=binance_combined where they
assert the Binance machinery; new tests cover the bybit default, the Backtest
title, the dropped intro, the honest Binance caption, and leak-clean Replay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:59:13 +02:00

173 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Web smoke for the cockpit Backtest COMPARE mode (/paper/sim?book=compare): run the Binance combined
book AND the Bybit 4-edge book over the SAME OVERLAP window, overlaid on one chart with a side-by-side
metrics table — apples-to-apples (the live equity figures are misleading: Binance is 4.5y/broad, Bybit ~1
day). The compare charges a per-COIN MEASURED cost (taker fee + half the CorwinSchultz spread), NOT a flat
cost — the measured-cost de-inflation is covered in test_compare_measured_cost.py; this file is the window /
overlay / graceful-degradation web smoke.
Asserts:
* /paper/sim?book=compare renders a third book pill ("Compare A/B") + lazy-loads the compare result;
* /paper/sim/run?book=compare overlays BOTH equity curves (dual SVG) + a metrics table with both books'
Sharpe / CAGR / maxDD over the same window;
* the default window is the OVERLAP: start=max(first_binance, first_bybit), end=min(last_*) — both books
have data the whole window;
* an explicit start/end overrides the overlap default;
* a no-overlap window degrades gracefully (200, not 500);
* the flat cost_bps knob is INERT on the compare path (the measured per-coin cost replaced it) and both
books are normalized to the SAME start capital;
* the honest note about Binance's broader/illiquid universe is present;
* the single-book modes are untouched (binance_combined / bybit_4edge still render their own way).
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
# Binance sleeves span an EARLIER, LONGER window; Bybit a LATER, SHORTER window. The overlap is the
# intersection, which is what compare must default to.
_BINANCE_START = dt.date(2021, 1, 1)
_BINANCE_DAYS = 400
_BYBIT_START = dt.date(2021, 7, 1) # starts later
_BYBIT_DAYS = 300 # ends earlier than binance's 400-day run
_BINANCE_SLEEVES = {"crypto_tstrend": 0.004, "unlock": 0.0025}
_BYBIT_SLEEVES = {"crypto_tstrend": 0.004, "unlock": 0.0025, "xsfunding": 0.001, "positioning": 0.0018}
def _seeded_repo() -> PaperRepo:
repo = PaperRepo("sqlite://")
repo.migrate()
at = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
for i in range(_BINANCE_DAYS):
d = (_BINANCE_START + dt.timedelta(days=i)).isoformat()
for sleeve, amp in _BINANCE_SLEEVES.items():
repo.upsert_sleeve_ret(d, sleeve, amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8), at=at)
for i in range(_BYBIT_DAYS):
d = (_BYBIT_START + dt.timedelta(days=i)).isoformat()
for sleeve, amp in _BYBIT_SLEEVES.items():
repo.upsert_bybit_sleeve_ret(d, sleeve, amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8), at=at)
return repo
def _client(repo: PaperRepo) -> TestClient:
fwd = ForwardNavRepo("sqlite://")
fwd.migrate()
return TestClient(create_app(fwd, paper_repo=repo))
def _overlap() -> tuple[str, str]:
start = max(_BINANCE_START, _BYBIT_START)
end = min(_BINANCE_START + dt.timedelta(days=_BINANCE_DAYS - 1),
_BYBIT_START + dt.timedelta(days=_BYBIT_DAYS - 1))
return start.isoformat(), end.isoformat()
def test_compare_page_renders_third_pill_and_lazy_loads() -> None:
c = _client(_seeded_repo())
r = c.get("/paper/sim", params={"book": "compare"})
assert r.status_code == 200
assert "Compare A/B" in r.text # the third book pill
assert 'hx-get="/paper/sim/run' in r.text and "book=compare" in r.text
assert "<svg" not in r.text # result is lazy-loaded
def test_compare_run_overlays_both_curves_and_metrics_table() -> None:
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000})
assert r.status_code == 200
# one dual-series overlay chart: two polylines in a single SVG.
assert r.text.count("<polyline") >= 2
# side-by-side metrics table naming BOTH books, with Sharpe/CAGR/maxDD rows.
assert "Binance" in r.text and "Bybit" in r.text
assert "Sharpe" in r.text and "CAGR" in r.text and ("max DD" in r.text or "maxDD" in r.text)
def test_compare_defaults_to_the_overlap_window() -> None:
"""With no explicit start/end, compare auto-picks max(starts)->min(ends) so both books have data the
whole window. The echoed cfg shows the overlap dates."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000})
assert r.status_code == 200
ov_start, ov_end = _overlap()
assert ov_start in r.text and ov_end in r.text
def test_compare_explicit_window_overrides_overlap() -> None:
c = _client(_seeded_repo())
explicit_start, explicit_end = "2021-09-01", "2021-12-01"
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000,
"start": explicit_start, "end": explicit_end})
assert r.status_code == 200
ov_start, _ = _overlap()
assert explicit_start in r.text and explicit_end in r.text
# the auto-overlap start should NOT have been substituted in (the user's window won).
assert explicit_start != ov_start
def test_compare_no_overlap_window_degrades_gracefully() -> None:
"""A user window with no data for one/both books must not 500 — say so gracefully."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000,
"start": "1990-01-01", "end": "1990-02-01"})
assert r.status_code == 200 # no 500
assert "no overlap" in r.text.lower() or "no simulated" in r.text.lower()
def test_compare_falls_back_to_flat_cost_when_no_cache() -> None:
"""With no precomputed measured-cost artifact (no `compare-measured-precompute` run), the compare falls
back to the LIGHT flat-cost compare so the page NEVER OOMs. The flat fallback DOES honor `cost_bps`
(it's the flat knob), so changing it moves both books' final equity — and the pending note is shown."""
c = _client(_seeded_repo())
cheap = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000, "cost_bps": 0})
pricey = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000, "cost_bps": 20})
assert cheap.status_code == 200 and pricey.status_code == 200
assert "pending precompute" in cheap.text.lower() # the flat-cost fallback note
def _finals(html: str) -> list[str]:
return re.findall(r'data-final="([-0-9.]+)"', html)
cf, pf = _finals(cheap.text), _finals(pricey.text)
assert len(cf) == 2 and len(pf) == 2, (cf, pf) # both books reported
# the flat knob is live in the fallback → a higher cost lowers the finals.
assert cf != pf
def test_compare_both_normalized_to_same_capital() -> None:
"""Both overlaid curves start from the SAME configured capital (so shapes compare directly)."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000})
assert r.status_code == 200
starts = re.findall(r'data-start-equity="([-0-9.]+)"', r.text)
assert len(starts) == 2
assert abs(float(starts[0]) - 100000.0) < 1.0 and abs(float(starts[1]) - 100000.0) < 1.0
def test_compare_has_honest_note() -> None:
"""No cache here → the flat-cost fallback. Its honest note says the measured view is pending precompute
and this is the light flat-cost view (both books over the same window, same start capital)."""
c = _client(_seeded_repo())
r = c.get("/paper/sim/run", params={"book": "compare", "capital": 100000})
assert r.status_code == 200
txt = r.text.lower()
assert "same window" in txt and "same" in txt and "cost" in txt
assert "pending precompute" in txt and "flat-cost" in txt # the honest fallback caveat
def test_single_book_modes_untouched() -> None:
"""binance_combined and bybit_4edge still render their own single-book way (no compare table)."""
c = _client(_seeded_repo())
bin_run = c.get("/paper/sim/run", params={"book": "binance_combined", "capital": 100000,
"sleeves": "crypto_tstrend,unlock"})
assert bin_run.status_code == 200
assert bin_run.text.count("<svg") == 1 # the single equity_curve (with cursor)
by_run = c.get("/paper/sim/run", params={"book": "bybit_4edge",
"sleeves": "crypto_tstrend,unlock,xsfunding,positioning"})
assert by_run.status_code == 200
assert "Backtest (configurable)" in by_run.text # the bybit single-book label still there