""" A professional-grade, multi-asset, multi-oracle price engine. v13.0 FINAL: This version uses FOUR independent data sources and calculates a resilient median price to be immune to single-source API failures. This is the most robust and definitive version. """ import asyncio import logging from typing import Dict, Optional, List import httpx import statistics logger = logging.getLogger(__name__) # --- CONFIGURATION --- ASSET_CONFIG = { "BTC": {"pyth_id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43", "coingecko_id": "bitcoin", "coincap_id": "bitcoin", "cryptocompare_id": "BTC"}, "ETH": {"pyth_id": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", "coingecko_id": "ethereum", "coincap_id": "ethereum", "cryptocompare_id": "ETH"}, "SOL": {"pyth_id": "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", "coingecko_id": "solana", "coincap_id": "solana", "cryptocompare_id": "SOL"}, "XRP": {"pyth_id": "02a01e69981d314fd8a723be08253181e53b4945b4bf376d15a51980a37330c3", "coingecko_id": "ripple", "coincap_id": "xrp", "cryptocompare_id": "XRP"}, "DOGE": {"pyth_id": "042f02faf4229babc62635593855b6a383d6a4a2a1b9b9a7c385a4a50b86a345", "coingecko_id": "dogecoin", "coincap_id": "dogecoin", "cryptocompare_id": "DOGE"}, "ADA": {"pyth_id": "34f544985c7943c093b5934963505a767f4749445244a852654c6017b28091ea", "coingecko_id": "cardano", "coincap_id": "cardano", "cryptocompare_id": "ADA"}, "AVAX": {"pyth_id": "0x141f2a3c34c8035443a01d64380b52207991b16c14c5145f617eb578a994753c", "coingecko_id": "avalanche-2", "coincap_id": "avalanche", "cryptocompare_id": "AVAX"}, "LINK": {"pyth_id": "0x63f4f4755a5a67c64c781d45763b33a72666a15e6b91c0fbdf3b2f205d5a6b01", "coingecko_id": "chainlink", "coincap_id": "chainlink", "cryptocompare_id": "LINK"}, "DOT": {"pyth_id": "0x00a896677493a74421b33362a7447785b13612f0e340d418641a33716a5067a3", "coingecko_id": "polkadot", "coincap_id": "polkadot", "cryptocompare_id": "DOT"}, "MATIC": {"pyth_id": "0x737ac3c13709b45da8128ff9e1058a984f86a048035656111b8a365e4921648a", "coingecko_id": "matic-network", "coincap_id": "polygon", "cryptocompare_id": "MATIC"}, } class PriceFetcher: PYTH_URL = "https://hermes.pyth.network/v2/price_feeds" COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price" COINCAP_URL = "https://api.coincap.io/v2/assets" CRYPTOCOMPARE_URL = "https://min-api.cryptocompare.com/data/pricemulti" def __init__(self, client: httpx.AsyncClient): self.client = client self._prices: Dict[str, Dict[str, Optional[float]]] = {} self._lock = asyncio.Lock() async def _fetch_pyth(self, ids: List[str]) -> Dict[str, float]: params = [("ids[]", f"0x{pid}") for pid in ids] try: resp = await self.client.get(self.PYTH_URL, params=params, timeout=10) resp.raise_for_status() id_map = {f"0x{v['pyth_id']}": k for k, v in ASSET_CONFIG.items()} return {id_map[item['id']]: int(item['price']['price']) / (10 ** abs(int(item['price']['expo']))) for item in resp.json() if item['id'] in id_map} except Exception as e: logger.error(f"❌ Oracle Error (Pyth): {e}") return {} async def _fetch_coingecko(self, ids: List[str]) -> Dict[str, float]: params = {"ids": ",".join(ids), "vs_currencies": "usd"} try: resp = await self.client.get(self.COINGECKO_URL, params=params, timeout=10) resp.raise_for_status() id_map = {v['coingecko_id']: k for k, v in ASSET_CONFIG.items()} return {id_map[cg_id]: prices['usd'] for cg_id, prices in resp.json().items()} except Exception as e: logger.error(f"❌ Oracle Error (CoinGecko): {e}") return {} async def _fetch_coincap(self, ids: List[str]) -> Dict[str, float]: # --- THE CRITICAL FIX IS HERE --- # CoinCap's multi-asset endpoint does not use 'ids', it returns all. We filter. try: resp = await self.client.get(self.COINCAP_URL, params={"limit": 200}, timeout=10) resp.raise_for_status() id_map = {v['coincap_id']: k for k, v in ASSET_CONFIG.items()} return {id_map[item['id']]: float(item['priceUsd']) for item in resp.json().get('data', []) if item['id'] in id_map} except Exception as e: logger.error(f"❌ Oracle Error (CoinCap): {e}") return {} async def _fetch_cryptocompare(self, ids: List[str]) -> Dict[str, float]: params = {"fsyms": ",".join(ids), "tsyms": "USD"} try: resp = await self.client.get(self.CRYPTOCOMPARE_URL, params=params, timeout=10) resp.raise_for_status() data = resp.json() id_map = {v['cryptocompare_id']: k for k, v in ASSET_CONFIG.items()} return {id_map[cc_id]: price_data['USD'] for cc_id, price_data in data.items() if 'USD' in price_data} except Exception as e: logger.error(f"❌ Oracle Error (CryptoCompare): {e}") return {} async def update_prices_async(self): # Prepare asset ID maps for each source pyth_ids = [v['pyth_id'] for v in ASSET_CONFIG.values()] coingecko_ids = [v['coingecko_id'] for v in ASSET_CONFIG.values()] coincap_ids = [v['coincap_id'] for v in ASSET_CONFIG.values()] cryptocompare_ids = [v['cryptocompare_id'] for v in ASSET_CONFIG.values()] tasks = [ self._fetch_pyth(pyth_ids), self._fetch_coingecko(coingecko_ids), self._fetch_coincap(coincap_ids), self._fetch_cryptocompare(cryptocompare_ids), ] pyth_prices, coingecko_prices, coincap_prices, cryptocompare_prices = await asyncio.gather(*tasks) async with self._lock: for symbol in ASSET_CONFIG.keys(): valid_agg_prices = [p for p in [coingecko_prices.get(symbol), coincap_prices.get(symbol), cryptocompare_prices.get(symbol)] if p] median_agg_price = statistics.median(valid_agg_prices) if valid_agg_prices else None self._prices[symbol] = { "pyth": pyth_prices.get(symbol), "chainlink_agg": median_agg_price } logger.info("✅ Multi-Source Prices Updated") def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]: return self._prices.copy()