Spaces:
Running
Running
Update app/price_fetcher.py
Browse files- app/price_fetcher.py +66 -59
app/price_fetcher.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
"""
|
2 |
A professional-grade, multi-asset, multi-oracle price engine.
|
3 |
-
|
4 |
-
resilient median price to be immune to single-source API failures.
|
|
|
5 |
"""
|
6 |
import asyncio
|
7 |
import logging
|
@@ -13,87 +14,93 @@ 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
|
39 |
-
"""
|
40 |
-
prices = {}
|
41 |
try:
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
prices[id_to_symbol[cg_id]] = price_data['usd']
|
61 |
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
except Exception as e:
|
72 |
-
logger.error(f"β Oracle Error (
|
73 |
-
|
74 |
|
75 |
async def update_prices_async(self):
|
76 |
# Prepare asset ID maps for each source
|
77 |
-
pyth_ids =
|
78 |
-
coingecko_ids =
|
79 |
-
coincap_ids =
|
|
|
80 |
|
81 |
-
# Fetch all sources concurrently
|
82 |
tasks = [
|
83 |
-
self.
|
84 |
-
self.
|
85 |
-
self.
|
|
|
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 |
-
|
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] = {
|
@@ -101,7 +108,7 @@ class PriceFetcher:
|
|
101 |
"chainlink_agg": median_agg_price
|
102 |
}
|
103 |
|
104 |
-
logger.info(
|
105 |
|
106 |
def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]:
|
107 |
return self._prices.copy()
|
|
|
1 |
"""
|
2 |
A professional-grade, multi-asset, multi-oracle price engine.
|
3 |
+
v13.0 FINAL: This version uses FOUR independent data sources and calculates a
|
4 |
+
resilient median price to be immune to single-source API failures. This is the
|
5 |
+
most robust and definitive version.
|
6 |
"""
|
7 |
import asyncio
|
8 |
import logging
|
|
|
14 |
|
15 |
# --- CONFIGURATION ---
|
16 |
ASSET_CONFIG = {
|
17 |
+
"BTC": {"pyth_id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43", "coingecko_id": "bitcoin", "coincap_id": "bitcoin", "cryptocompare_id": "BTC"},
|
18 |
+
"ETH": {"pyth_id": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", "coingecko_id": "ethereum", "coincap_id": "ethereum", "cryptocompare_id": "ETH"},
|
19 |
+
"SOL": {"pyth_id": "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", "coingecko_id": "solana", "coincap_id": "solana", "cryptocompare_id": "SOL"},
|
20 |
+
"XRP": {"pyth_id": "02a01e69981d314fd8a723be08253181e53b4945b4bf376d15a51980a37330c3", "coingecko_id": "ripple", "coincap_id": "xrp", "cryptocompare_id": "XRP"},
|
21 |
+
"DOGE": {"pyth_id": "042f02faf4229babc62635593855b6a383d6a4a2a1b9b9a7c385a4a50b86a345", "coingecko_id": "dogecoin", "coincap_id": "dogecoin", "cryptocompare_id": "DOGE"},
|
22 |
+
"ADA": {"pyth_id": "34f544985c7943c093b5934963505a767f4749445244a852654c6017b28091ea", "coingecko_id": "cardano", "coincap_id": "cardano", "cryptocompare_id": "ADA"},
|
23 |
+
"AVAX": {"pyth_id": "0x141f2a3c34c8035443a01d64380b52207991b16c14c5145f617eb578a994753c", "coingecko_id": "avalanche-2", "coincap_id": "avalanche", "cryptocompare_id": "AVAX"},
|
24 |
+
"LINK": {"pyth_id": "0x63f4f4755a5a67c64c781d45763b33a72666a15e6b91c0fbdf3b2f205d5a6b01", "coingecko_id": "chainlink", "coincap_id": "chainlink", "cryptocompare_id": "LINK"},
|
25 |
+
"DOT": {"pyth_id": "0x00a896677493a74421b33362a7447785b13612f0e340d418641a33716a5067a3", "coingecko_id": "polkadot", "coincap_id": "polkadot", "cryptocompare_id": "DOT"},
|
26 |
+
"MATIC": {"pyth_id": "0x737ac3c13709b45da8128ff9e1058a984f86a048035656111b8a365e4921648a", "coingecko_id": "matic-network", "coincap_id": "polygon", "cryptocompare_id": "MATIC"},
|
27 |
}
|
28 |
|
29 |
class PriceFetcher:
|
30 |
PYTH_URL = "https://hermes.pyth.network/v2/price_feeds"
|
31 |
COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
|
32 |
COINCAP_URL = "https://api.coincap.io/v2/assets"
|
33 |
+
CRYPTOCOMPARE_URL = "https://min-api.cryptocompare.com/data/pricemulti"
|
34 |
|
35 |
def __init__(self, client: httpx.AsyncClient):
|
36 |
self.client = client
|
37 |
self._prices: Dict[str, Dict[str, Optional[float]]] = {}
|
38 |
self._lock = asyncio.Lock()
|
39 |
|
40 |
+
async def _fetch_pyth(self, ids: List[str]) -> Dict[str, float]:
|
41 |
+
params = [("ids[]", f"0x{pid}") for pid in ids]
|
|
|
42 |
try:
|
43 |
+
resp = await self.client.get(self.PYTH_URL, params=params, timeout=10)
|
44 |
+
resp.raise_for_status()
|
45 |
+
id_map = {f"0x{v['pyth_id']}": k for k, v in ASSET_CONFIG.items()}
|
46 |
+
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}
|
47 |
+
except Exception as e:
|
48 |
+
logger.error(f"β Oracle Error (Pyth): {e}")
|
49 |
+
return {}
|
50 |
+
|
51 |
+
async def _fetch_coingecko(self, ids: List[str]) -> Dict[str, float]:
|
52 |
+
params = {"ids": ",".join(ids), "vs_currencies": "usd"}
|
53 |
+
try:
|
54 |
+
resp = await self.client.get(self.COINGECKO_URL, params=params, timeout=10)
|
55 |
+
resp.raise_for_status()
|
56 |
+
id_map = {v['coingecko_id']: k for k, v in ASSET_CONFIG.items()}
|
57 |
+
return {id_map[cg_id]: prices['usd'] for cg_id, prices in resp.json().items()}
|
58 |
+
except Exception as e:
|
59 |
+
logger.error(f"β Oracle Error (CoinGecko): {e}")
|
60 |
+
return {}
|
|
|
61 |
|
62 |
+
async def _fetch_coincap(self, ids: List[str]) -> Dict[str, float]:
|
63 |
+
# --- THE CRITICAL FIX IS HERE ---
|
64 |
+
# CoinCap's multi-asset endpoint does not use 'ids', it returns all. We filter.
|
65 |
+
try:
|
66 |
+
resp = await self.client.get(self.COINCAP_URL, params={"limit": 200}, timeout=10)
|
67 |
+
resp.raise_for_status()
|
68 |
+
id_map = {v['coincap_id']: k for k, v in ASSET_CONFIG.items()}
|
69 |
+
return {id_map[item['id']]: float(item['priceUsd']) for item in resp.json().get('data', []) if item['id'] in id_map}
|
70 |
+
except Exception as e:
|
71 |
+
logger.error(f"β Oracle Error (CoinCap): {e}")
|
72 |
+
return {}
|
73 |
|
74 |
+
async def _fetch_cryptocompare(self, ids: List[str]) -> Dict[str, float]:
|
75 |
+
params = {"fsyms": ",".join(ids), "tsyms": "USD"}
|
76 |
+
try:
|
77 |
+
resp = await self.client.get(self.CRYPTOCOMPARE_URL, params=params, timeout=10)
|
78 |
+
resp.raise_for_status()
|
79 |
+
data = resp.json()
|
80 |
+
id_map = {v['cryptocompare_id']: k for k, v in ASSET_CONFIG.items()}
|
81 |
+
return {id_map[cc_id]: price_data['USD'] for cc_id, price_data in data.items() if 'USD' in price_data}
|
82 |
except Exception as e:
|
83 |
+
logger.error(f"β Oracle Error (CryptoCompare): {e}")
|
84 |
+
return {}
|
85 |
|
86 |
async def update_prices_async(self):
|
87 |
# Prepare asset ID maps for each source
|
88 |
+
pyth_ids = [v['pyth_id'] for v in ASSET_CONFIG.values()]
|
89 |
+
coingecko_ids = [v['coingecko_id'] for v in ASSET_CONFIG.values()]
|
90 |
+
coincap_ids = [v['coincap_id'] for v in ASSET_CONFIG.values()]
|
91 |
+
cryptocompare_ids = [v['cryptocompare_id'] for v in ASSET_CONFIG.values()]
|
92 |
|
|
|
93 |
tasks = [
|
94 |
+
self._fetch_pyth(pyth_ids),
|
95 |
+
self._fetch_coingecko(coingecko_ids),
|
96 |
+
self._fetch_coincap(coincap_ids),
|
97 |
+
self._fetch_cryptocompare(cryptocompare_ids),
|
98 |
]
|
99 |
+
pyth_prices, coingecko_prices, coincap_prices, cryptocompare_prices = await asyncio.gather(*tasks)
|
100 |
|
101 |
async with self._lock:
|
102 |
for symbol in ASSET_CONFIG.keys():
|
103 |
+
valid_agg_prices = [p for p in [coingecko_prices.get(symbol), coincap_prices.get(symbol), cryptocompare_prices.get(symbol)] if p]
|
|
|
|
|
|
|
|
|
|
|
104 |
median_agg_price = statistics.median(valid_agg_prices) if valid_agg_prices else None
|
105 |
|
106 |
self._prices[symbol] = {
|
|
|
108 |
"chainlink_agg": median_agg_price
|
109 |
}
|
110 |
|
111 |
+
logger.info("β
Multi-Source Prices Updated")
|
112 |
|
113 |
def get_all_prices(self) -> Dict[str, Dict[str, Optional[float]]]:
|
114 |
return self._prices.copy()
|