feat: 实现订单推送服务和 WebSocket 重连机制

- 实现订单推送服务,支持多账户订单实时推送
- 添加 WebSocket 自动重连机制,支持指数退避策略
- 修复订单详情接口 L2 认证问题,通过 PolymarketClobService 获取
- 配置 Gson lenient 模式,支持解析格式不严格的 JSON
- 添加响应日志拦截器,便于调试 API 响应问题
- 使用 Gamma API 获取市场信息,支持通过 condition_ids 查询
- 修复字段映射问题,使用 @SerializedName 替代 @JsonProperty
- 订单推送消息包含订单详情和市场信息
This commit is contained in:
WrBug
2025-11-27 14:51:46 +08:00
parent 47357995af
commit 5b458b9b0c
21 changed files with 1765 additions and 66 deletions
+88 -2
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useEffect, useCallback } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { ConfigProvider } from 'antd'
import { ConfigProvider, notification } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import Layout from './components/Layout'
import AccountList from './pages/AccountList'
@@ -13,8 +13,83 @@ import ConfigPage from './pages/ConfigPage'
import PositionList from './pages/PositionList'
import Statistics from './pages/Statistics'
import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
function App() {
/**
* 获取订单类型文本
*/
const getOrderTypeText = useCallback((type: string): string => {
switch (type) {
case 'PLACEMENT':
return '订单创建'
case 'UPDATE':
return '订单更新'
case 'CANCELLATION':
return '订单取消'
default:
return '订单事件'
}
}, [])
/**
* 处理订单推送消息,显示全局通知
*/
const handleOrderPush = useCallback((message: OrderPushMessage) => {
const { accountName, order, orderDetail } = message
// 根据订单类型和操作类型确定通知内容
const orderTypeText = getOrderTypeText(order.type)
const sideText = order.side === 'BUY' ? '买入' : '卖出'
// 如果有市场名称,在标题中显示
const marketName = orderDetail?.marketName || order.market.substring(0, 8) + '...'
const title = `${accountName} - ${orderTypeText}`
// 优先使用订单详情中的数据,如果没有则使用 WebSocket 消息中的数据
const price = orderDetail ? parseFloat(orderDetail.price).toFixed(4) : parseFloat(order.price).toFixed(4)
const size = orderDetail ? parseFloat(orderDetail.size).toFixed(2) : parseFloat(order.original_size).toFixed(2)
const filled = orderDetail ? parseFloat(orderDetail.filled).toFixed(2) : parseFloat(order.size_matched).toFixed(2)
const status = orderDetail?.status || 'UNKNOWN'
// 构建描述信息
let description = `市场: ${marketName}\n${sideText} ${size} @ ${price}`
// 如果有订单详情,显示更详细的信息
if (orderDetail) {
description += `\n状态: ${status}`
if (parseFloat(filled) > 0) {
description += ` | 已成交: ${filled}`
}
const remaining = (parseFloat(size) - parseFloat(filled)).toFixed(2)
if (parseFloat(remaining) > 0) {
description += ` | 剩余: ${remaining}`
}
} else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) {
// 如果没有订单详情,使用 WebSocket 消息中的已成交数量
description += `\n已成交: ${filled}`
}
// 根据订单类型选择通知类型
let notificationType: 'info' | 'success' | 'warning' | 'error' = 'info'
if (order.type === 'PLACEMENT') {
notificationType = 'info'
} else if (order.type === 'UPDATE') {
notificationType = 'success'
} else if (order.type === 'CANCELLATION') {
notificationType = 'warning'
}
// 显示通知
notification[notificationType]({
message: title,
description: description,
placement: 'topRight',
duration: order.type === 'CANCELLATION' ? 3 : 5, // 取消订单通知显示时间短一些
key: `order-${order.id}`, // 使用订单 ID 作为 key,避免重复通知
})
}, [getOrderTypeText])
// 应用启动时立即建立全局 WebSocket 连接
useEffect(() => {
// 立即建立连接(如果还未连接)
@@ -26,6 +101,17 @@ function App() {
// WebSocket 连接会在整个应用生命周期中保持,并自动重连
}, [])
// 订阅订单推送并显示全局通知
useEffect(() => {
const unsubscribe = wsManager.subscribe('order', (data: OrderPushMessage) => {
handleOrderPush(data)
})
return () => {
unsubscribe()
}
}, [handleOrderPush])
return (
<ConfigProvider locale={zhCN}>
<BrowserRouter>
+2 -2
View File
@@ -1,12 +1,12 @@
import { useEffect, useState, useRef } from 'react'
import { wsManager, SubscriptionCallback } from '../services/websocket'
import { wsManager } from '../services/websocket'
/**
* 使用 WebSocket 订阅
*/
export function useWebSocketSubscription<T = any>(
channel: string,
callback: SubscriptionCallback,
callback: (data: T) => void,
payload?: any
): { connected: boolean } {
const [connected, setConnected] = useState(wsManager.isConnected())
+48
View File
@@ -210,3 +210,51 @@ export function getPositionKey(position: AccountPosition): string {
return `${position.accountId}-${position.marketId}-${position.side}`
}
/**
* Polymarket 订单消息(来自 WebSocket User Channel
*/
export interface OrderMessage {
asset_id: string
associate_trades?: string[]
event_type: string // "order"
id: string // order id
market: string // condition ID of market
order_owner: string // owner of order
original_size: string // original order size
outcome: string // outcome
owner: string // owner of orders
price: string // price of order
side: string // BUY/SELL
size_matched: string // size of order that has been matched
timestamp: string // time of event
type: string // PLACEMENT/UPDATE/CANCELLATION
}
/**
* 订单详情(通过 API 获取)
*/
export interface OrderDetail {
id: string // 订单 ID
market: string // 市场 ID (condition ID)
side: string // BUY/SELL
price: string // 价格
size: string // 订单大小
filled: string // 已成交数量
status: string // 订单状态
createdAt: string // 创建时间(ISO 8601 格式)
marketName?: string // 市场名称
marketSlug?: string // 市场 slug
marketIcon?: string // 市场图标
}
/**
* 订单推送消息
*/
export interface OrderPushMessage {
accountId: number
accountName: string
order: OrderMessage // 订单信息(来自 WebSocket
orderDetail?: OrderDetail // 订单详情(通过 API 获取)
timestamp?: number // 推送时间戳
}