# 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()` 中基底开仓用限价单(取盘口卖一/买一价),减少滑点