Administrator
2026-09-20 cf62512abd929b6f9461448d8fcfbad6ca087cb7
feat(gate): 添加网格交易手动控制功能

- 扩展 GateCommand 指令类型,支持止盈止损管理和平仓操作
- 实现取消多仓/空仓止盈止损、重新生成止盈止损等功能
- 添加多单/空单市价平仓及挂单清理逻辑
- 集成到 GateCommandConsumer 中处理远程指令
- 提供手动任务提交机制确保操作安全执行
3 files modified
215 ■■■■■ changed files
src/main/java/com/xcong/excoin/modules/gateApi/GateCommandConsumer.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java 172 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/station/model/GateCommand.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateCommandConsumer.java
@@ -73,6 +73,42 @@
                    log.info("[Gate] 远程参数已持久化");
                    break;
                case "CANCEL_LONG_TP_SL":
                    strategy.cancelLongTakeProfitAndStopLoss();
                    message = "已取消多仓止盈止损";
                    log.info("[Gate] 远程取消多仓止盈止损");
                    break;
                case "CANCEL_SHORT_TP_SL":
                    strategy.cancelShortTakeProfitAndStopLoss();
                    message = "已取消空仓止盈止损";
                    log.info("[Gate] 远程取消空仓止盈止损");
                    break;
                case "REBUILD_LONG_TP_SL":
                    strategy.rebuildLongTakeProfitAndStopLoss();
                    message = "已生成多仓止盈止损";
                    log.info("[Gate] 远程生成多仓止盈止损");
                    break;
                case "REBUILD_SHORT_TP_SL":
                    strategy.rebuildShortTakeProfitAndStopLoss();
                    message = "已生成空仓止盈止损";
                    log.info("[Gate] 远程生成空仓止盈止损");
                    break;
                case "CLOSE_LONG":
                    strategy.closeLongPosition();
                    message = "已提交多单平仓";
                    log.info("[Gate] 远程多单平仓");
                    break;
                case "CLOSE_SHORT":
                    strategy.closeShortPosition();
                    message = "已提交空单平仓";
                    log.info("[Gate] 远程空单平仓");
                    break;
                default:
                    success = false;
                    message = "未知指令类型: " + cmd.getCommandType();
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -2272,6 +2272,178 @@
    /** 注入WS客户端,用于订阅状态检查 */
    public void setWsClient(GateKlineWebSocketClient wsClient) { this.wsClient = wsClient; }
    // ========== 手动控制(Station 指令) ==========
    /**
     * 手动取消多仓止盈 + 止损挂单,并同步清理对应 GridElement 的挂单记录。
     */
    public void cancelLongTakeProfitAndStopLoss() {
        submitManualTask(this::cancelAllLongTakeProfitsAndStopLosses);
    }
    /**
     * 手动取消空仓止盈 + 止损挂单,并同步清理对应 GridElement 的挂单记录。
     */
    public void cancelShortTakeProfitAndStopLoss() {
        submitManualTask(this::cancelAllShortTakeProfitsAndStopLosses);
    }
    /**
     * 手动重新生成多仓止盈 + 止损挂单,逻辑与挂单成交(onAutoOrder)一致:
     * 先撤多仓所有止盈止损,再按交易所最新持仓重挂止损阶梯,超额部分挂止盈到空仓第一止损位。
     */
    public void rebuildLongTakeProfitAndStopLoss() {
        submitManualTask(() -> {
            cancelAllLongTakeProfitsAndStopLosses();
            int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue());
            if (posSize <= 0) {
                log.warn("[Gate] 生成多仓止盈止损失败:无多仓持仓");
                return;
            }
            extendLongStopLoss(posSize, 0);
            if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
                placeExcessTakeProfit(posSize, true);
            } else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
                placeDispersedTakeProfit(posSize, true, 0);
            }
            log.info("[Gate] 生成多仓止盈止损完成, 仓位:{}张", posSize);
        });
    }
    /**
     * 手动重新生成空仓止盈 + 止损挂单,逻辑与挂单成交(onAutoOrder)一致:
     * 先撤空仓所有止盈止损,再按交易所最新持仓重挂止损阶梯,超额部分挂止盈到多仓第一止损位。
     */
    public void rebuildShortTakeProfitAndStopLoss() {
        submitManualTask(() -> {
            cancelAllShortTakeProfitsAndStopLosses();
            int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue());
            if (posSize <= 0) {
                log.warn("[Gate] 生成空仓止盈止损失败:无空仓持仓");
                return;
            }
            extendShortStopLoss(posSize, 0);
            if (GateConfig.EXCESS_TP_CONCENTRATED.equals(config.getPlaceExcessTakeProfit())) {
                placeExcessTakeProfit(posSize, false);
            } else if (GateConfig.EXCESS_TP_DISPERSED.equals(config.getPlaceExcessTakeProfit())) {
                placeDispersedTakeProfit(posSize, false, 0);
            }
            log.info("[Gate] 生成空仓止盈止损完成, 仓位:{}张", posSize);
        });
    }
    /**
     * 多单平仓:市价平掉多仓 → 撤多仓所有挂单并更新 GridElement → 交由价格驱动(onKline→processGrid)重新入场。
     */
    public void closeLongPosition() {
        submitManualTask(() -> closePositionByMode(Position.ModeEnum.DUAL_LONG, true));
    }
    /**
     * 空单平仓:市价平掉空仓 → 撤空仓所有挂单并更新 GridElement → 交由价格驱动(onKline→processGrid)重新入场。
     */
    public void closeShortPosition() {
        submitManualTask(() -> closePositionByMode(Position.ModeEnum.DUAL_SHORT, false));
    }
    /**
     * 统一提交手动操作任务。策略停止(executor 已关闭)时直接忽略,避免 RejectedExecutionException。
     */
    private void submitManualTask(Runnable task) {
        if (state == StrategyState.STOPPED) {
            log.warn("[Gate] 策略已停止,忽略手动操作");
            return;
        }
        executor.submitTask(task);
    }
    /**
     * 按持仓模式市价平仓(reduceOnly + IOC),随后撤该方向所有挂单(止盈/止损/入场单)、
     * 更新 GridElement,并清空 active 与持仓量标记,让价格驱动在下一根 K 线重新挂入场单。
     *
     * @param mode   持仓模式(DUAL_LONG / DUAL_SHORT)
     * @param isLong true=多仓,false=空仓
     */
    private void closePositionByMode(Position.ModeEnum mode, boolean isLong) {
        String sideLabel = isLong ? "多" : "空";
        // 1. 市价平掉对应方向仓位
        try {
            Position pos = queryPosition(mode);
            if (pos != null) {
                String sizeStr = pos.getSize();
                long size = Long.parseLong(sizeStr);
                if (size != 0) {
                    Position.ModeEnum posMode = pos.getMode();
                    FuturesOrder closeOrder = new FuturesOrder();
                    closeOrder.setContract(config.getContract());
                    closeOrder.setPrice("0");
                    closeOrder.setTif(FuturesOrder.TifEnum.IOC);
                    closeOrder.setReduceOnly(true);
                    if (posMode != null && posMode.getValue() != null && posMode.getValue().contains("dual")) {
                        closeOrder.setSize("0");
                        closeOrder.setClose(false);
                        closeOrder.setAutoSize(size > 0 ? FuturesOrder.AutoSizeEnum.LONG : FuturesOrder.AutoSizeEnum.SHORT);
                    } else {
                        closeOrder.setSize(size > 0 ? String.valueOf(-size) : String.valueOf(Math.abs(size)));
                    }
                    closeOrder.setText("t-grid-manual-close");
                    futuresApi.createFuturesOrder(SETTLE, closeOrder, null);
                    log.info("[Gate] 手动平{}仓, size:{}, mode:{}", sideLabel, sizeStr, posMode);
                }
            }
        } catch (Exception e) {
            log.warn("[Gate] 手动平{}仓失败", sideLabel, e);
        }
        // 2. 撤该方向所有挂单 + 更新 GridElement
        if (isLong) {
            cancelAllLongTakeProfitsAndStopLosses();
            cancelAllLongEntryOrders();
            longActive = false;
            longPositionSize = BigDecimal.ZERO;
        } else {
            cancelAllShortTakeProfitsAndStopLosses();
            cancelAllShortEntryOrders();
            shortActive = false;
            shortPositionSize = BigDecimal.ZERO;
        }
        log.info("[Gate] {}仓已平仓并清理挂单,交由价格驱动重新入场", sideLabel);
    }
    /**
     * 取消所有多仓入场单(条件开仓单),并清空 GridElement 的多仓挂单记录。
     */
    private void cancelAllLongEntryOrders() {
        for (GridElement e : config.getGridElements()) {
            for (String oid : new ArrayList<>(e.getLongOrderIds())) {
                executor.cancelConditionalOrder(oid, o -> {});
            }
            e.getLongTraderParam().setEntryOrderId(null);
            e.getLongTraderParam().setEntryOrderPlaced(false);
            e.setHasLongOrder(false);
            e.clearLongOrderIds();
        }
        GridElement.refreshIndices();
        log.info("[Gate] 已提交取消所有多仓入场单");
    }
    /**
     * 取消所有空仓入场单(条件开仓单),并清空 GridElement 的空仓挂单记录。
     */
    private void cancelAllShortEntryOrders() {
        for (GridElement e : config.getGridElements()) {
            for (String oid : new ArrayList<>(e.getShortOrderIds())) {
                executor.cancelConditionalOrder(oid, o -> {});
            }
            e.getShortTraderParam().setEntryOrderId(null);
            e.getShortTraderParam().setEntryOrderPlaced(false);
            e.setHasShortOrder(false);
            e.clearShortOrderIds();
        }
        GridElement.refreshIndices();
        log.info("[Gate] 已提交取消所有空仓入场单");
    }
    // ========== 止损查表辅助方法 ==========
    /** 找到第一个有多仓止损单的网格(首个匹配即返回) */
src/main/java/com/xcong/excoin/modules/station/model/GateCommand.java
@@ -15,7 +15,12 @@
public class GateCommand {
    /** 指令唯一 ID(用于 ACK 对应) */
    private String commandId;
    /** START / STOP / UPDATE_CONFIG */
    /**
     * START / STOP / UPDATE_CONFIG /
     * CANCEL_LONG_TP_SL(取消多仓止盈止损)/ CANCEL_SHORT_TP_SL(取消空仓止盈止损)/
     * REBUILD_LONG_TP_SL(生成多仓止盈止损)/ REBUILD_SHORT_TP_SL(生成空仓止盈止损)/
     * CLOSE_LONG(多单平仓)/ CLOSE_SHORT(空单平仓)
     */
    private String commandType;
    /** 目标 apiKey 的 MD5 */
    private String apiKeyMd5;