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>
90 lines
4.1 KiB
Python
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)"
|