Files
fxhnt/tests/integration/test_bybit_paper_backfill_cli.py

93 lines
4.4 KiB
Python

"""The `fxhnt bybit-paper-backfill` CLI: walks the full Bybit history and persists the live eq-wt 4-edge book
daily under venue="bybit" 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
from fxhnt.application.bybit_paper_book import BYBIT_VENUE
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, venue=BYBIT_VENUE)
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))
# The binance book is untouched (no rows under the default venue).
assert PaperRepo(dsn, venue="binance").nav_history() == []
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, venue=BYBIT_VENUE).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, venue=BYBIT_VENUE).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, venue=BYBIT_VENUE).nav_history()]
assert hist3 == hist1, "rebuild diverged from the original (should be idempotent)"