""" External data signals for Bayesian probability estimation. Sources: CoinGecko (crypto prices), Alternative.me (Fear&Greed), Polymarket trends """ import logging from dataclasses import dataclass import httpx log = logging.getLogger(__name__) @dataclass class ExternalSignals: btc_price: float = 0.0 btc_change_24h: float = 0.0 # % change eth_price: float = 0.0 eth_change_24h: float = 0.0 btc_dominance: float = 50.0 # BTC market dominance % fear_greed_index: int = 50 # 0=extreme fear, 100=extreme greed fear_greed_label: str = "neutral" total_market_cap_change: float = 0.0 valid: bool = False class ExternalDataClient: """Fetches external market signals used to calibrate probability estimates.""" def __init__(self) -> None: self._client = httpx.AsyncClient(timeout=15) async def get_all_signals(self) -> ExternalSignals: """Aggregate all external signals. Returns best-effort (partial ok).""" signals = ExternalSignals() try: prices = await self._get_crypto_prices() signals.btc_price = prices.get("bitcoin", {}).get("usd", 0) signals.btc_change_24h = prices.get("bitcoin", {}).get("usd_24h_change", 0) signals.eth_price = prices.get("ethereum", {}).get("usd", 0) signals.eth_change_24h = prices.get("ethereum", {}).get("usd_24h_change", 0) except Exception as e: log.warning("CoinGecko fetch failed: %s", e) try: fg = await self._get_fear_greed() signals.fear_greed_index = fg["value"] signals.fear_greed_label = fg["label"] except Exception as e: log.warning("Fear&Greed fetch failed: %s", e) try: global_data = await self._get_global_market() signals.btc_dominance = global_data.get("btc_dominance", 50) signals.total_market_cap_change = global_data.get("market_cap_change_24h", 0) except Exception as e: log.warning("Global market data fetch failed: %s", e) signals.valid = signals.btc_price > 0 log.info( "External signals: BTC=$%.0f (%.1f%%) F&G=%d/%s", signals.btc_price, signals.btc_change_24h, signals.fear_greed_index, signals.fear_greed_label, ) return signals async def _get_crypto_prices(self) -> dict: resp = await self._client.get( "https://api.coingecko.com/api/v3/simple/price", params={ "ids": "bitcoin,ethereum", "vs_currencies": "usd", "include_24hr_change": True, }, ) resp.raise_for_status() return resp.json() async def _get_fear_greed(self) -> dict: resp = await self._client.get("https://api.alternative.me/fng/?limit=1") resp.raise_for_status() data = resp.json()["data"][0] return { "value": int(data["value"]), "label": data["value_classification"], } async def _get_global_market(self) -> dict: resp = await self._client.get("https://api.coingecko.com/api/v3/global") resp.raise_for_status() data = resp.json()["data"] return { "btc_dominance": data.get("market_cap_percentage", {}).get("btc", 50), "market_cap_change_24h": data.get("market_cap_change_percentage_24h_usd", 0), } async def close(self) -> None: await self._client.aclose()