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>
96 lines
4.8 KiB
Python
96 lines
4.8 KiB
Python
"""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
|