Spaces:
Running
Running
| /** | |
| * binanceWebSocket.js | |
| * Real-time kline streaming from Binance WebSocket API. | |
| * No API key required. | |
| */ | |
| const BINANCE_WS_BASE = 'wss://stream.binance.com:9443/ws'; | |
| const MAX_RECONNECT_DELAY = 30_000; // 30 seconds | |
| export class BinanceStream { | |
| constructor() { | |
| /** @type {WebSocket|null} */ | |
| this._ws = null; | |
| /** @type {string|null} */ | |
| this._symbol = null; | |
| /** @type {string|null} */ | |
| this._interval = null; | |
| /** @type {{ onCandleUpdate?: Function, onTick?: Function }|null} */ | |
| this._callbacks = null; | |
| this._reconnectAttempts = 0; | |
| this._reconnectTimer = null; | |
| this._intentionallyClosed = false; | |
| } | |
| /** | |
| * Subscribe to a kline stream. | |
| * | |
| * @param {string} symbol - Lowercase symbol, e.g. 'btcusdt' | |
| * @param {string} interval - Kline interval, e.g. '1m', '5m', '1h' | |
| * @param {{ onCandleUpdate?: (candle: object) => void, onTick?: (price: number) => void }} callbacks | |
| */ | |
| subscribe(symbol, interval, callbacks) { | |
| // Clean up any existing connection first | |
| this.unsubscribe(); | |
| this._symbol = symbol.toLowerCase(); | |
| this._interval = interval; | |
| this._callbacks = callbacks; | |
| this._intentionallyClosed = false; | |
| this._reconnectAttempts = 0; | |
| this._connect(); | |
| } | |
| /** Disconnect from the current stream. */ | |
| unsubscribe() { | |
| this._intentionallyClosed = true; | |
| clearTimeout(this._reconnectTimer); | |
| this._reconnectTimer = null; | |
| if (this._ws) { | |
| this._ws.onopen = null; | |
| this._ws.onmessage = null; | |
| this._ws.onerror = null; | |
| this._ws.onclose = null; | |
| this._ws.close(); | |
| this._ws = null; | |
| } | |
| this._symbol = null; | |
| this._interval = null; | |
| this._callbacks = null; | |
| } | |
| /* ------------------------------------------------------------------ */ | |
| /* Internal */ | |
| /* ------------------------------------------------------------------ */ | |
| _connect() { | |
| const streamName = `${this._symbol}@kline_${this._interval}`; | |
| const url = `${BINANCE_WS_BASE}/${streamName}`; | |
| console.log(`[BinanceStream] Connecting to ${url}`); | |
| this._ws = new WebSocket(url); | |
| this._ws.onopen = () => { | |
| console.log(`[BinanceStream] Connected β ${streamName}`); | |
| this._reconnectAttempts = 0; | |
| }; | |
| this._ws.onmessage = (event) => { | |
| try { | |
| this._handleMessage(JSON.parse(event.data)); | |
| } catch (err) { | |
| console.error('[BinanceStream] Error handling message:', err); | |
| } | |
| }; | |
| this._ws.onerror = (err) => { | |
| console.error('[BinanceStream] WebSocket error:', err); | |
| }; | |
| this._ws.onclose = (event) => { | |
| console.warn(`[BinanceStream] Connection closed (code=${event.code})`); | |
| if (!this._intentionallyClosed) { | |
| this._reconnect(); | |
| } | |
| }; | |
| } | |
| /** | |
| * Parse a Binance kline WebSocket event. | |
| * | |
| * Binance event shape: | |
| * { | |
| * e: 'kline', | |
| * k: { t, o, h, l, c, v, x, ... } | |
| * } | |
| */ | |
| _handleMessage(msg) { | |
| if (msg.e !== 'kline' || !msg.k) return; | |
| const k = msg.k; | |
| const candle = { | |
| time: Math.floor(k.t / 1000), // ms β seconds | |
| open: parseFloat(k.o), | |
| high: parseFloat(k.h), | |
| low: parseFloat(k.l), | |
| close: parseFloat(k.c), | |
| volume: parseFloat(k.v), | |
| isClosed: k.x, | |
| }; | |
| // Always emit the latest close as a tick | |
| if (this._callbacks?.onTick) { | |
| this._callbacks.onTick(candle.close); | |
| } | |
| if (this._callbacks?.onCandleUpdate) { | |
| this._callbacks.onCandleUpdate(candle); | |
| } | |
| } | |
| /** Reconnect with exponential backoff: 1 s β 2 s β 4 s β β¦ β 30 s max. */ | |
| _reconnect() { | |
| const delay = Math.min( | |
| 1000 * 2 ** this._reconnectAttempts, | |
| MAX_RECONNECT_DELAY, | |
| ); | |
| this._reconnectAttempts += 1; | |
| console.log(`[BinanceStream] Reconnecting in ${delay}ms (attempt ${this._reconnectAttempts})`); | |
| this._reconnectTimer = setTimeout(() => { | |
| if (!this._intentionallyClosed) { | |
| this._connect(); | |
| } | |
| }, delay); | |
| } | |
| } | |