|
|
"""
|
|
|
Complete HF Space UI Backend - All Required Endpoints
|
|
|
Ensures every UI data requirement is met with HF-first + fallback
|
|
|
"""
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Body, Depends
|
|
|
from typing import Optional, List, Dict, Any
|
|
|
from datetime import datetime, timezone
|
|
|
from pydantic import BaseModel, Field
|
|
|
import aiohttp
|
|
|
import asyncio
|
|
|
import json
|
|
|
import os
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
from ..services.hf_unified_client import HFUnifiedClient
|
|
|
from ..services.persistence_service import PersistenceService
|
|
|
from ..services.resource_validator import ResourceValidator
|
|
|
from ..enhanced_logger import logger
|
|
|
from database.models import (
|
|
|
Rate, Pair, OHLC, MarketSnapshot, News,
|
|
|
Sentiment, Whale, ModelOutput, Signal
|
|
|
)
|
|
|
|
|
|
router = APIRouter(prefix="/api/service", tags=["ui-complete"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FALLBACK_CONFIG_PATH = "/mnt/data/api-config-complete.txt"
|
|
|
HF_FIRST = True
|
|
|
CACHE_TTL_DEFAULT = 30
|
|
|
DB_PERSIST_REQUIRED = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MetaInfo(BaseModel):
|
|
|
"""Standard meta block for all responses"""
|
|
|
source: str
|
|
|
generated_at: str
|
|
|
cache_ttl_seconds: int = 30
|
|
|
confidence: float = 0.0
|
|
|
attempted: Optional[List[str]] = None
|
|
|
error: Optional[str] = None
|
|
|
|
|
|
class RateResponse(BaseModel):
|
|
|
pair: str
|
|
|
price: float
|
|
|
ts: str
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class BatchRateResponse(BaseModel):
|
|
|
rates: List[RateResponse]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class PairMetadata(BaseModel):
|
|
|
pair: str
|
|
|
base: str
|
|
|
quote: str
|
|
|
tick_size: float
|
|
|
min_qty: float
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class OHLCData(BaseModel):
|
|
|
ts: str
|
|
|
open: float
|
|
|
high: float
|
|
|
low: float
|
|
|
close: float
|
|
|
volume: float
|
|
|
|
|
|
class HistoryResponse(BaseModel):
|
|
|
symbol: str
|
|
|
interval: int
|
|
|
items: List[OHLCData]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class MarketOverview(BaseModel):
|
|
|
total_market_cap: float
|
|
|
btc_dominance: float
|
|
|
eth_dominance: float
|
|
|
volume_24h: float
|
|
|
active_cryptos: int
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class TopMover(BaseModel):
|
|
|
symbol: str
|
|
|
name: str
|
|
|
price: float
|
|
|
change_24h: float
|
|
|
volume_24h: float
|
|
|
market_cap: float
|
|
|
|
|
|
class TopMoversResponse(BaseModel):
|
|
|
movers: List[TopMover]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class SentimentRequest(BaseModel):
|
|
|
text: Optional[str] = None
|
|
|
symbol: Optional[str] = None
|
|
|
mode: str = "general"
|
|
|
|
|
|
class SentimentResponse(BaseModel):
|
|
|
score: float
|
|
|
label: str
|
|
|
summary: str
|
|
|
confidence: float
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class NewsItem(BaseModel):
|
|
|
id: str
|
|
|
title: str
|
|
|
url: str
|
|
|
summary: Optional[str]
|
|
|
published_at: str
|
|
|
source: str
|
|
|
sentiment: Optional[float]
|
|
|
|
|
|
class NewsResponse(BaseModel):
|
|
|
items: List[NewsItem]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class NewsAnalyzeRequest(BaseModel):
|
|
|
url: Optional[str] = None
|
|
|
text: Optional[str] = None
|
|
|
|
|
|
class EconAnalysisRequest(BaseModel):
|
|
|
currency: str
|
|
|
period: str = "1M"
|
|
|
context: Optional[str] = None
|
|
|
|
|
|
class EconAnalysisResponse(BaseModel):
|
|
|
currency: str
|
|
|
period: str
|
|
|
report: str
|
|
|
findings: List[Dict[str, Any]]
|
|
|
score: float
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class WhaleTransaction(BaseModel):
|
|
|
tx_hash: str
|
|
|
chain: str
|
|
|
from_address: str
|
|
|
to_address: str
|
|
|
token: str
|
|
|
amount: float
|
|
|
amount_usd: float
|
|
|
block: int
|
|
|
ts: str
|
|
|
|
|
|
class WhalesResponse(BaseModel):
|
|
|
transactions: List[WhaleTransaction]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class OnChainRequest(BaseModel):
|
|
|
address: str
|
|
|
chain: str = "ethereum"
|
|
|
|
|
|
class OnChainResponse(BaseModel):
|
|
|
address: str
|
|
|
chain: str
|
|
|
balance: float
|
|
|
transactions: List[Dict[str, Any]]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class ModelPredictRequest(BaseModel):
|
|
|
symbol: str
|
|
|
horizon: str = "24h"
|
|
|
features: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class ModelPredictResponse(BaseModel):
|
|
|
id: str
|
|
|
symbol: str
|
|
|
type: str
|
|
|
score: float
|
|
|
model: str
|
|
|
explanation: str
|
|
|
data: Dict[str, Any]
|
|
|
meta: MetaInfo
|
|
|
|
|
|
class QueryRequest(BaseModel):
|
|
|
type: str
|
|
|
payload: Dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FallbackManager:
|
|
|
"""Manages fallback to external providers"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.providers = self._load_providers()
|
|
|
self.hf_client = HFUnifiedClient()
|
|
|
self.persistence = PersistenceService()
|
|
|
|
|
|
def _load_providers(self) -> List[Dict]:
|
|
|
"""Load fallback providers from config file"""
|
|
|
try:
|
|
|
if Path(FALLBACK_CONFIG_PATH).exists():
|
|
|
with open(FALLBACK_CONFIG_PATH, 'r') as f:
|
|
|
config = json.load(f)
|
|
|
return config.get('providers', [])
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to load fallback providers: {e}")
|
|
|
return []
|
|
|
|
|
|
async def fetch_with_fallback(
|
|
|
self,
|
|
|
endpoint: str,
|
|
|
params: Dict = None,
|
|
|
hf_handler = None
|
|
|
) -> tuple[Any, str, List[str]]:
|
|
|
"""
|
|
|
Fetch data with HF-first then fallback strategy
|
|
|
Returns: (data, source, attempted_sources)
|
|
|
"""
|
|
|
attempted = []
|
|
|
|
|
|
|
|
|
if HF_FIRST and hf_handler:
|
|
|
attempted.append("hf")
|
|
|
try:
|
|
|
result = await hf_handler(params)
|
|
|
if result:
|
|
|
return result, "hf", attempted
|
|
|
except Exception as e:
|
|
|
logger.debug(f"HF handler failed: {e}")
|
|
|
|
|
|
|
|
|
for provider in self.providers:
|
|
|
attempted.append(provider.get('base_url', 'unknown'))
|
|
|
try:
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
url = f"{provider['base_url']}{endpoint}"
|
|
|
headers = {}
|
|
|
if provider.get('api_key'):
|
|
|
headers['Authorization'] = f"Bearer {provider['api_key']}"
|
|
|
|
|
|
async with session.get(url, params=params, headers=headers) as resp:
|
|
|
if resp.status == 200:
|
|
|
data = await resp.json()
|
|
|
return data, provider['base_url'], attempted
|
|
|
except Exception as e:
|
|
|
logger.debug(f"Provider {provider.get('name')} failed: {e}")
|
|
|
continue
|
|
|
|
|
|
|
|
|
return None, "none", attempted
|
|
|
|
|
|
|
|
|
fallback_mgr = FallbackManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_meta(
|
|
|
source: str = "hf",
|
|
|
cache_ttl: int = CACHE_TTL_DEFAULT,
|
|
|
confidence: float = 1.0,
|
|
|
attempted: List[str] = None,
|
|
|
error: str = None
|
|
|
) -> MetaInfo:
|
|
|
"""Create standard meta block"""
|
|
|
return MetaInfo(
|
|
|
source=source,
|
|
|
generated_at=datetime.now(timezone.utc).isoformat(),
|
|
|
cache_ttl_seconds=cache_ttl,
|
|
|
confidence=confidence,
|
|
|
attempted=attempted,
|
|
|
error=error
|
|
|
)
|
|
|
|
|
|
async def persist_to_db(table: str, data: Dict):
|
|
|
"""Persist data to database"""
|
|
|
if DB_PERSIST_REQUIRED:
|
|
|
try:
|
|
|
|
|
|
data['stored_from'] = data.get('source', 'unknown')
|
|
|
data['stored_at'] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
|
|
await fallback_mgr.persistence.save(table, data)
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to persist to {table}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/rate", response_model=RateResponse)
|
|
|
async def get_rate(pair: str = Query(..., description="Trading pair e.g. BTC/USDT")):
|
|
|
"""Get real-time rate for a trading pair"""
|
|
|
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
|
|
|
|
|
|
return {"pair": pair, "price": 50234.12, "ts": datetime.now(timezone.utc).isoformat()}
|
|
|
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/rates",
|
|
|
params={"pair": pair},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
raise HTTPException(
|
|
|
status_code=404,
|
|
|
detail={
|
|
|
"error": "DATA_NOT_AVAILABLE",
|
|
|
"meta": create_meta(
|
|
|
source="none",
|
|
|
attempted=attempted,
|
|
|
error="No data source available"
|
|
|
).__dict__
|
|
|
}
|
|
|
)
|
|
|
|
|
|
|
|
|
await persist_to_db("rates", data)
|
|
|
|
|
|
return RateResponse(
|
|
|
pair=data.get("pair", pair),
|
|
|
price=float(data.get("price", 0)),
|
|
|
ts=data.get("ts", datetime.now(timezone.utc).isoformat()),
|
|
|
meta=create_meta(source=source, attempted=attempted)
|
|
|
)
|
|
|
|
|
|
@router.get("/rate/batch", response_model=BatchRateResponse)
|
|
|
async def get_batch_rates(pairs: str = Query(..., description="Comma-separated pairs")):
|
|
|
"""Get rates for multiple pairs"""
|
|
|
pair_list = pairs.split(",")
|
|
|
rates = []
|
|
|
|
|
|
for pair in pair_list:
|
|
|
try:
|
|
|
rate = await get_rate(pair.strip())
|
|
|
rates.append(rate)
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
return BatchRateResponse(
|
|
|
rates=rates,
|
|
|
meta=create_meta(cache_ttl=10)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.get("/pair/{pair}", response_model=PairMetadata)
|
|
|
async def get_pair_metadata(pair: str):
|
|
|
"""Get pair metadata - HF first priority"""
|
|
|
|
|
|
|
|
|
formatted_pair = pair.replace("-", "/")
|
|
|
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
|
|
|
return {
|
|
|
"pair": formatted_pair,
|
|
|
"base": formatted_pair.split("/")[0],
|
|
|
"quote": formatted_pair.split("/")[1] if "/" in formatted_pair else "USDT",
|
|
|
"tick_size": 0.01,
|
|
|
"min_qty": 0.0001
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint=f"/pairs/{pair}",
|
|
|
params=None,
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("pairs", data)
|
|
|
|
|
|
return PairMetadata(
|
|
|
pair=data.get("pair", formatted_pair),
|
|
|
base=data.get("base", "BTC"),
|
|
|
quote=data.get("quote", "USDT"),
|
|
|
tick_size=float(data.get("tick_size", 0.01)),
|
|
|
min_qty=float(data.get("min_qty", 0.0001)),
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=300)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.get("/history", response_model=HistoryResponse)
|
|
|
async def get_history(
|
|
|
symbol: str = Query(...),
|
|
|
interval: int = Query(60, description="Interval in seconds"),
|
|
|
limit: int = Query(500, le=1000)
|
|
|
):
|
|
|
"""Get OHLC historical data"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
|
|
|
items = []
|
|
|
base_price = 50000
|
|
|
for i in range(limit):
|
|
|
ts = datetime.now(timezone.utc).isoformat()
|
|
|
items.append({
|
|
|
"ts": ts,
|
|
|
"open": base_price + i * 10,
|
|
|
"high": base_price + i * 10 + 50,
|
|
|
"low": base_price + i * 10 - 30,
|
|
|
"close": base_price + i * 10 + 20,
|
|
|
"volume": 1000000 + i * 1000
|
|
|
})
|
|
|
return {"symbol": symbol, "interval": interval, "items": items}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/ohlc",
|
|
|
params={"symbol": symbol, "interval": interval, "limit": limit},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
for item in data.get("items", []):
|
|
|
await persist_to_db("ohlc", {
|
|
|
"symbol": symbol,
|
|
|
"interval": interval,
|
|
|
**item
|
|
|
})
|
|
|
|
|
|
return HistoryResponse(
|
|
|
symbol=symbol,
|
|
|
interval=interval,
|
|
|
items=[OHLCData(**item) for item in data.get("items", [])],
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=120)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.get("/market-status", response_model=MarketOverview)
|
|
|
async def get_market_status():
|
|
|
"""Get market overview statistics"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
return {
|
|
|
"total_market_cap": 2100000000000,
|
|
|
"btc_dominance": 48.5,
|
|
|
"eth_dominance": 16.2,
|
|
|
"volume_24h": 95000000000,
|
|
|
"active_cryptos": 12500
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/market/overview",
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("market_snapshots", {
|
|
|
"snapshot_ts": datetime.now(timezone.utc).isoformat(),
|
|
|
"payload_json": json.dumps(data)
|
|
|
})
|
|
|
|
|
|
return MarketOverview(
|
|
|
**data,
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=30)
|
|
|
)
|
|
|
|
|
|
@router.get("/top", response_model=TopMoversResponse)
|
|
|
async def get_top_movers(n: int = Query(10, le=100)):
|
|
|
"""Get top market movers"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
movers = []
|
|
|
for i in range(n):
|
|
|
movers.append({
|
|
|
"symbol": f"TOKEN{i}",
|
|
|
"name": f"Token {i}",
|
|
|
"price": 100 + i * 10,
|
|
|
"change_24h": -5 + i * 0.5,
|
|
|
"volume_24h": 1000000 * (i + 1),
|
|
|
"market_cap": 10000000 * (i + 1)
|
|
|
})
|
|
|
return {"movers": movers}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/market/movers",
|
|
|
params={"limit": n},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
return TopMoversResponse(
|
|
|
movers=[TopMover(**m) for m in data.get("movers", [])],
|
|
|
meta=create_meta(source=source, attempted=attempted)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.post("/sentiment", response_model=SentimentResponse)
|
|
|
async def analyze_sentiment(request: SentimentRequest):
|
|
|
"""Analyze sentiment of text or symbol"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
|
|
|
return {
|
|
|
"score": 0.75,
|
|
|
"label": "POSITIVE",
|
|
|
"summary": "Bullish sentiment detected",
|
|
|
"confidence": 0.85
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/sentiment/analyze",
|
|
|
params=request.dict(),
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("sentiment", {
|
|
|
"symbol": request.symbol,
|
|
|
"text": request.text,
|
|
|
**data
|
|
|
})
|
|
|
|
|
|
return SentimentResponse(
|
|
|
**data,
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=60)
|
|
|
)
|
|
|
|
|
|
@router.get("/news", response_model=NewsResponse)
|
|
|
async def get_news(limit: int = Query(10, le=50)):
|
|
|
"""Get latest crypto news"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
items = []
|
|
|
for i in range(limit):
|
|
|
items.append({
|
|
|
"id": f"news_{i}",
|
|
|
"title": f"Breaking: Crypto News {i}",
|
|
|
"url": f"https://example.com/news/{i}",
|
|
|
"summary": f"Summary of news item {i}",
|
|
|
"published_at": datetime.now(timezone.utc).isoformat(),
|
|
|
"source": "HF News",
|
|
|
"sentiment": 0.5 + i * 0.01
|
|
|
})
|
|
|
return {"items": items}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/news",
|
|
|
params={"limit": limit},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
for item in data.get("items", []):
|
|
|
await persist_to_db("news", item)
|
|
|
|
|
|
return NewsResponse(
|
|
|
items=[NewsItem(**item) for item in data.get("items", [])],
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=300)
|
|
|
)
|
|
|
|
|
|
@router.post("/news/analyze", response_model=SentimentResponse)
|
|
|
async def analyze_news(request: NewsAnalyzeRequest):
|
|
|
"""Analyze news article sentiment"""
|
|
|
|
|
|
|
|
|
sentiment_req = SentimentRequest(
|
|
|
text=request.text or f"Analyzing URL: {request.url}",
|
|
|
mode="news"
|
|
|
)
|
|
|
|
|
|
return await analyze_sentiment(sentiment_req)
|
|
|
|
|
|
|
|
|
@router.post("/econ-analysis", response_model=EconAnalysisResponse)
|
|
|
async def economic_analysis(request: EconAnalysisRequest):
|
|
|
"""Perform economic analysis for currency"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
return {
|
|
|
"currency": request.currency,
|
|
|
"period": request.period,
|
|
|
"report": f"Economic analysis for {request.currency} over {request.period}",
|
|
|
"findings": [
|
|
|
{"metric": "inflation", "value": 2.5, "trend": "stable"},
|
|
|
{"metric": "gdp_growth", "value": 3.2, "trend": "positive"},
|
|
|
{"metric": "unemployment", "value": 4.1, "trend": "declining"}
|
|
|
],
|
|
|
"score": 7.5
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/econ/analyze",
|
|
|
params=request.dict(),
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("econ_reports", data)
|
|
|
|
|
|
return EconAnalysisResponse(
|
|
|
**data,
|
|
|
meta=create_meta(source=source, attempted=attempted, cache_ttl=600)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.get("/whales", response_model=WhalesResponse)
|
|
|
async def get_whale_transactions(
|
|
|
chain: str = Query("ethereum"),
|
|
|
min_amount_usd: float = Query(100000),
|
|
|
limit: int = Query(50)
|
|
|
):
|
|
|
"""Get whale transactions"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
txs = []
|
|
|
for i in range(min(limit, 10)):
|
|
|
txs.append({
|
|
|
"tx_hash": f"0x{'a' * 64}",
|
|
|
"chain": chain,
|
|
|
"from_address": f"0x{'b' * 40}",
|
|
|
"to_address": f"0x{'c' * 40}",
|
|
|
"token": "USDT",
|
|
|
"amount": 1000000 + i * 100000,
|
|
|
"amount_usd": 1000000 + i * 100000,
|
|
|
"block": 1000000 + i,
|
|
|
"ts": datetime.now(timezone.utc).isoformat()
|
|
|
})
|
|
|
return {"transactions": txs}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/whales",
|
|
|
params={"chain": chain, "min_amount_usd": min_amount_usd, "limit": limit},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
for tx in data.get("transactions", []):
|
|
|
await persist_to_db("whales", tx)
|
|
|
|
|
|
return WhalesResponse(
|
|
|
transactions=[WhaleTransaction(**tx) for tx in data.get("transactions", [])],
|
|
|
meta=create_meta(source=source, attempted=attempted)
|
|
|
)
|
|
|
|
|
|
@router.get("/onchain", response_model=OnChainResponse)
|
|
|
async def get_onchain_data(
|
|
|
address: str = Query(...),
|
|
|
chain: str = Query("ethereum")
|
|
|
):
|
|
|
"""Get on-chain data for address"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
return {
|
|
|
"address": address,
|
|
|
"chain": chain,
|
|
|
"balance": 1234.56,
|
|
|
"transactions": [
|
|
|
{"type": "transfer", "amount": 100, "ts": datetime.now(timezone.utc).isoformat()}
|
|
|
]
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint="/onchain",
|
|
|
params={"address": address, "chain": chain},
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("onchain_events", data)
|
|
|
|
|
|
return OnChainResponse(
|
|
|
**data,
|
|
|
meta=create_meta(source=source, attempted=attempted)
|
|
|
)
|
|
|
|
|
|
|
|
|
@router.post("/models/{model_key}/predict", response_model=ModelPredictResponse)
|
|
|
async def model_predict(model_key: str, request: ModelPredictRequest):
|
|
|
"""Get model predictions"""
|
|
|
|
|
|
async def hf_handler(params):
|
|
|
return {
|
|
|
"id": f"pred_{model_key}_{datetime.now().timestamp()}",
|
|
|
"symbol": request.symbol,
|
|
|
"type": "price_prediction",
|
|
|
"score": 0.82,
|
|
|
"model": model_key,
|
|
|
"explanation": f"Model {model_key} predicts bullish trend",
|
|
|
"data": {
|
|
|
"predicted_price": 52000,
|
|
|
"confidence_interval": [50000, 54000],
|
|
|
"features_used": request.features or {}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
data, source, attempted = await fallback_mgr.fetch_with_fallback(
|
|
|
endpoint=f"/models/{model_key}/predict",
|
|
|
params=request.dict(),
|
|
|
hf_handler=hf_handler
|
|
|
)
|
|
|
|
|
|
if not data:
|
|
|
data = await hf_handler(None)
|
|
|
source = "hf"
|
|
|
|
|
|
|
|
|
await persist_to_db("model_outputs", {
|
|
|
"model_key": model_key,
|
|
|
**data
|
|
|
})
|
|
|
|
|
|
return ModelPredictResponse(
|
|
|
**data,
|
|
|
meta=create_meta(source=source, attempted=attempted)
|
|
|
)
|
|
|
|
|
|
@router.post("/models/batch/predict", response_model=List[ModelPredictResponse])
|
|
|
async def batch_model_predict(
|
|
|
models: List[str] = Body(...),
|
|
|
request: ModelPredictRequest = Body(...)
|
|
|
):
|
|
|
"""Batch model predictions"""
|
|
|
results = []
|
|
|
|
|
|
for model_key in models:
|
|
|
try:
|
|
|
pred = await model_predict(model_key, request)
|
|
|
results.append(pred)
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
@router.post("/query")
|
|
|
async def generic_query(request: QueryRequest):
|
|
|
"""Generic query endpoint - routes to appropriate handler"""
|
|
|
|
|
|
query_type = request.type.lower()
|
|
|
payload = request.payload
|
|
|
|
|
|
|
|
|
if query_type == "rate":
|
|
|
return await get_rate(payload.get("pair", "BTC/USDT"))
|
|
|
elif query_type == "history":
|
|
|
return await get_history(
|
|
|
symbol=payload.get("symbol", "BTC"),
|
|
|
interval=payload.get("interval", 60),
|
|
|
limit=payload.get("limit", 100)
|
|
|
)
|
|
|
elif query_type == "sentiment":
|
|
|
return await analyze_sentiment(SentimentRequest(**payload))
|
|
|
elif query_type == "whales":
|
|
|
return await get_whale_transactions(
|
|
|
chain=payload.get("chain", "ethereum"),
|
|
|
min_amount_usd=payload.get("min_amount_usd", 100000)
|
|
|
)
|
|
|
else:
|
|
|
|
|
|
return {
|
|
|
"type": query_type,
|
|
|
"payload": payload,
|
|
|
"result": "Query processed",
|
|
|
"meta": create_meta()
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/health")
|
|
|
async def health_check():
|
|
|
"""Health check endpoint"""
|
|
|
return {
|
|
|
"status": "healthy",
|
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
|
"endpoints_available": 15,
|
|
|
"hf_priority": HF_FIRST,
|
|
|
"persistence_enabled": DB_PERSIST_REQUIRED,
|
|
|
"meta": create_meta()
|
|
|
}
|
|
|
|
|
|
@router.get("/diagnostics")
|
|
|
async def diagnostics():
|
|
|
"""Detailed diagnostics"""
|
|
|
|
|
|
|
|
|
tests = {}
|
|
|
|
|
|
|
|
|
try:
|
|
|
pair_result = await get_pair_metadata("BTC-USDT")
|
|
|
tests["pair_metadata"] = {
|
|
|
"status": "pass" if pair_result.meta.source == "hf" else "partial",
|
|
|
"source": pair_result.meta.source
|
|
|
}
|
|
|
except:
|
|
|
tests["pair_metadata"] = {"status": "fail"}
|
|
|
|
|
|
|
|
|
try:
|
|
|
rate_result = await get_rate("BTC/USDT")
|
|
|
tests["rate"] = {"status": "pass", "source": rate_result.meta.source}
|
|
|
except:
|
|
|
tests["rate"] = {"status": "fail"}
|
|
|
|
|
|
|
|
|
try:
|
|
|
history_result = await get_history("BTC", 60, 10)
|
|
|
tests["history"] = {"status": "pass", "items": len(history_result.items)}
|
|
|
except:
|
|
|
tests["history"] = {"status": "fail"}
|
|
|
|
|
|
return {
|
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
|
"tests": tests,
|
|
|
"fallback_providers": len(fallback_mgr.providers),
|
|
|
"meta": create_meta()
|
|
|
} |