Refactor MakerPointsEngine and BinanceDepthTracker for improved connection management

- Renamed connection state variable in MakerPointsEngine for clarity.
- Added connection state change listeners in BinanceDepthTracker to handle connection status updates.
- Implemented heartbeat monitoring and connection duration checks in BinanceDepthTracker to enhance WebSocket reliability.
- Introduced data staleness checks and improved error handling for WebSocket connections.
- Enhanced logging for connection events to provide better insights into connection status changes.
This commit is contained in:
discountry
2026-01-21 16:03:34 +08:00
parent f1140f106a
commit 69271d33ca
2 changed files with 244 additions and 22 deletions
+218 -18
View File
@@ -8,6 +8,23 @@ const WebSocketCtor: typeof globalThis.WebSocket =
const DEFAULT_BASE_URL = "wss://fstream.binance.com/ws"; const DEFAULT_BASE_URL = "wss://fstream.binance.com/ws";
// ========== Binance WebSocket 连接管理常量 ==========
// Binance 服务器每 3 分钟发送 ping,10 分钟无 pong 会断连
// 我们设置 5 分钟作为心跳超时阈值(保守值)
const HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000;
// 心跳检查间隔(每 30 秒检查一次)
const HEARTBEAT_CHECK_INTERVAL_MS = 30_000;
// Binance 连接最长有效期 24 小时,我们设置 23 小时主动重连
const MAX_CONNECTION_DURATION_MS = 23 * 60 * 60 * 1000;
// 数据过时阈值(毫秒)- 超过此时间未收到数据,标记为不可用
const DATA_STALE_THRESHOLD_MS = 5_000;
// 基础重连延迟
const RECONNECT_DELAY_BASE_MS = 3000;
// 最大重连延迟
const RECONNECT_DELAY_MAX_MS = 60_000;
export type BinanceConnectionState = "connected" | "disconnected" | "stale";
export interface BinanceDepthSnapshot { export interface BinanceDepthSnapshot {
symbol: string; symbol: string;
buySum: number; buySum: number;
@@ -18,13 +35,29 @@ export interface BinanceDepthSnapshot {
updatedAt: number; updatedAt: number;
} }
export type BinanceConnectionListener = (state: BinanceConnectionState) => void;
export class BinanceDepthTracker { export class BinanceDepthTracker {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null; private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private reconnectDelayMs = 3000; private reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
private stopped = false; private stopped = false;
private snapshot: BinanceDepthSnapshot | null = null; private snapshot: BinanceDepthSnapshot | null = null;
private listeners = new Set<(snapshot: BinanceDepthSnapshot) => void>(); private listeners = new Set<(snapshot: BinanceDepthSnapshot) => void>();
private connectionListeners = new Set<BinanceConnectionListener>();
// ========== 心跳与连接管理 ==========
// 上次收到消息的时间戳
private lastMessageTime = 0;
// 心跳检查定时器
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
// 连接建立时间(用于日志记录)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private connectionStartTime = 0;
// 24 小时重连定时器
private maxDurationTimer: ReturnType<typeof setTimeout> | null = null;
// 当前连接状态
private connectionState: BinanceConnectionState = "disconnected";
constructor( constructor(
private readonly symbol: string, private readonly symbol: string,
@@ -43,18 +76,7 @@ export class BinanceDepthTracker {
stop(): void { stop(): void {
this.stopped = true; this.stopped = true;
if (this.reconnectTimer) { this.cleanup();
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
try {
this.ws.close();
} catch {
// Ignore close errors
}
this.ws = null;
}
} }
onUpdate(handler: (snapshot: BinanceDepthSnapshot) => void): void { onUpdate(handler: (snapshot: BinanceDepthSnapshot) => void): void {
@@ -65,37 +87,122 @@ export class BinanceDepthTracker {
this.listeners.delete(handler); this.listeners.delete(handler);
} }
/**
* 监听连接状态变化
*/
onConnectionChange(handler: BinanceConnectionListener): void {
this.connectionListeners.add(handler);
}
offConnectionChange(handler: BinanceConnectionListener): void {
this.connectionListeners.delete(handler);
}
getSnapshot(): BinanceDepthSnapshot | null { getSnapshot(): BinanceDepthSnapshot | null {
return this.snapshot ? { ...this.snapshot } : null; return this.snapshot ? { ...this.snapshot } : null;
} }
/**
* 获取当前连接状态
*/
getConnectionState(): BinanceConnectionState {
return this.connectionState;
}
/**
* 检查数据是否过时
*/
isDataStale(): boolean {
if (!this.snapshot) return true;
return Date.now() - this.snapshot.updatedAt > DATA_STALE_THRESHOLD_MS;
}
private cleanup(): void {
// 停止心跳监控
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
// 停止 24 小时重连定时器
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
// 停止重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 关闭 WebSocket
if (this.ws) {
try {
this.ws.close();
} catch {
// Ignore close errors
}
this.ws = null;
}
}
private connect(): void { private connect(): void {
if (this.ws || this.stopped) return; if (this.ws || this.stopped) return;
const url = this.buildUrl(); const url = this.buildUrl();
this.ws = new WebSocketCtor(url); this.ws = new WebSocketCtor(url);
const handleOpen = () => { const handleOpen = () => {
this.reconnectDelayMs = 3000; this.reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
this.connectionStartTime = Date.now();
this.lastMessageTime = Date.now();
this.updateConnectionState("connected");
// 启动心跳监控
this.startHeartbeatMonitor();
// 启动 24 小时自动重连定时器
this.startMaxDurationTimer();
this.options?.logger?.("binanceDepth", "WebSocket connected");
}; };
const handleClose = () => { const handleClose = () => {
this.ws = null; this.ws = null;
this.stopHeartbeatMonitor();
this.stopMaxDurationTimer();
this.updateConnectionState("disconnected");
if (!this.stopped) { if (!this.stopped) {
this.options?.logger?.("binanceDepth", "WebSocket closed, scheduling reconnect");
this.scheduleReconnect(); this.scheduleReconnect();
} }
}; };
const handleError = (error: unknown) => { const handleError = (error: unknown) => {
this.options?.logger?.("binanceDepth", error); this.options?.logger?.("binanceDepth", error);
// 如果连接从未成功建立,需要清理并重连
if (this.ws && this.connectionState === "disconnected") {
this.ws = null;
this.scheduleReconnect();
}
}; };
const handleMessage = (event: { data: unknown }) => { const handleMessage = (event: { data: unknown }) => {
this.lastMessageTime = Date.now();
// 如果之前是 stale 状态,恢复为 connected
if (this.connectionState === "stale") {
this.updateConnectionState("connected");
}
this.handlePayload(event.data); this.handlePayload(event.data);
}; };
// 处理 Binance 服务器的 ping 帧
// 根据文档:必须尽快回复 pongpayload 为 ping 的 payload 副本
const handlePing = (data: unknown) => { const handlePing = (data: unknown) => {
this.lastMessageTime = Date.now();
if (this.ws && "pong" in this.ws && typeof this.ws.pong === "function") { if (this.ws && "pong" in this.ws && typeof this.ws.pong === "function") {
this.ws.pong(data as any); try {
this.ws.pong(data as any);
} catch (error) {
this.options?.logger?.("binanceDepth pong", error);
}
} }
}; };
@@ -130,11 +237,101 @@ export class BinanceDepthTracker {
if (this.reconnectTimer || this.stopped) return; if (this.reconnectTimer || this.stopped) return;
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null; this.reconnectTimer = null;
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, 60_000); this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, RECONNECT_DELAY_MAX_MS);
this.connect(); this.connect();
}, this.reconnectDelayMs); }, this.reconnectDelayMs);
} }
/**
* 启动心跳监控
* 根据 Binance 文档:10 分钟无 pong 会断连
* 我们设置 5 分钟作为心跳超时阈值
*/
private startHeartbeatMonitor(): void {
this.stopHeartbeatMonitor();
this.heartbeatTimer = setInterval(() => {
const now = Date.now();
const elapsed = now - this.lastMessageTime;
// 检查数据是否过时(5 秒无数据)
if (elapsed > DATA_STALE_THRESHOLD_MS && this.connectionState === "connected") {
this.updateConnectionState("stale");
this.options?.logger?.("binanceDepth", `Data stale: ${elapsed}ms since last message`);
}
// 检查心跳超时(5 分钟无消息)
if (elapsed > HEARTBEAT_TIMEOUT_MS) {
this.options?.logger?.("binanceDepth", `Heartbeat timeout: ${elapsed}ms, forcing reconnect`);
this.forceReconnect("heartbeat_timeout");
}
}, HEARTBEAT_CHECK_INTERVAL_MS);
}
private stopHeartbeatMonitor(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* 启动 24 小时自动重连定时器
* 根据 Binance 文档:连接最长有效期 24 小时
* 我们设置 23 小时主动重连,避免被服务器断开
*/
private startMaxDurationTimer(): void {
this.stopMaxDurationTimer();
this.maxDurationTimer = setTimeout(() => {
this.options?.logger?.("binanceDepth", "Max connection duration reached (23h), reconnecting");
this.forceReconnect("max_duration");
}, MAX_CONNECTION_DURATION_MS);
}
private stopMaxDurationTimer(): void {
if (this.maxDurationTimer) {
clearTimeout(this.maxDurationTimer);
this.maxDurationTimer = null;
}
}
/**
* 强制重连
*/
private forceReconnect(reason: string): void {
this.options?.logger?.("binanceDepth", `Force reconnect: ${reason}`);
this.stopHeartbeatMonitor();
this.stopMaxDurationTimer();
if (this.ws) {
try {
this.ws.close();
} catch {
// ignore
}
this.ws = null;
}
this.updateConnectionState("disconnected");
// 立即重连(不使用指数退避)
this.reconnectDelayMs = RECONNECT_DELAY_BASE_MS;
this.scheduleReconnect();
}
/**
* 更新连接状态并通知监听器
*/
private updateConnectionState(state: BinanceConnectionState): void {
if (this.connectionState === state) return;
this.connectionState = state;
for (const listener of this.connectionListeners) {
try {
listener(state);
} catch (error) {
this.options?.logger?.("binanceDepth connectionListener", error);
}
}
}
private handlePayload(data: unknown): void { private handlePayload(data: unknown): void {
const payload = this.parsePayload(data); const payload = this.parsePayload(data);
if (!payload) return; if (!payload) return;
@@ -158,7 +355,11 @@ export class BinanceDepthTracker {
updatedAt: Date.now(), updatedAt: Date.now(),
}; };
for (const listener of this.listeners) { for (const listener of this.listeners) {
listener({ ...this.snapshot }); try {
listener({ ...this.snapshot });
} catch (error) {
this.options?.logger?.("binanceDepth listener", error);
}
} }
} }
@@ -174,4 +375,3 @@ export class BinanceDepthTracker {
} }
} }
} }
+26 -4
View File
@@ -1,5 +1,5 @@
import type { MakerPointsConfig } from "../config"; import type { MakerPointsConfig } from "../config";
import type { ExchangeAdapter, ConnectionEventType } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AsterAccountSnapshot,
AsterDepth, AsterDepth,
@@ -154,7 +154,7 @@ export class MakerPointsEngine {
private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT"; private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT";
// 连接保护相关状态 // 连接保护相关状态
private connectionState: "connected" | "disconnected" = "connected"; private standxConnectionState: "connected" | "disconnected" = "connected";
private reconnectResetPending = false; private reconnectResetPending = false;
private lastRepriceQueryTime = 0; private lastRepriceQueryTime = 0;
private readonly repriceQueryIntervalMs = 3000; // 最小查询间隔 private readonly repriceQueryIntervalMs = 3000; // 最小查询间隔
@@ -179,6 +179,19 @@ export class MakerPointsEngine {
this.feedStatus.binance = true; this.feedStatus.binance = true;
this.emitUpdate(); this.emitUpdate();
}); });
// 监听 Binance 连接状态变化
this.binanceDepth.onConnectionChange((state) => {
if (state === "disconnected") {
this.feedStatus.binance = false;
this.tradeLog.push("warn", "Binance 深度连接断开");
} else if (state === "stale") {
this.tradeLog.push("warn", "Binance 深度数据过时");
} else if (state === "connected") {
this.feedStatus.binance = true;
this.tradeLog.push("info", "Binance 深度连接恢复");
}
this.emitUpdate();
});
this.syncPrecision(); this.syncPrecision();
this.bootstrap(); this.bootstrap();
} }
@@ -325,7 +338,7 @@ export class MakerPointsEngine {
* 处理断连事件 * 处理断连事件
*/ */
private handleDisconnect(symbol: string): void { private handleDisconnect(symbol: string): void {
this.connectionState = "disconnected"; this.standxConnectionState = "disconnected";
this.tradeLog.push("warn", `WebSocket 断连 (${symbol}),启动断连保护`); this.tradeLog.push("warn", `WebSocket 断连 (${symbol}),启动断连保护`);
this.notify({ this.notify({
type: "token_expired", type: "token_expired",
@@ -342,7 +355,7 @@ export class MakerPointsEngine {
* 重连后需要重新查询挂单并取消所有挂单 * 重连后需要重新查询挂单并取消所有挂单
*/ */
private async handleReconnect(symbol: string): Promise<void> { private async handleReconnect(symbol: string): Promise<void> {
this.connectionState = "connected"; this.standxConnectionState = "connected";
this.reconnectResetPending = true; this.reconnectResetPending = true;
this.tradeLog.push("info", `WebSocket 重连成功 (${symbol}),开始重连保护流程`); this.tradeLog.push("info", `WebSocket 重连成功 (${symbol}),开始重连保护流程`);
@@ -420,6 +433,10 @@ export class MakerPointsEngine {
private async tick(): Promise<void> { private async tick(): Promise<void> {
if (this.processing) return; if (this.processing) return;
// 重连处理期间不执行主循环,避免状态竞争
if (this.reconnectResetPending) return;
// 止损执行期间不执行主循环,避免订单冲突
if (this.stopLossProcessing) return;
this.processing = true; this.processing = true;
let hadRateLimit = false; let hadRateLimit = false;
try { try {
@@ -747,6 +764,11 @@ export class MakerPointsEngine {
} }
private async syncOrders(targets: DesiredOrder[], _closeOnly: boolean): Promise<void> { private async syncOrders(targets: DesiredOrder[], _closeOnly: boolean): Promise<void> {
// 止损执行期间不进行挂单操作,避免订单冲突
if (this.stopLossProcessing) return;
// 重连处理期间不进行挂单操作
if (this.reconnectResetPending) return;
// 价格变化保护:如果需要 reprice 且距上次查询已过足够时间,先查询真实挂单 // 价格变化保护:如果需要 reprice 且距上次查询已过足够时间,先查询真实挂单
const shouldVerifyOrders = await this.verifyOrdersIfNeeded(); const shouldVerifyOrders = await this.verifyOrdersIfNeeded();
if (shouldVerifyOrders) { if (shouldVerifyOrders) {