xtraderClient/sdk/clobSocket.ts
2026-02-25 14:46:10 +08:00

297 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 前端 CLOB WebSocket SDK
// 该 SDK 提供了连接到 CLOB WebSocket 服务的 TypeScript 接口。
// 它支持针对不同消息类型的特定回调:
// - Welcome (连接建立)
// - PriceSizeAll (全量订单簿快照)
// - PriceSizeDelta (增量订单簿更新)
// - Trade (成交记录)
export interface ClobSocketOptions {
// WebSocket URL (例如: "ws://localhost:8888/clob/ws" 或 "/api/clob/ws")
url?: string;
// 断开连接时是否自动重连
autoReconnect?: boolean;
// 重连间隔时间(毫秒)
reconnectInterval?: number;
// 最大重连尝试次数
maxReconnectAttempts?: number;
}
// 基于 Go 结构体的消息接口定义
export interface WelcomeMsg {
type: 'welcome';
t: number;
}
export interface PriceSizePolyMsg {
i: number; // 索引
e: 'price_size_all' | 'price_size_delta'; // 事件类型
m: string; // 市场 ID
t: number; // 时间戳
b?: Record<string, number>; // 买单: 价格 -> 数量
s?: Record<string, number>; // 卖单: 价格 -> 数量
}
export interface TradePolyMsg {
e: 'trade'; // 事件类型
m: string; // 市场 ID
p: string; // 价格
s: string; // 数量
side: string; // "buy" 或 "sell"
}
export type ClobMsg = WelcomeMsg | PriceSizePolyMsg | TradePolyMsg;
// 回调函数类型定义
export type WelcomeCallback = (data: WelcomeMsg) => void;
export type PriceSizeAllCallback = (data: PriceSizePolyMsg) => void;
export type PriceSizeDeltaCallback = (data: PriceSizePolyMsg) => void;
export type TradeCallback = (data: TradePolyMsg) => void;
export type ConnectCallback = (event: Event) => void;
export type DisconnectCallback = (event: CloseEvent) => void;
export type ErrorCallback = (event: Event) => void;
export class ClobSdk {
private url: string;
private tokenIds: string[];
private ws: WebSocket | null = null;
private autoReconnect: boolean;
private reconnectInterval: number;
private maxReconnectAttempts: number;
private reconnectAttempts = 0;
private isExplicitClose = false;
private listeners: {
welcome: WelcomeCallback[];
priceSizeAll: PriceSizeAllCallback[];
priceSizeDelta: PriceSizeDeltaCallback[];
trade: TradeCallback[];
connect: ConnectCallback[];
disconnect: DisconnectCallback[];
error: ErrorCallback[];
} = {
welcome: [],
priceSizeAll: [],
priceSizeDelta: [],
trade: [],
connect: [],
disconnect: [],
error: []
};
/**
* 初始化 CLOB SDK
* @param tokenIds Yes/No 的 token ID可传单个或数组如 [yesTokenId, noTokenId]
* @param options 配置选项
*/
constructor(tokenIds: string | string[], options: ClobSocketOptions = {}) {
this.tokenIds = Array.isArray(tokenIds) ? tokenIds : [tokenIds];
// 如果未提供 URL默认为相对路径
// 修改为 Node.js 测试环境适配:如果 window 未定义,回退到 localhost
let protocol = 'ws:';
let host = '10.117.63.212:8888';
if (typeof window !== 'undefined' && window.location) {
protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
host = window.location.host;
}
this.url = options.url || `${protocol}//${host}/clob/ws`;
this.autoReconnect = options.autoReconnect ?? true;
this.reconnectInterval = options.reconnectInterval || 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
console.log('[ClobSdk] 构造完成,最终参数:', {
tokenIds: this.tokenIds,
url: this.url,
autoReconnect: this.autoReconnect,
reconnectInterval: this.reconnectInterval,
maxReconnectAttempts: this.maxReconnectAttempts,
});
}
/**
* 连接到 WebSocket 服务
*/
public connect(): void {
this.isExplicitClose = false;
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = (event: any) => {
console.log(`[ClobSdk] 已连接到 ${this.url}`);
this.reconnectAttempts = 0;
this.notifyConnect(event);
this.subscribe();
};
this.ws.onmessage = (event: any) => {
try {
const rawData = JSON.parse(event.data);
// console.log('[ClobSdk] 收到消息:', rawData);
this.handleMessage(rawData);
} catch (e) {
console.error('[ClobSdk] 消息解析失败:', e);
}
};
this.ws.onclose = (event: any) => {
console.log('[ClobSdk] 连接关闭', { code: event.code, reason: event.reason, explicit: this.isExplicitClose });
if (!this.isExplicitClose) {
this.notifyDisconnect(event);
if (this.autoReconnect) {
this.attemptReconnect();
}
} else {
this.notifyDisconnect(event);
}
};
this.ws.onerror = (event: any) => {
console.error('[ClobSdk] 错误:', event);
this.notifyError(event);
};
} catch (e) {
console.error('[ClobSdk] 连接错误:', e);
if (this.autoReconnect) {
this.attemptReconnect();
}
}
}
/**
* 断开 WebSocket 服务连接
*/
public disconnect(): void {
this.isExplicitClose = true;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// --- 事件监听器 ---
public onWelcome(callback: WelcomeCallback): () => void {
this.listeners.welcome.push(callback);
return () => this.removeListener('welcome', callback);
}
public onPriceSizeAll(callback: PriceSizeAllCallback): () => void {
this.listeners.priceSizeAll.push(callback);
return () => this.removeListener('priceSizeAll', callback);
}
public onPriceSizeDelta(callback: PriceSizeDeltaCallback): () => void {
this.listeners.priceSizeDelta.push(callback);
return () => this.removeListener('priceSizeDelta', callback);
}
public onTrade(callback: TradeCallback): () => void {
this.listeners.trade.push(callback);
return () => this.removeListener('trade', callback);
}
public onConnect(callback: ConnectCallback): () => void {
this.listeners.connect.push(callback);
return () => this.removeListener('connect', callback);
}
public onDisconnect(callback: DisconnectCallback): () => void {
this.listeners.disconnect.push(callback);
return () => this.removeListener('disconnect', callback);
}
public onError(callback: ErrorCallback): () => void {
this.listeners.error.push(callback);
return () => this.removeListener('error', callback);
}
private removeListener(type: keyof typeof this.listeners, callback: any): void {
this.listeners[type] = (this.listeners[type] as any[]).filter((cb: any) => cb !== callback);
}
// --- 内部方法 ---
private subscribe(): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const msg = {
type: 'market',
assets_ids: this.tokenIds
};
this.ws.send(JSON.stringify(msg));
console.log('[ClobSdk] 已订阅 tokenIds:', this.tokenIds);
}
}
private handleMessage(data: any): void {
// Polymarket 消息有时是数组,有时是对象 (welcome)
if (Array.isArray(data)) {
data.forEach(msg => this.dispatchMessage(msg));
} else {
this.dispatchMessage(data);
}
}
private dispatchMessage(msg: any): void {
// 1. Welcome 消息
if (msg.type === 'welcome') {
console.log('[ClobSdk] 回调: welcome', msg);
this.listeners.welcome.forEach(cb => cb(msg as WelcomeMsg));
return;
}
// 2. 基于 'e' 字段的事件
switch (msg.e) {
case 'price_size_all':
console.log('[ClobSdk] 回调: price_size_all', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg));
break;
case 'price_size_delta':
console.log('[ClobSdk] 回调: price_size_delta', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg));
break;
case 'trade':
// console.log('[ClobSdk] 回调: trade', { m: msg.m, p: msg.p, s: msg.s, side: msg.side });
this.listeners.trade.forEach(cb => cb(msg as TradePolyMsg));
break;
default:
console.warn('[ClobSdk] 未知消息类型:', msg);
}
}
private notifyConnect(event: Event): void {
console.log('[ClobSdk] 回调: onConnect');
this.listeners.connect.forEach(cb => cb(event));
}
private notifyDisconnect(event: CloseEvent): void {
console.log('[ClobSdk] 回调: onDisconnect', { code: event.code, reason: event.reason });
this.listeners.disconnect.forEach(cb => cb(event));
}
private notifyError(event: Event): void {
console.log('[ClobSdk] 回调: onError', event);
this.listeners.error.forEach(cb => cb(event));
}
private attemptReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[ClobSdk] 达到最大重连次数。停止重连。');
return;
}
this.reconnectAttempts++;
console.log(`[ClobSdk] ${this.reconnectInterval}ms 后尝试重连 (尝试次数 ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
}
}