Initial commit: Gold Trading Simulator with AI-powered analysis
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Dict, Set, Tuple, Any
|
||||
|
||||
import websockets
|
||||
|
||||
from app.streaming.live_store import live_store
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StreamKey:
|
||||
symbol: str
|
||||
timeframe: str # only '1m' supported in hub
|
||||
|
||||
|
||||
class BinanceStreamHub:
|
||||
"""
|
||||
Maintains a single upstream websocket per (symbol,timeframe) and fans out
|
||||
kline events to multiple subscribers via asyncio.Queues.
|
||||
"""
|
||||
|
||||
def __init__(self, base_ws: str | None = None) -> None:
|
||||
self.base_ws = (base_ws or os.getenv("BINANCE_WS_URL", "wss://stream.binance.com:9443/ws")).rstrip("/")
|
||||
self._subs: Dict[StreamKey, Set[asyncio.Queue]] = {}
|
||||
self._tasks: Dict[StreamKey, asyncio.Task] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def get_status(self) -> list[dict]:
|
||||
"""Return status snapshot of active streams."""
|
||||
out: list[dict] = []
|
||||
for key, subs in self._subs.items():
|
||||
hist = live_store.get_history(key.symbol, key.timeframe)
|
||||
last_ts = hist[-1]["time"] if hist else None
|
||||
last_iso = None
|
||||
if isinstance(last_ts, (int, float)):
|
||||
try:
|
||||
last_iso = datetime.utcfromtimestamp(int(last_ts)).isoformat() + "Z"
|
||||
except Exception:
|
||||
last_iso = None
|
||||
out.append({
|
||||
"symbol": key.symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"subscribers": len(subs),
|
||||
"last_event_time": last_iso,
|
||||
})
|
||||
return out
|
||||
|
||||
async def subscribe(self, symbol: str, timeframe: str = "1m") -> Tuple[asyncio.Queue, Any]:
|
||||
"""Subscribe to a stream. Returns (queue, unsubscribe_cb)."""
|
||||
if timeframe != "1m":
|
||||
raise ValueError("BinanceStreamHub currently supports timeframe '1m' only")
|
||||
key = StreamKey(symbol=symbol.upper().replace("/", ""), timeframe=timeframe)
|
||||
q: asyncio.Queue = asyncio.Queue(maxsize=1000)
|
||||
async with self._lock:
|
||||
subs = self._subs.get(key)
|
||||
if not subs:
|
||||
subs = set()
|
||||
self._subs[key] = subs
|
||||
subs.add(q)
|
||||
if key not in self._tasks:
|
||||
self._tasks[key] = asyncio.create_task(self._run_stream(key))
|
||||
|
||||
async def _unsubscribe() -> None:
|
||||
async with self._lock:
|
||||
s = self._subs.get(key)
|
||||
if s and q in s:
|
||||
s.remove(q)
|
||||
# Close queue to unblock listeners
|
||||
try:
|
||||
q.put_nowait(None)
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None and len(s) == 0:
|
||||
# cancel task and cleanup
|
||||
t = self._tasks.pop(key, None)
|
||||
if t:
|
||||
t.cancel()
|
||||
self._subs.pop(key, None)
|
||||
return q, _unsubscribe
|
||||
|
||||
async def _run_stream(self, key: StreamKey) -> None:
|
||||
symbol = key.symbol
|
||||
stream = f"{symbol.lower()}@kline_{key.timeframe}"
|
||||
url = self.base_ws.replace("/ws", "/stream") + f"?streams={stream}"
|
||||
# Reconnect loop
|
||||
while True:
|
||||
try:
|
||||
async with websockets.connect(url, ping_interval=20, ping_timeout=20) as ws:
|
||||
async for message in ws:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
k = (data.get("data") or {}).get("k") or {}
|
||||
if not k:
|
||||
continue
|
||||
# Normalize event
|
||||
evt = {
|
||||
"symbol": symbol,
|
||||
"timeframe": key.timeframe,
|
||||
"open_time": datetime.fromtimestamp(k["t"] / 1000.0).isoformat(),
|
||||
"close_time": datetime.fromtimestamp(k["T"] / 1000.0).isoformat(),
|
||||
"open": float(k["o"]),
|
||||
"high": float(k["h"]),
|
||||
"low": float(k["l"]),
|
||||
"close": float(k["c"]),
|
||||
"volume": float(k.get("v", 0.0)),
|
||||
"is_closed": bool(k.get("x", False)),
|
||||
"source": "binance",
|
||||
}
|
||||
# Update live store (1m bar)
|
||||
try:
|
||||
tsec = int(k["T"] // 1000)
|
||||
live_store.ingest_bar(symbol=symbol, timeframe=key.timeframe, bar={
|
||||
"time": tsec, "open": evt["open"], "high": evt["high"], "low": evt["low"], "close": evt["close"], "volume": evt["volume"],
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
# Fan-out to subscribers
|
||||
subs = self._subs.get(key) or set()
|
||||
for q in list(subs):
|
||||
try:
|
||||
if q.full():
|
||||
q.get_nowait()
|
||||
q.put_nowait(evt)
|
||||
except Exception:
|
||||
# Drop failed subscriber
|
||||
try:
|
||||
subs.remove(q)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
|
||||
# Singleton hub instance
|
||||
hub = BinanceStreamHub()
|
||||
Reference in New Issue
Block a user