|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
from backend.orchestration.provider_manager import provider_manager |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(tags=["HF Space Complete API"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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 = [] |
|
|
|
|
|
|
|
|
|
|
|
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""" |
|
|
|
|
|
|
|
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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"}, |
|
|
use_cache=True, |
|
|
ttl=300 |
|
|
) |
|
|
|
|
|
if not response["success"]: |
|
|
return NewsResponse(articles=[], meta=MetaInfo(source="error")) |
|
|
|
|
|
data = response["data"] |
|
|
articles = [] |
|
|
|
|
|
|
|
|
if "results" in data: |
|
|
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: |
|
|
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" |
|
|
|
|
|
|
|
|
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() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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": |
|
|
|
|
|
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: |
|
|
|
|
|
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") |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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() |
|
|
} |
|
|
|