|
|
|
|
|
"""
|
|
|
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": "/static-proxy?url=https%3A%2F%2Fapi-inference.huggingface.co%2Fmodels%26quot%3B%3C%2Fspan%3E%2C
|
|
|
"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"]
|
|
|
|