From 1b55621d4dcf3b4ee6b9c4beb81ad69e5b7a5856 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Fri, 05 Jun 2026 17:58:08 +0800
Subject: [PATCH] refactor(okxNewPrice): 优化止损管理器的挂单数量计算逻辑

---
 src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java |  311 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 311 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java b/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
new file mode 100644
index 0000000..b534a98
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
@@ -0,0 +1,311 @@
+package com.xcong.excoin.modules.okxNewPrice;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+
+/**
+ * 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。
+ *
+ * <h3>止损策略(对齐 Gate 版本)</h3>
+ * <ol>
+ *   <li>基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity</li>
+ *   <li>条件单成交后以成交量为粒度追挂止损(extend 系列)</li>
+ *   <li>止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N&gt;2 时先取消旧挂单</li>
+ * </ol>
+ *
+ * <h3>线程安全</h3>
+ * 本类通过 executor 的异步回调串行化下单操作;对 OkxGridElement 的读写依赖调用方保证有序。
+ *
+ * @author Administrator
+ */
+@Slf4j
+public class StopLossManager {
+
+    private final OkxConfig config;
+    private final OkxTradeExecutor executor;
+
+    /** 多仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */
+    private volatile int longEntryQty = 1;
+    /** 空仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */
+    private volatile int shortEntryQty = 1;
+
+    public StopLossManager(OkxConfig config, OkxTradeExecutor executor) {
+        this.config = config;
+        this.executor = executor;
+    }
+
+    // ========== 基底止损挂载 ==========
+
+    /**
+     * 在基底开仓完成后挂载初始止损单。
+     * 多仓止损: id=-2 到 -11,每格 quantity 张,方向 sell/long
+     * 空仓止损: id=2 到 11,每格 quantity 张,方向 buy/short
+     */
+    public void setupBaseStopLosses() {
+        // 挂多仓止损 (id=-2 到 -11)
+        for (int id = -2; id >= -11; id--) {
+            OkxGridElement elem = OkxGridElement.findById(id);
+            if (elem == null) continue;
+            BigDecimal triggerPrice = elem.getGridPrice();
+            int finalId = id;
+            executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
+                    profitId -> {
+                        elem.setLongStopLossOrderId(profitId);
+                        OkxGridElement.refreshIndices();
+                        log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
+                                finalId, triggerPrice, config.getQuantity(), profitId);
+                    });
+        }
+
+        // 挂空仓止损 (id=2 到 11)
+        for (int id = 2; id <= 11; id++) {
+            OkxGridElement elem = OkxGridElement.findById(id);
+            if (elem == null) continue;
+            BigDecimal triggerPrice = elem.getGridPrice();
+            int finalId = id;
+            executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
+                    profitId -> {
+                        elem.setShortStopLossOrderId(profitId);
+                        OkxGridElement.refreshIndices();
+                        log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
+                                finalId, triggerPrice, config.getQuantity(), profitId);
+                    });
+        }
+
+        log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11");
+    }
+
+    // ========== 止损触发处理 ==========
+
+    /**
+     * 多仓止损触发处理。
+     * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
+     * 若 N &gt; 2,先取消上一步的旧挂单。
+     */
+    public void handleLongStopLossTriggered(OkxGridElement gridElement, BigDecimal longEntryPrice) {
+        int gridId = gridElement.getId();
+        int N = Math.abs(gridId);
+        gridElement.setLongStopLossOrderId(null);
+        log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId);
+
+        int newEntryGridId = -(N - 1);
+        OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
+        if (newEntryGrid == null) {
+            OkxGridElement.refreshIndices();
+            log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
+            return;
+        }
+
+        if (N > 2) {
+            int cancelGridId = -(N - 2);
+            OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
+            if (cancelGrid != null && cancelGrid.isHasLongOrder()) {
+                executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> {
+                    clearLongEntryState(cancelGrid);
+                    log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId);
+                });
+            }
+        }
+
+        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
+        longEntryQty++;
+        int entryQty = longEntryQty * Integer.parseInt(config.getQuantity());
+        String size = String.valueOf(entryQty);
+        log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(计数器:{}, qty:{})",
+                gridId, newEntryGridId, entryQty, longEntryQty, config.getQuantity());
+        newEntryGrid.getLongTraderParam().setQuantity(size);
+        placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size);
+    }
+
+    /**
+     * 空仓止损触发处理。
+     * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
+     * 若 N &gt; 2,先取消上一步的旧挂单。
+     */
+    public void handleShortStopLossTriggered(OkxGridElement gridElement, BigDecimal shortEntryPrice) {
+        int gridId = gridElement.getId();
+        int N = gridId;
+        gridElement.setShortStopLossOrderId(null);
+        log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId);
+
+        int newEntryGridId = N - 1;
+        OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
+        if (newEntryGrid == null) {
+            OkxGridElement.refreshIndices();
+            log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
+            return;
+        }
+
+        if (N > 2) {
+            int cancelGridId = N - 2;
+            OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
+            if (cancelGrid != null && cancelGrid.isHasShortOrder()) {
+                executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> {
+                    clearShortEntryState(cancelGrid);
+                    log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId);
+                });
+            }
+        }
+
+        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
+        shortEntryQty++;
+        int entryQty = shortEntryQty * Integer.parseInt(config.getQuantity());
+        String size = String.valueOf(entryQty);
+        log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(计数器:{}, qty:{})",
+                gridId, newEntryGridId, entryQty, shortEntryQty, config.getQuantity());
+        newEntryGrid.getShortTraderParam().setQuantity(size);
+        placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size);
+    }
+
+    // ========== 止损追挂 ==========
+
+    /**
+     * 多仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。
+     */
+    public void extendLongStopLoss(int filledQty) {
+        int qty = Integer.parseInt(config.getQuantity());
+        int stopLossCount = filledQty / qty;
+        int furthestSlId = 0;
+        for (OkxGridElement e : config.getGridElements()) {
+            if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) {
+                furthestSlId = e.getId();
+            }
+        }
+        if (furthestSlId == 0) furthestSlId = -11;
+        log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
+        for (int i = 0; i < stopLossCount; i++) {
+            int newSlId = furthestSlId - i - 1;
+            OkxGridElement elem = OkxGridElement.findById(newSlId);
+            if (elem == null) continue;
+            BigDecimal triggerPrice = elem.getGridPrice();
+            int finalSlId = newSlId;
+            executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
+                    profitId -> {
+                        elem.setLongStopLossOrderId(profitId);
+                        OkxGridElement.refreshIndices();
+                        log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
+                    });
+        }
+    }
+
+    /**
+     * 空仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。
+     */
+    public void extendShortStopLoss(int filledQty) {
+        int qty = Integer.parseInt(config.getQuantity());
+        int stopLossCount = filledQty / qty;
+        int furthestSlId = 0;
+        for (OkxGridElement e : config.getGridElements()) {
+            if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) {
+                furthestSlId = e.getId();
+            }
+        }
+        if (furthestSlId == 0) furthestSlId = 11;
+        log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
+        for (int i = 0; i < stopLossCount; i++) {
+            int newSlId = furthestSlId + i + 1;
+            OkxGridElement elem = OkxGridElement.findById(newSlId);
+            if (elem == null) continue;
+            BigDecimal triggerPrice = elem.getGridPrice();
+            int finalSlId = newSlId;
+            executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
+                    profitId -> {
+                        elem.setShortStopLossOrderId(profitId);
+                        OkxGridElement.refreshIndices();
+                        log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
+                    });
+        }
+    }
+
+    // ========== 辅助:条件单创建 & 状态回写 ==========
+
+    /**
+     * 预置标志位后发送条件入单请求,成功/失败均通过回调写入 GridElement 状态。
+     */
+    void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong,
+                                     BigDecimal triggerPrice, String size) {
+        if (isLong) {
+            gridElement.setHasLongOrder(true);
+        } else {
+            gridElement.setHasShortOrder(true);
+        }
+        String side = isLong ? "buy" : "sell";
+        String posSide = isLong ? "long" : "short";
+        executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, size,
+                orderId -> {
+                    if (isLong) {
+                        setLongEntryState(gridElement, orderId, true);
+                    } else {
+                        setShortEntryState(gridElement, orderId, true);
+                    }
+                },
+                () -> {
+                    if (isLong) {
+                        gridElement.setHasLongOrder(false);
+                        gridElement.setLongOrderId(null);
+                    } else {
+                        gridElement.setHasShortOrder(false);
+                        gridElement.setShortOrderId(null);
+                    }
+                    OkxGridElement.refreshIndices();
+                    log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong);
+                }
+        );
+    }
+
+    // ---- GridElement 状态修改(package-private,OkxGridTradeService 也可调用) ----
+
+    void setLongEntryState(OkxGridElement gridElement, String entryId, boolean flag) {
+        OkxTraderParam tp = gridElement.getLongTraderParam();
+        tp.setEntryOrderId(entryId);
+        tp.setEntryOrderPlaced(flag);
+        gridElement.setHasLongOrder(flag);
+        gridElement.setLongOrderId(entryId);
+        OkxGridElement.refreshIndices();
+    }
+
+    void setShortEntryState(OkxGridElement gridElement, String entryId, boolean flag) {
+        OkxTraderParam tp = gridElement.getShortTraderParam();
+        tp.setEntryOrderId(entryId);
+        tp.setEntryOrderPlaced(flag);
+        gridElement.setHasShortOrder(flag);
+        gridElement.setShortOrderId(entryId);
+        OkxGridElement.refreshIndices();
+    }
+
+    void clearLongEntryState(OkxGridElement gridElement) {
+        setLongEntryState(gridElement, null, false);
+    }
+
+    void clearShortEntryState(OkxGridElement gridElement) {
+        setShortEntryState(gridElement, null, false);
+    }
+
+    void setLongTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) {
+        OkxTraderParam tp = gridElement.getLongTraderParam();
+        tp.setTakeProfitOrderId(profitId);
+        tp.setTakeProfitPlaced(flag);
+        gridElement.setLongTakeProfitOrderId(profitId);
+        OkxGridElement.refreshIndices();
+    }
+
+    void setShortTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) {
+        OkxTraderParam tp = gridElement.getShortTraderParam();
+        tp.setTakeProfitOrderId(profitId);
+        tp.setTakeProfitPlaced(flag);
+        gridElement.setShortTakeProfitOrderId(profitId);
+        OkxGridElement.refreshIndices();
+    }
+
+    // ========== 计数器管理 ==========
+
+    /** 重置多仓挂单张数计数器(挂单成交后调用) */
+    public void resetLongEntryQty() { longEntryQty = 1; }
+
+    /** 重置空仓挂单张数计数器(挂单成交后调用) */
+    public void resetShortEntryQty() { shortEntryQty = 1; }
+
+    /** 重置全部挂单张数计数器(startGrid时调用) */
+    public void resetAllEntryQuantities() { longEntryQty = 1; shortEntryQty = 1; }
+}

--
Gitblit v1.9.1