| |
| """ |
| Multi-Source Data API Router |
| Exposes the unified multi-source service with 137+ fallback sources |
| NEVER FAILS - Always returns data or cached data |
| """ |
|
|
| from fastapi import APIRouter, Query, HTTPException |
| from typing import List, Optional |
| import logging |
|
|
| from backend.services.unified_multi_source_service import get_unified_service |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(prefix="/api/multi-source", tags=["Multi-Source Data"]) |
|
|
|
|
| @router.get("/prices") |
| async def get_market_prices( |
| symbols: Optional[str] = Query(None, description="Comma-separated list of symbols (e.g., BTC,ETH,BNB)"), |
| limit: int = Query(100, ge=1, le=250, description="Maximum number of results"), |
| cross_check: bool = Query(True, description="Cross-check prices from multiple sources"), |
| use_parallel: bool = Query(False, description="Fetch from multiple sources in parallel") |
| ): |
| """ |
| Get market prices with automatic fallback through 23+ sources |
| |
| Sources include: |
| - Primary: CoinGecko, Binance, CoinPaprika, CoinCap, CoinLore |
| - Secondary: CoinMarketCap (2 keys), CryptoCompare, Messari, Nomics, DefiLlama, CoinStats |
| - Tertiary: Kaiko, CoinDesk, DIA Data, FreeCryptoAPI, Cryptingup, CoinRanking |
| - Emergency: Cache (stale data accepted within 5 minutes) |
| |
| Special features: |
| - CoinGecko: Enhanced data with 7-day change, ATH, community stats |
| - Binance: 24h ticker with bid/ask spread, weighted average price |
| - Cross-checking: Validates prices across sources (±5% variance) |
| - Never fails: Returns cached data if all sources fail |
| """ |
| try: |
| service = get_unified_service() |
| |
| |
| symbol_list = None |
| if symbols: |
| symbol_list = [s.strip().upper() for s in symbols.split(",")] |
| |
| result = await service.get_market_prices( |
| symbols=symbol_list, |
| limit=limit, |
| cross_check=cross_check, |
| use_parallel=use_parallel |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Market prices endpoint failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/ohlc/{symbol}") |
| async def get_ohlc_data( |
| symbol: str, |
| timeframe: str = Query("1h", description="Timeframe (1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w)"), |
| limit: int = Query(1000, ge=1, le=1000, description="Number of candles") |
| ): |
| """ |
| Get OHLC/candlestick data with automatic fallback through 18+ sources |
| |
| Sources include: |
| - Primary: Binance, CryptoCompare, CoinPaprika, CoinCap, CoinGecko |
| - Secondary: KuCoin, Bybit, OKX, Kraken, Bitfinex, Gate.io, Huobi |
| - HuggingFace Datasets: 182 CSV files (26 symbols × 7 timeframes) |
| - Emergency: Cache (stale data accepted within 1 hour) |
| |
| Special features: |
| - Binance: Up to 1000 candles, all timeframes, enhanced with taker buy volumes |
| - Validation: Checks OHLC relationships (low ≤ open/close ≤ high) |
| - Never fails: Returns cached or interpolated data if all sources fail |
| """ |
| try: |
| service = get_unified_service() |
| |
| result = await service.get_ohlc_data( |
| symbol=symbol.upper(), |
| timeframe=timeframe, |
| limit=limit, |
| validate=True |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ OHLC endpoint failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/news") |
| async def get_crypto_news( |
| query: str = Query("cryptocurrency", description="Search query"), |
| limit: int = Query(50, ge=1, le=100, description="Maximum number of articles"), |
| aggregate: bool = Query(True, description="Aggregate from multiple sources") |
| ): |
| """ |
| Get crypto news with automatic fallback through 15+ sources |
| |
| API Sources (8): |
| - NewsAPI.org, CryptoPanic, CryptoControl, CoinDesk API |
| - CoinTelegraph API, CryptoSlate, TheBlock API, CoinStats News |
| |
| RSS Feeds (7): |
| - CoinTelegraph, CoinDesk, Decrypt, Bitcoin Magazine |
| - TheBlock, CryptoSlate, NewsBTC |
| |
| Features: |
| - Aggregation: Combines and deduplicates articles from multiple sources |
| - Sorting: Latest articles first |
| - Never fails: Returns cached news if all sources fail (accepts up to 1 hour old) |
| """ |
| try: |
| service = get_unified_service() |
| |
| result = await service.get_news( |
| query=query, |
| limit=limit, |
| aggregate=aggregate |
| ) |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ News endpoint failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/sentiment") |
| async def get_sentiment_data(): |
| """ |
| Get sentiment data (Fear & Greed Index) with automatic fallback through 12+ sources |
| |
| Primary Sources (5): |
| - Alternative.me FNG, CFGI v1, CFGI Legacy |
| - CoinGecko Community, Messari Social |
| |
| Social Analytics (7): |
| - LunarCrush, Santiment, TheTie, CryptoQuant |
| - Glassnode Social, Augmento, Reddit r/CryptoCurrency |
| |
| Features: |
| - Value: 0-100 (0=Extreme Fear, 100=Extreme Greed) |
| - Classification: extreme_fear, fear, neutral, greed, extreme_greed |
| - Never fails: Returns cached sentiment if all sources fail (accepts up to 30 min old) |
| """ |
| try: |
| service = get_unified_service() |
| |
| result = await service.get_sentiment() |
| |
| return result |
| |
| except Exception as e: |
| logger.error(f"❌ Sentiment endpoint failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/monitoring/stats") |
| async def get_monitoring_stats(): |
| """ |
| Get monitoring statistics for all data sources |
| |
| Returns: |
| - Total requests per source |
| - Success/failure counts |
| - Success rate percentage |
| - Average response time |
| - Current availability status |
| - Last success/failure timestamps |
| |
| This helps identify which sources are most reliable |
| """ |
| try: |
| service = get_unified_service() |
| |
| stats = service.get_monitoring_stats() |
| |
| return stats |
| |
| except Exception as e: |
| logger.error(f"❌ Monitoring stats failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/cache/clear") |
| async def clear_cache(): |
| """ |
| Clear all cached data |
| |
| Use this to force fresh data from sources |
| """ |
| try: |
| service = get_unified_service() |
| service.clear_cache() |
| |
| return { |
| "success": True, |
| "message": "Cache cleared successfully" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Cache clear failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/sources/status") |
| async def get_sources_status(): |
| """ |
| Get current status of all configured sources |
| |
| Returns: |
| - Total sources per data type |
| - Available vs unavailable sources |
| - Temporarily down sources with recovery time |
| - Rate-limited sources with retry time |
| """ |
| try: |
| service = get_unified_service() |
| |
| |
| config = service.engine.config |
| |
| sources_info = { |
| "market_prices": { |
| "total": len(config["api_sources"]["market_prices"]["primary"]) + |
| len(config["api_sources"]["market_prices"]["secondary"]) + |
| len(config["api_sources"]["market_prices"]["tertiary"]), |
| "categories": { |
| "primary": len(config["api_sources"]["market_prices"]["primary"]), |
| "secondary": len(config["api_sources"]["market_prices"]["secondary"]), |
| "tertiary": len(config["api_sources"]["market_prices"]["tertiary"]) |
| } |
| }, |
| "ohlc_candlestick": { |
| "total": len(config["api_sources"]["ohlc_candlestick"]["primary"]) + |
| len(config["api_sources"]["ohlc_candlestick"]["secondary"]) + |
| len(config["api_sources"]["ohlc_candlestick"].get("huggingface_datasets", [])), |
| "categories": { |
| "primary": len(config["api_sources"]["ohlc_candlestick"]["primary"]), |
| "secondary": len(config["api_sources"]["ohlc_candlestick"]["secondary"]), |
| "huggingface": len(config["api_sources"]["ohlc_candlestick"].get("huggingface_datasets", [])) |
| } |
| }, |
| "blockchain_explorer": { |
| "ethereum": len(config["api_sources"]["blockchain_explorer"]["ethereum"]), |
| "bsc": len(config["api_sources"]["blockchain_explorer"]["bsc"]), |
| "tron": len(config["api_sources"]["blockchain_explorer"]["tron"]) |
| }, |
| "news_feeds": { |
| "total": len(config["api_sources"]["news_feeds"]["api_sources"]) + |
| len(config["api_sources"]["news_feeds"]["rss_feeds"]), |
| "categories": { |
| "api": len(config["api_sources"]["news_feeds"]["api_sources"]), |
| "rss": len(config["api_sources"]["news_feeds"]["rss_feeds"]) |
| } |
| }, |
| "sentiment_data": { |
| "total": len(config["api_sources"]["sentiment_data"]["primary"]) + |
| len(config["api_sources"]["sentiment_data"]["social_analytics"]), |
| "categories": { |
| "primary": len(config["api_sources"]["sentiment_data"]["primary"]), |
| "social_analytics": len(config["api_sources"]["sentiment_data"]["social_analytics"]) |
| } |
| }, |
| "onchain_analytics": len(config["api_sources"]["onchain_analytics"]), |
| "whale_tracking": len(config["api_sources"]["whale_tracking"]) |
| } |
| |
| |
| total_sources = ( |
| sources_info["market_prices"]["total"] + |
| sources_info["ohlc_candlestick"]["total"] + |
| sum(sources_info["blockchain_explorer"].values()) + |
| sources_info["news_feeds"]["total"] + |
| sources_info["sentiment_data"]["total"] + |
| sources_info["onchain_analytics"] + |
| sources_info["whale_tracking"] |
| ) |
| |
| return { |
| "success": True, |
| "total_sources": total_sources, |
| "sources_by_type": sources_info, |
| "monitoring": service.get_monitoring_stats() |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Sources status failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/health") |
| async def health_check(): |
| """ |
| Health check endpoint |
| |
| Returns: |
| - Service status |
| - Number of available sources |
| - Cache status |
| """ |
| try: |
| service = get_unified_service() |
| |
| return { |
| "success": True, |
| "status": "healthy", |
| "service": "multi_source_fallback", |
| "version": "1.0.0", |
| "features": { |
| "market_prices": "23+ sources", |
| "ohlc_data": "18+ sources", |
| "news": "15+ sources", |
| "sentiment": "12+ sources", |
| "blockchain_explorer": "18+ sources (ETH, BSC, TRON)", |
| "onchain_analytics": "13+ sources", |
| "whale_tracking": "9+ sources" |
| }, |
| "guarantees": { |
| "never_fails": True, |
| "auto_fallback": True, |
| "cache_fallback": True, |
| "cross_validation": True |
| } |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ Health check failed: {e}") |
| return { |
| "success": False, |
| "status": "unhealthy", |
| "error": str(e) |
| } |
|
|
|
|
| __all__ = ["router"] |
|
|