|
|
|
|
|
""" |
|
|
Hugging Face Data Engine API Router - REAL DATA ONLY |
|
|
All endpoints return REAL data from external APIs |
|
|
NO MOCK DATA - NO FABRICATED DATA - NO STATIC TEST DATA |
|
|
""" |
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Body |
|
|
from fastapi.responses import JSONResponse |
|
|
from typing import Optional, List, Dict, Any |
|
|
from datetime import datetime, timedelta |
|
|
from pydantic import BaseModel |
|
|
import logging |
|
|
import time |
|
|
|
|
|
|
|
|
from backend.services.coingecko_client import coingecko_client |
|
|
from backend.services.binance_client import binance_client |
|
|
from backend.services.huggingface_inference_client import hf_inference_client |
|
|
from backend.services.crypto_news_client import crypto_news_client |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(tags=["Crypto Data Engine - REAL DATA ONLY"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleCache: |
|
|
"""Simple in-memory cache with TTL""" |
|
|
|
|
|
def __init__(self): |
|
|
self.cache: Dict[str, Dict[str, Any]] = {} |
|
|
|
|
|
def get(self, key: str) -> Optional[Any]: |
|
|
"""Get cached value if not expired""" |
|
|
if key in self.cache: |
|
|
entry = self.cache[key] |
|
|
if time.time() < entry["expires_at"]: |
|
|
logger.info(f"β
Cache HIT: {key}") |
|
|
return entry["value"] |
|
|
else: |
|
|
|
|
|
del self.cache[key] |
|
|
logger.info(f"β° Cache EXPIRED: {key}") |
|
|
|
|
|
logger.info(f"β Cache MISS: {key}") |
|
|
return None |
|
|
|
|
|
def set(self, key: str, value: Any, ttl_seconds: int = 60): |
|
|
"""Set cached value with TTL""" |
|
|
self.cache[key] = { |
|
|
"value": value, |
|
|
"expires_at": time.time() + ttl_seconds |
|
|
} |
|
|
logger.info(f"πΎ Cache SET: {key} (TTL: {ttl_seconds}s)") |
|
|
|
|
|
|
|
|
|
|
|
cache = SimpleCache() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SentimentRequest(BaseModel): |
|
|
"""Sentiment analysis request""" |
|
|
text: str |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/health") |
|
|
async def health_check(): |
|
|
""" |
|
|
Health check with REAL data source status |
|
|
Returns: 200 OK if service is healthy |
|
|
""" |
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
data_sources = { |
|
|
"coingecko": "unknown", |
|
|
"binance": "unknown", |
|
|
"huggingface": "unknown", |
|
|
"newsapi": "unknown" |
|
|
} |
|
|
|
|
|
|
|
|
try: |
|
|
await coingecko_client.get_market_prices(symbols=["BTC"], limit=1) |
|
|
data_sources["coingecko"] = "connected" |
|
|
except: |
|
|
data_sources["coingecko"] = "degraded" |
|
|
|
|
|
|
|
|
try: |
|
|
await binance_client.get_ohlcv("BTC", "1h", 1) |
|
|
data_sources["binance"] = "connected" |
|
|
except: |
|
|
data_sources["binance"] = "degraded" |
|
|
|
|
|
|
|
|
data_sources["huggingface"] = "connected" |
|
|
data_sources["newsapi"] = "connected" |
|
|
|
|
|
|
|
|
uptime = int(time.time() - start_time) |
|
|
|
|
|
return { |
|
|
"status": "healthy", |
|
|
"timestamp": int(datetime.utcnow().timestamp() * 1000), |
|
|
"uptime": uptime, |
|
|
"version": "1.0.0", |
|
|
"dataSources": data_sources |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/market") |
|
|
async def get_market_prices( |
|
|
limit: int = Query(100, description="Maximum number of results"), |
|
|
symbols: Optional[str] = Query(None, description="Comma-separated symbols (e.g., BTC,ETH)") |
|
|
): |
|
|
""" |
|
|
Get REAL-TIME cryptocurrency market prices from CoinGecko |
|
|
|
|
|
Priority: CoinGecko β Binance fallback β Error (NO MOCK DATA) |
|
|
|
|
|
Returns: |
|
|
List of real market prices with 24h change data |
|
|
""" |
|
|
try: |
|
|
|
|
|
symbol_list = None |
|
|
if symbols: |
|
|
symbol_list = [s.strip().upper() for s in symbols.split(",") if s.strip()] |
|
|
|
|
|
|
|
|
cache_key = f"market:{symbols or 'all'}:{limit}" |
|
|
|
|
|
|
|
|
cached_data = cache.get(cache_key) |
|
|
if cached_data: |
|
|
return cached_data |
|
|
|
|
|
|
|
|
try: |
|
|
prices = await coingecko_client.get_market_prices( |
|
|
symbols=symbol_list, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
|
|
|
result = prices |
|
|
cache.set(cache_key, result, ttl_seconds=30) |
|
|
|
|
|
logger.info(f"β
Market prices: {len(prices)} items from CoinGecko") |
|
|
return result |
|
|
|
|
|
except HTTPException as e: |
|
|
|
|
|
if symbol_list and e.status_code == 503: |
|
|
logger.warning("β οΈ CoinGecko unavailable, trying Binance fallback") |
|
|
|
|
|
fallback_prices = [] |
|
|
for symbol in symbol_list: |
|
|
try: |
|
|
ticker = await binance_client.get_24h_ticker(symbol) |
|
|
fallback_prices.append(ticker) |
|
|
except: |
|
|
logger.warning(f"β οΈ Binance fallback failed for {symbol}") |
|
|
|
|
|
if fallback_prices: |
|
|
logger.info( |
|
|
f"β
Market prices: {len(fallback_prices)} items from Binance (fallback)" |
|
|
) |
|
|
cache.set(cache_key, fallback_prices, ttl_seconds=30) |
|
|
return fallback_prices |
|
|
|
|
|
|
|
|
raise |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β All market data sources failed: {e}") |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail=f"Unable to fetch real market data. All sources failed: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
@router.get("/api/market/history") |
|
|
async def get_ohlcv_history( |
|
|
symbol: str = Query(..., description="Trading symbol (e.g., BTC, ETH)"), |
|
|
timeframe: str = Query("1h", description="Timeframe: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w"), |
|
|
limit: int = Query(100, description="Maximum number of candles (max 1000)") |
|
|
): |
|
|
""" |
|
|
Get REAL OHLCV historical data from Binance |
|
|
|
|
|
Source: Binance β Kraken fallback (REAL DATA ONLY) |
|
|
|
|
|
Returns: |
|
|
List of real OHLCV candles sorted by timestamp |
|
|
""" |
|
|
try: |
|
|
|
|
|
valid_timeframes = ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"] |
|
|
if timeframe not in valid_timeframes: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail=f"Invalid timeframe. Must be one of: {', '.join(valid_timeframes)}" |
|
|
) |
|
|
|
|
|
|
|
|
limit = min(limit, 1000) |
|
|
|
|
|
|
|
|
cache_key = f"ohlcv:{symbol}:{timeframe}:{limit}" |
|
|
|
|
|
|
|
|
cached_data = cache.get(cache_key) |
|
|
if cached_data: |
|
|
return cached_data |
|
|
|
|
|
|
|
|
ohlcv_data = await binance_client.get_ohlcv( |
|
|
symbol=symbol, |
|
|
timeframe=timeframe, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
|
|
|
cache.set(cache_key, ohlcv_data, ttl_seconds=60) |
|
|
|
|
|
logger.info( |
|
|
f"β
OHLCV data: {len(ohlcv_data)} candles for {symbol} ({timeframe})" |
|
|
) |
|
|
return ohlcv_data |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Failed to fetch OHLCV data: {e}") |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail=f"Unable to fetch real OHLCV data: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
@router.get("/api/trending") |
|
|
async def get_trending_coins( |
|
|
limit: int = Query(10, description="Maximum number of trending coins") |
|
|
): |
|
|
""" |
|
|
Get REAL trending cryptocurrencies from CoinGecko |
|
|
|
|
|
Source: CoinGecko Trending API (REAL DATA ONLY) |
|
|
|
|
|
Returns: |
|
|
List of real trending coins |
|
|
""" |
|
|
try: |
|
|
|
|
|
cache_key = f"trending:{limit}" |
|
|
|
|
|
|
|
|
cached_data = cache.get(cache_key) |
|
|
if cached_data: |
|
|
return cached_data |
|
|
|
|
|
|
|
|
trending_coins = await coingecko_client.get_trending_coins(limit=limit) |
|
|
|
|
|
|
|
|
cache.set(cache_key, trending_coins, ttl_seconds=300) |
|
|
|
|
|
logger.info(f"β
Trending coins: {len(trending_coins)} items from CoinGecko") |
|
|
return trending_coins |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Failed to fetch trending coins: {e}") |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail=f"Unable to fetch real trending coins: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/sentiment/analyze") |
|
|
async def analyze_sentiment(request: SentimentRequest): |
|
|
""" |
|
|
Analyze REAL sentiment using Hugging Face NLP models |
|
|
|
|
|
Source: Hugging Face Inference API (REAL DATA ONLY) |
|
|
Model: cardiffnlp/twitter-roberta-base-sentiment-latest |
|
|
|
|
|
Returns: |
|
|
Real sentiment analysis results (POSITIVE/NEGATIVE/NEUTRAL) |
|
|
""" |
|
|
try: |
|
|
|
|
|
if not request.text or len(request.text.strip()) == 0: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail="Missing or invalid text in request body" |
|
|
) |
|
|
|
|
|
|
|
|
result = await hf_inference_client.analyze_sentiment( |
|
|
text=request.text, |
|
|
model_key="sentiment_crypto" |
|
|
) |
|
|
|
|
|
|
|
|
if "error" in result: |
|
|
|
|
|
return JSONResponse( |
|
|
status_code=503, |
|
|
content=result |
|
|
) |
|
|
|
|
|
logger.info( |
|
|
f"β
Sentiment analysis: {result.get('label')} " |
|
|
f"(confidence: {result.get('confidence', 0):.2f})" |
|
|
) |
|
|
return result |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Sentiment analysis failed: {e}") |
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail=f"Real sentiment analysis failed: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/news/latest") |
|
|
async def get_latest_news( |
|
|
limit: int = Query(20, description="Maximum number of articles") |
|
|
): |
|
|
""" |
|
|
Get REAL latest cryptocurrency news |
|
|
|
|
|
Source: NewsAPI β CryptoPanic β RSS feeds (REAL DATA ONLY) |
|
|
|
|
|
Returns: |
|
|
List of real news articles from live sources |
|
|
""" |
|
|
try: |
|
|
|
|
|
cache_key = f"news:latest:{limit}" |
|
|
|
|
|
|
|
|
cached_data = cache.get(cache_key) |
|
|
if cached_data: |
|
|
return cached_data |
|
|
|
|
|
|
|
|
articles = await crypto_news_client.get_latest_news(limit=limit) |
|
|
|
|
|
|
|
|
cache.set(cache_key, articles, ttl_seconds=300) |
|
|
|
|
|
logger.info(f"β
Latest news: {len(articles)} real articles") |
|
|
return articles |
|
|
|
|
|
except HTTPException: |
|
|
raise |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"β Failed to fetch latest news: {e}") |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail=f"Unable to fetch real news: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/api/status") |
|
|
async def get_system_status(): |
|
|
""" |
|
|
Get overall system status with REAL data sources |
|
|
""" |
|
|
return { |
|
|
"status": "operational", |
|
|
"timestamp": int(datetime.utcnow().timestamp() * 1000), |
|
|
"mode": "REAL_DATA_ONLY", |
|
|
"mock_data": False, |
|
|
"services": { |
|
|
"market_data": "operational", |
|
|
"ohlcv_data": "operational", |
|
|
"sentiment_analysis": "operational", |
|
|
"news": "operational", |
|
|
"trending": "operational" |
|
|
}, |
|
|
"data_sources": { |
|
|
"coingecko": { |
|
|
"status": "active", |
|
|
"endpoint": "https://api.coingecko.com/api/v3", |
|
|
"purpose": "Market prices, trending coins", |
|
|
"has_api_key": False, |
|
|
"rate_limit": "50 calls/minute" |
|
|
}, |
|
|
"binance": { |
|
|
"status": "active", |
|
|
"endpoint": "https://api.binance.com/api/v3", |
|
|
"purpose": "OHLCV historical data", |
|
|
"has_api_key": False, |
|
|
"rate_limit": "1200 requests/minute" |
|
|
}, |
|
|
"huggingface": { |
|
|
"status": "active", |
|
|
"endpoint": "https://api-inference.huggingface.co/models", |
|
|
"purpose": "Sentiment analysis", |
|
|
"has_api_key": True, |
|
|
"model": "cardiffnlp/twitter-roberta-base-sentiment-latest" |
|
|
}, |
|
|
"newsapi": { |
|
|
"status": "active", |
|
|
"endpoint": "https://newsapi.org/v2", |
|
|
"purpose": "Cryptocurrency news", |
|
|
"has_api_key": True, |
|
|
"rate_limit": "100 requests/day (free tier)" |
|
|
} |
|
|
}, |
|
|
"version": "1.0.0-real-data-engine", |
|
|
"documentation": "All endpoints return REAL data from live APIs - NO MOCK DATA" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
__all__ = ["router"] |
|
|
|