mgbam commited on
Commit
fcfadb5
Β·
verified Β·
1 Parent(s): c4fd2ca

Update app/price_fetcher.py

Browse files
Files changed (1) hide show
  1. 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
- 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
@@ -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 _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] = {
@@ -101,7 +108,7 @@ class PriceFetcher:
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()
 
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()