diff --git a/src/fxhnt/adapters/data/binance_xsfunding_live.py b/src/fxhnt/adapters/data/binance_xsfunding_live.py new file mode 100644 index 0000000..388a85e --- /dev/null +++ b/src/fxhnt/adapters/data/binance_xsfunding_live.py @@ -0,0 +1,38 @@ +"""Live per-coin snapshot for the cross-sectional funding paper track: trailing funding, +today's funding, perp price, spot price, quote-volume — for the liquid universe.""" +from __future__ import annotations + +import json +import urllib.request +from typing import Any + +from fxhnt.adapters.data.binance_funding import BinanceFundingClient + +_FAPI = "https://fapi.binance.com" +_SPOT = "https://api.binance.com" + + +class BinanceXsFundingLive: + def __init__(self, funding: Any | None = None) -> None: + self._f = funding if funding is not None else BinanceFundingClient() + + def _get(self, url: str) -> Any: + with urllib.request.urlopen(url, timeout=30) as r: + return json.loads(r.read()) + + def _perp_prices(self) -> dict[str, float]: + return {x["symbol"]: float(x["price"]) for x in self._get(f"{_FAPI}/fapi/v1/ticker/price")} + + def _spot_prices(self) -> dict[str, float]: + return {x["symbol"]: float(x["price"]) for x in self._get(f"{_SPOT}/api/v3/ticker/price")} + + 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.""" + 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: + 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 new file mode 100644 index 0000000..8985761 --- /dev/null +++ b/tests/integration/test_binance_xsfunding_live.py @@ -0,0 +1,25 @@ +from fxhnt.adapters.data.binance_xsfunding_live import BinanceXsFundingLive + + +class _FakeFunding: + def scan(self, prev): + return ({"AAAUSDT": 5e6, "BBBUSDT": 3e6}, + {"AAAUSDT": 0.001, "BBBUSDT": 0.0003}, + {"AAAUSDT": 0.0012, "BBBUSDT": 0.0004}) + + +def test_live_source_merges_funding_and_prices(monkeypatch): + src = BinanceXsFundingLive(funding=_FakeFunding()) + monkeypatch.setattr(src, "_perp_prices", lambda: {"AAAUSDT": 100.0, "BBBUSDT": 10.0}) + monkeypatch.setattr(src, "_spot_prices", lambda: {"AAAUSDT": 99.5, "BBBUSDT": 10.1}) + snap = src.snapshot(prev={}) + assert snap["AAAUSDT"] == (0.001, 0.0012, 100.0, 99.5, 5e6) # (tf30, today_funding, perp, spot, qvol) + assert snap["BBBUSDT"] == (0.0003, 0.0004, 10.0, 10.1, 3e6) + + +def test_live_source_omits_coin_without_spot(monkeypatch): + src = BinanceXsFundingLive(funding=_FakeFunding()) + monkeypatch.setattr(src, "_perp_prices", lambda: {"AAAUSDT": 100.0, "BBBUSDT": 10.0}) + 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)