Administrator
14 hours ago cd6a9821d0e9e4f7f13d7b5dab00f22b267cc222
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -1765,7 +1765,7 @@
//                        String quantity = String.valueOf(config.getBaseQuantity());
                        String quantity = calculateEntryQuantity(true, false);
                        String quantity = calculateEntryQuantity(true, false, 0);
                        // 向下检查是否已有多单挂在更低价格网格,有则跳过(防止价格回升后重复挂单)
                        boolean hasLongOrderBelow = false;
                        GridElement checkDownCursor = GridElement.findById(newEntryGrid.getDownId());
@@ -1839,7 +1839,7 @@
//                        String quantity = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()) + Integer.parseInt(config.getQuantity()));
//                        String quantity = String.valueOf(config.getBaseQuantity());
                        String quantity = calculateEntryQuantity(false, false);
                        String quantity = calculateEntryQuantity(false, false, 0);
                        // 向上检查是否已有空单挂在更高价格网格,有则跳过(防止价格回落后重复挂单)
                        boolean hasShortOrderAbove = false;
                        GridElement checkUpCursor = GridElement.findById(newEntryGrid.getUpId());
@@ -1925,9 +1925,10 @@
     *
     * @param isLong                 {@code true}=多仓方向,{@code false}=空仓方向
     * @param respectCurrentPosition {@code true}=按「单边总持仓 ≤ maxPositionPerSide」扣减当前持仓;{@code false}=仅限制单次下单量
     * @param stopLossSize           本次止损单张数(仅止损追单场景传入,用于修正 REST 时序竞态;其余场景传 0)
     * @return 实际下单张数(字符串)
     */
    private String calculateEntryQuantity(boolean isLong, boolean respectCurrentPosition) {
    private String calculateEntryQuantity(boolean isLong, boolean respectCurrentPosition, int stopLossSize) {
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int interval = config.getAddPositionInterval();
        int addQtyPerUnit = config.getAddPositionQuantity();
@@ -1948,11 +1949,14 @@
            int upperLimit = maxPerSide;
            if (respectCurrentPosition) {
                // 止损追单:单边总持仓不得超过 maxPerSide,追单量 = maxPerSide - 当前持仓。
                // 止损单刚成交、WS 持仓推送尚未到达,必须用 REST 实时查询绕过 WS 缓存时序竞态,
                // 否则会取到滞后的旧持仓(如止损前仍为 10),把追单量错误扣减。
                // 止损单刚成交、REST 查询可能仍返回止损前持仓(时序竞态),
                // 减去本次止损单张数得到止损后的真实持仓,避免追单量被错误扣减。
                int currentPos = isLong
                        ? queryPositionSize(Position.ModeEnum.DUAL_LONG)
                        : queryPositionSize(Position.ModeEnum.DUAL_SHORT);
                if (stopLossSize > 0) {
                    currentPos = Math.max(0, currentPos - stopLossSize);
                }
                upperLimit = maxPerSide - currentPos;
            }
            if (upperLimit <= 0) {
@@ -1998,7 +2002,7 @@
        // 止损追单:同一网格可有多笔挂单,不判断 isHasLongOrder,直接挂单
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        String size = calculateEntryQuantity(true, true);
        String size = calculateEntryQuantity(true, true, Integer.parseInt(config.getQuantity()));
        log.info("[Gate] 多仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
                getEffectiveStopLossCount(true), config.getBaseQuantity(), size, config.getStopLossCountMode());
        if (Integer.parseInt(size) <= 0) {
@@ -2061,7 +2065,7 @@
        // 止损追单:同一网格可有多笔挂单,不判断 isHasShortOrder,直接挂单
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        String size = calculateEntryQuantity(false, true);
        String size = calculateEntryQuantity(false, true, Integer.parseInt(config.getQuantity()));
        log.info("[Gate] 空仓止损追单 有效次数:{}, 基础:{}张 → 实际:{}张, 模式:{}",
                getEffectiveStopLossCount(false), config.getBaseQuantity(), size, config.getStopLossCountMode());
        if (Integer.parseInt(size) <= 0) {
@@ -2321,14 +2325,17 @@
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int gridCount = baseQty / qty;
        GridElement entryElem = GridElement.findById(gridId);
        if (gridCount <= 0 || filledQty < qty) {
        if (gridCount <= 0 || filledQty <= 0) {
            // 早期返回需重置防重入标记
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
        // [Gate] 止损只针对初始开仓仓位(baseQuantity),加仓部分只做止盈
        int totalOrders = filledQty / qty;
        // [Gate] 止损只针对初始开仓仓位(baseQuantity),加仓部分只做止盈;
        // 持仓不足一个网格量时也按剩余张数挂一张止损(手动重建场景,避免裸奔)
        int slSize = Math.min(filledQty, baseQty);
        int totalOrders = slSize / qty;
        int remainder = slSize % qty;
        int[] orders = new int[gridCount];
        if (gridCount == 1) {
@@ -2352,6 +2359,7 @@
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) { totalOrdersToPlace += orders[i]; }
        }
        if (remainder > 0) { totalOrdersToPlace += 1; }
        if (totalOrdersToPlace == 0) {
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
@@ -2362,13 +2370,23 @@
        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,
                placeStopLossOrders(sgId, orders[i], String.valueOf(qty), FuturesPriceTrigger.RuleEnum.NUMBER_2,
                        ORDER_TYPE_CLOSE_LONG, true, remainCount, entryElem);
                if (detail.length() > 0) {
                    detail.append("  ");
                }
                detail.append(sgId).append("x").append(orders[i]);
            }
        }
        // 不足一个网格量的余数:单独挂一张止损
        if (remainder > 0) {
            int sgId = gridId - 2 - totalOrders;
            placeStopLossOrders(sgId, 1, String.valueOf(remainder), FuturesPriceTrigger.RuleEnum.NUMBER_2,
                    ORDER_TYPE_CLOSE_LONG, true, remainCount, entryElem);
            if (detail.length() > 0) {
                detail.append("  ");
            }
            detail.append(sgId).append("x").append(remainder);
        }
        log.info("[Gate] 多仓追挂止损, 成交{}张, gridId:{}, 网格数:{}, 分配:{}",
@@ -2384,14 +2402,17 @@
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int gridCount = baseQty / qty;
        GridElement entryElem = GridElement.findById(gridId);
        if (gridCount <= 0 || filledQty < qty) {
        if (gridCount <= 0 || filledQty <= 0) {
            // 早期返回需重置防重入标记
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
        }
        // [Gate] 止损只针对初始开仓仓位(baseQuantity),加仓部分只做止盈
        int totalOrders = filledQty / qty;
        // [Gate] 止损只针对初始开仓仓位(baseQuantity),加仓部分只做止盈;
        // 持仓不足一个网格量时也按剩余张数挂一张止损(手动重建场景,避免裸奔)
        int slSize = Math.min(filledQty, baseQty);
        int totalOrders = slSize / qty;
        int remainder = slSize % qty;
        int[] orders = new int[gridCount];
        if (gridCount == 1) {
@@ -2415,6 +2436,7 @@
        for (int i = 0; i < gridCount; i++) {
            if (orders[i] > 0) { totalOrdersToPlace += orders[i]; }
        }
        if (remainder > 0) { totalOrdersToPlace += 1; }
        if (totalOrdersToPlace == 0) {
            if (entryElem != null) { entryElem.setExtendStopLossInProgress(false); }
            return;
@@ -2425,13 +2447,23 @@
        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,
                placeStopLossOrders(sgId, orders[i], String.valueOf(qty), FuturesPriceTrigger.RuleEnum.NUMBER_1,
                        ORDER_TYPE_CLOSE_SHORT, false, remainCount, entryElem);
                if (detail.length() > 0) {
                    detail.append("  ");
                }
                detail.append(sgId).append("x").append(orders[i]);
            }
        }
        // 不足一个网格量的余数:单独挂一张止损
        if (remainder > 0) {
            int sgId = gridId + 2 + totalOrders;
            placeStopLossOrders(sgId, 1, String.valueOf(remainder), FuturesPriceTrigger.RuleEnum.NUMBER_1,
                    ORDER_TYPE_CLOSE_SHORT, false, remainCount, entryElem);
            if (detail.length() > 0) {
                detail.append("  ");
            }
            detail.append(sgId).append("x").append(remainder);
        }
        log.info("[Gate] 空仓追挂止损, 成交{}张, gridId:{}, 网格数:{}, 分配:{}",
@@ -2469,7 +2501,7 @@
    /**
     * 在指定网格位置挂 count 个独立止损单,每个 size 张。
     */
    private void placeStopLossOrders(int gridId, int count, int qty,
    private void placeStopLossOrders(int gridId, int count, String qtyStr,
                                      FuturesPriceTrigger.RuleEnum rule,
                                      String orderType, boolean isLong,
                                      AtomicInteger remainCount,
@@ -2502,8 +2534,8 @@
            return;
        }
        BigDecimal triggerPrice = elem.getGridPrice();
        String size = isLong ? negate(qtyStr) : qtyStr;
        for (int i = 0; i < count; i++) {
            String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty);
            int finalGridId = gridId;
            int finalI = i;
            executor.placeTakeProfit(