Administrator
2026-08-12 bb0263cbffa0d282ea60a6d24647f78d6b7bcb9d
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -672,7 +672,9 @@
                int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
                extendShortStopLoss(posSize, shortGridElement.getId());
                // [Gate] 止盈挂单:超出基础仓位的部分,挂在多仓第一止损位
//                placeExcessTakeProfit(posSize, false);
                if (config.isPlaceExcessTakeProfit()) {
                    placeExcessTakeProfit(posSize, false);
                }
                log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
@@ -703,7 +705,9 @@
                int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
                extendLongStopLoss(posSize, longGridElement.getId());
                // [Gate] 止盈挂单:超出基础仓位的部分,挂在空仓第一止损位
//                placeExcessTakeProfit(posSize, true);
                if (config.isPlaceExcessTakeProfit()) {
                    placeExcessTakeProfit(posSize, true);
                }
                log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
            }
@@ -1295,7 +1299,8 @@
     * 根据加仓配置计算止损追单时的实际下单量。
     * <p>公式:</p>
     * <pre>
     * addMultiplier = floor(effectiveStopLossCount / addPositionInterval)
     * divisor = addPositionInterval + 1
     * addMultiplier = floor(effectiveStopLossCount / divisor)
     * addQty = addMultiplier × addPositionQuantity
     * finalQty = min(baseQuantity + addQty, maxPositionPerSide > 0 ? maxPositionPerSide : ∞)
     * </pre>
@@ -1310,7 +1315,13 @@
        int maxPerSide = config.getMaxPositionPerSide();
        int effectiveCount = getEffectiveStopLossCount(isLong);
        int addMultiplier = interval > 0 ? effectiveCount / interval : 0;
        int startThreshold = config.getAddPositionStartThreshold();
        if (startThreshold > 0) {
            effectiveCount = Math.max(0, effectiveCount - startThreshold);
        }
        // divisor = interval + 1:interval=0→每次加仓, interval=1→每2次加仓(2,4,6...), interval=3→每4次加仓(4,8,12...)
        int divisor = interval + 1;
        int addMultiplier = interval >= 0 ? effectiveCount / divisor : 0;
        int addQty = addMultiplier * addQtyPerUnit;
        int totalQty = baseQty + addQty;
@@ -1979,6 +1990,66 @@
    }
    /**
     * 挂单成交后,将超出基础仓位的部分挂止盈单,挂在对向仓位的第一止损位上。
     *
     * <p>遍历所有 GridElement,找到对向仓位第一个有止损单的网格作为止盈挂单位置。
     *
     * <p>例:空仓成交后持仓 8 张,基础 4 张 → 超出 4 张,
     * 找到多仓第一止损位(如 gridId=-2)→ 在该位置挂空仓止盈单。
     *
     * @param posSize  当前总持仓张数
     * @param isLong   true=多仓成交,false=空仓成交
     */
    private void placeExcessTakeProfit(int posSize, boolean isLong) {
        int baseQty = Integer.parseInt(config.getBaseQuantity());
        int excessQty = posSize - baseQty;
        if (excessQty <= 0) {
            return;
        }
        // 遍历找到对向仓位第一个有止损单的网格
        GridElement tpElem = isLong ? findFirstShortStopLossGrid() : findFirstLongStopLossGrid();
        if (tpElem == null) {
            log.warn("[Gate] {}止盈挂单失败:未找到对向仓止损位", isLong ? "多仓" : "空仓");
            return;
        }
        int tpGridId = tpElem.getId();
        BigDecimal triggerPrice = tpElem.getGridPrice();
        String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT;
        // 多仓止盈:价格≥触发价时平仓(NUMBER_1);空仓止盈:价格≤触发价时平仓(NUMBER_2)
        FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1
                : FuturesPriceTrigger.RuleEnum.NUMBER_2;
        String size = isLong ? negate(String.valueOf(excessQty)) : String.valueOf(excessQty);
//        if (isLong && tpElem.getLongTakeProfitOrderId() != null) {
//            executor.cancelConditionalOrder(tpElem.getLongTakeProfitOrderId(), oid -> {
//                longTakeProfitTraderIdParam(tpElem, null, false);
//                log.info("[Gate] 取消旧止盈, gridId:{}, orderId:{}", tpGridId, oid);
//            });
//        } else if (!isLong && tpElem.getShortTakeProfitOrderId() != null) {
//            executor.cancelConditionalOrder(tpElem.getShortTakeProfitOrderId(), oid -> {
//                shortTakeProfitTraderIdParam(tpElem, null, false);
//                log.info("[Gate] 取消旧止盈, gridId:{}, orderId:{}", tpGridId, oid);
//            });
//        }
        String finalSize = size;
        int finalTpGridId = tpGridId;
        executor.placeTakeProfit(triggerPrice, rule, orderType, size,
                profitId -> {
                    if (isLong) {
                        longTakeProfitTraderIdParam(tpElem, profitId, true);
                    } else {
                        shortTakeProfitTraderIdParam(tpElem, profitId, true);
                    }
                    log.info("[Gate] {}止盈挂单, gridId:{}, 触发价:{}, 数量:{}, takeProfitId:{}",
                            isLong ? "多仓" : "空仓", finalTpGridId, triggerPrice, finalSize, profitId);
                }
        );
    }
    /**
     * 在指定网格挂一笔对手止盈单(非满仓超额止盈,挂在止损触发位的下一格)。
     */
    private void placeTakeProfitAtGrid(GridElement tpElem, boolean isLong, int qty, int times) {