edit | blame | history | raw

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

四、完整策略时序图

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

五、详细逻辑说明

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 时撤销所有未成交挂单
  1. 队列长度上限
  • replenishOwnQueue 中限制 queue.size() <= gridQueueSize * 2
  • 超过上限时清理队尾(远离当前价的元素)

🟡 P1 — 重要

  1. realizedPnl 累加到独立字段
  • 当前用 volatile longRealizedPnl/shortRealizedPnl 直接赋值会被归零覆盖
  • 新增 totalRealizedPnl 累加字段,onPositionUpdate 中 delta 累加而非覆盖
  1. 基底价与队列同步更新
  • longEntryPrice 偏离 longBaseEntryPrice 超过 N 个 step 时,重新生成队列
  1. 爆仓熔断
  • 每分钟开仓次数超过阈值 → 暂停策略 + 告警
  • 未实现亏损超过保证金 80% → 暂停
  1. 账户频道接入保证金计算
  • isMarginSafe() 改用 account 频道的 availBal,更精确

🟢 P2 — 优化

  1. 订单幂等
  • Set<clOrdId> 缓存已处理订单 ID,防止重复挂单
  1. 步长优化
  • 考虑用 (longBase + shortBase) / 2 × gridRate 作为统一基准,而非只用空基价
  1. 基底开仓改用限价单
  • startGrid() 中基底开仓用限价单(取盘口卖一/买一价),减少滑点