| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- """
- CoinGecko data provider.
- No API key required for basic endpoints.
- """
- import time
- import httpx
- from config import COINGECKO_BASE_URL, SYMBOL_TO_COINGECKO_ID
- from errors import SymbolNotFoundError, ProviderError
- def _resolve_coingecko_id(symbol: str) -> str:
- sym = symbol.upper()
- cg_id = SYMBOL_TO_COINGECKO_ID.get(sym)
- if not cg_id:
- raise SymbolNotFoundError(f"Unknown symbol: {symbol}. Not in CoinGecko map.")
- return cg_id
- async def fetch_price(symbol: str) -> dict:
- cg_id = _resolve_coingecko_id(symbol)
- url = f"{COINGECKO_BASE_URL}/simple/price"
- params = {"ids": cg_id, "vs_currencies": "usd", "include_last_updated_at": "true"}
- async with httpx.AsyncClient(timeout=10.0) as client:
- try:
- resp = await client.get(url, params=params)
- resp.raise_for_status()
- except httpx.HTTPStatusError as e:
- raise ProviderError(f"CoinGecko HTTP error: {e.response.status_code}") from e
- except httpx.RequestError as e:
- raise ProviderError(f"CoinGecko request failed: {e}") from e
- data = resp.json()
- if cg_id not in data:
- raise SymbolNotFoundError(f"CoinGecko returned no data for {symbol}")
- entry = data[cg_id]
- return {"symbol": symbol.upper(), "price": float(entry["usd"]), "timestamp": int(entry.get("last_updated_at", time.time()))}
- async def fetch_ohlcv(symbol: str, timeframe: str, limit: int = 100) -> dict:
- cg_id = _resolve_coingecko_id(symbol)
- days_map = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 14, "1d": 90}
- days = days_map.get(timeframe, 7)
- url = f"{COINGECKO_BASE_URL}/coins/{cg_id}/ohlc"
- params = {"vs_currency": "usd", "days": days}
- async with httpx.AsyncClient(timeout=15.0) as client:
- try:
- resp = await client.get(url, params=params)
- resp.raise_for_status()
- except httpx.HTTPStatusError as e:
- raise ProviderError(f"CoinGecko OHLC HTTP error: {e.response.status_code}") from e
- except httpx.RequestError as e:
- raise ProviderError(f"CoinGecko OHLC request failed: {e}") from e
- raw = resp.json()
- if not raw:
- raise ProviderError(f"CoinGecko returned empty OHLCV for {symbol}")
- candles = [[int(row[0] / 1000), float(row[1]), float(row[2]), float(row[3]), float(row[4]), 0.0] for row in raw]
- return {"symbol": symbol.upper(), "timeframe": timeframe, "candles": candles[-limit:]}
- async def fetch_top_movers(limit: int = 10) -> dict:
- url = f"{COINGECKO_BASE_URL}/coins/markets"
- params = {"vs_currency": "usd", "order": "market_cap_desc", "per_page": 100, "page": 1, "price_change_percentage": "24h", "sparkline": "false"}
- async with httpx.AsyncClient(timeout=15.0) as client:
- try:
- resp = await client.get(url, params=params)
- resp.raise_for_status()
- except httpx.HTTPStatusError as e:
- raise ProviderError(f"CoinGecko markets HTTP error: {e.response.status_code}") from e
- except httpx.RequestError as e:
- raise ProviderError(f"CoinGecko markets request failed: {e}") from e
- coins = resp.json()
- def _format(coin: dict) -> dict:
- 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")}
- 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)
- return {"gainers": [_format(c) for c in sorted_by_change[:limit]], "losers": [_format(c) for c in sorted_by_change[-limit:][::-1]]}
|