nimazasinich
Replace mock data with real (#115)
8ff9278
"""
HF Space Complete API Router
Implements all required endpoints for Hugging Face Space deployment
using REAL data providers managed by the Orchestrator.
"""
from fastapi import APIRouter, HTTPException, Query, Body, Depends
from fastapi.responses import JSONResponse
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from pydantic import BaseModel, Field
import logging
import asyncio
import json
import os
from pathlib import Path
# Import Orchestrator
from backend.orchestration.provider_manager import provider_manager
logger = logging.getLogger(__name__)
router = APIRouter(tags=["HF Space Complete API"])
# ============================================================================
# Pydantic Models for Request/Response
# ============================================================================
class MetaInfo(BaseModel):
"""Metadata for all responses"""
cache_ttl_seconds: int = Field(default=30, description="Cache TTL in seconds")
generated_at: str = Field(default_factory=lambda: datetime.now().isoformat())
source: str = Field(default="live", description="Data source")
latency_ms: Optional[float] = None
class MarketItem(BaseModel):
"""Market ticker item"""
symbol: str
price: float
change_24h: float
volume_24h: float
source: str = "live"
class MarketResponse(BaseModel):
"""Market snapshot response"""
last_updated: str
items: List[MarketItem]
meta: MetaInfo
class NewsArticle(BaseModel):
"""News article"""
id: str
title: str
url: str
source: str
summary: Optional[str] = None
published_at: str
class NewsResponse(BaseModel):
"""News response"""
articles: List[NewsArticle]
meta: MetaInfo
class SentimentResponse(BaseModel):
"""Sentiment analysis response"""
score: float
label: str # positive, negative, neutral
details: Optional[Dict[str, Any]] = None
meta: MetaInfo
class GasPrice(BaseModel):
"""Gas price information"""
fast: float
standard: float
slow: float
unit: str = "gwei"
class GasResponse(BaseModel):
"""Gas price response"""
chain: str
gas_prices: Optional[GasPrice] = None
timestamp: str
meta: MetaInfo
# ============================================================================
# Market & Pairs Endpoints
# ============================================================================
@router.get("/api/market", response_model=MarketResponse)
async def get_market_snapshot():
"""
Get current market snapshot with prices, changes, and volumes.
Uses Provider Orchestrator (CoinGecko, Binance, etc.)
"""
response = await provider_manager.fetch_data(
"market",
params={"ids": "bitcoin,ethereum,tron,solana,binancecoin,ripple", "vs_currency": "usd"},
use_cache=True,
ttl=60
)
if not response["success"]:
raise HTTPException(status_code=503, detail=response["error"])
data = response["data"]
items = []
# Handle different provider formats if needed, but fetch functions should normalize
# Assuming coingecko format for "market" category list
if isinstance(data, list):
for coin in data:
items.append(MarketItem(
symbol=coin.get('symbol', '').upper(),
price=coin.get('current_price', 0),
change_24h=coin.get('price_change_percentage_24h', 0),
volume_24h=coin.get('total_volume', 0),
source=response["source"]
))
return MarketResponse(
last_updated=response["timestamp"],
items=items,
meta=MetaInfo(
cache_ttl_seconds=60,
source=response["source"],
latency_ms=response.get("latency_ms")
)
)
@router.get("/api/market/ohlc")
async def get_ohlc(
symbol: str = Query(..., description="Trading symbol (e.g., BTC)"),
interval: int = Query(60, description="Interval in minutes"),
limit: int = Query(100, description="Number of candles")
):
"""Get OHLC candlestick data via Orchestrator"""
# Map minutes to common string format if needed by providers,
# but fetch_binance_klines handles it.
interval_str = "1h"
if interval < 60:
interval_str = f"{interval}m"
elif interval == 60:
interval_str = "1h"
elif interval == 240:
interval_str = "4h"
elif interval == 1440:
interval_str = "1d"
response = await provider_manager.fetch_data(
"ohlc",
params={
"symbol": symbol,
"interval": interval_str,
"limit": limit
},
use_cache=True,
ttl=60
)
if not response["success"]:
raise HTTPException(status_code=503, detail=response["error"])
# Transform Binance Klines to standard OHLC
# [time, open, high, low, close, volume, ...]
klines = response["data"]
ohlc_data = []
if isinstance(klines, list):
for k in klines:
if isinstance(k, list) and len(k) >= 6:
ohlc_data.append({
"ts": int(k[0] / 1000),
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5])
})
return {
"symbol": symbol,
"interval": interval,
"data": ohlc_data,
"meta": MetaInfo(
cache_ttl_seconds=60,
source=response["source"],
latency_ms=response.get("latency_ms")
).dict()
}
# ============================================================================
# News & Sentiment Endpoints
# ============================================================================
@router.get("/api/news", response_model=NewsResponse)
async def get_news(
limit: int = Query(20, description="Number of articles"),
source: Optional[str] = Query(None, description="Filter by source")
):
"""Get cryptocurrency news via Orchestrator"""
response = await provider_manager.fetch_data(
"news",
params={"filter": "hot", "query": "crypto"}, # Params for different providers
use_cache=True,
ttl=300
)
if not response["success"]:
return NewsResponse(articles=[], meta=MetaInfo(source="error"))
data = response["data"]
articles = []
# Normalize CryptoPanic / NewsAPI formats
if "results" in data: # CryptoPanic
for post in data.get('results', [])[:limit]:
articles.append(NewsArticle(
id=str(post.get('id')),
title=post.get('title', ''),
url=post.get('url', ''),
source=post.get('source', {}).get('title', 'Unknown'),
summary=post.get('slug', ''),
published_at=post.get('published_at', datetime.now().isoformat())
))
elif "articles" in data: # NewsAPI
for post in data.get('articles', [])[:limit]:
articles.append(NewsArticle(
id=str(hash(post.get('url', ''))),
title=post.get('title', ''),
url=post.get('url', ''),
source=post.get('source', {}).get('name', 'Unknown'),
summary=post.get('description', ''),
published_at=post.get('publishedAt', datetime.now().isoformat())
))
return NewsResponse(
articles=articles,
meta=MetaInfo(
cache_ttl_seconds=300,
source=response["source"],
latency_ms=response.get("latency_ms")
)
)
@router.get("/api/sentiment/global")
async def get_global_sentiment():
"""Get global market sentiment via Orchestrator"""
response = await provider_manager.fetch_data(
"sentiment",
params={"limit": 1},
use_cache=True,
ttl=3600
)
if not response["success"]:
raise HTTPException(status_code=503, detail=response["error"])
data = response["data"]
fng_value = 50
classification = "Neutral"
# Alternative.me format
if data.get('data'):
item = data['data'][0]
fng_value = int(item.get('value', 50))
classification = item.get('value_classification', 'Neutral')
return {
"score": fng_value,
"label": classification,
"meta": MetaInfo(
cache_ttl_seconds=3600,
source=response["source"],
latency_ms=response.get("latency_ms")
).dict()
}
# ============================================================================
# Blockchain Endpoints
# ============================================================================
@router.get("/api/crypto/blockchain/gas", response_model=GasResponse)
async def get_gas_prices(chain: str = Query("ethereum", description="Blockchain network")):
"""Get gas prices via Orchestrator"""
if chain.lower() != "ethereum":
# Fallback or implement other chains
return GasResponse(
chain=chain,
gas_prices=None,
timestamp=datetime.now().isoformat(),
meta=MetaInfo(source="unavailable")
)
response = await provider_manager.fetch_data(
"onchain",
params={},
use_cache=True,
ttl=15
)
if not response["success"]:
return GasResponse(
chain=chain,
gas_prices=None,
timestamp=datetime.now().isoformat(),
meta=MetaInfo(source="unavailable")
)
data = response["data"]
result = data.get("result", {})
gas_price = None
if result:
# Etherscan returns data in result
try:
gas_price = GasPrice(
fast=float(result.get("FastGasPrice", 0)),
standard=float(result.get("ProposeGasPrice", 0)),
slow=float(result.get("SafeGasPrice", 0))
)
except:
pass
return GasResponse(
chain=chain,
gas_prices=gas_price,
timestamp=datetime.now().isoformat(),
meta=MetaInfo(
cache_ttl_seconds=15,
source=response["source"],
latency_ms=response.get("latency_ms")
)
)
# ============================================================================
# System Management
# ============================================================================
@router.get("/api/status")
async def get_system_status():
"""Get overall system status"""
stats = provider_manager.get_stats()
return {
'status': 'operational',
'timestamp': datetime.now().isoformat(),
'providers': stats,
'version': '2.0.0',
'meta': MetaInfo(source="system").dict()
}