""" #5 On-chain + derivatives signals (free, no key). - Funding rates (BTC, ETH) : Binance USD-M futures premiumIndex - Open interest + 24h change (BTC): Binance futures openInterest - Liquidation spike proxy : Binance 24h taker buy/sell imbalance + range - Stablecoin inflow/outflow proxy : 24h market-cap change of USDT+USDC (CoinGecko) All endpoints are public. Each is wrapped so one failure (or a geo-block on Binance) degrades gracefully and is logged to system health. """ import logging import requests logger = logging.getLogger(__name__) _BINANCE_FAPI = "https://fapi.binance.com" _CG = "https://api.coingecko.com/api/v3" _HEADERS = {"accept": "application/json"} def _get(url, params=None, timeout=12): r = requests.get(url, params=params, headers=_HEADERS, timeout=timeout) r.raise_for_status() return r.json() class OnChainCollector: def fetch(self, prev_oi_btc: float | None = None) -> dict: from monitoring import monitor sig: dict = {} # Funding rates try: for sym, key in (("BTCUSDT", "funding_btc"), ("ETHUSDT", "funding_eth")): d = _get(f"{_BINANCE_FAPI}/fapi/v1/premiumIndex", {"symbol": sym}) sig[key] = round(float(d.get("lastFundingRate", 0)) * 100, 5) # % monitor.source_ok("Binance:funding") except Exception as exc: monitor.source_failure("Binance:funding", str(exc)[:100]) # Open interest + change try: d = _get(f"{_BINANCE_FAPI}/fapi/v1/openInterest", {"symbol": "BTCUSDT"}) oi = float(d.get("openInterest", 0)) sig["oi_btc"] = oi if prev_oi_btc: sig["oi_btc_change"] = round((oi - prev_oi_btc) / prev_oi_btc * 100, 3) monitor.source_ok("Binance:oi") except Exception as exc: monitor.source_failure("Binance:oi", str(exc)[:100]) # Liquidation spike proxy: 24h price range × volume imbalance try: t = _get(f"{_BINANCE_FAPI}/fapi/v1/ticker/24hr", {"symbol": "BTCUSDT"}) high, low = float(t.get("highPrice", 0)), float(t.get("lowPrice", 0)) last = float(t.get("lastPrice", 1)) or 1 rng = (high - low) / last * 100 if last else 0 pct_chg = abs(float(t.get("priceChangePercent", 0))) sig["liq_proxy"] = round(rng + pct_chg, 3) # higher = more violent 24h monitor.source_ok("Binance:ticker") except Exception as exc: monitor.source_failure("Binance:ticker", str(exc)[:100]) # Stablecoin flow proxy: 24h mcap change of USDT + USDC try: d = _get(f"{_CG}/coins/markets", { "vs_currency": "usd", "ids": "tether,usd-coin", "price_change_percentage": "24h", }) flow = 0.0 for c in d: mc = c.get("market_cap") or 0 chg = c.get("market_cap_change_percentage_24h") or c.get("price_change_percentage_24h") or 0 flow += mc * (chg / 100.0) sig["stablecoin_flow"] = round(flow / 1e9, 3) # $B net change (proxy) monitor.source_ok("CoinGecko:stables") except Exception as exc: monitor.source_failure("CoinGecko:stables", str(exc)[:100]) return sig @staticmethod def context_summary(sig: dict) -> dict: """Compact derivatives read used by the scorer + UI.""" if not sig: return {} f = sig.get("funding_btc") oi_chg = sig.get("oi_btc_change") liq = sig.get("liq_proxy") flow = sig.get("stablecoin_flow") notes = [] if f is not None: notes.append("funding " + ("hot/long-heavy" if f > 0.03 else "negative/short-heavy" if f < -0.01 else "neutral")) if oi_chg is not None: notes.append(f"OI {('+' if oi_chg>=0 else '')}{oi_chg:.1f}%") if flow is not None: notes.append("stablecoins " + ("inflow" if flow > 0 else "outflow")) return { "funding_btc": f, "funding_eth": sig.get("funding_eth"), "oi_btc_change": oi_chg, "liq_proxy": liq, "stablecoin_flow": flow, "summary": " · ".join(notes), }