dongzp 497236b8b4 feat(ws): realtime Baidu quote via tick/snapshot WebSocket
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 09:56:39 +08:00

168 lines
4.2 KiB
TypeScript

import { contractConfig } from '../../config/contract'
import type { BaiduWsItem, BaiduWsMessage, BaiduWsOutbound } from './wsTypes'
const WS_URL = 'wss://finance-ws.pae.baidu.com/'
const PING_MS = 6_000
const PATCH_MS = 60_000
const RECONNECT_BASE_MS = 1_000
const RECONNECT_MAX_MS = 30_000
export type BaiduQuoteWsHandlers = {
onMessage: (msg: BaiduWsMessage) => void
onError?: (err: unknown) => void
}
function buildItem(): BaiduWsItem {
return {
code: contractConfig.code,
name: contractConfig.name,
market: 'ab',
financeType: 'futures',
}
}
/**
* Baidu finance quote WebSocket client.
* Handles subscribe (tick + snapshot), ping every 6s, patch every 60s, and reconnect.
*/
export class BaiduQuoteWs {
private ws: WebSocket | null = null
private pingTimer: ReturnType<typeof setInterval> | null = null
private patchTimer: ReturnType<typeof setInterval> | null = null
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
private reconnectAttempt = 0
private intentionalClose = false
private handlers: BaiduQuoteWsHandlers
constructor(handlers: BaiduQuoteWsHandlers) {
this.handlers = handlers
}
connect(): void {
this.intentionalClose = false
this.clearReconnect()
if (
this.ws &&
(this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
) {
return
}
this.openSocket()
}
disconnect(): void {
this.intentionalClose = true
this.clearTimers()
this.clearReconnect()
if (this.ws) {
this.ws.onopen = null
this.ws.onmessage = null
this.ws.onerror = null
this.ws.onclose = null
try {
this.ws.close()
} catch {
/* ignore */
}
this.ws = null
}
}
private openSocket(): void {
if (this.intentionalClose) return
const ws = new WebSocket(WS_URL)
this.ws = ws
ws.onopen = () => {
if (this.intentionalClose) {
try {
ws.close()
} catch {
/* ignore */
}
return
}
this.reconnectAttempt = 0
this.sendSubscribe()
this.startTimers()
}
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(String(ev.data)) as BaiduWsMessage
this.handlers.onMessage(msg)
} catch (e) {
console.error('[baidu-ws] parse failed', e)
this.handlers.onError?.(e)
}
}
ws.onerror = (ev) => {
console.error('[baidu-ws] error', ev)
this.handlers.onError?.(ev)
}
ws.onclose = () => {
this.clearTimers()
this.ws = null
if (!this.intentionalClose) this.scheduleReconnect()
}
}
private send(payload: BaiduWsOutbound): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return
this.ws.send(JSON.stringify(payload))
}
private sendSubscribe(): void {
const item = buildItem()
this.send({ method: 'subscribe', source: 'pc-web', product: 'tick', items: [item] })
this.send({ method: 'subscribe', source: 'pc-web', product: 'snapshot', items: [item] })
}
private sendPing(): void {
this.send({ method: 'ping', source: 'pc-web' })
}
private sendPatch(): void {
const item = buildItem()
this.send({ method: 'patch', source: 'pc-web', product: 'snapshot', items: [item] })
}
private startTimers(): void {
this.clearTimers()
this.pingTimer = setInterval(() => this.sendPing(), PING_MS)
this.patchTimer = setInterval(() => this.sendPatch(), PATCH_MS)
}
private clearTimers(): void {
if (this.pingTimer) {
clearInterval(this.pingTimer)
this.pingTimer = null
}
if (this.patchTimer) {
clearInterval(this.patchTimer)
this.patchTimer = null
}
}
private scheduleReconnect(): void {
this.clearReconnect()
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS)
this.reconnectAttempt += 1
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
if (this.intentionalClose) return
this.openSocket()
}, delay)
}
private clearReconnect(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
}
}