# OKX 永续合约双边网格策略 — 逻辑文档 > 最后更新: 2026-05-13 --- ## 一、模块架构 ``` OkxWebSocketClientMain ← 入口,持有主运行线程 └── OkxWebSocketClientManager ← 管理 WS 连接(公共 + 私有) ├── OkxKlineWebSocketClient (公共) ← K线推送 └── OkxKlineWebSocketClient (私有) ← 私有频道(login → 订阅) ├── OkxCandlestickChannelHandler → onKline() ├── OkxOrderInfoChannelHandler → onOrderFilled() ├── OkxPositionsChannelHandler → onPositionUpdate() └── OkxAccountChannelHandler → 仅日志,供后续扩展 └── OkxGridTradeService ← 核心策略引擎(状态机) ├── OkxConfig ← 策略配置(Builder 模式) └── OkxTradeExecutor ← REST 下单执行器 ``` ### WS 连接层职责 | 类 | 职责 | |----|------| | `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` | --- ## 四、完整策略时序图 ```mermaid 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 状态
检查标记价是否在基底价±步长内 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: 止盈止损检查
队列匹配 alt 空仓队列触发 (price <= queue[0]) S->>S: processShortGrid() Note over S: 收集所有 price<=queue[i] 的元素
check maxPosSize
开空 + 填充队列 + 转移到多队列 else 多仓队列触发 (price >= queue[0]) S->>S: processLongGrid() Note over S: 收集所有 price>=queue[i] 的元素
check maxPosSize
开多 + 填充队列 + 转移到空队列 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 ``` --- ## 五、详细逻辑说明 ### 5.1 K线处理 `onKline()` — 策略核心驱动 ``` WAITING_KLINE: └── 标记价在 [shortBasePrice - step, shortBasePrice + step] 范围 → OPENING └── 开基多限价单(baseStepLongPrice) + 基空限价单(baseStepShortPrice) ACTIVE: ├── 止盈止损检查(累计已实现盈亏 vs overallTp / maxLoss) ├── 保证金安全阀检查(isMarginSafe) ├── processShortGrid(): 当前价 <= 空队列首 → 触发空仓网格 └── processLongGrid(): 当前价 >= 多队列首 → 触发多仓网格 ``` ### 5.2 订单成交处理 `onOrderFilled()` — 基底识别 + 挂止盈 **数据来源**:orders 频道 `state=filled`,取 `avgPx`(成交量加权均价)。 ``` !baseLongOpened → 基底多成交 → longBaseEntryPrice = avgPx → tryGenerateQueues() !baseShortOpened → 基底空成交 → shortBaseEntryPrice = avgPx → tryGenerateQueues() baseOpened → 加仓成交 → queue[0] = 止盈价 → placeTakeProfit() ``` > `tryGenerateQueues()`:双基底都成交后,计算 step,生成多空队列,状态 → ACTIVE。 ### 5.3 队列生成逻辑 ``` step = shortBaseEntryPrice × config.gridRate (统一用空基价) 空队列 (shortPriceQueue): [shortBase - s, shortBase - 2s, ...] → 降序(最近的在前) 多队列 (longPriceQueue): [longBase + s, longBase + 2s, ...] → 升序(最近的在前) ``` **步长统一来源**:`config.step`,由 `tryGenerateQueues()` 计算并写入。 ### 5.4 网格触发 `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 ``` ### 5.5 止盈止损 `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` | --- ## 七、优化方向(按优先级排序) ### 🔴 P0 — 紧急 1. **止盈挂单生命周期管理** - 挂单后记录 `tpOrdId`,用 orders 频道监控成交 - 止盈单未成交超过 N 根 K 线 → 撤销重挂 - 策略 STOPPED 时撤销所有未成交挂单 2. **队列长度上限** - `replenishOwnQueue` 中限制 `queue.size() <= gridQueueSize * 2` - 超过上限时清理队尾(远离当前价的元素) ### 🟡 P1 — 重要 3. **`realizedPnl` 累加到独立字段** - 当前用 volatile `longRealizedPnl/shortRealizedPnl` 直接赋值会被归零覆盖 - 新增 `totalRealizedPnl` 累加字段,`onPositionUpdate` 中 delta 累加而非覆盖 4. **基底价与队列同步更新** - 当 `longEntryPrice` 偏离 `longBaseEntryPrice` 超过 N 个 step 时,重新生成队列 5. **爆仓熔断** - 每分钟开仓次数超过阈值 → 暂停策略 + 告警 - 未实现亏损超过保证金 80% → 暂停 6. **账户频道接入保证金计算** - `isMarginSafe()` 改用 account 频道的 `availBal`,更精确 ### 🟢 P2 — 优化 7. **订单幂等** - 用 `Set` 缓存已处理订单 ID,防止重复挂单 8. **步长优化** - 考虑用 `(longBase + shortBase) / 2 × gridRate` 作为统一基准,而非只用空基价 9. **基底开仓改用限价单** - `startGrid()` 中基底开仓用限价单(取盘口卖一/买一价),减少滑点