import logging from typing import List import httpx from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import Response from app.ratelimit import limiter 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")