import logging from typing import List import httpx from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import Response from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) from app.config import settings from app.schemas import Candle from app.services.price_store import price_store router = APIRouter() logger = logging.getLogger(__name__) VALID_ASSETS = {"BTC", "ETH"} VALID_TIMEFRAMES = {"5m", "15m", "1H", "4H", "1D", "1W"} BINANCE_INTERVAL = { "5m": "5m", "15m": "15m", "1H": "1h", "4H": "4h", "1D": "1d", "1W": "1w", } SYMBOL_MAP = {"BTC": "BTCUSDT", "ETH": "ETHUSDT"} async def fetch_binance_candles(asset: str, tf: str, limit: int) -> List[Candle]: symbol = SYMBOL_MAP[asset] interval = BINANCE_INTERVAL[tf] url = f"{settings.binance_rest_url}/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}" async with httpx.AsyncClient(timeout=15) as client: resp = await client.get(url) resp.raise_for_status() rows = resp.json() return [ Candle( time=row[0] // 1000, # ms → seconds open=float(row[1]), high=float(row[2]), low=float(row[3]), close=float(row[4]), volume=float(row[5]), ) for row in rows ] @router.get("/prices/{asset}", response_model=List[Candle]) @limiter.limit("30/minute") async def get_prices( request: Request, asset: str, tf: str = Query(default="4H"), limit: int = Query(default=200, ge=1, le=1000), response: Response = None, ): asset = asset.upper() if asset not in VALID_ASSETS: raise HTTPException(status_code=400, detail=f"Asset must be one of {VALID_ASSETS}") if tf not in VALID_TIMEFRAMES: raise HTTPException(status_code=400, detail=f"Timeframe must be one of {VALID_TIMEFRAMES}") # 1m: use in-memory store (updated in real-time) — don't cache at CDN level if tf == "1m": candles = price_store.get_candles(asset, "1m", limit) if response is not None: response.headers["Cache-Control"] = "public, max-age=5, stale-while-revalidate=10" return [Candle(**c) for c in candles] # Larger timeframes change infrequently — safe to cache longer if response is not None: ttl = 60 if tf in ("5m", "15m") else 300 # 1m for short TFs, 5m for 1H+ response.headers["Cache-Control"] = f"public, max-age={ttl}, stale-while-revalidate={ttl * 2}" try: return await fetch_binance_candles(asset, tf, limit) except Exception as exc: logger.error("Binance REST fetch failed for %s %s: %s", asset, tf, exc) raise HTTPException(status_code=502, detail="Failed to fetch price data")