mgbam commited on
Commit
3bb8a2f
Β·
verified Β·
1 Parent(s): 2aa11ed

Update app/price_fetcher.py

Browse files
Files changed (1) hide show
  1. app/price_fetcher.py +59 -26
app/price_fetcher.py CHANGED
@@ -1,62 +1,95 @@
1
  """
2
- A high-availability price fetcher using a decentralized oracle (Pyth)
3
- and a reliable data aggregator to bypass geoblocking.
4
  """
5
  import asyncio
6
  import logging
7
- from typing import Dict, Optional
8
  import httpx
 
 
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
  class PriceFetcher:
13
- # Pyth provides real-time, on-chain prices. We will use their public API.
14
- # We will fetch BTC/USD.
15
  PYTH_URL = "https://hermes.pyth.network/v2/updates/price/latest?ids[]=e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43"
16
 
17
- # A reliable aggregator that is not typically geoblocked.
18
- AGGREGATOR_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
 
 
 
 
 
 
19
 
20
  def __init__(self, client: httpx.AsyncClient):
21
  self.client = client
22
  self._prices: Dict[str, Optional[float]] = {"on_chain_pyth": None, "off_chain_agg": None}
23
  self._lock = asyncio.Lock()
 
24
 
25
  async def _fetch_pyth(self) -> Optional[float]:
26
  try:
27
  resp = await self.client.get(self.PYTH_URL, timeout=5)
28
  resp.raise_for_status()
29
- data = resp.json()
30
- # The price is in the 'parsed' part of the response
31
- price_data = data['parsed'][0]['price']
32
- # Price is given with an exponent, e.g., price=119123, expo=-2 -> 1191.23
33
- # But for BTC/USD, the expo is -8, and price is in sats. We need to adjust.
34
- price = int(price_data['price']) / (10 ** abs(int(price_data['expo'])))
35
- return price
36
- except Exception as e:
37
- logger.error(f"❌ Failed to fetch from Pyth: {e}")
38
- return None
39
 
40
- async def _fetch_aggregator(self) -> Optional[float]:
41
  try:
42
- resp = await self.client.get(self.AGGREGATOR_URL, timeout=5)
 
 
 
 
 
 
 
 
43
  resp.raise_for_status()
44
- return float(resp.json()['bitcoin']['usd'])
 
 
 
 
45
  except Exception as e:
46
- logger.error(f"❌ Failed to fetch from Aggregator: {e}")
47
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  async def update_prices_async(self):
 
50
  pyth_task = self._fetch_pyth()
51
- agg_task = self._fetch_aggregator()
 
 
 
 
 
52
 
53
- pyth_price, agg_price = await asyncio.gather(pyth_task, agg_task)
54
 
55
  async with self._lock:
56
  self._prices["on_chain_pyth"] = pyth_price
57
- self._prices["off_chain_agg"] = agg_price
58
 
59
- logger.info(f"βœ… Prices updated: {self._prices}")
60
 
61
  def get_current_prices(self) -> Dict[str, Optional[float]]:
62
  return self._prices.copy()
 
1
  """
2
+ A high-throughput, fault-tolerant, multi-source price fetcher engine.
3
+ Designed to bypass rate-limiting and ensure constant data flow.
4
  """
5
  import asyncio
6
  import logging
7
+ from typing import Dict, Optional, List
8
  import httpx
9
+ import os
10
+ import statistics
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
  class PriceFetcher:
15
+ # Pyth for our on-chain data
 
16
  PYTH_URL = "https://hermes.pyth.network/v2/updates/price/latest?ids[]=e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415B43"
17
 
18
+ # A list of our off-chain aggregator sources
19
+ AGGREGATOR_SOURCES = {
20
+ "coingecko": "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
21
+ "coincap": "https://api.coincap.io/v2/assets/bitcoin",
22
+ }
23
+
24
+ # Use Pro API if a key is provided as a secret
25
+ COINGECKO_PRO_URL = "https://pro-api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
26
 
27
  def __init__(self, client: httpx.AsyncClient):
28
  self.client = client
29
  self._prices: Dict[str, Optional[float]] = {"on_chain_pyth": None, "off_chain_agg": None}
30
  self._lock = asyncio.Lock()
31
+ self.coingecko_api_key = os.getenv("COINGECKO_API_KEY")
32
 
33
  async def _fetch_pyth(self) -> Optional[float]:
34
  try:
35
  resp = await self.client.get(self.PYTH_URL, timeout=5)
36
  resp.raise_for_status()
37
+ price_data = resp.json()['parsed'][0]['price']
38
+ return int(price_data['price']) / (10 ** abs(int(price_data['expo'])))
39
+ except Exception:
40
+ return None # Fail silently, we have other sources
 
 
 
 
 
 
41
 
42
+ async def _fetch_aggregator(self, name: str, url: str) -> Optional[float]:
43
  try:
44
+ headers = {}
45
+ if name == "coingecko" and self.coingecko_api_key:
46
+ url = self.COINGECKO_PRO_URL
47
+ headers["X-Cg-Pro-Api-Key"] = self.coingecko_api_key
48
+
49
+ resp = await self.client.get(url, timeout=5, headers=headers)
50
+ if resp.status_code == 429: # Explicitly handle rate limiting
51
+ logger.warning(f"⚠️ Rate limited by {name}. Skipping.")
52
+ return None
53
  resp.raise_for_status()
54
+ data = resp.json()
55
+
56
+ if name == "coingecko": return float(data['bitcoin']['usd'])
57
+ if name == "coincap": return float(data['data']['priceUsd'])
58
+
59
  except Exception as e:
60
+ logger.error(f"❌ Failed to fetch from {name}: {e}")
61
+ return None
62
+
63
+ def _get_sane_average(self, prices: List[float]) -> Optional[float]:
64
+ """Calculates the average price, discarding outliers."""
65
+ if not prices: return None
66
+ if len(prices) < 3: return statistics.mean(prices)
67
+
68
+ # For 3 or more sources, discard any price that is more than 2%
69
+ # different from the median, then average the rest.
70
+ median = statistics.median(prices)
71
+ sane_prices = [p for p in prices if abs(p - median) / median < 0.02]
72
+
73
+ if not sane_prices: return median # Fallback to median if all are outliers
74
+ return statistics.mean(sane_prices)
75
 
76
  async def update_prices_async(self):
77
+ # Fetch all sources concurrently
78
  pyth_task = self._fetch_pyth()
79
+ agg_tasks = [self._fetch_aggregator(name, url) for name, url in self.AGGREGATOR_SOURCES.items()]
80
+
81
+ results = await asyncio.gather(pyth_task, *agg_tasks)
82
+
83
+ pyth_price = results[0]
84
+ agg_prices = [p for p in results[1:] if p is not None]
85
 
86
+ sane_agg_price = self._get_sane_average(agg_prices)
87
 
88
  async with self._lock:
89
  self._prices["on_chain_pyth"] = pyth_price
90
+ self._prices["off_chain_agg"] = sane_agg_price
91
 
92
+ logger.info(f"βœ… Prices updated: On-Chain={pyth_price}, Off-Chain Agg={sane_agg_price} (from {len(agg_prices)} sources)")
93
 
94
  def get_current_prices(self) -> Dict[str, Optional[float]]:
95
  return self._prices.copy()