From 3f2a5a15d15052833f1eb864d9f04bc02ecd6cc0 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 24 Jun 2026 16:51:47 +0800
Subject: [PATCH] fix(okx): 修复OKX交易执行器和WebSocket客户端配置问题

---
 src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java |  131 +++++++++++++++++++++++++++++++++----------
 1 files changed, 99 insertions(+), 32 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java b/src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java
index 0b8cb40..29874b6 100644
--- a/src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxTradeExecutor.java
@@ -208,17 +208,12 @@
         });
     }
 
-    // ==================== 止盈条件单 ====================
+    // ==================== 止盈/止损条件单 ====================
 
     /**
-     * 异步创建止盈条件单(OKX 算法订单 — conditional 类型)。
+     * 异步创建止盈条件单(OKX 算法订单 — conditional 类型,tpTriggerPx)。
      *
-     * <p>服务器监控价格,达到触发价后自动以市价平仓。
-     * 使用 OKX 的 {@code order-algo} 接口,ordType=conditional。
-     *
-     * <h3>止盈失败兜底</h3>
-     * 止盈单创建失败时立即调用 {@link #marketClose(String, String)} 市价平仓,
-     * 确保仓位不会因止损条件未挂上而无保护。
+     * <p>止盈单创建失败时立即调用 {@link #marketClose(String, String)} 市价平仓兜底。
      *
      * @param triggerPrice 触发价格
      * @param orderType    平仓类型:"close_long" 平多 / "close_short" 平空
@@ -229,6 +224,34 @@
                                  String orderType,
                                  String size,
                                  Consumer<String> onSuccess) {
+        placeConditionalClose(triggerPrice, orderType, size, onSuccess, false);
+    }
+
+    /**
+     * 异步创建止损条件单(OKX 算法订单 — conditional 类型,slTriggerPx)。
+     *
+     * <p>止损单创建失败时立即调用 {@link #marketClose(String, String)} 市价平仓兜底。
+     *
+     * @param triggerPrice 触发价格
+     * @param orderType    平仓类型:"close_long" 平多 / "close_short" 平空
+     * @param size         平仓张数(正数,如 "15")
+     * @param onSuccess    成功回调,接收 algoId(可为 null)
+     */
+    public void placeStopLoss(BigDecimal triggerPrice,
+                               String orderType,
+                               String size,
+                               Consumer<String> onSuccess) {
+        placeConditionalClose(triggerPrice, orderType, size, onSuccess, true);
+    }
+
+    /**
+     * 通用平仓条件单:isStopLoss=true 用 slTriggerPx/slOrdPx,false 用 tpTriggerPx/tpOrdPx。
+     */
+    private void placeConditionalClose(BigDecimal triggerPrice,
+                                        String orderType,
+                                        String size,
+                                        Consumer<String> onSuccess,
+                                        boolean isStopLoss) {
         executor.execute(() -> {
             String posSide = null;
             try {
@@ -240,11 +263,11 @@
                     side = "buy";
                     posSide = "short";
                 } else {
-                    log.error("[TradeExec-OKX] 未知止盈类型: {}", orderType);
+                    log.error("[TradeExec-OKX] 未知平仓类型: {}", orderType);
                     return;
                 }
 
-                // OKX conditional 止盈止损使用 tpTriggerPx/tpOrdPx 或 slTriggerPx/slOrdPx
+                String label = isStopLoss ? "止损" : "止盈";
                 JSONObject body = new JSONObject();
                 body.put("instId", contract);
                 body.put("tdMode", "cross");
@@ -252,29 +275,35 @@
                 body.put("posSide", posSide);
                 body.put("ordType", "conditional");
                 body.put("sz", size);
-                // "close_long"=平多(止盈), "close_short"=平空(止盈)
-                body.put("tpTriggerPx", triggerPrice.stripTrailingZeros().toPlainString());
-                body.put("tpTriggerPxType", "last");
-                body.put("tpOrdPx", "-1");
+                // 止盈用 tp 系列字段,止损用 sl 系列字段
+                if (isStopLoss) {
+                    body.put("slTriggerPx", triggerPrice.stripTrailingZeros().toPlainString());
+                    body.put("slTriggerPxType", "last");
+                    body.put("slOrdPx", "-1");
+                } else {
+                    body.put("tpTriggerPx", triggerPrice.stripTrailingZeros().toPlainString());
+                    body.put("tpTriggerPxType", "last");
+                    body.put("tpOrdPx", "-1");
+                }
 
                 JSONObject resp = okPost("/api/v5/trade/order-algo", body.toJSONString());
                 String code = resp.getString("code");
                 if (!"0".equals(code)) {
-                    log.error("[TradeExec-OKX] 止盈单创建失败, code:{}, msg:{}, 立即市价止盈",
-                            code, resp.getString("msg"));
+                    log.error("[TradeExec-OKX] {}单创建失败, code:{}, msg:{}, 立即市价{}",
+                            label, code, resp.getString("msg"), label);
                     marketClose(size, posSide);
                     return;
                 }
                 JSONArray data = resp.getJSONArray("data");
                 String algoId = (data != null && !data.isEmpty())
                         ? data.getJSONObject(0).getString("algoId") : null;
-                log.info("[TradeExec-OKX] 止盈单已创建, triggerPx:{}, type:{}, sz:{}, algoId:{}",
-                        triggerPrice, orderType, size, algoId);
+                log.info("[TradeExec-OKX] {}单已创建, triggerPx:{}, type:{}, sz:{}, algoId:{}",
+                        label, triggerPrice, orderType, size, algoId);
                 if (onSuccess != null) {
                     onSuccess.accept(algoId);
                 }
             } catch (Exception e) {
-                log.error("[TradeExec-OKX] 止盈单创建失败, triggerPx:{}, sz:{}, 立即市价止盈",
+                log.error("[TradeExec-OKX] 创建失败, triggerPx:{}, sz:{}, 立即市价{}",
                         triggerPrice, size, e);
                 if (posSide != null) {
                     marketClose(size, posSide);
@@ -363,13 +392,17 @@
         executor.execute(() -> {
             try {
                 String side = isLong ? "buy" : "sell";
+                String posSide = isLong ? "long" : "short";
+                // OKX sz 必须为正数,strategy 层传入的负数需转正
+                String absSz = size.startsWith("-") ? size.substring(1) : size;
 
                 JSONObject body = new JSONObject();
                 body.put("instId", contract);
                 body.put("tdMode", "cross");
                 body.put("side", side);
+                body.put("posSide", posSide);            // 双向持仓模式必须指定
                 body.put("ordType", "trigger");          // 计划委托 = 触发后开仓
-                body.put("sz", size);
+                body.put("sz", absSz);
                 body.put("triggerPx", triggerPrice.stripTrailingZeros().toPlainString());
                 body.put("triggerPxType", "last");
                 body.put("orderPx", "-1");               // OKX 使用 orderPx,非 ordPx
@@ -440,25 +473,59 @@
     }
 
     /**
-     * 异步清除指定合约的所有算法订单(条件单)。
-     * 发送不含 algoId 的取消请求,OKX 会取消该合约下所有待触发算法单。
+     * 异步清除指定合约的所有算法订单(条件单/止盈止损单)。
+     *
+     * <p>OKX 的 cancel-algos 接口要求必须传 algoId 或 algoClOrdId,
+     * 不能仅凭 instId 批量取消。因此先查询待处理列表,再逐个取消。
      */
     public void cancelAllPriceTriggeredOrders() {
         executor.execute(() -> {
             try {
-                JSONArray bodyArr = new JSONArray();
-                JSONObject item = new JSONObject();
-                item.put("instId", contract);
-                bodyArr.add(item);
+                // ordType 是 orders-algo-pending 的必填参数,需分别查询 conditional 和 trigger
+                JSONArray cancelBody = new JSONArray();
+                for (String ordType : new String[]{"conditional", "trigger"}) {
+                    String queryPath = "/api/v5/trade/orders-algo-pending?instId=" + contract
+                            + "&ordType=" + ordType;
+                    try {
+                        JSONObject queryResp = okGet(queryPath);
+                        if (!"0".equals(queryResp.getString("code"))) {
+                            log.warn("[TradeExec-OKX] 查询 pending ordType={} 失败, code:{}, msg:{}",
+                                    ordType, queryResp.getString("code"), queryResp.getString("msg"));
+                            continue;
+                        }
+                        JSONArray data = queryResp.getJSONArray("data");
+                        if (data != null) {
+                            for (int i = 0; i < data.size(); i++) {
+                                JSONObject order = data.getJSONObject(i);
+                                String algoId = order.getString("algoId");
+                                if (algoId == null) {
+                                    continue;
+                                }
+                                JSONObject item = new JSONObject();
+                                item.put("algoId", algoId);
+                                item.put("instId", contract);
+                                cancelBody.add(item);
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.warn("[TradeExec-OKX] 查询待处理条件单失败, ordType:{}", ordType, e);
+                    }
+                }
 
-                JSONObject resp = okPost("/api/v5/trade/cancel-algos", bodyArr.toJSONString());
-                String code = resp.getString("code");
-                if (!"0".equals(code)) {
-                    log.warn("[TradeExec-OKX] 清除所有条件单失败, code:{}, msg:{}",
-                            code, resp.getString("msg"));
+                if (cancelBody.isEmpty()) {
+                    log.info("[TradeExec-OKX] 无待处理条件单");
                     return;
                 }
-                log.info("[TradeExec-OKX] 已清除所有条件单");
+
+                // 批量取消
+                JSONObject cancelResp = okPost("/api/v5/trade/cancel-algos", cancelBody.toJSONString());
+                String cancelCode = cancelResp.getString("code");
+                if (!"0".equals(cancelCode)) {
+                    log.warn("[TradeExec-OKX] 清除条件单部分失败, code:{}, msg:{}",
+                            cancelCode, cancelResp.getString("msg"));
+                    return;
+                }
+                log.info("[TradeExec-OKX] 已清除{}个条件单", cancelBody.size());
             } catch (Exception e) {
                 log.error("[TradeExec-OKX] 清除条件单失败", e);
             }

--
Gitblit v1.9.1