From 31aba36b132e7fbc7c699edf5fc53adc812cce59 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 19 Jun 2026 08:12:51 +0200 Subject: [PATCH] fix(xsfunding): guard live snapshot against decoupled-basis tickers (LIT-type artifacts) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/fxhnt/adapters/data/binance_xsfunding_live.py | 10 +++++++--- tests/integration/test_binance_xsfunding_live.py | 8 ++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/fxhnt/adapters/data/binance_xsfunding_live.py b/src/fxhnt/adapters/data/binance_xsfunding_live.py index ed31962..a1b5e81 100644 --- a/src/fxhnt/adapters/data/binance_xsfunding_live.py +++ b/src/fxhnt/adapters/data/binance_xsfunding_live.py @@ -14,8 +14,9 @@ _SPOT = "https://api.binance.com" class BinanceXsFundingLive: - def __init__(self, funding: Any | None = None) -> None: + def __init__(self, funding: Any | None = None, *, max_basis: float = 0.30) -> None: self._f = funding if funding is not None else BinanceFundingClient() + self._max_basis = max_basis def _get(self, url: str, tries: int = 4) -> Any: """Single live HTTP GET with backoff retry (mirrors BinanceFundingClient._get): @@ -38,11 +39,14 @@ class BinanceXsFundingLive: def snapshot(self, prev: dict[str, float]) -> dict[str, tuple[float, float, float, float, float]]: """{sym: (trailing_funding, today_funding, perp_price, spot_price, qvol)} over the liquid - universe; coins lacking a spot price (can't model basis) are omitted.""" + universe; coins lacking a spot price (can't model basis) are omitted, and coins whose + perp/spot basis exceeds max_basis (decoupled/relisted ticker-reuse artifacts, e.g. + LITUSDT) are dropped — the historical panel applies the same |basis| guard.""" liq, tf30, last24 = self._f.scan(set(prev)) perp, spot = self._perp_prices(), self._spot_prices() out: dict[str, tuple[float, float, float, float, float]] = {} for s in liq: - if s in perp and s in spot and perp[s] > 0 and spot[s] > 0: + if s in perp and s in spot and perp[s] > 0 and spot[s] > 0 \ + and abs((perp[s] - spot[s]) / spot[s]) <= self._max_basis: out[s] = (tf30.get(s, 0.0), last24.get(s, 0.0), perp[s], spot[s], liq[s]) return out diff --git a/tests/integration/test_binance_xsfunding_live.py b/tests/integration/test_binance_xsfunding_live.py index f1ca265..354cba9 100644 --- a/tests/integration/test_binance_xsfunding_live.py +++ b/tests/integration/test_binance_xsfunding_live.py @@ -45,3 +45,11 @@ def test_live_source_omits_coin_without_spot(monkeypatch): monkeypatch.setattr(src, "_spot_prices", lambda: {"AAAUSDT": 99.5}) # BBB has no spot snap = src.snapshot(prev={}) assert set(snap) == {"AAAUSDT"} # BBB omitted (no spot leg -> can't model basis) + + +def test_live_source_rejects_decoupled_basis(monkeypatch): + src = BinanceXsFundingLive(funding=_FakeFunding()) # AAAUSDT, BBBUSDT + monkeypatch.setattr(src, "_perp_prices", lambda: {"AAAUSDT": 100.0, "BBBUSDT": 1.574}) + monkeypatch.setattr(src, "_spot_prices", lambda: {"AAAUSDT": 99.5, "BBBUSDT": 0.743}) # BBB |basis| huge + snap = src.snapshot(prev={}) + assert set(snap) == {"AAAUSDT"} # BBB rejected (decoupled ticker-reuse artifact)