Files
fxhnt/tests/integration/test_bybit_paper_backfill_cli.py
jgrusewski 375cd108bc feat(0b): remove the vestigial dual-venue PaperRepo abstraction (Task 7f)
Task 7e only renamed venue="binance" -> "legacy" to pass the substring guard;
the dual-venue repo machinery was left in place even though bybit is the ONLY
live venue. PaperRepo now hardcodes every discriminated read/write to a
_BYBIT_VENUE = "bybit" module constant instead of a constructor venue param
(removed) -- reads/writes ONLY the bybit rows, so the orphaned legacy/binance
rows from the retired combined-crypto book (Task 7d) are never touched.
nav_summary's venue arg is dropped the same way (every caller always passed
"bybit"). cockpit_models keeps the venue column (dropping it from the PK is a
separate prod schema migration, documented in the report for the merge
runbook) with reworded comments reflecting the single-venue reality.

Test fallout: deleted test_paper_repo_venue.py (tested the now-removed
dual-venue isolation) and 3 "does-not-touch-binance" tests that constructed
PaperRepo(venue="binance"); mechanically dropped the venue kwarg everywhere
else. Added a guard assertion that PaperRepo.__init__ has no venue param.
Full suite green (1853 passed); binance substring guard still 0 hits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:20:57 +02:00

90 lines
4.1 KiB
Python

"""The `fxhnt bybit-paper-backfill` CLI: walks the full Bybit history and persists the live eq-wt 4-edge book
daily into the operational DB. NO network (in-memory bybit_features seeded into a
file-sqlite DSN shared by the warehouse store + the paper repo; the unlock loader is stubbed to the seeded
calendar). Proves the command wires args + the incremental resume, and writes a multi-day venue=bybit curve."""
from __future__ import annotations
from typer.testing import CliRunner
import fxhnt.application.bybit_liquidity as bybit_liquidity
import fxhnt.application.unlock_calendar_loader as unlock_loader
import fxhnt.cli as cli
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
runner = CliRunner()
_DAY = 86_400
def _seed(store: TimescaleFeatureStore, *, days: int = 200) -> list[dict]:
import math as _m
syms = ["BTCUSDT", "AAAUSDT", "BBBUSDT", "CCCUSDT"]
px = {s: 100.0 for s in syms}
px["BTCUSDT"] = 50_000.0
for d in range(days):
crowd = (d % 3 == 0)
for s in syms:
drift = {"BTCUSDT": 0.001, "AAAUSDT": 0.002, "BBBUSDT": -0.001, "CCCUSDT": 0.0015}[s]
px[s] *= (1.0 + drift + 0.003 * _m.sin(d / 5.0 + hash(s) % 7))
ratio = (0.70 if crowd else 0.30) if s == "AAAUSDT" else \
(0.30 if crowd else 0.70) if s == "BBBUSDT" else 0.50
store.write_features(s, [(d * _DAY, {
"close": px[s], "funding": 0.0005, "spot_close": px[s] * 1.0002, "long_ratio": ratio})])
return [{"sym": "AAA", "cliff_day": days - 5, "n": 5_000_000.0, "cat": "insiders"}]
class _settings:
def __init__(self, dsn: str) -> None:
self.operational_dsn = dsn
self.paper_capital = 100_000.0
self.paper_enabled = True
def _wire(tmp_path, monkeypatch) -> str:
"""Seed bybit_features into a file-sqlite DB, stub settings + the unlock loader, return the DSN."""
dsn = f"sqlite:///{tmp_path / 'cockpit.db'}"
store = TimescaleFeatureStore(dsn, table="bybit_features")
events = _seed(store)
store.close()
monkeypatch.setattr(cli, "get_settings", lambda: _settings(dsn))
# Full universe (no liquid floor) so the seed's symbols all participate, and the seeded calendar.
monkeypatch.setattr(bybit_liquidity, "liquid_universe", lambda *a, **k: set())
monkeypatch.setattr(unlock_loader, "operational_unlock_repo", lambda s: None)
monkeypatch.setattr(unlock_loader, "load_unlock_events", lambda repo, path: events)
return dsn
def test_cli_persists_multi_day_bybit_curve(tmp_path, monkeypatch):
dsn = _wire(tmp_path, monkeypatch)
result = runner.invoke(cli.app, ["bybit-paper-backfill", "--min-dollar-vol", "0"])
assert result.exit_code == 0, result.output
assert "venue=bybit" in result.output
repo = PaperRepo(dsn)
hist = repo.nav_history()
assert len(hist) > 1, "expected a multi-day venue=bybit nav series"
dates = [p.run_date for p in hist]
assert dates == sorted(dates) == sorted(set(dates))
def test_cli_incremental_resume_is_noop_then_rebuild_matches(tmp_path, monkeypatch):
dsn = _wire(tmp_path, monkeypatch)
r1 = runner.invoke(cli.app, ["bybit-paper-backfill", "--min-dollar-vol", "0"])
assert r1.exit_code == 0, r1.output
hist1 = [(p.run_date, p.equity) for p in PaperRepo(dsn).nav_history()]
assert len(hist1) > 1
# Default (incremental): nothing new after the last persisted day → persists 0, curve unchanged.
r2 = runner.invoke(cli.app, ["bybit-paper-backfill", "--min-dollar-vol", "0"])
assert r2.exit_code == 0, r2.output
assert "persisted 0 day(s) (incremental)" in r2.output
hist2 = [(p.run_date, p.equity) for p in PaperRepo(dsn).nav_history()]
assert hist2 == hist1
# --rebuild recomputes the whole history; idempotent → identical curve.
r3 = runner.invoke(cli.app, ["bybit-paper-backfill", "--min-dollar-vol", "0", "--rebuild"])
assert r3.exit_code == 0, r3.output
hist3 = [(p.run_date, p.equity) for p in PaperRepo(dsn).nav_history()]
assert hist3 == hist1, "rebuild diverged from the original (should be idempotent)"