trade-analyzer / src /data /binanceWebSocket.js
Aditya4573's picture
feat: migrate to full-stack cloud autopilot terminal with mongodb atlas
66b7a93
Raw
History Blame Contribute Delete
4.07 kB
/**
* 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);
}
}