最后更新: 2026-05-13
OkxWebSocketClientMain ← 入口,持有主运行线程
└── OkxWebSocketClientManager ← 管理 WS 连接(公共 + 私有)
├── OkxKlineWebSocketClient (公共) ← K线推送
└── OkxKlineWebSocketClient (私有) ← 私有频道(login → 订阅)
├── OkxCandlestickChannelHandler → onKline()
├── OkxOrderInfoChannelHandler → onOrderFilled()
├── OkxPositionsChannelHandler → onPositionUpdate()
└── OkxAccountChannelHandler → 仅日志,供后续扩展
└── OkxGridTradeService ← 核心策略引擎(状态机)
├── OkxConfig ← 策略配置(Builder 模式)
└── OkxTradeExecutor ← REST 下单执行器
| 类 | 职责 |
|---|---|
OkxKlineWebSocketClient |
TCP 连接管理、心跳(10s ping)、指数退避重连(最多3次)、消息路由(login/subscribe/pong → handler.handleMessage) |
OkxWebSocketClientManager |
双重连接(公共/私有)的初始化、销毁、共享线程池 |
OkxWsUtil |
SSL 配置、HMAC-SHA256 签名、订单 ID 生成、ISO8601 时间戳 |
OkxConfig| 参数 | 默认值 | 说明 |
|---|---|---|
contract |
BTC-USDT-SWAP |
合约 ID |
gridRate |
0.003 (0.3%) |
网格步长比率 |
overallTp |
100 USDT |
整体止盈线 |
maxLoss |
100 USDT |
最大亏损线 |
quantity |
"1" |
单次开仓张数(字符串,1 张 = 100 合约) |
gridQueueSize |
20 |
多/空队列各 20 个元素 |
marginRatioLimit |
0.05 (5%) |
保证金安全阀 |
contractMultiplier |
0.01 |
合约乘数(BTC 0.01,ETH 0.1) |
unrealizedPnlPriceMode |
LAST_PRICE |
未实现盈亏计算基准价(LAST_PRICE / MARK_PRICE) |
maxPosSize |
10 张 |
新 单方向最大持仓张数 |
step |
运行时计算 | 新 网格步长 = shortBaseEntryPrice × gridRate,队列生成时写入 |
INIT ──startGrid()──▶ WAITING_KLINE ──首根K线稳定──▶ OPENING ──双基底成交──▶ ACTIVE ──止盈/止损──▶ STOPPED
▲ │
│ K线重连 / 重订阅 │
└──────────────────────────────────────────────────────────┘
| 状态 | 说明 | 触发条件 |
|---|---|---|
INIT |
初始态 | startGrid() |
WAITING_KLINE |
等待 K 线推送稳定(标记价在基底持仓价 ± 步长范围内) | 基底开仓成功 |
OPENING |
等待双基底订单成交 | K线稳定后开仓 |
ACTIVE |
网格运行中 | 双基底成交,队列生成完成 |
STOPPED |
策略停止 | cumulativePnl >= overallTp 或 <= -maxLoss |
sequenceDiagram
participant M as Main
participant S as OkxGridTradeService
participant C as OkxConfig
participant K as K线Handler
participant O as 订单Handler
participant P as 仓位Handler
participant E as OkxTradeExecutor
participant OKX as OKX交易所
Note over M,OKX: ═══ 阶段①: 初始化 ═══
M->>S: startGrid()
S->>S: state = INIT
S->>E: setPositionMode("long_short_mode")
E->>OKX: POST /set-position-mode
S->>E: setLeverage(instId, lever, "isolated")
E->>OKX: POST /set-leverage
S->>E: openShort(quantity, null, null) 基底空单
E->>OKX: 市价做空
S->>E: openLong(quantity, null, null) 基底多单
E->>OKX: 市价做多
S->>S: state = WAITING_KLINE
S->>S: 重置所有字段(step=null, baseOpened=false, ...)
Note over M,OKX: ═══ 阶段②: 等待K线稳定 ═══
K-->>S: onKline(price)
Note over S: WAITING_KLINE 状态 <br/> 检查标记价是否在基底价±步长内
S->>S: step = shortBaseEntryPrice × gridRate
S->>S: state = OPENING
S->>E: openLong(quantity, baseStepLongPrice, null)
E->>OKX: 限价做多
S->>E: openShort(quantity, baseStepShortPrice, null)
E->>OKX: 限价做空
Note over M,OKX: ═══ 阶段③: 订单成交 → 基底识别 + 队列生成 ═══
O-->>S: onOrderFilled(posSide=long, avgPx, fillSz, pnl)
Note over S: !baseLongOpened → 首次多基底
S->>S: longBaseEntryPrice = avgPx
S->>S: baseLongOpened = true
S->>S: tryGenerateQueues()
O-->>S: onOrderFilled(posSide=short, avgPx, fillSz, pnl)
Note over S: !baseShortOpened → 首次空基底
S->>S: shortBaseEntryPrice = avgPx
S->>S: baseShortOpened = true
S->>S: tryGenerateQueues()
Note over S: 双基底就绪 ✓
S->>C: setStep(shortBaseEntryPrice × gridRate)
S->>S: generateShortQueue() → [s0-s, s0-2s, ..., s0-Ns]
S->>S: generateLongQueue() → [l0+s, l0+2s, ..., l0+Ns]
S->>S: state = ACTIVE
Note over M,OKX: ═══ 阶段④: 网格运行 (ACTIVE) ═══
loop 每根K线
K-->>S: onKline(price)
Note over S: 止盈止损检查<br/>队列匹配
alt 空仓队列触发 (price <= queue[0])
S->>S: processShortGrid()
Note over S: 收集所有 price<=queue[i] 的元素<br/>check maxPosSize<br/>开空 + 填充队列 + 转移到多队列
else 多仓队列触发 (price >= queue[0])
S->>S: processLongGrid()
Note over S: 收集所有 price>=queue[i] 的元素<br/>check maxPosSize<br/>开多 + 填充队列 + 转移到空队列
end
end
Note over M,OKX: ═══ 阶段⑤: 加仓成交 → 挂止盈 ═══
O-->>S: onOrderFilled(posSide, avgPx, fillSz, pnl)
Note over S: baseOpened && fillSz > 0 → 加仓
S->>S: tpPrice = queue.get(0)
S->>E: placeTakeProfit(tpPrice, CLOSE_LONG, quantity)
E->>OKX: 挂止盈限价单
Note over M,OKX: ═══ 阶段⑥: 仓位更新 → 累计盈亏 → 停止判断 ═══
loop 每次仓位变化
P-->>S: onPositionUpdate(posSide, size, avgPx, realizedPnl)
S->>S: 更新 longRealizedPnl / shortRealizedPnl
S->>S: cumulativePnl = longRealizedPnl + shortRealizedPnl
alt cumulativePnl >= overallTp
S->>S: state = STOPPED
Note over S: 🏁 盈利目标达成
else cumulativePnl <= -maxLoss
S->>S: state = STOPPED
Note over S: 🏁 亏损上限触发
end
end
onKline() — 策略核心驱动WAITING_KLINE:
└── 标记价在 [shortBasePrice - step, shortBasePrice + step] 范围 → OPENING
└── 开基多限价单(baseStepLongPrice) + 基空限价单(baseStepShortPrice)
ACTIVE:
├── 止盈止损检查(累计已实现盈亏 vs overallTp / maxLoss)
├── 保证金安全阀检查(isMarginSafe)
├── processShortGrid(): 当前价 <= 空队列首 → 触发空仓网格
└── processLongGrid(): 当前价 >= 多队列首 → 触发多仓网格
onOrderFilled() — 基底识别 + 挂止盈数据来源:orders 频道 state=filled,取 avgPx(成交量加权均价)。
!baseLongOpened → 基底多成交 → longBaseEntryPrice = avgPx → tryGenerateQueues()
!baseShortOpened → 基底空成交 → shortBaseEntryPrice = avgPx → tryGenerateQueues()
baseOpened → 加仓成交 → queue[0] = 止盈价 → placeTakeProfit()
tryGenerateQueues():双基底都成交后,计算 step,生成多空队列,状态 → ACTIVE。
step = shortBaseEntryPrice × config.gridRate (统一用空基价)
空队列 (shortPriceQueue): [shortBase - s, shortBase - 2s, ...] → 降序(最近的在前)
多队列 (longPriceQueue): [longBase + s, longBase + 2s, ...] → 升序(最近的在前)
步长统一来源:config.step,由 tryGenerateQueues() 计算并写入。
processShortGrid() / processLongGrid()processShortGrid():
1. 收集所有 price <= queue[i] 的元素
2. 开空(shortPositionSize < maxPosSize 限制)
3. 填补自身队列(从队尾追加 step 递减)
4. 转移到多仓队列(在 longBaseEntryPrice - step 下方插入)
5. 额外开多条件: 价格在多/空持仓价之间 && 多>空 && 远离多均价 && longPositionSize < maxPosSize
processLongGrid():
1. 收集所有 price >= queue[i] 的元素
2. 开多(longPositionSize < maxPosSize 限制)
3. 填补自身队列(从队尾追加 step 递增)
4. 转移到空仓队列(在 shortBaseEntryPrice + step 上方插入)
5. 额外开空条件: 价格在多/空持仓价之间 && 多>空 && 远离空均价 && shortPositionSize < maxPosSize
onPositionUpdate() — 累计已实现盈亏数据来源:positions 频道 realizedPnl 字段(含手续费、资金费的综合已实现盈亏)。
cumulativePnl = longRealizedPnl + shortRealizedPnl
≥ overallTp → STOPPED (盈利)
≤ -maxLoss → STOPPED (亏损)
仓位 size=0 时该方向的
realizedPnl置为 0,避免重复累加。
| # | 方面 | 说明 |
|---|---|---|
| 1 | 双边网格 + 队列转换 | 多空队列互为补仓,价格在区间内震荡时持续收割网格利润。止盈触发后队列元素转移到对方队列,形成"无限网格"循环 |
| 2 | WS 推送驱动,非轮询 | orders + positions + kline 三频道覆盖,低延迟,不浪费 API 额度 |
| 3 | 用 avgPx 而非 fillPx |
均价比单笔成交价更能反映真实持仓成本,避免分笔成交造成的偏差 |
| 4 | 用 realizedPnl 而非手动累加 |
positions 频道的 realizedPnl 包含手续费 + 资金费,比 orders 频道的逐笔 pnl 累加更完整 |
| 5 | 步长统一存储 | config.step 全局唯一来源,避免多空各自计算不一致 |
| 6 | maxPosSize 持仓上限 | 防止单方向无限加仓,控制风险敞口 |
| 7 | 保证金安全阀 | isMarginSafe() 检查,防止爆仓 |
| 8 | 状态机清晰 | INIT → WAITING_KLINE → OPENING → ACTIVE → STOPPED,每个状态职责明确 |
| 9 | 连接层健壮 | 心跳保活、指数退避重连、订阅恢复 |
| # | 问题 | 风险等级 | 说明 |
|---|---|---|---|
| 1 | 无订单状态追踪 | 🔴 高 | onOrderFilled 触发了止盈挂单,但没有记录止盈单的 ordId。如果止盈单一直未成交但策略已标记 STOPPED,可能出现"策略已停止但仍有挂单"的幽灵状态 |
| 2 | 止盈挂单无超时/撤销机制 | 🔴 高 | placeTakeProfit 挂出的限价单没有定时检查和撤销逻辑。市场反向走时止盈单长期挂单,占用保证金且可能过期(OKX 限价单默认 30 天) |
| 3 | 基底开仓用市价单 | 🟡 中 | startGrid() 中 openShort/openLong 传 null 即市价,滑点风险 |
| 4 | 队列队列无限增长风险 | 🟡 中 | replenishOwnQueue 从队尾追加新元素,没有队列长度上限检查。如果价格长时间单边运动,队列可能增长到远超 gridQueueSize 的规模 |
| 5 | 基底价更新与队列不同步 | 🟡 中 | onPositionUpdate 更新 longEntryPrice/shortEntryPrice(加权均价),但队列仍用初始 longBaseEntryPrice/shortBaseEntryPrice 计算。加仓后均价变化,队列位置可能不再合理 |
| 6 | realizedPnl 归零逻辑有漏洞 |
🟡 中 | 仓位 size=0 时 realizedPnl 置为 0,但如果仓位先平仓再重新开仓,累计盈亏会丢失。正确的做法是记录到另一个持久累加字段 |
| 7 | 步长只依赖空基价 | 🟡 中 | 多空各自用 空基价 × gridRate,如果多空基底价相差较大,多的队列步长偏大/偏小 |
| 8 | 无断路器(熔断) | 🟡 中 | 极端行情下没有暂停交易的机制,可能连续触发网格大量开仓 |
| 9 | 多线程安全依赖 volatile | 🟢 低 | 状态字段用 volatile,但对于复合操作(判断-修改)没有锁保护。实际风险较低,因为 WS 回调是单线程 |
| 10 | 无订单幂等保护 | 🟢 低 | 如果 OKX 重复推送同一订单(小概率),会重复挂止盈单 |
| 11 | K线处理无防重 | 🟢 低 | 同一根 K 线可能被 onKline 和 processGrid 多次触发(WS 推送更新),不过 markprice 变更才会进入处理,影响有限 |
| 12 | 账户频道未使用 | 🟢 低 | OkxAccountChannelHandler 只输出日志,isMarginSafe() 用的是交易量/可用余额估算,不如直接用 account 频道的 availBal |
tpOrdId,用 orders 频道监控成交replenishOwnQueue 中限制 queue.size() <= gridQueueSize * 2realizedPnl 累加到独立字段longRealizedPnl/shortRealizedPnl 直接赋值会被归零覆盖totalRealizedPnl 累加字段,onPositionUpdate 中 delta 累加而非覆盖longEntryPrice 偏离 longBaseEntryPrice 超过 N 个 step 时,重新生成队列isMarginSafe() 改用 account 频道的 availBal,更精确Set<clOrdId> 缓存已处理订单 ID,防止重复挂单(longBase + shortBase) / 2 × gridRate 作为统一基准,而非只用空基价startGrid() 中基底开仓用限价单(取盘口卖一/买一价),减少滑点