feat(xsfunding): live per-coin snapshot (funding + perp + spot + qvol)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
src/fxhnt/adapters/data/binance_xsfunding_live.py
Normal file
38
src/fxhnt/adapters/data/binance_xsfunding_live.py
Normal file
@@ -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
|
||||
25
tests/integration/test_binance_xsfunding_live.py
Normal file
25
tests/integration/test_binance_xsfunding_live.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user