Files
fxhnt/tests/integration/test_ingest_worst_basis_cli.py
jgrusewski b5301a0fff feat(carry): 1m worst_basis ingest (binance.vision) + intraday-liquidation charge in carry accounting (Parquet side-store)
Charges the carry book's INTRADAY basis-squeeze / liquidation tail that the
daily-close basis accounting was blind to (probed: LUNAUSDT 2022-05-12 intraday
worst_basis ≈ −163% vs daily-close −0.7%).

- application/worst_basis.py — PURE cumulative-MAE: {epoch_day: worst_basis} =
  min over the day's aligned minutes of Σ(spot_1m_ret − perp_1m_ret).
- adapters/data/binance_vision_klines.py — resumable/cached 1m monthly-kline
  fetch+parse → {epoch_ms: close}; µs→ms, missing month = empty (no crash),
  injectable fetch (no network in tests).
- adapters/persistence/worst_basis_store.py — Parquet SIDE-STORE (duckdb I/O,
  NOT the warehouse → no single-writer contention) with load/write round-trip.
- paper_book.carry_return_by_symbol — optional worst_basis_by_symbol overrides
  the daily-close basis with max(worst_basis, −1.0) (capped at total leg loss);
  absent → daily-close basis, bit-identical. Threaded through paper_backfill
  (worst_basis_by_date) + paper_snapshot (worst_basis) + assets, live==replay.
- CLI: fxhnt ingest-worst-basis --symbols <list|@file> --from --to (resumable).
- TDD: LUNA-pattern fixture, parquet round-trip, charge+cap+fallback, absent=
  bit-identical backfill curve. Full suite 812 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:38:51 +02:00

72 lines
2.8 KiB
Python

"""`fxhnt ingest-worst-basis` CLI — no network: the binance.vision klines adapter is monkeypatched to a
fake that returns a LUNA-pattern fixture, and the side-store path is redirected to a tmp file. Verifies the
command resolves symbols (comma + @file), runs the ingest, and writes the Parquet side-store."""
from typer.testing import CliRunner
from fxhnt.adapters.persistence.worst_basis_store import load_worst_basis
from fxhnt.cli import _resolve_symbols, app
_MIN_MS = 60_000
def test_resolve_symbols_comma_list():
assert _resolve_symbols("LUNAUSDT,BTCUSDT,lunausdt") == ["LUNAUSDT", "BTCUSDT"] # upper + dedup
def test_resolve_symbols_at_file(tmp_path):
f = tmp_path / "syms.txt"
f.write_text("# comment\nLUNAUSDT\n\nBTCUSDT\n")
assert _resolve_symbols(f"@{f}") == ["LUNAUSDT", "BTCUSDT"]
def test_resolve_symbols_missing_file_errors():
import pytest
import typer
with pytest.raises(typer.BadParameter):
_resolve_symbols("@/nope/does/not/exist.txt")
class _FakeKlines:
def __init__(self, cache_dir, **_k):
self._cache_dir = cache_dir
def monthly_closes(self, market, symbol, month):
if symbol != "LUNAUSDT" or month != "2022-05":
return {}
day0 = 1_652_313_600_000
if market == "spot":
return {day0 + i * _MIN_MS: 100.0 for i in range(5)}
return {day0: 100.0, day0 + _MIN_MS: 200.0, day0 + 2 * _MIN_MS: 150.0,
day0 + 3 * _MIN_MS: 120.0, day0 + 4 * _MIN_MS: 100.0}
def test_cli_ingest_worst_basis_writes_side_store(tmp_path, monkeypatch):
out = tmp_path / "worst_basis.parquet"
monkeypatch.setenv("FXHNT_WORST_BASIS_PATH", str(out))
monkeypatch.setenv("FXHNT_SURFER_DATA_DIR", str(tmp_path))
import fxhnt.adapters.data.binance_vision_klines as kl
monkeypatch.setattr(kl, "BinanceVisionKlines", _FakeKlines)
from fxhnt import cli as climod
climod.get_settings.cache_clear()
res = CliRunner().invoke(app, ["ingest-worst-basis", "--symbols", "LUNAUSDT",
"--from", "2022-05", "--to", "2022-05"])
assert res.exit_code == 0, res.output
assert "wrote 1 symbols" in res.output
panel = load_worst_basis(str(out))
epoch_day = 1_652_313_600_000 // 86_400_000
assert abs(panel["LUNAUSDT"][epoch_day] - (-1.0)) < 1e-9
climod.get_settings.cache_clear()
def test_cli_ingest_rejects_inverted_range(tmp_path, monkeypatch):
monkeypatch.setenv("FXHNT_WORST_BASIS_PATH", str(tmp_path / "wb.parquet"))
monkeypatch.setenv("FXHNT_SURFER_DATA_DIR", str(tmp_path))
from fxhnt import cli as climod
climod.get_settings.cache_clear()
res = CliRunner().invoke(app, ["ingest-worst-basis", "--symbols", "X",
"--from", "2022-06", "--to", "2022-05"])
assert res.exit_code != 0
climod.get_settings.cache_clear()