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