Files
fxhnt/tests/integration/test_bybit_sim_web.py
jgrusewski fd2316b1e8 fix(cockpit): Backtest state panel said "active sleeves" for a coin count
Verifying the Backtest page: the scrub state panel labelled n_active "active
sleeves", but n_active counts the instruments HELD that day — the Bybit book's
~64-coin liquid universe (compare_measured_cost sets n_active=[n_coins]*len),
the ETF book's ~5 ETFs — NOT the book's sleeves (its 4 edges). So a 4-edge book
read as having "64 sleeves". Relabelled "active positions", accurate for both
the coin book and the ETF book.

Regression test pins "active positions" (and asserts "active sleeves" absent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:56:06 +02:00

231 lines
11 KiB
Python

"""Web smoke for the Bybit 4-edge book in the cockpit Backtest: the book selector loads the precomputed
per-coin MEASURED-cost curve (the honest real-cost artifact) and renders it scaled by capital / windowed by
period, clearly distinguished from the LIVE FORWARD track — WITHOUT ever recomputing the heavy 4-edge book on
the request path. There is NO configurable-cost sandbox (cost is a measured fact, not a slider).
Asserts:
* /paper/sim?book=bybit_4edge renders the book selector + lazy-loads the result;
* /paper/sim/run?book=bybit_4edge returns the measured curve + metrics read from the precomputed artifact;
* capital scales end-value (the curve rescales linearly);
* the request path NEVER calls the heavy edge computation (a tripwire on sleeve_returns_from_store);
* the Bybit view shows the live forward track (labelled distinctly) with its gate status / days;
* bybit_4edge is the default book, and a stale binance_combined book param (Task 7a removed the Binance
venue) degrades gracefully to it — never a 500.
NO network — in-memory SQLite repos, seeded directly.
"""
from __future__ import annotations
import datetime as dt
import json
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
from fxhnt.application.compare_measured_cost import SINGLE_CACHE_KEYS
from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO
from fxhnt.application.forward_models import ForwardSummary
from fxhnt.application.paper_sim import _metrics
_BYBIT_SLEEVES = {"crypto_tstrend": 0.004, "unlock": 0.0025, "xsfunding": 0.001, "positioning": 0.0018}
def _seed_measured_cache(repo: PaperRepo, book: str = "bybit_4edge", *,
start: dt.date = dt.date(2022, 1, 1), days: int = 200,
capital: float = 100_000.0) -> None:
"""Persist a synthetic single-book MEASURED-cost payload — the honest real-cost curve the Backtest reads.
The cockpit reads ONLY this precomputed artifact on the request path (no heavy per-coin compute)."""
iso, equity, drawdown, net_rets, epoch = [], [], [], [], []
eq, peak = capital, capital
for i in range(days):
d = start + dt.timedelta(days=i)
r = 0.004 * (1.0 if (i + hash(book)) % 3 else -0.8)
eq *= (1.0 + r)
peak = max(peak, eq)
iso.append(d.isoformat()); equity.append(eq); net_rets.append(r)
drawdown.append((eq - peak) / peak if peak > 0 else 0.0)
epoch.append((d - dt.date(1970, 1, 1)).days)
payload = {
"book": book, "dates_iso": iso, "equity": equity, "drawdown": drawdown,
"leverage": [1.0] * days, "killed": [False] * days, "n_active": [4] * days,
"metrics": _metrics(epoch, equity, net_rets, drawdown, capital),
"median_spread_bps": 1.0, "cost_drag_bps": 6.5,
"used_real_spread": book == "bybit_4edge", "capital": capital, "taker_fee_bps": 5.5,
}
repo.upsert_compare_measured_cache(SINGLE_CACHE_KEYS[book],
payload, at=dt.datetime(2026, 6, 24, tzinfo=dt.UTC))
def _seeded_paper_repo(*, with_cache: bool = True) -> PaperRepo:
"""A repo with ~120 days of precomputed BYBIT per-sleeve returns in bybit_sleeve_ret PLUS (by default) the
precomputed measured-cost artifact the Backtest renders."""
repo = PaperRepo("sqlite://")
repo.migrate()
at = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
base = dt.date(2026, 1, 1)
for i in range(120):
d = (base + dt.timedelta(days=i)).isoformat()
for sleeve, amp in _BYBIT_SLEEVES.items():
ret = amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8)
repo.upsert_bybit_sleeve_ret(d, sleeve, ret, at=at)
if with_cache:
_seed_measured_cache(repo, "bybit_4edge")
return repo
def _seeded_forward_repo(*, with_track: bool = True) -> ForwardNavRepo:
fwd = ForwardNavRepo("sqlite://")
fwd.migrate()
if with_track:
at = dt.datetime(2026, 6, 24)
base = dt.date(2026, 4, 1)
nav = 1.0
rows = []
for i in range(15):
nav *= 1.001
rows.append(NavRowDTO(strategy_id="bybit_4edge",
date=(base + dt.timedelta(days=i)).isoformat(), ret=0.001, nav=nav))
fwd.upsert_rows(rows, at=at)
sm = ForwardSummary(strategy_id="bybit_4edge", as_of="2026-04-15", days=15, nav=nav,
total_return=nav - 1.0, sharpe=1.2, maxdd=-0.02)
fwd.upsert_summary(sm, gate_status="WAIT", gate_reason="15/60 forward days", at=at)
return fwd
def _client(paper: PaperRepo, fwd: ForwardNavRepo | None = None) -> TestClient:
return TestClient(create_app(fwd or _seeded_forward_repo(), paper_repo=paper))
def _sim_data(html: str) -> dict:
m = re.search(r'<script type="application/json" id="sim-data">(.*?)</script>', html, re.DOTALL)
assert m, "embedded #sim-data JSON not found"
return json.loads(m.group(1))
def test_bybit_page_renders_selector_and_lazy_loads_backtest() -> None:
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim", params={"book": "bybit_4edge"})
assert r.status_code == 200
assert "Bybit 4-edge" in r.text # book selector (painted instantly)
# The curve is lazy-loaded for the bybit book too (hx-get carries book=bybit_4edge).
assert 'hx-get="/paper/sim/run' in r.text and "book=bybit_4edge" in r.text
assert "<svg" not in r.text
def test_bybit_run_fragment_has_backtest_label_and_curve() -> None:
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "Backtest (real measured cost)" in r.text # the honest real-cost backtest is labelled
assert 'data-cost-mode="measured"' in r.text # the measured-cost marker
assert "<svg" in r.text
# The scrub state-panel counts the instruments/coins HELD, not the book's sleeves — a 4-edge book holds
# ~64 coins, so it must read "active positions", never "active sleeves" (which read as "64 sleeves").
assert "active positions" in r.text and "active sleeves" not in r.text
def _visible_text(html: str) -> str:
"""The user-FACING text of a rendered page: `<style>`/`<script>` blocks dropped and every HTML tag
(with its attributes) stripped, so a `data-cost-drag="6.50"` attribute or a `title="..."` string can't
false-trip a plain-language copy assertion — only what a reader actually sees is left."""
html = re.sub(r"<(style|script)\b[^>]*>.*?</\1>", " ", html, flags=re.DOTALL | re.IGNORECASE)
return re.sub(r"<[^>]+>", " ", html).lower()
def test_bybit_measured_cost_caption_shows_plain_percent_not_bp() -> None:
# MINOR whole-branch-review fix: "avg drag X bp" leaked visible `bp` jargon. The seeded artifact's
# cost_drag_bps=6.5 -> 0.07% (6.5 / 100, 2dp) is now the rendered avg-drag figure, plain-language.
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "avg drag 0.07%" in r.text
text = _visible_text(r.text)
assert "bp" not in text and "bps" not in text
def test_bybit_run_returns_curve_and_metrics_from_precomputed_artifact() -> None:
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 50000})
assert r.status_code == 200
assert "<svg" in r.text and "CAGR" in r.text and "Sharpe" in r.text
data = _sim_data(r.text)
assert data["dates"] and data["equity"]
assert 40000 < data["equity"][0] < 60000 # curve starts near the configured capital
def test_bybit_capital_scales_the_measured_curve() -> None:
c = _client(_seeded_paper_repo())
small = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 50000})
big = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 200000})
# capital scales end-value ~4x (linear) — the only cost is the measured fact baked into the curve.
e_small = _sim_data(small.text)["equity"][-1]
e_big = _sim_data(big.text)["equity"][-1]
assert abs(e_big / e_small - 4.0) < 0.01
def test_bybit_run_does_not_recompute_the_heavy_edges(monkeypatch) -> None:
"""Tripwire: the Backtest request path reads the precomputed measured artifact and must NEVER call the
heavy 4-edge runner (`sleeve_returns_from_store`). Detonate it and prove the curve still serves."""
import fxhnt.application.bybit_book_eval as bbe
def _boom(*a, **k):
raise AssertionError("heavy 4-edge compute called on a web request — must read the cached artifact!")
monkeypatch.setattr(bbe, "sleeve_returns_from_store", _boom)
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert _sim_data(r.text)["dates"] # got a real curve, tripwire never fired
def test_bybit_view_shows_live_forward_track_with_gate() -> None:
# The forward track renders in the lazy-loaded result fragment (alongside the backtest curve).
c = _client(_seeded_paper_repo(), _seeded_forward_repo(with_track=True))
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "Live forward" in r.text # forward track labelled distinctly
assert "Backtest (real measured cost)" in r.text # both present on the same fragment
assert "15/60 forward days" in r.text # gate status / days
assert "WAIT" in r.text
def test_bybit_view_handles_no_forward_data() -> None:
fwd = ForwardNavRepo("sqlite://")
fwd.migrate() # registry seeds bybit_4edge but no nav rows
c = _client(_seeded_paper_repo(), fwd)
r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
assert r.status_code == 200
assert "Live forward" in r.text
assert "no live forward NAV recorded yet" in r.text
def test_bybit_is_the_default_book() -> None:
"""The default book is the Bybit 4-edge DEPLOY book. A bare /paper/sim pins bybit in the form + the
lazy-load result URL."""
c = _client(_seeded_paper_repo())
r = c.get("/paper/sim") # no book param → bybit_4edge (the deploy fund)
assert r.status_code == 200
assert 'data-book="bybit_4edge"' in r.text
m = re.search(r'hx-get="(/paper/sim/run\?[^"]+)"', r.text)
assert m, "lazy-load result URL not found"
assert "book=bybit_4edge" in m.group(1)
def test_stale_binance_book_param_degrades_to_the_bybit_default() -> None:
"""Task 7a removed the Binance venue + book from the cockpit. An old bookmarked
/paper/sim?book=binance_combined URL must degrade gracefully to the bybit default — never 500, never
render a Binance-labelled curve."""
repo = PaperRepo("sqlite://")
repo.migrate()
_seed_measured_cache(repo, "bybit_4edge")
c = _client(repo)
r = c.get("/paper/sim", params={"book": "binance_combined"})
assert r.status_code == 200
assert 'data-book="bybit_4edge"' in r.text
run = c.get("/paper/sim/run", params={"book": "binance_combined", "capital": 100000})
assert run.status_code == 200
assert "Backtest (real measured cost)" in run.text # rendered the bybit book, not a 404/500
assert _sim_data(run.text)["dates"]