Files
fxhnt/tests/unit/test_worst_basis.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

96 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""worst_basis pure computation — the intraday-liquidation tail the daily-close carry accounting misses.
`worst_basis(day)` = min over the day's aligned minutes of the cumulative Σ(spot_1m_ret perp_1m_ret)
since the day's first aligned minute. The LUNA-pattern fixture proves a within-day excursion that recovers
by close yields a worst_basis MUCH worse than the close-basis."""
from fxhnt.application.worst_basis import worst_basis_by_day
_DAY_MS = 86_400_000
_MIN_MS = 60_000
def _series(start_ms: int, closes: list[float]) -> dict[int, float]:
"""{epoch_ms: close} over consecutive 1m bars starting at start_ms."""
return {start_ms + i * _MIN_MS: c for i, c in enumerate(closes)}
def test_luna_pattern_intraday_excursion_recovering_to_flat_close():
# One day. Perp CRASHES intraday (the short leg moves violently against a delta-neutral carry) then
# recovers to ~flat by close; spot is steady. cumret(spot - perp): when perp drops, perp_ret<0 so
# (spot_ret - perp_ret) spikes POSITIVE for the long-spot/short-perp delta-neutral... but worst_basis
# is the MINIMUM cumulative basis. Construct a perp that SPIKES UP intraday (squeeze) so the short-perp
# leg loses: perp_ret>0 -> (spot_ret - perp_ret)<0 -> cumulative basis goes deeply negative mid-day,
# then perp falls back to open by close so the close-basis ~0.
day0 = 1_652_313_600_000 # 2022-05-12T00:00:00Z, an epoch-ms day boundary
# spot flat at 100 the whole day
spot = _series(day0, [100.0, 100.0, 100.0, 100.0, 100.0])
# perp: 100 -> 200 (squeeze, short loses ~ -100% basis) -> back to 100 by close (recovers)
perp = _series(day0, [100.0, 200.0, 150.0, 120.0, 100.0])
out = worst_basis_by_day(perp_close=perp, spot_close=spot)
epoch_day = day0 // _DAY_MS
assert epoch_day in out
wb = out[epoch_day]
# Mid-day cumulative basis at the spike: spot_ret=0, perp_ret=+1.0 -> step -1.0 -> cum -1.0 (worst).
assert abs(wb - (-1.0)) < 1e-9
# Close basis = cumulative to the LAST minute = (spot 100/100 -1) - (perp 100/100 -1) = 0 -> ~0.
# worst_basis is far worse than close-basis (the whole point).
close_basis = (spot[day0 + 4 * _MIN_MS] / spot[day0] - 1.0) - (perp[day0 + 4 * _MIN_MS] / perp[day0] - 1.0)
assert abs(close_basis) < 1e-9
assert wb < close_basis - 0.5
def test_worst_basis_is_negative_or_zero_and_bounded_by_first_minute():
# Cumulative basis starts at 0 at the first aligned minute, so worst_basis <= 0 always.
day0 = 1_652_313_600_000
spot = _series(day0, [100.0, 101.0, 102.0]) # spot rises
perp = _series(day0, [100.0, 100.0, 100.0]) # perp flat -> basis only goes POSITIVE
out = worst_basis_by_day(perp_close=perp, spot_close=spot)
epoch_day = day0 // _DAY_MS
assert out[epoch_day] == 0.0 # never dips below the first-minute zero
def test_only_common_minutes_are_aligned():
# perp has an extra minute spot lacks; alignment uses the intersection only.
day0 = 1_652_313_600_000
spot = {day0: 100.0, day0 + 2 * _MIN_MS: 100.0} # minutes 0 and 2
perp = {day0: 100.0, day0 + 1 * _MIN_MS: 200.0, day0 + 2 * _MIN_MS: 100.0} # 0,1,2
out = worst_basis_by_day(perp_close=perp, spot_close=spot)
epoch_day = day0 // _DAY_MS
# Aligned minutes: {0, 2}. Step at minute 2: spot_ret 0, perp_ret 0 -> 0. The spike at minute 1 is
# DROPPED (spot lacks it). So worst_basis = 0 (the first-minute zero), NOT -1.0.
assert out[epoch_day] == 0.0
def test_single_aligned_bar_in_a_day_no_entry():
# A day with only one aligned minute has no return step -> no worst_basis entry.
day0 = 1_652_313_600_000
spot = {day0: 100.0}
perp = {day0: 100.0}
assert worst_basis_by_day(perp_close=perp, spot_close=spot) == {}
def test_empty_inputs_no_entry():
assert worst_basis_by_day(perp_close={}, spot_close={}) == {}
assert worst_basis_by_day(perp_close={1: 1.0}, spot_close={}) == {}
def test_multiple_days_keyed_independently():
day0 = 1_652_313_600_000
day1 = day0 + _DAY_MS
# day0: a -50% basis excursion; day1: a -100% excursion
spot = {**_series(day0, [100.0, 100.0, 100.0]), **_series(day1, [100.0, 100.0, 100.0])}
perp = {**_series(day0, [100.0, 150.0, 100.0]), **_series(day1, [100.0, 200.0, 100.0])}
out = worst_basis_by_day(perp_close=perp, spot_close=spot)
assert abs(out[day0 // _DAY_MS] - (-0.5)) < 1e-9
assert abs(out[day1 // _DAY_MS] - (-1.0)) < 1e-9
def test_nonpositive_close_skips_step_no_crash():
# A zero/negative close (bad bar) can't form a return; the step is skipped, no division error.
day0 = 1_652_313_600_000
spot = _series(day0, [100.0, 0.0, 100.0])
perp = _series(day0, [100.0, 100.0, 100.0])
out = worst_basis_by_day(perp_close=perp, spot_close=spot)
# Steps that involve a non-positive price are skipped; remaining steps are well-defined -> no crash.
assert (day0 // _DAY_MS) in out