|
|
|
|
|
"""
|
|
|
CoinGecko API Client - REAL DATA ONLY
|
|
|
Fetches real cryptocurrency market data from CoinGecko
|
|
|
NO MOCK DATA - All data from live CoinGecko API
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
import logging
|
|
|
from typing import Dict, Any, List, Optional
|
|
|
from datetime import datetime
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class CoinGeckoClient:
|
|
|
"""
|
|
|
Real CoinGecko API Client
|
|
|
Primary source for real-time cryptocurrency market prices
|
|
|
"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.base_url = "https://api.coingecko.com/api/v3"
|
|
|
self.timeout = 15.0
|
|
|
|
|
|
|
|
|
self.symbol_to_id = {
|
|
|
"BTC": "bitcoin",
|
|
|
"ETH": "ethereum",
|
|
|
"BNB": "binancecoin",
|
|
|
"XRP": "ripple",
|
|
|
"ADA": "cardano",
|
|
|
"DOGE": "dogecoin",
|
|
|
"SOL": "solana",
|
|
|
"TRX": "tron",
|
|
|
"DOT": "polkadot",
|
|
|
"MATIC": "matic-network",
|
|
|
"LTC": "litecoin",
|
|
|
"SHIB": "shiba-inu",
|
|
|
"AVAX": "avalanche-2",
|
|
|
"UNI": "uniswap",
|
|
|
"LINK": "chainlink",
|
|
|
"ATOM": "cosmos",
|
|
|
"XLM": "stellar",
|
|
|
"ETC": "ethereum-classic",
|
|
|
"XMR": "monero",
|
|
|
"BCH": "bitcoin-cash"
|
|
|
}
|
|
|
|
|
|
|
|
|
self.id_to_symbol = {v: k for k, v in self.symbol_to_id.items()}
|
|
|
|
|
|
def _symbol_to_coingecko_id(self, symbol: str) -> str:
|
|
|
"""Convert crypto symbol to CoinGecko coin ID"""
|
|
|
symbol = symbol.upper().replace("USDT", "").replace("USD", "")
|
|
|
return self.symbol_to_id.get(symbol, symbol.lower())
|
|
|
|
|
|
def _coingecko_id_to_symbol(self, coin_id: str) -> str:
|
|
|
"""Convert CoinGecko coin ID to symbol"""
|
|
|
return self.id_to_symbol.get(coin_id, coin_id.upper())
|
|
|
|
|
|
async def get_market_prices(
|
|
|
self,
|
|
|
symbols: Optional[List[str]] = None,
|
|
|
limit: int = 100
|
|
|
) -> List[Dict[str, Any]]:
|
|
|
"""
|
|
|
Fetch REAL market prices from CoinGecko
|
|
|
|
|
|
Args:
|
|
|
symbols: List of crypto symbols (e.g., ["BTC", "ETH"])
|
|
|
limit: Maximum number of results
|
|
|
|
|
|
Returns:
|
|
|
List of real market data
|
|
|
"""
|
|
|
try:
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
|
if symbols:
|
|
|
|
|
|
coin_ids = [self._symbol_to_coingecko_id(s) for s in symbols]
|
|
|
|
|
|
response = await client.get(
|
|
|
f"{self.base_url}/simple/price",
|
|
|
params={
|
|
|
"ids": ",".join(coin_ids),
|
|
|
"vs_currencies": "usd",
|
|
|
"include_24hr_change": "true",
|
|
|
"include_24hr_vol": "true",
|
|
|
"include_market_cap": "true"
|
|
|
}
|
|
|
)
|
|
|
response.raise_for_status()
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
prices = []
|
|
|
for coin_id, coin_data in data.items():
|
|
|
symbol = self._coingecko_id_to_symbol(coin_id)
|
|
|
prices.append({
|
|
|
"symbol": symbol,
|
|
|
"name": symbol,
|
|
|
"price": coin_data.get("usd", 0),
|
|
|
"change24h": coin_data.get("usd_24h_change", 0),
|
|
|
"changePercent24h": coin_data.get("usd_24h_change", 0),
|
|
|
"volume24h": coin_data.get("usd_24h_vol", 0),
|
|
|
"marketCap": coin_data.get("usd_market_cap", 0),
|
|
|
"source": "coingecko",
|
|
|
"timestamp": int(datetime.utcnow().timestamp() * 1000)
|
|
|
})
|
|
|
|
|
|
logger.info(f"✅ CoinGecko: Fetched {len(prices)} real prices for specific symbols")
|
|
|
return prices
|
|
|
|
|
|
else:
|
|
|
|
|
|
response = await client.get(
|
|
|
f"{self.base_url}/coins/markets",
|
|
|
params={
|
|
|
"vs_currency": "usd",
|
|
|
"order": "market_cap_desc",
|
|
|
"per_page": min(limit, 250),
|
|
|
"page": 1,
|
|
|
"sparkline": "false",
|
|
|
"price_change_percentage": "24h"
|
|
|
}
|
|
|
)
|
|
|
response.raise_for_status()
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
prices = []
|
|
|
for coin in data:
|
|
|
prices.append({
|
|
|
"symbol": coin.get("symbol", "").upper(),
|
|
|
"name": coin.get("name", ""),
|
|
|
"price": coin.get("current_price", 0),
|
|
|
"change24h": coin.get("price_change_24h", 0),
|
|
|
"changePercent24h": coin.get("price_change_percentage_24h", 0),
|
|
|
"volume24h": coin.get("total_volume", 0),
|
|
|
"marketCap": coin.get("market_cap", 0),
|
|
|
"source": "coingecko",
|
|
|
"timestamp": int(datetime.utcnow().timestamp() * 1000)
|
|
|
})
|
|
|
|
|
|
logger.info(f"✅ CoinGecko: Fetched {len(prices)} real market prices")
|
|
|
return prices
|
|
|
|
|
|
except httpx.HTTPError as e:
|
|
|
logger.error(f"❌ CoinGecko API HTTP error: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"CoinGecko API temporarily unavailable: {str(e)}"
|
|
|
)
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ CoinGecko API failed: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"Failed to fetch real market data from CoinGecko: {str(e)}"
|
|
|
)
|
|
|
|
|
|
async def get_ohlcv(self, symbol: str, days: int = 7) -> Dict[str, Any]:
|
|
|
"""
|
|
|
Fetch REAL OHLCV (price history) data from CoinGecko
|
|
|
|
|
|
Args:
|
|
|
symbol: Cryptocurrency symbol (e.g., "BTC", "ETH")
|
|
|
days: Number of days of historical data (1, 7, 14, 30, 90, 180, 365, max)
|
|
|
|
|
|
Returns:
|
|
|
Dict with OHLCV data
|
|
|
"""
|
|
|
try:
|
|
|
coin_id = self._symbol_to_coingecko_id(symbol)
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
|
|
|
|
response = await client.get(
|
|
|
f"{self.base_url}/coins/{coin_id}/market_chart",
|
|
|
params={
|
|
|
"vs_currency": "usd",
|
|
|
"days": str(days),
|
|
|
"interval": "daily" if days > 1 else "hourly"
|
|
|
}
|
|
|
)
|
|
|
response.raise_for_status()
|
|
|
data = response.json()
|
|
|
|
|
|
logger.info(f"✅ CoinGecko: Fetched {days} days of OHLCV data for {symbol}")
|
|
|
return data
|
|
|
|
|
|
except httpx.HTTPError as e:
|
|
|
logger.error(f"❌ CoinGecko OHLCV API HTTP error: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"CoinGecko OHLCV API unavailable: {str(e)}"
|
|
|
)
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ CoinGecko OHLCV API failed: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"Failed to fetch OHLCV data from CoinGecko: {str(e)}"
|
|
|
)
|
|
|
|
|
|
async def get_trending_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
|
"""
|
|
|
Fetch REAL trending coins from CoinGecko
|
|
|
|
|
|
Returns:
|
|
|
List of real trending coins
|
|
|
"""
|
|
|
try:
|
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
|
|
|
|
response = await client.get(f"{self.base_url}/search/trending")
|
|
|
response.raise_for_status()
|
|
|
data = response.json()
|
|
|
|
|
|
trending = []
|
|
|
coins = data.get("coins", [])[:limit]
|
|
|
|
|
|
|
|
|
if coins:
|
|
|
coin_ids = [coin["item"]["id"] for coin in coins]
|
|
|
|
|
|
|
|
|
price_response = await client.get(
|
|
|
f"{self.base_url}/simple/price",
|
|
|
params={
|
|
|
"ids": ",".join(coin_ids),
|
|
|
"vs_currencies": "usd",
|
|
|
"include_24hr_change": "true"
|
|
|
}
|
|
|
)
|
|
|
price_response.raise_for_status()
|
|
|
price_data = price_response.json()
|
|
|
|
|
|
for idx, coin_obj in enumerate(coins):
|
|
|
coin = coin_obj["item"]
|
|
|
coin_id = coin["id"]
|
|
|
prices = price_data.get(coin_id, {})
|
|
|
|
|
|
trending.append({
|
|
|
"symbol": coin.get("symbol", "").upper(),
|
|
|
"name": coin.get("name", ""),
|
|
|
"rank": idx + 1,
|
|
|
"price": prices.get("usd", 0),
|
|
|
"change24h": prices.get("usd_24h_change", 0),
|
|
|
"marketCapRank": coin.get("market_cap_rank", 0),
|
|
|
"source": "coingecko",
|
|
|
"timestamp": int(datetime.utcnow().timestamp() * 1000)
|
|
|
})
|
|
|
|
|
|
logger.info(f"✅ CoinGecko: Fetched {len(trending)} real trending coins")
|
|
|
return trending
|
|
|
|
|
|
except httpx.HTTPError as e:
|
|
|
logger.error(f"❌ CoinGecko trending API HTTP error: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"CoinGecko trending API unavailable: {str(e)}"
|
|
|
)
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ CoinGecko trending API failed: {e}")
|
|
|
raise HTTPException(
|
|
|
status_code=503,
|
|
|
detail=f"Failed to fetch trending coins: {str(e)}"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
coingecko_client = CoinGeckoClient()
|
|
|
|
|
|
|
|
|
__all__ = ["CoinGeckoClient", "coingecko_client"]
|
|
|
|