Administrator
2026-07-20 5f433d543f4526a3a45af9d7a7134a0566e2461a
fix(gateApi): 解决网格交易止损追单并发问题

- 添加防重入标记 extendStopLossInProgress 避免同一网格多次成交时重复处理
- 实现 pendingStopLossReExtend 机制处理被跳过的入场单成交后续处理
- 引入 AtomicInteger 计数器确保异步回调完成后重置防重入状态
- 新增 reExtendLongStopLoss 和 reExtendShortStopLoss 方法重挂最新仓位止损
- 优化止损订单分配算法确保订单数不足时合理分配
- 修复 early return 时未重置防重入标记导致的状态异常
2 files modified
183 ■■■■ changed files
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java 157 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GridElement.java 26 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -15,6 +15,7 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import com.xcong.excoin.modules.gateApi.wsHandler.handler.CandlestickChannelHandler;
import com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionClosesChannelHandler;
@@ -634,6 +635,17 @@
            if (StrUtil.isNotEmpty(tradeId) && !tradeId.equals("0") ){
                int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity());
                shortEntryTraderIdParam(shortGridElement, orderId, false);
                // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend,
                // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。
                if (shortGridElement.isExtendStopLossInProgress()) {
                    shortGridElement.setPendingStopLossReExtend(true);
                    log.info("[Gate] 空单成交 gridId:{}, 止损追单已在进行中,标记待重挂, orderId:{}",
                            shortGridElement.getId(), orderId);
                    return;
                }
                shortGridElement.setExtendStopLossInProgress(true);
                accumulatedShortLossCount = 0;
                lastShortStopLossGridId = Integer.MAX_VALUE;
                // [Gate-需求2] 加仓后先撤空仓所有止盈+止损,再查交易所持仓后重挂
@@ -703,6 +715,17 @@
                int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity());
                longEntryTraderIdParam(longGridElement, orderId, false);
                // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend,
                // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。
                if (longGridElement.isExtendStopLossInProgress()) {
                    longGridElement.setPendingStopLossReExtend(true);
                    log.info("[Gate] 多单成交 gridId:{}, 止损追单已在进行中,标记待重挂, orderId:{}",
                            longGridElement.getId(), orderId);
                    return;
                }
                longGridElement.setExtendStopLossInProgress(true);
                accumulatedLongLossCount = 0;
                lastLongStopLossGridId = Integer.MAX_VALUE;
                // [Gate-需求2] 加仓后先撤多仓所有止盈+止损,再查交易所持仓后重挂
@@ -1666,7 +1689,10 @@
        int qty = Integer.parseInt(config.getQuantity());
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int gridCount = baseQty / qty;
        GridElement entryElem = GridElement.findById(gridId);
        if (gridCount <= 0 || filledQty < qty) {
            // 早期返回需重置防重入标记
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
@@ -1674,31 +1700,38 @@
        int[] orders = new int[gridCount];
        if (gridCount == 1) {
            // 只有1个网格,全量放在上面
            // 只有1个网格,全量挂在上面
            orders[0] = totalOrders;
        } else {
            int aCount = Math.max(0, totalOrders - 2);
            if (aCount == 0) {
                orders[0] = 2;
            } else if (aCount == 1) {
                orders[0] = 2;
                orders[1] = 1;
            // 最近网格(orders[0])挂剩余张数,其余每个网格各挂1张
            orders[0] = totalOrders - (gridCount - 1);
            if (orders[0] < 1) {
                // 订单数不够覆盖所有网格(理论上不会出现),全量挂在最近网格
                orders[0] = totalOrders;
            } else {
                orders[0] = aCount;
                int remainder = totalOrders - aCount;
                for (int i = 1; i < gridCount && remainder > 0; i++) {
                for (int i = 1; i < gridCount; i++) {
                    orders[i] = 1;
                    remainder--;
                }
            }
        }
        // 计算总订单数,用于异步回调完成时重置防重入标记
        int totalOrdersToPlace = 0;
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) { totalOrdersToPlace += orders[i]; }
        }
        if (totalOrdersToPlace == 0) {
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
        AtomicInteger remainCount = new AtomicInteger(totalOrdersToPlace);
        StringBuilder detail = new StringBuilder();
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) {
                int sgId = gridId - 2 - i;
                placeStopLossOrders(sgId, orders[i], qty, FuturesPriceTrigger.RuleEnum.NUMBER_2,
                        ORDER_TYPE_CLOSE_LONG, true);
                        ORDER_TYPE_CLOSE_LONG, true, remainCount, entryElem);
                if (detail.length() > 0) {
                    detail.append("  ");
                }
@@ -1718,7 +1751,10 @@
        int qty = Integer.parseInt(config.getQuantity());
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int gridCount = baseQty / qty;
        GridElement entryElem = GridElement.findById(gridId);
        if (gridCount <= 0 || filledQty < qty) {
            // 早期返回需重置防重入标记
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
@@ -1726,31 +1762,38 @@
        int[] orders = new int[gridCount];
        if (gridCount == 1) {
            // 只有1个网格,全量放在上面
            // 只有1个网格,全量挂在上面
            orders[0] = totalOrders;
        } else {
            int aCount = Math.max(0, totalOrders - 2);
            if (aCount == 0) {
                orders[0] = 2;
            } else if (aCount == 1) {
                orders[0] = 2;
                orders[1] = 1;
            // 最近网格(orders[0])挂剩余张数,其余每个网格各挂1张
            orders[0] = totalOrders - (gridCount - 1);
            if (orders[0] < 1) {
                // 订单数不够覆盖所有网格(理论上不会出现),全量挂在最近网格
                orders[0] = totalOrders;
            } else {
                orders[0] = aCount;
                int remainder = totalOrders - aCount;
                for (int i = 1; i < gridCount && remainder > 0; i++) {
                for (int i = 1; i < gridCount; i++) {
                    orders[i] = 1;
                    remainder--;
                }
            }
        }
        // 计算总订单数,用于异步回调完成时重置防重入标记
        int totalOrdersToPlace = 0;
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) { totalOrdersToPlace += orders[i]; }
        }
        if (totalOrdersToPlace == 0) {
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
        AtomicInteger remainCount = new AtomicInteger(totalOrdersToPlace);
        StringBuilder detail = new StringBuilder();
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) {
                int sgId = gridId + 2 + i;
                placeStopLossOrders(sgId, orders[i], qty, FuturesPriceTrigger.RuleEnum.NUMBER_1,
                        ORDER_TYPE_CLOSE_SHORT, false);
                        ORDER_TYPE_CLOSE_SHORT, false, remainCount, entryElem);
                if (detail.length() > 0) {
                    detail.append("  ");
                }
@@ -1763,17 +1806,66 @@
    }
    /**
     * 延展完成后重挂止损(处理被跳过的入场单成交)。
     * 取消已有止损单并用最新仓位重新挂单,确保止损覆盖最新持仓数。
     */
    private void reExtendLongStopLoss(GridElement entryElem) {
        if (entryElem.isExtendStopLossInProgress()) {
            log.info("[Gate] 多仓重挂止损跳过, entryGridId:{}, 仍在进行中", entryElem.getId());
            return;
        }
        entryElem.setExtendStopLossInProgress(true);
        cancelAllLongTakeProfitsAndStopLosses();
        int latestPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
        log.info("[Gate] 多仓重挂止损, entryGridId:{}, 最新仓位:{}张", entryElem.getId(), latestPos);
        extendLongStopLoss(latestPos, entryElem.getId());
    }
    private void reExtendShortStopLoss(GridElement entryElem) {
        if (entryElem.isExtendStopLossInProgress()) {
            log.info("[Gate] 空仓重挂止损跳过, entryGridId:{}, 仍在进行中", entryElem.getId());
            return;
        }
        entryElem.setExtendStopLossInProgress(true);
        cancelAllShortTakeProfitsAndStopLosses();
        int latestPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
        log.info("[Gate] 空仓重挂止损, entryGridId:{}, 最新仓位:{}张", entryElem.getId(), latestPos);
        extendShortStopLoss(latestPos, entryElem.getId());
    }
    /**
     * 在指定网格位置挂 count 个独立止损单,每个 size 张。
     */
    private void placeStopLossOrders(int gridId, int count, int qty,
                                      FuturesPriceTrigger.RuleEnum rule,
                                      String orderType, boolean isLong) {
                                      String orderType, boolean isLong,
                                      AtomicInteger remainCount,
                                      GridElement entryElement) {
        if (count <= 0) {
            return;
        }
        GridElement elem = GridElement.findById(gridId);
        if (elem == null) {
            log.warn("[Gate] 止损挂单位置不存在, gridId:{}", gridId);
            // 即使挂单位置不存在也需递减计数器,避免标志永不重置
            if (remainCount != null && entryElement != null) {
                for (int i = 0; i < count; i++) {
                    if (remainCount.decrementAndGet() == 0) {
                        entryElement.setExtendStopLossInProgress(false);
                        log.info("[Gate] {}止损追单全部完成(部分位置缺失), entryGridId:{}, 防重入标记已重置",
                                isLong ? "多仓" : "空仓", entryElement.getId());
                        // 检查待重挂请求
                        if (entryElement.isPendingStopLossReExtend()) {
                            entryElement.setPendingStopLossReExtend(false);
                            if (isLong) {
                                reExtendLongStopLoss(entryElement);
                            } else {
                                reExtendShortStopLoss(entryElement);
                            }
                        }
                    }
                }
            }
            return;
        }
        BigDecimal triggerPrice = elem.getGridPrice();
@@ -1792,6 +1884,21 @@
                        GridElement.refreshIndices();
                        log.info("[Gate] {}止损追加, gridId:{}, 触发价:{}, 第{}单, stopLossId:{}",
                                isLong ? "多仓" : "空仓", finalGridId, triggerPrice, finalI + 1, profitId);
                        // 计数器归零时重置防重入标记,并检查是否有待重挂请求
                        if (remainCount != null && remainCount.decrementAndGet() == 0 && entryElement != null) {
                            entryElement.setExtendStopLossInProgress(false);
                            log.info("[Gate] {}止损追单全部完成, entryGridId:{}, 防重入标记已重置",
                                    isLong ? "多仓" : "空仓", entryElement.getId());
                            // 如果有被跳过的入场单成交,用最新仓位重挂一次止损
                            if (entryElement.isPendingStopLossReExtend()) {
                                entryElement.setPendingStopLossReExtend(false);
                                if (isLong) {
                                    reExtendLongStopLoss(entryElement);
                                } else {
                                    reExtendShortStopLoss(entryElement);
                                }
                            }
                        }
                    }
            );
        }
src/main/java/com/xcong/excoin/modules/gateApi/GridElement.java
@@ -92,6 +92,21 @@
    /** 空仓止损订单 ID 列表(支持一个网格多个止损单) */
    private List<String> shortStopLossOrderIds = new ArrayList<>();
    /**
     * 止损追单是否正在进行中(防重入标记)。
     * 同一网格存在多个入场单且相近时间成交时,确保只有第一次 extend 触发止损挂单,
     * 后续入场单成交仅移除订单记录,不再重复触发 cancelAll + extend 流程。
     * 异步回调全部完成后由 placeStopLossOrders 中计数器归零时重置。
     */
    private volatile boolean extendStopLossInProgress = false;
    /**
     * 是否有待重挂的止损追单请求。
     * 当入场单成交因 extendStopLossInProgress=true 被跳过时置为 true,
     * 延展完成后由回调检查此标记并触发一次仓位重查+重挂,确保止损覆盖最新持仓数。
     */
    private volatile boolean pendingStopLossReExtend = false;
    /** 索引重建锁,保证 refreshIndices() 与计数读取之间互斥,避免 clear→rebuild 窗口期读到 0 */
    private static final Object INDEX_LOCK = new Object();
@@ -507,6 +522,17 @@
    /** @return 是否有止损单 */
    public boolean hasShortStopLossOrders() { return !shortStopLossOrderIds.isEmpty(); }
    // ==================== 止损追单防重入标记 ====================
    /** @return 止损追单是否正在进行中 */
    public boolean isExtendStopLossInProgress() { return extendStopLossInProgress; }
    /** 设置止损追单进行标记 */
    public void setExtendStopLossInProgress(boolean v) { this.extendStopLossInProgress = v; }
    /** @return 是否有待重挂请求 */
    public boolean isPendingStopLossReExtend() { return pendingStopLossReExtend; }
    /** 设置待重挂标记 */
    public void setPendingStopLossReExtend(boolean v) { this.pendingStopLossReExtend = v; }
    public static Builder builder() {
        return new Builder();
    }