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