From 83017f78860526483a24e89052534222fd2e6466 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 24 Jun 2026 15:46:42 +0800
Subject: [PATCH] refactor(okx): 替换条件单处理器为订单处理器
---
src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientMain.java | 4
src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OrdersOkxChannelHandler.java | 173 +++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java | 8 +-
3 files changed, 179 insertions(+), 6 deletions(-)
diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientMain.java b/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientMain.java
index 8fdcdbb..5e8823e 100644
--- a/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientMain.java
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientMain.java
@@ -1,7 +1,7 @@
package com.xcong.excoin.modules.okxApi;
import com.xcong.excoin.modules.okxApi.wsHandler.handler.MarkPriceOkxChannelHandler;
-import com.xcong.excoin.modules.okxApi.wsHandler.handler.OrderAlgoOkxChannelHandler;
+import com.xcong.excoin.modules.okxApi.wsHandler.handler.OrdersOkxChannelHandler;
import com.xcong.excoin.modules.okxApi.wsHandler.handler.PositionsOkxChannelHandler;
import java.math.BigDecimal;
@@ -43,7 +43,7 @@
config.getContract(), gridTradeService));
wsClient.addPrivateHandler(new PositionsOkxChannelHandler(
config, gridTradeService));
- wsClient.addPrivateHandler(new OrderAlgoOkxChannelHandler(
+ wsClient.addPrivateHandler(new OrdersOkxChannelHandler(
config, gridTradeService));
gridTradeService.setWsClient(wsClient);
diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java b/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java
index a0c933b..2021fd5 100644
--- a/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxWebSocketClientManager.java
@@ -1,7 +1,7 @@
package com.xcong.excoin.modules.okxApi;
import com.xcong.excoin.modules.okxApi.wsHandler.handler.MarkPriceOkxChannelHandler;
-import com.xcong.excoin.modules.okxApi.wsHandler.handler.OrderAlgoOkxChannelHandler;
+import com.xcong.excoin.modules.okxApi.wsHandler.handler.OrdersOkxChannelHandler;
import com.xcong.excoin.modules.okxApi.wsHandler.handler.PositionsOkxChannelHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -25,7 +25,7 @@
* <ol>
* <li>MarkPriceOkxChannelHandler — 公开频道,标记价格 → onKline() + setMarkPrice()</li>
* <li>PositionsOkxChannelHandler — 私有频道,仓位 → onPositionUpdate()</li>
- * <li>OrderAlgoOkxChannelHandler — 私有频道,条件单状态 → onAutoOrder()</li>
+ * <li>OrdersOkxChannelHandler — 私有频道,订单成交(含algoId) → onAutoOrder()</li>
* </ol>
*
* <h3>销毁顺序({@code @PreDestroy})</h3>
@@ -85,8 +85,8 @@
// 私有频道:仓位
wsClient.addPrivateHandler(new PositionsOkxChannelHandler(
config, gridTradeService));
- // 私有频道:条件单
- wsClient.addPrivateHandler(new OrderAlgoOkxChannelHandler(
+ // 私有频道:条件单(orders 频道含 algoId,可追溯到源条件单)
+ wsClient.addPrivateHandler(new OrdersOkxChannelHandler(
config, gridTradeService));
gridTradeService.setWsClient(wsClient);
diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OrdersOkxChannelHandler.java b/src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OrdersOkxChannelHandler.java
new file mode 100644
index 0000000..abe2bd0
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OrdersOkxChannelHandler.java
@@ -0,0 +1,173 @@
+package com.xcong.excoin.modules.okxApi.wsHandler.handler;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.xcong.excoin.modules.okxApi.OkxConfig;
+import com.xcong.excoin.modules.okxApi.OkxGridTradeService;
+import com.xcong.excoin.modules.okxApi.wsHandler.AbstractOkxPrivateChannelHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.java_websocket.client.WebSocketClient;
+
+/**
+ * OKX 订单频道处理器(orders),替代 orders-algo 追踪条件单成交。
+ *
+ * <h3>为什么用 orders 替代 orders-algo</h3>
+ * 当 algo 订单(conditional/trigger)触发后,OKX 会生成对应的普通市价单并成交。
+ * orders 频道的 fill 数据中包含 {@code algoId} 字段,可直接关联到原始条件单 ID。
+ * 相比 orders-algo(只推送状态变更),orders 频道能拿到真实的 {@code fillPx/fillSz/tradeId}。
+ *
+ * <h3>订阅格式</h3>
+ * 私有频道,标准格式无需 instFamily:
+ * <pre>
+ * {"op":"subscribe","args":[{"channel":"orders","instType":"SWAP"}]}
+ * </pre>
+ *
+ * <h3>数据推送格式(含 algoId 的 fill)</h3>
+ * <pre>
+ * {
+ * "arg": {"channel":"orders","instType":"SWAP"},
+ * "data": [{
+ * "instId": "ETH-USDT-SWAP",
+ * "ordId": "...",
+ * "algoId": "...", // 链接回原始条件单
+ * "state": "filled", // 只处理 filled 状态
+ * "side": "sell",
+ * "posSide": "long",
+ * "fillPx": "3000",
+ * "fillSz": "1",
+ * "tradeId": "...",
+ * "ordType": "market"
+ * }]
+ * }
+ * </pre>
+ *
+ * <h3>状态映射 — 仅处理 filled</h3>
+ * <ul>
+ * <li>"filled" → status="finished" → 回调 onAutoOrder()</li>
+ * <li>"canceled"/"live"/"partially_filled" → 忽略</li>
+ * </ul>
+ *
+ * <h3>订单类型映射(同 orders-algo 逻辑)</h3>
+ * <table>
+ * <tr><th>posSide</th><th>side</th><th>含义</th><th>orderType</th></tr>
+ * <tr><td>long</td><td>sell</td><td>平多仓(止盈/止损)</td><td>plan-close-long-position</td></tr>
+ * <tr><td>short</td><td>buy</td><td>平空仓(止盈/止损)</td><td>plan-close-short-position</td></tr>
+ * <tr><td>long</td><td>buy</td><td>开多仓(条件单触发)</td><td>entry-long</td></tr>
+ * <tr><td>short</td><td>sell</td><td>开空仓(条件单触发)</td><td>entry-short</td></tr>
+ * </table>
+ *
+ * @author Administrator
+ */
+@Slf4j
+public class OrdersOkxChannelHandler extends AbstractOkxPrivateChannelHandler {
+
+ /** OKX 订单频道名称 */
+ private static final String CHANNEL_NAME = "orders";
+
+ /** OKX 配置,用于按合约名称过滤 */
+ private final OkxConfig config;
+
+ /**
+ * 构造订单频道处理器。
+ *
+ * @param config OKX 配置实例
+ * @param gridTradeService OKX 网格交易策略服务实例
+ */
+ public OrdersOkxChannelHandler(OkxConfig config, OkxGridTradeService gridTradeService) {
+ super(CHANNEL_NAME,
+ config.getApiKey(), config.getApiSecret(), config.getPassphrase(),
+ config.getContract(),
+ gridTradeService);
+ this.config = config;
+ }
+
+ /**
+ * 处理订单推送消息。
+ *
+ * <h3>处理流程</h3>
+ * <ol>
+ * <li>按 instId 精确过滤出目标合约</li>
+ * <li>仅处理 state="filled"(成交),忽略其余状态</li>
+ * <li>过滤无 algoId 的订单(非条件单触发的普通订单)</li>
+ * <li>从 posSide + side 推断 orderType</li>
+ * <li>回调 gridTradeService.onAutoOrder(algoId, "finished", "filled", orderType, tradeId)</li>
+ * </ol>
+ *
+ * @param response WebSocket 推送的完整 JSON
+ * @return true 表示已处理(匹配成功)
+ */
+ @Override
+ public boolean handleMessage(JSONObject response) {
+ JSONObject arg = response.getJSONObject("arg");
+ if (arg == null || !CHANNEL_NAME.equals(arg.getString("channel"))) {
+ return false;
+ }
+ try {
+ JSONArray dataArray = response.getJSONArray("data");
+ if (dataArray == null || dataArray.isEmpty()) {
+ return true;
+ }
+
+ String contract = config.getContract();
+ for (int i = 0; i < dataArray.size(); i++) {
+ JSONObject orderData = dataArray.getJSONObject(i);
+
+ // 按合约名称精确过滤
+ String dataInstId = orderData.getString("instId");
+ if (dataInstId == null || !contract.equals(dataInstId)) {
+ continue;
+ }
+
+ // 仅处理已成交
+ String state = orderData.getString("state");
+ if (!"filled".equals(state)) {
+ continue;
+ }
+
+ // 必须有 algoId(否则不是条件单触发的订单)
+ String algoId = orderData.getString("algoId");
+ if (algoId == null || algoId.isEmpty()) {
+ continue;
+ }
+
+ String posSide = orderData.getString("posSide");
+ String side = orderData.getString("side");
+ String tradeId = orderData.getString("tradeId");
+ String fillSz = orderData.getString("fillSz");
+
+ // 推断订单类型
+ String orderType = mapOrderType(posSide, side);
+
+ log.info("[OKX-WS] orders fill, algoId:{}, state:filled, orderType:{}, side:{}, posSide:{}, fillSz:{}, tradeId:{}",
+ algoId, orderType, side, posSide, fillSz, tradeId);
+
+ if (getGridTradeService() != null) {
+ getGridTradeService().onAutoOrder(algoId, "finished", state, orderType, tradeId);
+ }
+ }
+ } catch (Exception e) {
+ log.error("[OKX-WS] 处理 orders 数据失败", e);
+ }
+ return true;
+ }
+
+ /**
+ * 根据 posSide 和 side 推断订单类型。
+ */
+ private String mapOrderType(String posSide, String side) {
+ // 平仓方向(止盈/止损触发)
+ if ("long".equals(posSide) && "sell".equals(side)) {
+ return "plan-close-long-position";
+ } else if ("short".equals(posSide) && "buy".equals(side)) {
+ return "plan-close-short-position";
+ }
+ // 开仓方向 — 覆盖 long_short_mode 和 net_mode
+ if (("long".equals(posSide) || "net".equals(posSide)) && "buy".equals(side)) {
+ return "entry-long";
+ } else if (("short".equals(posSide) || "net".equals(posSide)) && "sell".equals(side)) {
+ return "entry-short";
+ }
+ log.warn("[OKX-WS] orders 未知订单类型, posSide:{}, side:{}", posSide, side);
+ return "unknown";
+ }
+}
--
Gitblit v1.9.1