coingecko.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """
  2. CoinGecko data provider.
  3. No API key required for basic endpoints.
  4. """
  5. import time
  6. import httpx
  7. from config import COINGECKO_BASE_URL, SYMBOL_TO_COINGECKO_ID
  8. from errors import SymbolNotFoundError, ProviderError
  9. def _resolve_coingecko_id(symbol: str) -> str:
  10. sym = symbol.upper()
  11. cg_id = SYMBOL_TO_COINGECKO_ID.get(sym)
  12. if not cg_id:
  13. raise SymbolNotFoundError(f"Unknown symbol: {symbol}. Not in CoinGecko map.")
  14. return cg_id
  15. async def fetch_price(symbol: str) -> dict:
  16. cg_id = _resolve_coingecko_id(symbol)
  17. url = f"{COINGECKO_BASE_URL}/simple/price"
  18. params = {"ids": cg_id, "vs_currencies": "usd", "include_last_updated_at": "true"}
  19. async with httpx.AsyncClient(timeout=10.0) as client:
  20. try:
  21. resp = await client.get(url, params=params)
  22. resp.raise_for_status()
  23. except httpx.HTTPStatusError as e:
  24. raise ProviderError(f"CoinGecko HTTP error: {e.response.status_code}") from e
  25. except httpx.RequestError as e:
  26. raise ProviderError(f"CoinGecko request failed: {e}") from e
  27. data = resp.json()
  28. if cg_id not in data:
  29. raise SymbolNotFoundError(f"CoinGecko returned no data for {symbol}")
  30. entry = data[cg_id]
  31. return {"symbol": symbol.upper(), "price": float(entry["usd"]), "timestamp": int(entry.get("last_updated_at", time.time()))}
  32. async def fetch_ohlcv(symbol: str, timeframe: str, limit: int = 100) -> dict:
  33. cg_id = _resolve_coingecko_id(symbol)
  34. days_map = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 14, "1d": 90}
  35. days = days_map.get(timeframe, 7)
  36. url = f"{COINGECKO_BASE_URL}/coins/{cg_id}/ohlc"
  37. params = {"vs_currency": "usd", "days": days}
  38. async with httpx.AsyncClient(timeout=15.0) as client:
  39. try:
  40. resp = await client.get(url, params=params)
  41. resp.raise_for_status()
  42. except httpx.HTTPStatusError as e:
  43. raise ProviderError(f"CoinGecko OHLC HTTP error: {e.response.status_code}") from e
  44. except httpx.RequestError as e:
  45. raise ProviderError(f"CoinGecko OHLC request failed: {e}") from e
  46. raw = resp.json()
  47. if not raw:
  48. raise ProviderError(f"CoinGecko returned empty OHLCV for {symbol}")
  49. candles = [[int(row[0] / 1000), float(row[1]), float(row[2]), float(row[3]), float(row[4]), 0.0] for row in raw]
  50. return {"symbol": symbol.upper(), "timeframe": timeframe, "candles": candles[-limit:]}
  51. async def fetch_top_movers(limit: int = 10) -> dict:
  52. url = f"{COINGECKO_BASE_URL}/coins/markets"
  53. params = {"vs_currency": "usd", "order": "market_cap_desc", "per_page": 100, "page": 1, "price_change_percentage": "24h", "sparkline": "false"}
  54. async with httpx.AsyncClient(timeout=15.0) as client:
  55. try:
  56. resp = await client.get(url, params=params)
  57. resp.raise_for_status()
  58. except httpx.HTTPStatusError as e:
  59. raise ProviderError(f"CoinGecko markets HTTP error: {e.response.status_code}") from e
  60. except httpx.RequestError as e:
  61. raise ProviderError(f"CoinGecko markets request failed: {e}") from e
  62. coins = resp.json()
  63. def _format(coin: dict) -> dict:
  64. return {"symbol": coin["symbol"].upper(), "name": coin["name"], "price": coin.get("current_price"), "change_24h_pct": coin.get("price_change_percentage_24h"), "market_cap": coin.get("market_cap")}
  65. sorted_by_change = sorted([c for c in coins if c.get("price_change_percentage_24h") is not None], key=lambda c: c["price_change_percentage_24h"], reverse=True)
  66. return {"gainers": [_format(c) for c in sorted_by_change[:limit]], "losers": [_format(c) for c in sorted_by_change[-limit:][::-1]]}