Spaces:
Running
Running
Update app/price_fetcher.py
Browse files- app/price_fetcher.py +70 -84
app/price_fetcher.py
CHANGED
|
@@ -1,121 +1,107 @@
|
|
| 1 |
"""
|
| 2 |
A professional-grade, multi-asset, multi-oracle price engine.
|
| 3 |
-
This
|
| 4 |
-
|
| 5 |
-
v11.1: Corrected Pyth Network API endpoint.
|
| 6 |
"""
|
| 7 |
import asyncio
|
| 8 |
import logging
|
| 9 |
-
from typing import Dict, Optional
|
| 10 |
import httpx
|
|
|
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
# --- CONFIGURATION ---
|
| 15 |
ASSET_CONFIG = {
|
| 16 |
-
"BTC": {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
},
|
| 20 |
-
"
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
},
|
| 24 |
-
"
|
| 25 |
-
|
| 26 |
-
"coingecko_id": "solana",
|
| 27 |
-
},
|
| 28 |
-
"XRP": {
|
| 29 |
-
"pyth_id": "02a01e69981d314fd8a723be08253181e53b4945b4bf376d15a51980a37330c3",
|
| 30 |
-
"coingecko_id": "ripple",
|
| 31 |
-
},
|
| 32 |
-
"DOGE": {
|
| 33 |
-
"pyth_id": "042f02faf4229babc62635593855b6a383d6a4a2a1b9b9a7c385a4a50b86a345",
|
| 34 |
-
"coingecko_id": "dogecoin",
|
| 35 |
-
},
|
| 36 |
-
"ADA": {
|
| 37 |
-
"pyth_id": "34f544985c7943c093b5934963505a767f4749445244a852654c6017b28091ea",
|
| 38 |
-
"coingecko_id": "cardano",
|
| 39 |
-
},
|
| 40 |
-
"AVAX": {
|
| 41 |
-
"pyth_id": "0x141f2a3c34c8035443a01d64380b52207991b16c14c5145f617eb578a994753c",
|
| 42 |
-
"coingecko_id": "avalanche-2",
|
| 43 |
-
},
|
| 44 |
-
"LINK": {
|
| 45 |
-
"pyth_id": "0x63f4f4755a5a67c64c781d45763b33a72666a15e6b91c0fbdf3b2f205d5a6b01",
|
| 46 |
-
"coingecko_id": "chainlink",
|
| 47 |
-
},
|
| 48 |
-
"DOT": {
|
| 49 |
-
"pyth_id": "0x00a896677493a74421b33362a7447785b13612f0e340d418641a33716a5067a3",
|
| 50 |
-
"coingecko_id": "polkadot",
|
| 51 |
-
},
|
| 52 |
-
"MATIC": {
|
| 53 |
-
"pyth_id": "0x737ac3c13709b45da8128ff9e1058a984f86a048035656111b8a365e4921648a",
|
| 54 |
-
"coingecko_id": "matic-network",
|
| 55 |
-
},
|
| 56 |
}
|
| 57 |
|
| 58 |
class PriceFetcher:
|
| 59 |
-
# ====================================================================
|
| 60 |
-
# THE CRITICAL FIX IS HERE
|
| 61 |
-
# ====================================================================
|
| 62 |
-
# Using the new, correct endpoint for Pyth Network price feeds.
|
| 63 |
PYTH_URL = "https://hermes.pyth.network/v2/price_feeds"
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
AGGREGATOR_URL = "https://api.coingecko.com/api/v3/simple/price"
|
| 67 |
|
| 68 |
def __init__(self, client: httpx.AsyncClient):
|
| 69 |
self.client = client
|
| 70 |
self._prices: Dict[str, Dict[str, Optional[float]]] = {}
|
| 71 |
self._lock = asyncio.Lock()
|
| 72 |
|
| 73 |
-
async def
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
params = [("ids[]", f"0x{pid}") for pid in pyth_ids]
|
| 77 |
try:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
async def _fetch_aggregator_prices(self) -> Dict[str, float]:
|
| 94 |
-
coingecko_ids = ",".join([v['coingecko_id'] for v in ASSET_CONFIG.values()])
|
| 95 |
-
params = {"ids": coingecko_ids, "vs_currencies": "usd"}
|
| 96 |
-
try:
|
| 97 |
-
resp = await self.client.get(self.AGGREGATOR_URL, params=params, timeout=10)
|
| 98 |
-
resp.raise_for_status()
|
| 99 |
-
data = resp.json()
|
| 100 |
-
id_to_symbol = {v['coingecko_id']: k for k, v in ASSET_CONFIG.items()}
|
| 101 |
-
return {id_to_symbol[cg_id]: prices['usd'] for cg_id, prices in data.items()}
|
| 102 |
except Exception as e:
|
| 103 |
-
logger.error(f"❌ Oracle Error (
|
| 104 |
-
|
| 105 |
|
| 106 |
async def update_prices_async(self):
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
async with self._lock:
|
| 112 |
for symbol in ASSET_CONFIG.keys():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
self._prices[symbol] = {
|
| 114 |
"pyth": pyth_prices.get(symbol),
|
| 115 |
-
"chainlink_agg":
|
| 116 |
}
|
| 117 |
|
| 118 |
-
logger.info(f"✅ Multi-
|
| 119 |
|
| 120 |
def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]:
|
| 121 |
return self._prices.copy()
|
|
|
|
| 1 |
"""
|
| 2 |
A professional-grade, multi-asset, multi-oracle price engine.
|
| 3 |
+
v12.0 FINAL: This version uses three independent data sources and calculates a
|
| 4 |
+
resilient median price to be immune to single-source API failures.
|
|
|
|
| 5 |
"""
|
| 6 |
import asyncio
|
| 7 |
import logging
|
| 8 |
+
from typing import Dict, Optional, List
|
| 9 |
import httpx
|
| 10 |
+
import statistics
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
# --- CONFIGURATION ---
|
| 15 |
ASSET_CONFIG = {
|
| 16 |
+
"BTC": {"pyth_id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43", "coingecko_id": "bitcoin", "coincap_id": "bitcoin"},
|
| 17 |
+
"ETH": {"pyth_id": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", "coingecko_id": "ethereum", "coincap_id": "ethereum"},
|
| 18 |
+
"SOL": {"pyth_id": "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", "coingecko_id": "solana", "coincap_id": "solana"},
|
| 19 |
+
"XRP": {"pyth_id": "02a01e69981d314fd8a723be08253181e53b4945b4bf376d15a51980a37330c3", "coingecko_id": "ripple", "coincap_id": "xrp"},
|
| 20 |
+
"DOGE": {"pyth_id": "042f02faf4229babc62635593855b6a383d6a4a2a1b9b9a7c385a4a50b86a345", "coingecko_id": "dogecoin", "coincap_id": "dogecoin"},
|
| 21 |
+
"ADA": {"pyth_id": "34f544985c7943c093b5934963505a767f4749445244a852654c6017b28091ea", "coingecko_id": "cardano", "coincap_id": "cardano"},
|
| 22 |
+
"AVAX": {"pyth_id": "0x141f2a3c34c8035443a01d64380b52207991b16c14c5145f617eb578a994753c", "coingecko_id": "avalanche-2", "coincap_id": "avalanche"},
|
| 23 |
+
"LINK": {"pyth_id": "0x63f4f4755a5a67c64c781d45763b33a72666a15e6b91c0fbdf3b2f205d5a6b01", "coingecko_id": "chainlink", "coincap_id": "chainlink"},
|
| 24 |
+
"DOT": {"pyth_id": "0x00a896677493a74421b33362a7447785b13612f0e340d418641a33716a5067a3", "coingecko_id": "polkadot", "coincap_id": "polkadot"},
|
| 25 |
+
"MATIC": {"pyth_id": "0x737ac3c13709b45da8128ff9e1058a984f86a048035656111b8a365e4921648a", "coingecko_id": "matic-network", "coincap_id": "polygon"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
class PriceFetcher:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
PYTH_URL = "https://hermes.pyth.network/v2/price_feeds"
|
| 30 |
+
COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
|
| 31 |
+
COINCAP_URL = "https://api.coincap.io/v2/assets"
|
|
|
|
| 32 |
|
| 33 |
def __init__(self, client: httpx.AsyncClient):
|
| 34 |
self.client = client
|
| 35 |
self._prices: Dict[str, Dict[str, Optional[float]]] = {}
|
| 36 |
self._lock = asyncio.Lock()
|
| 37 |
|
| 38 |
+
async def _fetch_from_source(self, source_name: str, asset_ids: Dict[str, str]) -> Dict[str, float]:
|
| 39 |
+
"""A generic fetcher for all our sources."""
|
| 40 |
+
prices = {}
|
|
|
|
| 41 |
try:
|
| 42 |
+
if source_name == "pyth":
|
| 43 |
+
params = [("ids[]", f"0x{pid}") for pid in asset_ids.values()]
|
| 44 |
+
resp = await self.client.get(self.PYTH_URL, params=params, timeout=10)
|
| 45 |
+
resp.raise_for_status()
|
| 46 |
+
data = resp.json()
|
| 47 |
+
id_to_symbol = {f"0x{v}": k for k, v in asset_ids.items()}
|
| 48 |
+
for item in data:
|
| 49 |
+
price = item.get('price', {})
|
| 50 |
+
if price and 'price' in price:
|
| 51 |
+
prices[id_to_symbol[item['id']]] = int(price['price']) / (10 ** abs(int(price['expo'])))
|
| 52 |
|
| 53 |
+
elif source_name == "coingecko":
|
| 54 |
+
params = {"ids": ",".join(asset_ids.values()), "vs_currencies": "usd"}
|
| 55 |
+
resp = await self.client.get(self.COINGECKO_URL, params=params, timeout=10)
|
| 56 |
+
resp.raise_for_status()
|
| 57 |
+
data = resp.json()
|
| 58 |
+
id_to_symbol = {v: k for k, v in asset_ids.items()}
|
| 59 |
+
for cg_id, price_data in data.items():
|
| 60 |
+
prices[id_to_symbol[cg_id]] = price_data['usd']
|
| 61 |
+
|
| 62 |
+
elif source_name == "coincap":
|
| 63 |
+
params = {"ids": ",".join(asset_ids.values())}
|
| 64 |
+
resp = await self.client.get(self.COINCAP_URL, params=params, timeout=10)
|
| 65 |
+
resp.raise_for_status()
|
| 66 |
+
data = resp.json().get('data', [])
|
| 67 |
+
id_to_symbol = {v: k for k, v in asset_ids.items()}
|
| 68 |
+
for item in data:
|
| 69 |
+
prices[id_to_symbol[item['id']]] = float(item['priceUsd'])
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
except Exception as e:
|
| 72 |
+
logger.error(f"❌ Oracle Error ({source_name}): {e}")
|
| 73 |
+
return prices
|
| 74 |
|
| 75 |
async def update_prices_async(self):
|
| 76 |
+
# Prepare asset ID maps for each source
|
| 77 |
+
pyth_ids = {k: v['pyth_id'] for k, v in ASSET_CONFIG.items()}
|
| 78 |
+
coingecko_ids = {k: v['coingecko_id'] for k, v in ASSET_CONFIG.items()}
|
| 79 |
+
coincap_ids = {k: v['coincap_id'] for k, v in ASSET_CONFIG.items()}
|
| 80 |
+
|
| 81 |
+
# Fetch all sources concurrently
|
| 82 |
+
tasks = [
|
| 83 |
+
self._fetch_from_source("pyth", pyth_ids),
|
| 84 |
+
self._fetch_from_source("coingecko", coingecko_ids),
|
| 85 |
+
self._fetch_from_source("coincap", coincap_ids),
|
| 86 |
+
]
|
| 87 |
+
pyth_prices, coingecko_prices, coincap_prices = await asyncio.gather(*tasks)
|
| 88 |
|
| 89 |
async with self._lock:
|
| 90 |
for symbol in ASSET_CONFIG.keys():
|
| 91 |
+
# Get a list of all valid prices for the current asset
|
| 92 |
+
valid_agg_prices = [
|
| 93 |
+
p for p in [coingecko_prices.get(symbol), coincap_prices.get(symbol)] if p is not None
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
# Calculate a resilient median price for the off-chain aggregators
|
| 97 |
+
median_agg_price = statistics.median(valid_agg_prices) if valid_agg_prices else None
|
| 98 |
+
|
| 99 |
self._prices[symbol] = {
|
| 100 |
"pyth": pyth_prices.get(symbol),
|
| 101 |
+
"chainlink_agg": median_agg_price
|
| 102 |
}
|
| 103 |
|
| 104 |
+
logger.info(f"✅ Multi-Source Prices Updated")
|
| 105 |
|
| 106 |
def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]:
|
| 107 |
return self._prices.copy()
|