from __future__ import annotations import asyncio import contextlib import json from typing import List, Callable, Awaitable from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse from app.streaming.binance_hub import hub as binance_hub from app.streaming.alpha_hub import alpha_hub router = APIRouter(prefix="/stream", tags=["Stream"]) @router.get("/sse") async def stream_sse(symbols: str = "BTCUSDT,XAUUSD", timeframe: str = "1m"): """ Server-Sent Events (SSE) multiplexer for multiple symbols over a single connection. - Supports 1m timeframe (server streams 1m updates; clients can resample locally). - symbols: comma-separated list (e.g., BTCUSDT,ETHUSDT,XAUUSD) """ if not symbols: raise HTTPException(status_code=400, detail="symbols must not be empty") if timeframe != "1m": raise HTTPException(status_code=400, detail="Only timeframe=1m is supported") syms: List[str] = [s.strip().upper().replace("/", "") for s in symbols.split(",") if s.strip()] if not syms: raise HTTPException(status_code=400, detail="No valid symbols provided") out_queue: asyncio.Queue = asyncio.Queue(maxsize=1000) tasks: List[asyncio.Task] = [] unsubscribers: List[Callable[[], Awaitable[None]]] = [] async def add_subscription(sym: str): if sym.startswith("XAU"): q, unsubscribe = await alpha_hub.subscribe(sym, timeframe="1m") else: q, unsubscribe = await binance_hub.subscribe(sym, timeframe="1m") unsubscribers.append(unsubscribe) async def worker(): try: while True: evt = await q.get() if evt is None: break try: await out_queue.put(evt) except Exception: await asyncio.sleep(0) except asyncio.CancelledError: pass tasks.append(asyncio.create_task(worker())) for s in syms: await add_subscription(s) async def event_generator(): try: while True: try: evt = await asyncio.wait_for(out_queue.get(), timeout=15.0) data = json.dumps(evt, separators=(",", ":")) yield f"event: kline\n".encode("utf-8") yield f"data: {data}\n\n".encode("utf-8") except asyncio.TimeoutError: # Keep-alive comment yield b": ping\n\n" finally: for t in tasks: t.cancel() for t in tasks: with contextlib.suppress(Exception): await t for u in unsubscribers: with contextlib.suppress(Exception): await u() headers = { "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", } return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)