|
|
""" |
|
|
HuggingFace Space API Endpoints - REAL DATA ONLY |
|
|
Provides endpoints for market data, sentiment analysis, and system health |
|
|
|
|
|
═══════════════════════════════════════════════════════════════ |
|
|
⚠️ ABSOLUTELY NO FAKE DATA ⚠️ |
|
|
|
|
|
❌ NO mock data |
|
|
❌ NO placeholder data |
|
|
❌ NO hardcoded responses |
|
|
❌ NO random numbers |
|
|
❌ NO fake timestamps |
|
|
❌ NO invented prices |
|
|
❌ NO simulated responses |
|
|
|
|
|
✅ ONLY real data from database cache |
|
|
✅ ONLY real data from free APIs (via background workers) |
|
|
✅ ONLY real AI model inference |
|
|
✅ If data not available → return error |
|
|
✅ If cache empty → return error |
|
|
✅ If model fails → return error |
|
|
═══════════════════════════════════════════════════════════════ |
|
|
""" |
|
|
|
|
|
import time |
|
|
import logging |
|
|
from datetime import datetime |
|
|
from typing import Optional, List |
|
|
from fastapi import APIRouter, Depends, Query, Body, HTTPException |
|
|
from pydantic import BaseModel |
|
|
|
|
|
from api.hf_auth import verify_hf_token |
|
|
from database.cache_queries import get_cache_queries |
|
|
from database.db_manager import db_manager |
|
|
from ai_models import _registry |
|
|
from utils.logger import setup_logger |
|
|
|
|
|
logger = setup_logger("hf_endpoints") |
|
|
|
|
|
router = APIRouter(prefix="/api", tags=["hf_space"]) |
|
|
|
|
|
|
|
|
cache = get_cache_queries(db_manager) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SentimentRequest(BaseModel): |
|
|
"""Request model for sentiment analysis""" |
|
|
text: str |
|
|
|
|
|
class Config: |
|
|
json_schema_extra = { |
|
|
"example": { |
|
|
"text": "Bitcoin is pumping! Great news for crypto!" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/market") |
|
|
async def get_market_data( |
|
|
limit: int = Query(100, ge=1, le=1000, description="Number of symbols to return"), |
|
|
symbols: Optional[str] = Query(None, description="Comma-separated list of symbols (e.g., BTC,ETH,BNB)"), |
|
|
auth: bool = Depends(verify_hf_token) |
|
|
): |
|
|
""" |
|
|
Get real-time market data from database cache |
|
|
|
|
|
CRITICAL RULES: |
|
|
1. ONLY read from cached_market_data table in database |
|
|
2. NEVER invent/generate/fake price data |
|
|
3. If cache is empty → return error with status code 503 |
|
|
4. If symbol not found → return empty array, not fake data |
|
|
5. Timestamps MUST be from actual database records |
|
|
6. Prices MUST be from actual fetched data |
|
|
|
|
|
Returns: |
|
|
JSON with real market data or error if no data available |
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
symbol_list = None |
|
|
if symbols: |
|
|
symbol_list = [s.strip().upper() for s in symbols.split(',')] |
|
|
logger.info(f"Filtering for symbols: {symbol_list}") |
|
|
|
|
|
|
|
|
market_data = cache.get_cached_market_data( |
|
|
symbols=symbol_list, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
|
|
|
if not market_data or len(market_data) == 0: |
|
|
logger.warning("No market data available in cache") |
|
|
return { |
|
|
"success": False, |
|
|
"error": "No market data available. Background workers syncing data from free APIs. Please wait.", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
response = { |
|
|
"success": True, |
|
|
"data": [ |
|
|
{ |
|
|
"symbol": row["symbol"], |
|
|
"price": float(row["price"]), |
|
|
"market_cap": float(row["market_cap"]) if row.get("market_cap") else None, |
|
|
"volume_24h": float(row["volume_24h"]) if row.get("volume_24h") else None, |
|
|
"change_24h": float(row["change_24h"]) if row.get("change_24h") else None, |
|
|
"high_24h": float(row["high_24h"]) if row.get("high_24h") else None, |
|
|
"low_24h": float(row["low_24h"]) if row.get("low_24h") else None, |
|
|
"last_updated": int(row["fetched_at"].timestamp() * 1000) |
|
|
} |
|
|
for row in market_data |
|
|
], |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000), |
|
|
"cached": True, |
|
|
"count": len(market_data) |
|
|
} |
|
|
|
|
|
logger.info(f"Returned {len(market_data)} real market records") |
|
|
return response |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Market endpoint error: {e}", exc_info=True) |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Database error: {str(e)}", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/market/history") |
|
|
async def get_market_history( |
|
|
symbol: str = Query(..., description="Trading pair symbol (e.g., BTCUSDT, ETHUSDT)"), |
|
|
timeframe: str = Query("1h", description="Timeframe (1m, 5m, 15m, 1h, 4h, 1d)"), |
|
|
limit: int = Query(1000, ge=1, le=5000, description="Number of candles"), |
|
|
auth: bool = Depends(verify_hf_token) |
|
|
): |
|
|
""" |
|
|
Get OHLCV (candlestick) data from database cache |
|
|
|
|
|
CRITICAL RULES: |
|
|
1. ONLY read from cached_ohlc table in database |
|
|
2. NEVER generate/fake candle data |
|
|
3. If cache empty → return error with 404 |
|
|
4. If symbol not found → return error, not fake data |
|
|
5. All OHLC values MUST be from actual database records |
|
|
6. Timestamps MUST be actual candle timestamps |
|
|
|
|
|
Returns: |
|
|
JSON with real OHLCV data or error if no data available |
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
normalized_symbol = symbol.upper() |
|
|
logger.info(f"Fetching OHLC for {normalized_symbol} {timeframe}") |
|
|
|
|
|
|
|
|
ohlcv_data = cache.get_cached_ohlc( |
|
|
symbol=normalized_symbol, |
|
|
interval=timeframe, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
|
|
|
if not ohlcv_data or len(ohlcv_data) == 0: |
|
|
logger.warning(f"No OHLCV data for {normalized_symbol} {timeframe}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"No OHLCV data for {symbol}. Background workers syncing data. Symbol may not be cached yet.", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
response = { |
|
|
"success": True, |
|
|
"data": [ |
|
|
{ |
|
|
"timestamp": int(candle["timestamp"].timestamp() * 1000), |
|
|
"open": float(candle["open"]), |
|
|
"high": float(candle["high"]), |
|
|
"low": float(candle["low"]), |
|
|
"close": float(candle["close"]), |
|
|
"volume": float(candle["volume"]) |
|
|
} |
|
|
for candle in ohlcv_data |
|
|
], |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000), |
|
|
"cached": True, |
|
|
"count": len(ohlcv_data) |
|
|
} |
|
|
|
|
|
logger.info(f"Returned {len(ohlcv_data)} real OHLC candles for {normalized_symbol}") |
|
|
return response |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"History endpoint error: {e}", exc_info=True) |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Database error: {str(e)}", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/sentiment/analyze") |
|
|
async def analyze_sentiment( |
|
|
request: SentimentRequest = Body(...), |
|
|
auth: bool = Depends(verify_hf_token) |
|
|
): |
|
|
""" |
|
|
Analyze sentiment using REAL AI model |
|
|
|
|
|
CRITICAL RULES: |
|
|
1. MUST use actual loaded AI model from ai_models.py |
|
|
2. MUST run REAL model inference |
|
|
3. NEVER return random sentiment scores |
|
|
4. NEVER fake confidence values |
|
|
5. If model not loaded → return error |
|
|
6. If inference fails → return error |
|
|
|
|
|
Returns: |
|
|
JSON with real sentiment analysis or error |
|
|
""" |
|
|
|
|
|
try: |
|
|
text = request.text |
|
|
|
|
|
|
|
|
if not text or len(text.strip()) == 0: |
|
|
return { |
|
|
"success": False, |
|
|
"error": "Text parameter is required and cannot be empty", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
logger.info(f"Analyzing sentiment for text (length={len(text)})") |
|
|
|
|
|
|
|
|
sentiment_model = None |
|
|
tried_models = [] |
|
|
|
|
|
|
|
|
for model_key in ["crypto_sent_kk08", "sentiment_twitter", "sentiment_financial", "crypto_sent_0"]: |
|
|
tried_models.append(model_key) |
|
|
try: |
|
|
sentiment_model = _registry.get_pipeline(model_key) |
|
|
if sentiment_model: |
|
|
logger.info(f"Using sentiment model: {model_key}") |
|
|
break |
|
|
except Exception as e: |
|
|
logger.warning(f"Failed to load {model_key}: {e}") |
|
|
continue |
|
|
|
|
|
|
|
|
if not sentiment_model: |
|
|
logger.error(f"No sentiment model available. Tried: {tried_models}") |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"No sentiment model available. Tried: {', '.join(tried_models)}. Please ensure HuggingFace models are properly configured.", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = sentiment_model(text[:512]) |
|
|
|
|
|
|
|
|
if isinstance(result, list) and len(result) > 0: |
|
|
result = result[0] |
|
|
|
|
|
|
|
|
label = result.get("label", "NEUTRAL").upper() |
|
|
score = float(result.get("score", 0.5)) |
|
|
|
|
|
|
|
|
if "POSITIVE" in label or "BULLISH" in label or "LABEL_2" in label: |
|
|
sentiment = "positive" |
|
|
elif "NEGATIVE" in label or "BEARISH" in label or "LABEL_0" in label: |
|
|
sentiment = "negative" |
|
|
else: |
|
|
sentiment = "neutral" |
|
|
|
|
|
|
|
|
response = { |
|
|
"success": True, |
|
|
"data": { |
|
|
"label": sentiment, |
|
|
"score": score, |
|
|
"sentiment": sentiment, |
|
|
"confidence": score, |
|
|
"text": text, |
|
|
"model_label": label, |
|
|
"timestamp": int(time.time() * 1000) |
|
|
}, |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
logger.info(f"Sentiment analysis completed: {sentiment} (score={score:.3f})") |
|
|
return response |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Sentiment analysis failed: {e}", exc_info=True) |
|
|
return { |
|
|
"success": False, |
|
|
"error": f"Model inference error: {str(e)}", |
|
|
"source": "hf_engine", |
|
|
"timestamp": int(time.time() * 1000) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/health") |
|
|
async def health_check(auth: bool = Depends(verify_hf_token)): |
|
|
""" |
|
|
Health check endpoint |
|
|
|
|
|
RULES: |
|
|
- Return REAL system status |
|
|
- Use REAL uptime calculation |
|
|
- Check REAL database connection |
|
|
- NEVER return fake status |
|
|
|
|
|
Returns: |
|
|
JSON with real system health status |
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
db_status = "connected" |
|
|
try: |
|
|
|
|
|
health = db_manager.health_check() |
|
|
if health.get("status") != "healthy": |
|
|
db_status = "degraded" |
|
|
except Exception as e: |
|
|
logger.error(f"Database health check failed: {e}") |
|
|
db_status = "disconnected" |
|
|
|
|
|
|
|
|
cache_stats = { |
|
|
"market_data_count": 0, |
|
|
"ohlc_count": 0 |
|
|
} |
|
|
|
|
|
try: |
|
|
with db_manager.get_session() as session: |
|
|
from database.models import CachedMarketData, CachedOHLC |
|
|
from sqlalchemy import func, distinct |
|
|
|
|
|
|
|
|
cache_stats["market_data_count"] = session.query( |
|
|
func.count(distinct(CachedMarketData.symbol)) |
|
|
).scalar() or 0 |
|
|
|
|
|
cache_stats["ohlc_count"] = session.query( |
|
|
func.count(CachedOHLC.id) |
|
|
).scalar() or 0 |
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get cache stats: {e}") |
|
|
|
|
|
|
|
|
model_status = _registry.get_registry_status() |
|
|
|
|
|
response = { |
|
|
"success": True, |
|
|
"status": "healthy" if db_status == "connected" else "degraded", |
|
|
"timestamp": int(time.time() * 1000), |
|
|
"version": "1.0.0", |
|
|
"database": db_status, |
|
|
"cache": cache_stats, |
|
|
"ai_models": { |
|
|
"loaded": model_status.get("models_loaded", 0), |
|
|
"failed": model_status.get("models_failed", 0), |
|
|
"total": model_status.get("models_total", 0) |
|
|
}, |
|
|
"source": "hf_engine" |
|
|
} |
|
|
|
|
|
logger.info(f"Health check completed: {response['status']}") |
|
|
return response |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Health check error: {e}", exc_info=True) |
|
|
return { |
|
|
"success": False, |
|
|
"status": "unhealthy", |
|
|
"error": str(e), |
|
|
"timestamp": int(time.time() * 1000), |
|
|
"source": "hf_engine" |
|
|
} |
|
|
|