From 89cf7bfdfd23d55fae47c7e2cf5f9ee9f4a2bcbb Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Mon, 21 Sep 2026 09:52:58 +0800
Subject: [PATCH] ``` fix(gateApi): 修复网格交易队列生成的并发安全问题

---
 src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java |  219 ++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 176 insertions(+), 43 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
index 220b39f..a62052e 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -37,8 +37,8 @@
  *   onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened
  *     ↓
  *   tryGenerateQueues()
- *     ├── generateShortQueue()   ← 空仓价格队列(降序,从 shortBaseEntryPrice-step 向下)
- *     ├── generateLongQueue()    ← 多仓价格队列(升序,从 shortBaseEntryPrice+step 向上)
+ *     ├── generateShortQueue()   ← 空仓价格队列(降序,基价×(1−gridRate) 等比向下)
+ *     ├── generateLongQueue()    ← 多仓价格队列(升序,基价×(1+gridRate) 等比向上)
  *     ├── updateGridElements()   ← 构建 GridElement 列表 + TraderParam + 全局索引
  *     ├── 挂基座止盈单(ID=0 的 long/short takeProfit)
  *     └── 挂初始条件单(up=-1 多单, down=1 空单)
@@ -68,11 +68,11 @@
  *
  * <h3>关键公式</h3>
  * <pre>
- *   step  = shortBaseEntryPrice × gridRate                      ← 网格绝对步长
+ *   step  = shortBaseEntryPrice × gridRate                      ← 首档间距(0档与±1档的距离度量)
  *   minTick = 10^(-priceScale)                                  ← 交易所最小价格单位
- *   多止盈 = gridPrice + (step - minTick)                       ← 多仓止盈价
- *   空止盈 = gridPrice - (step - minTick)                       ← 空仓止盈价
- *   单笔盈利 = (step - minTick) × contractMultiplier × quantity  ← USDT
+ *   等比网格: 下一档 = 本档 × (1 ± gridRate)                     ← 向下×(1−r),向上×(1+r)
+ *   止盈价 = 本档价 × (1 ± gridRate)(与相邻网格价联动)         ← 多仓向上,空仓向下
+ *   单笔盈利 ≈ 档价 × gridRate × contractMultiplier × quantity(随网格位置浮动)
  * </pre>
  *
  * <h3>线程模型</h3>
@@ -354,6 +354,8 @@
         shortPositionSize = BigDecimal.ZERO;
         baseLongOpened = false;
         baseShortOpened = false;
+        config.setBaseLongTraderParam(null);
+        config.setBaseShortTraderParam(null);
         longActive = false;
         shortActive = false;
         accumulatedLongLossCount = 0;
@@ -453,12 +455,14 @@
                         .entryOrderId(orderId)
                         .build();
                 config.setBaseLongTraderParam(baseLongTp);
+                tryGenerateQueues();
             }, null);
             executor.openShort(negate(size), (orderId) -> {
                 TraderParam baseShortTp = TraderParam.builder()
                         .entryOrderId(orderId)
                         .build();
                 config.setBaseShortTraderParam(baseShortTp);
+                tryGenerateQueues();
             }, null);
             return;
         }
@@ -757,8 +761,10 @@
                 int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
                 extendShortStopLoss(posSize, shortGridElement.getId());
                 // [Gate] 止盈挂单:超出基础仓位的部分,挂在多仓第一止损位
-                if (config.isPlaceExcessTakeProfit()) {
+                if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
                     placeExcessTakeProfit(posSize, false);
+                } else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
+                    placeDispersedTakeProfit(posSize, false, shortGridElement.getId());
                 }
                 log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
 
@@ -797,8 +803,10 @@
                 int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
                 extendLongStopLoss(posSize, longGridElement.getId());
                 // [Gate] 止盈挂单:超出基础仓位的部分,挂在空仓第一止损位
-                if (config.isPlaceExcessTakeProfit()) {
+                if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
                     placeExcessTakeProfit(posSize, true);
+                } else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
+                    placeDispersedTakeProfit(posSize, true, longGridElement.getId());
                 }
                 log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
 
@@ -868,17 +876,27 @@
      *   <li>状态切换为 ACTIVE</li>
      * </ol>
      */
-    private void tryGenerateQueues() {
-        if (baseLongOpened && baseShortOpened) {
+    private synchronized void tryGenerateQueues() {
+        // 幂等保护:生成队列后 state 切换为 ACTIVE,避免多线程重复执行
+        if (state != StrategyState.OPENING) {
+            return;
+        }
+        TraderParam baseLongTraderParam = config.getBaseLongTraderParam();
+        TraderParam baseShortTraderParam = config.getBaseShortTraderParam();
+        // 需同时满足:多空基底均已成交,且两个下单参数(orderId)均已通过下单成功回调拿到。
+        // IOC 市价单成交后 WS 持仓推送可能先于 REST 下单响应返回,必须等参数就绪再生成队列。
+        if (!(baseLongOpened && baseShortOpened)
+                || baseLongTraderParam == null || baseShortTraderParam == null) {
+            return;
+        }
+        {
             generateShortQueue();
             generateLongQueue();
             updateGridElements();
 
             GridElement baseGridElement = GridElement.findById(0);
-            TraderParam baseLongTraderParam = config.getBaseLongTraderParam();
             baseGridElement.setLongOrderId(baseLongTraderParam.getEntryOrderId());
             baseGridElement.setHasLongOrder(true);
-            TraderParam baseShortTraderParam = config.getBaseShortTraderParam();
             baseGridElement.setShortOrderId(baseShortTraderParam.getEntryOrderId());
             baseGridElement.setHasShortOrder(true);
 
@@ -1065,51 +1083,73 @@
     }
 
     /**
-     * 生成空仓价格队列。
-     * 以 shortBaseEntryPrice × gridRate 作为绝对步长 step,存到 config。
-     * 第1个元素 = shortBaseEntryPrice − step,后续依次递减 step,共 gridQueueSize 个。
-     * 队列降序排列(大→小),方便 processShortGrid 中从头遍历。
+     * 生成空仓价格队列(等比 / 复合百分比网格)。
+     *
+     * <p>每个网格价 = 上一档 × (1 − gridRate),即自 shortBaseEntryPrice 向下等比递减:
+     * <pre>
+     *   第1档 = shortBaseEntryPrice × (1 − gridRate)
+     *   第2档 = 第1档 × (1 − gridRate)
+     *   ...
+     * </pre>
+     * 档间距 = 该档价格 × gridRate,随价格下降而收窄。
+     *
+     * <p>同时将 step(= shortBaseEntryPrice × gridRate,即 0档 与 ±1档 的间距)注入 config,
+     * 供跨度重启等需要固定距离度量的功能使用。
+     *
+     * <p>队列降序排列(大→小),方便 processShortGrid 中从头遍历。
      */
     private void generateShortQueue() {
         shortPriceQueue.clear();
         totalShortPriceQueue.clear();
         totalLongPriceQueue.clear();
         int prec = config.getPriceScale();
-        BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP);
+        BigDecimal gridRate = config.getGridRate();
+        // 首档间距 = 基价 × gridRate(0档与±1档的间距),保留作跨度等固定距离度量
+        BigDecimal step = shortBaseEntryPrice.multiply(gridRate).setScale(prec, RoundingMode.HALF_UP);
         config.setStep(step);
-        BigDecimal elem = shortBaseEntryPrice.subtract(step).setScale(prec, RoundingMode.HALF_UP);
-            for (int i = 0; i < config.getGridQueueSize(); i++) {
-                shortPriceQueue.add(elem);
-                totalLongPriceQueue.add( elem);
-                totalShortPriceQueue.add( elem);
+        BigDecimal factor = BigDecimal.ONE.subtract(gridRate);
+        BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
+        for (int i = 0; i < config.getGridQueueSize(); i++) {
+            shortPriceQueue.add(elem);
+            totalLongPriceQueue.add(elem);
+            totalShortPriceQueue.add(elem);
 
-                elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP);
-                if (elem.compareTo(BigDecimal.ZERO) <= 0) {
-                    break;
-                }
+            elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
+            if (elem.compareTo(BigDecimal.ZERO) <= 0) {
+                break;
             }
+        }
         shortPriceQueue.sort((a, b) -> b.compareTo(a));
-        log.info("[Gate] 空队列:{}", shortPriceQueue);
+        log.info("[Gate] 空队列(等比):{}", shortPriceQueue);
     }
 
     /**
-     * 生成多仓价格队列。
-     * 以 shortBaseEntryPrice + step 为首元素,后续依次递增 step,共 gridQueueSize 个。
-     * 队列升序排列(小→大),方便 processLongGrid 中从头遍历。
+     * 生成多仓价格队列(等比 / 复合百分比网格)。
+     *
+     * <p>每个网格价 = 上一档 × (1 + gridRate),即自 shortBaseEntryPrice 向上等比递增:
+     * <pre>
+     *   第1档 = shortBaseEntryPrice × (1 + gridRate)
+     *   第2档 = 第1档 × (1 + gridRate)
+     *   ...
+     * </pre>
+     * 档间距 = 该档价格 × gridRate,随价格上升而放大;
+     * 与空仓队列(下方 ×(1−gridRate))关于基价对称,保证上下网格连续一致。
+     *
+     * <p>队列升序排列(小→大),方便 processLongGrid 中从头遍历。
      */
     private void generateLongQueue() {
         longPriceQueue.clear();
         int prec = config.getPriceScale();
-        BigDecimal step = config.getStep();
-        BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP);
+        BigDecimal factor = BigDecimal.ONE.add(config.getGridRate());
+        BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
         for (int i = 0; i < config.getGridQueueSize(); i++) {
             longPriceQueue.add(elem);
-            totalLongPriceQueue.add( elem);
-            totalShortPriceQueue.add( elem);
-            elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP);
+            totalLongPriceQueue.add(elem);
+            totalShortPriceQueue.add(elem);
+            elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
         }
         longPriceQueue.sort(BigDecimal::compareTo);
-        log.info("[Gate] 多队列:{}", longPriceQueue);
+        log.info("[Gate] 多队列(等比):{}", longPriceQueue);
         totalShortPriceQueue.sort((a, b) -> b.compareTo(a));
         log.info("[Gate] 队列从大到小:{}", totalShortPriceQueue);
         totalLongPriceQueue.sort(BigDecimal::compareTo);
@@ -1136,7 +1176,9 @@
         int longSize = longPriceQueue.size();
         //根据精度转换成小数
         int prec = config.getPriceScale();
-        BigDecimal step = config.getStep();
+        // 等比网格:止盈价与本档相邻网格价联动 = 本档价 × (1 ± gridRate)
+        BigDecimal tpUpFactor = BigDecimal.ONE.add(config.getGridRate());
+        BigDecimal tpDownFactor = BigDecimal.ONE.subtract(config.getGridRate());
 //        String qty = config.getBaseQuantity();
         String qty = config.getQuantity();
 
@@ -1149,13 +1191,13 @@
             TraderParam longParam = TraderParam.builder()
                     .direction(TraderParam.Direction.LONG)
                     .entryPrice(price)
-                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             TraderParam shortParam = TraderParam.builder()
                     .direction(TraderParam.Direction.SHORT)
                     .entryPrice(price)
-                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             elements.add(GridElement.builder()
@@ -1174,13 +1216,13 @@
             TraderParam longParam = TraderParam.builder()
                     .direction(TraderParam.Direction.LONG)
                     .entryPrice(price)
-                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             TraderParam shortParam = TraderParam.builder()
                     .direction(TraderParam.Direction.SHORT)
                     .entryPrice(price)
-                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             elements.add(GridElement.builder()
@@ -1202,13 +1244,13 @@
             TraderParam longParam = TraderParam.builder()
                     .direction(TraderParam.Direction.LONG)
                     .entryPrice(price)
-                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             TraderParam shortParam = TraderParam.builder()
                     .direction(TraderParam.Direction.SHORT)
                     .entryPrice(price)
-                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
+                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                     .quantity(qty)
                     .build();
             elements.add(GridElement.builder()
@@ -2165,6 +2207,97 @@
     }
 
     /**
+     * 分散止盈:把超出基础仓位的部分,从入场网格下一格开始逐格挂止盈单,一直挂到对手盘止损位。
+     *
+     * <p>数量分配:excessQty ÷ 网格数向下取整,余数全部挂在最后一个止盈网格(对手盘止损位)。
+     *
+     * <p>方向:多仓成交向价格更高方向(id 递增)铺到空仓第一止损位;
+     * 空仓成交向价格更低方向(id 递减)铺到多仓第一止损位。
+     *
+     * @param posSize      当前总持仓张数
+     * @param isLong       true=多仓成交,false=空仓成交
+     * @param entryGridId  入场网格 ID
+     */
+    private void placeDispersedTakeProfit(int posSize, boolean isLong, int entryGridId) {
+        int baseQty = Integer.parseInt(config.getBaseQuantity());
+        int excessQty = posSize - baseQty;
+        if (excessQty <= 0) {
+            return;
+        }
+
+        GridElement endElem = isLong ? findFirstShortStopLossGrid() : findFirstLongStopLossGrid();
+        if (endElem == null) {
+            log.warn("[Gate] {}分散止盈失败:未找到对手盘止损位", isLong ? "多仓" : "空仓");
+            return;
+        }
+        int endId = endElem.getId();
+        int dir = isLong ? 1 : -1;
+        int startId = entryGridId + dir;
+
+        // 终点必须在起点前进方向上,否则退化为集中止盈(全部挂对手盘止损位)
+        boolean valid = isLong ? (endId >= startId) : (endId <= startId);
+        if (!valid) {
+            placeExcessTakeProfit(posSize, isLong);
+            return;
+        }
+
+        // 收集区间内全部网格(含两端)
+        List<GridElement> gridList = new ArrayList<>();
+        int id = startId;
+        while (true) {
+            GridElement e = GridElement.findById(id);
+            if (e == null) {
+                break;
+            }
+            gridList.add(e);
+            if (id == endId) {
+                break;
+            }
+            id += dir;
+            if (gridList.size() > config.getGridQueueSize() * 2 + 4) {
+                break;
+            }
+        }
+        if (gridList.isEmpty() || gridList.get(gridList.size() - 1).getId() != endId) {
+            // 未能铺满到终点(网格缺失),退化为集中止盈
+            placeExcessTakeProfit(posSize, isLong);
+            return;
+        }
+
+        int n = gridList.size();
+        int perGrid = excessQty / n;
+        int remainder = excessQty % n;
+
+        for (int i = 0; i < n; i++) {
+            int qty = perGrid;
+            if (i == n - 1) {
+                qty += remainder;
+            }
+            if (qty <= 0) {
+                continue;
+            }
+            final GridElement elem = gridList.get(i);
+            final int gid = elem.getId();
+            final BigDecimal triggerPrice = elem.getGridPrice();
+            final String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty);
+            final String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT;
+            final FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1
+                    : FuturesPriceTrigger.RuleEnum.NUMBER_2;
+            executor.placeTakeProfit(triggerPrice, rule, orderType, size,
+                    profitId -> {
+                        if (isLong) {
+                            longTakeProfitTraderIdParam(elem, profitId, true);
+                        } else {
+                            shortTakeProfitTraderIdParam(elem, profitId, true);
+                        }
+                        log.info("[Gate] 分散止盈 {} gridId:{}, 触发价:{}, 数量:{}, tpId:{}",
+                                isLong ? "多仓" : "空仓", gid, triggerPrice, size, profitId);
+                    }
+            );
+        }
+    }
+
+    /**
      * 在指定网格挂一笔对手止盈单(非满仓超额止盈,挂在止损触发位的下一格)。
      */
     private void placeTakeProfitAtGrid(GridElement tpElem, boolean isLong, int qty, int times) {

--
Gitblit v1.9.1