Administrator
2026-09-18 9fcbec6148cf598033d3537265bf084f4456774d
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -757,8 +757,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 +799,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);
@@ -2189,6 +2193,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) {