From e45e705c22df5bc979e72db6014dd1ff9637be42 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 24 Jun 2026 22:21:58 +0800
Subject: [PATCH] fix(okx): 修复网格交易成交日志记录问题

---
 src/main/java/com/xcong/excoin/modules/okxApi/wsHandler/handler/OrdersOkxChannelHandler.java |  173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 173 insertions(+), 0 deletions(-)

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