|
|
|
|
|
""" |
|
|
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"] |
|
|
|