Administrator
2026-09-04 46ba2ee6aa63259d9004acdf3690705d04f592be
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -37,8 +37,8 @@
 *   onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened
 *     ↓
 *   tryGenerateQueues()
 *     ├── generateShortQueue()   ← 空仓价格队列(降序,从 shortBaseEntryPrice-step 向下)
 *     ├── generateLongQueue()    ← 多仓价格队列(升序,从 shortBaseEntryPrice+step 向上)
 *     ├── generateShortQueue()   ← 空仓价格队列(降序,基价×(1−gridRate) 等比向下)
 *     ├── generateLongQueue()    ← 多仓价格队列(升序,基价×(1+gridRate) 等比向上)
 *     ├── updateGridElements()   ← 构建 GridElement 列表 + TraderParam + 全局索引
 *     ├── 挂基座止盈单(ID=0 的 long/short takeProfit)
 *     └── 挂初始条件单(up=-1 多单, down=1 空单)
@@ -68,11 +68,11 @@
 *
 * <h3>关键公式</h3>
 * <pre>
 *   step  = shortBaseEntryPrice × gridRate                      ← 网格绝对步长
 *   step  = shortBaseEntryPrice × gridRate                      ← 首档间距(0档与±1档的距离度量)
 *   minTick = 10^(-priceScale)                                  ← 交易所最小价格单位
 *   多止盈 = gridPrice + (step - minTick)                       ← 多仓止盈价
 *   空止盈 = gridPrice - (step - minTick)                       ← 空仓止盈价
 *   单笔盈利 = (step - minTick) × contractMultiplier × quantity  ← USDT
 *   等比网格: 下一档 = 本档 × (1 ± gridRate)                     ← 向下×(1−r),向上×(1+r)
 *   止盈价 = 本档价 × (1 ± gridRate)(与相邻网格价联动)         ← 多仓向上,空仓向下
 *   单笔盈利 ≈ 档价 × gridRate × contractMultiplier × quantity(随网格位置浮动)
 * </pre>
 *
 * <h3>线程模型</h3>
@@ -103,6 +103,8 @@
    private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position";
    private final GateConfig config;
    private final StatsEventProducer statsProducer;
    private String apiKeyMd5;
    private final GateTradeExecutor executor;
    private final FuturesApi futuresApi;
    private static final String SETTLE = "usdt";
@@ -157,10 +159,13 @@
    private volatile BigDecimal shortPositionSize = BigDecimal.ZERO;
    private Long userId;
    private volatile BigDecimal initialPrincipal = BigDecimal.ZERO;
    /** 上次 PNL 快照时间(毫秒),用于控制 PNL_SNAPSHOT 埋点频率 */
    private volatile long lastPnlSnapshotTime = 0;
    private volatile GateKlineWebSocketClient wsClient;
    public GateGridTradeService(GateConfig config) {
    public GateGridTradeService(GateConfig config, StatsEventProducer statsProducer) {
        this.config = config;
        this.statsProducer = statsProducer;
        ApiClient apiClient = new ApiClient();
        apiClient.setBasePath(config.getRestBasePath());
        apiClient.setApiKeySecret(config.getApiKey(), config.getApiSecret());
@@ -290,6 +295,44 @@
        }
    }
    // ---- 埋点 ----
    private String apiKeyMd5() {
        if (apiKeyMd5 == null) {
            try {
                java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
                byte[] digest = md.digest(config.getApiKey().getBytes(java.nio.charset.StandardCharsets.UTF_8));
                StringBuilder sb = new StringBuilder();
                for (byte b : digest) sb.append(String.format("%02x", b));
                apiKeyMd5 = sb.toString();
            } catch (Exception e) {
                apiKeyMd5 = Integer.toHexString(config.getApiKey().hashCode());
            }
        }
        return apiKeyMd5;
    }
    private void emitStats(String type, Object payload) {
        if (statsProducer == null) return;
        try {
            statsProducer.sendStats(statsProducer.newStats(type, apiKeyMd5(), payload));
        } catch (Exception e) {
            log.warn("[Gate] 埋点发送失败, type={}", type, e);
        }
    }
    /**
     * Java 8 兼容的 Map 构造工具(Map.of 为 Java 9 API,此处手动实现)。
     * 接受偶数个参数:key1, value1, key2, value2, ...
     */
    private static Map<String, Object> mapOf(Object... kv) {
        Map<String, Object> m = new LinkedHashMap<>();
        for (int i = 0; i < kv.length; i += 2) {
            m.put((String) kv[i], kv[i + 1]);
        }
        return m;
    }
    // ---- 启动/停止 ----
    /**
@@ -328,6 +371,13 @@
        currentRound = 0;
        log.info("[Gate] 网格策略已启动, 当前本金: {} USDT", initialPrincipal);
        // 埋点: STRATEGY_START — 附全量配置快照
        GateConfigDTO snapshot = GateConfigDTO.from(config);
        Map<String, Object> params = snapshot.toParamsMap();
        params.put("principal", initialPrincipal.toPlainString());
        params.put("contract", config.getContract());
        emitStats("STRATEGY_START", params);
    }
    /**
@@ -348,6 +398,14 @@
     */
    public void stopGrid() {
        state = StrategyState.STOPPED;
        // 埋点: STRATEGY_STOP
        emitStats("STRATEGY_STOP", mapOf(
                "reason", "manual",
                "rounds", currentRound,
                "pnl", cumulativePnl.toPlainString()
        ));
        executor.cancelAllPriceTriggeredOrders();
        closeExistingPositions();
        executor.shutdown();
@@ -451,8 +509,28 @@
                    .add(new BigDecimal(account.getUnrealisedPnl()))
                    .subtract(estimatedCloseFee);
            // 埋点: PNL_SNAPSHOT — 每60秒发射一次
            long now = System.currentTimeMillis();
            if (now - lastPnlSnapshotTime >= 60_000) {
                lastPnlSnapshotTime = now;
                BigDecimal total = new BigDecimal(account.getTotal());
                emitStats("PNL_SNAPSHOT", mapOf(
                        "cumulativePnl", cumulativePnl.toPlainString(),
                        "unrealizedPnl", new BigDecimal(account.getUnrealisedPnl()).toPlainString(),
                        "totalEquity", total.toPlainString(),
                        "markPrice", markPrice.toPlainString()
                ));
            }
            if (totalEquity.compareTo(target) > 0) {
                currentRound++;
                // 埋点: ROUND_COMPLETE
                emitStats("ROUND_COMPLETE", mapOf(
                        "roundNum", currentRound,
                        "totalEquity", totalEquity.toPlainString()
                ));
                int maxRounds = config.getRounds();
                log.info("[Gate] 盈亏达标(净权益{}→含手续费-{}=实际{}>目标{}),第{}轮完成",
                        new BigDecimal(account.getTotal()).add(new BigDecimal(account.getUnrealisedPnl())),
@@ -655,6 +733,13 @@
                int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity());
                shortEntryTraderIdParam(shortGridElement, orderId, false);
                // 埋点: ENTRY_FILLED — 空仓加仓成交
                emitStats("ENTRY_FILLED", mapOf(
                        "direction", "short",
                        "gridId", shortGridElement.getId(),
                        "filledQty", filledQty
                ));
                // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend,
                // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。
                if (shortGridElement.isExtendStopLossInProgress()) {
@@ -687,6 +772,13 @@
                int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity());
                longEntryTraderIdParam(longGridElement, orderId, false);
                // 埋点: ENTRY_FILLED — 多仓加仓成交
                emitStats("ENTRY_FILLED", mapOf(
                        "direction", "long",
                        "gridId", longGridElement.getId(),
                        "filledQty", filledQty
                ));
                // 防重入:同一网格存在多个入场单且相近时间成交时,只处理第一次 extend,
                // 后续成交打标 pendingReExtend,延展完成后自动用最新仓位重挂一次。
@@ -973,51 +1065,73 @@
    }
    /**
     * 生成空仓价格队列。
     * 以 shortBaseEntryPrice × gridRate 作为绝对步长 step,存到 config。
     * 第1个元素 = shortBaseEntryPrice − step,后续依次递减 step,共 gridQueueSize 个。
     * 队列降序排列(大→小),方便 processShortGrid 中从头遍历。
     * 生成空仓价格队列(等比 / 复合百分比网格)。
     *
     * <p>每个网格价 = 上一档 × (1 − gridRate),即自 shortBaseEntryPrice 向下等比递减:
     * <pre>
     *   第1档 = shortBaseEntryPrice × (1 − gridRate)
     *   第2档 = 第1档 × (1 − gridRate)
     *   ...
     * </pre>
     * 档间距 = 该档价格 × gridRate,随价格下降而收窄。
     *
     * <p>同时将 step(= shortBaseEntryPrice × gridRate,即 0档 与 ±1档 的间距)注入 config,
     * 供跨度重启等需要固定距离度量的功能使用。
     *
     * <p>队列降序排列(大→小),方便 processShortGrid 中从头遍历。
     */
    private void generateShortQueue() {
        shortPriceQueue.clear();
        totalShortPriceQueue.clear();
        totalLongPriceQueue.clear();
        int prec = config.getPriceScale();
        BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP);
        BigDecimal gridRate = config.getGridRate();
        // 首档间距 = 基价 × gridRate(0档与±1档的间距),保留作跨度等固定距离度量
        BigDecimal step = shortBaseEntryPrice.multiply(gridRate).setScale(prec, RoundingMode.HALF_UP);
        config.setStep(step);
        BigDecimal elem = shortBaseEntryPrice.subtract(step).setScale(prec, RoundingMode.HALF_UP);
            for (int i = 0; i < config.getGridQueueSize(); i++) {
                shortPriceQueue.add(elem);
                totalLongPriceQueue.add( elem);
                totalShortPriceQueue.add( elem);
        BigDecimal factor = BigDecimal.ONE.subtract(gridRate);
        BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
        for (int i = 0; i < config.getGridQueueSize(); i++) {
            shortPriceQueue.add(elem);
            totalLongPriceQueue.add(elem);
            totalShortPriceQueue.add(elem);
                elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP);
                if (elem.compareTo(BigDecimal.ZERO) <= 0) {
                    break;
                }
            elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
            if (elem.compareTo(BigDecimal.ZERO) <= 0) {
                break;
            }
        }
        shortPriceQueue.sort((a, b) -> b.compareTo(a));
        log.info("[Gate] 空队列:{}", shortPriceQueue);
        log.info("[Gate] 空队列(等比):{}", shortPriceQueue);
    }
    /**
     * 生成多仓价格队列。
     * 以 shortBaseEntryPrice + step 为首元素,后续依次递增 step,共 gridQueueSize 个。
     * 队列升序排列(小→大),方便 processLongGrid 中从头遍历。
     * 生成多仓价格队列(等比 / 复合百分比网格)。
     *
     * <p>每个网格价 = 上一档 × (1 + gridRate),即自 shortBaseEntryPrice 向上等比递增:
     * <pre>
     *   第1档 = shortBaseEntryPrice × (1 + gridRate)
     *   第2档 = 第1档 × (1 + gridRate)
     *   ...
     * </pre>
     * 档间距 = 该档价格 × gridRate,随价格上升而放大;
     * 与空仓队列(下方 ×(1−gridRate))关于基价对称,保证上下网格连续一致。
     *
     * <p>队列升序排列(小→大),方便 processLongGrid 中从头遍历。
     */
    private void generateLongQueue() {
        longPriceQueue.clear();
        int prec = config.getPriceScale();
        BigDecimal step = config.getStep();
        BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP);
        BigDecimal factor = BigDecimal.ONE.add(config.getGridRate());
        BigDecimal elem = shortBaseEntryPrice.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
        for (int i = 0; i < config.getGridQueueSize(); i++) {
            longPriceQueue.add(elem);
            totalLongPriceQueue.add( elem);
            totalShortPriceQueue.add( elem);
            elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP);
            totalLongPriceQueue.add(elem);
            totalShortPriceQueue.add(elem);
            elem = elem.multiply(factor).setScale(prec, RoundingMode.HALF_UP);
        }
        longPriceQueue.sort(BigDecimal::compareTo);
        log.info("[Gate] 多队列:{}", longPriceQueue);
        log.info("[Gate] 多队列(等比):{}", longPriceQueue);
        totalShortPriceQueue.sort((a, b) -> b.compareTo(a));
        log.info("[Gate] 队列从大到小:{}", totalShortPriceQueue);
        totalLongPriceQueue.sort(BigDecimal::compareTo);
@@ -1044,7 +1158,9 @@
        int longSize = longPriceQueue.size();
        //根据精度转换成小数
        int prec = config.getPriceScale();
        BigDecimal step = config.getStep();
        // 等比网格:止盈价与本档相邻网格价联动 = 本档价 × (1 ± gridRate)
        BigDecimal tpUpFactor = BigDecimal.ONE.add(config.getGridRate());
        BigDecimal tpDownFactor = BigDecimal.ONE.subtract(config.getGridRate());
//        String qty = config.getBaseQuantity();
        String qty = config.getQuantity();
@@ -1057,13 +1173,13 @@
            TraderParam longParam = TraderParam.builder()
                    .direction(TraderParam.Direction.LONG)
                    .entryPrice(price)
                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            TraderParam shortParam = TraderParam.builder()
                    .direction(TraderParam.Direction.SHORT)
                    .entryPrice(price)
                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            elements.add(GridElement.builder()
@@ -1082,13 +1198,13 @@
            TraderParam longParam = TraderParam.builder()
                    .direction(TraderParam.Direction.LONG)
                    .entryPrice(price)
                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            TraderParam shortParam = TraderParam.builder()
                    .direction(TraderParam.Direction.SHORT)
                    .entryPrice(price)
                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            elements.add(GridElement.builder()
@@ -1110,13 +1226,13 @@
            TraderParam longParam = TraderParam.builder()
                    .direction(TraderParam.Direction.LONG)
                    .entryPrice(price)
                    .takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpUpFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            TraderParam shortParam = TraderParam.builder()
                    .direction(TraderParam.Direction.SHORT)
                    .entryPrice(price)
                    .takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP))
                    .takeProfitPrice(price.multiply(tpDownFactor).setScale(prec, RoundingMode.HALF_UP))
                    .quantity(qty)
                    .build();
            elements.add(GridElement.builder()
@@ -1157,7 +1273,10 @@
                    if (newEntryGrid != null) {
//                        String quantity = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()) + Integer.parseInt(config.getQuantity()));
                        String quantity = String.valueOf(config.getBaseQuantity());
//                        String quantity = String.valueOf(config.getBaseQuantity());
                        String quantity = calculateEntryQuantity(true);
                        // 向下检查是否已有多单挂在更低价格网格,有则跳过(防止价格回升后重复挂单)
                        boolean hasLongOrderBelow = false;
                        GridElement checkDownCursor = GridElement.findById(newEntryGrid.getDownId());
@@ -1228,7 +1347,9 @@
//                        String quantity = String.valueOf((accumulatedShortLossCount + 1) * Integer.parseInt(config.getQuantity()));
//                        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 = String.valueOf(config.getBaseQuantity());
                        String quantity = calculateEntryQuantity(false);
                        // 向上检查是否已有空单挂在更高价格网格,有则跳过(防止价格回落后重复挂单)
                        boolean hasShortOrderAbove = false;
                        GridElement checkUpCursor = GridElement.findById(newEntryGrid.getUpId());
@@ -1347,6 +1468,13 @@
        accumulatedLongLossCount++;
        log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
                gridId, accumulatedLongLossCount, sameGrid ? "(同网格)" : "");
        // 埋点: STOP_LOSS_TRIGGERED
        emitStats("STOP_LOSS_TRIGGERED", mapOf(
                "direction", "long",
                "gridId", gridId,
                "lossCount", accumulatedLongLossCount
        ));
        int newEntryGridId = gridId + 1;
        GridElement newEntryGrid = GridElement.findById(newEntryGridId);
@@ -1392,6 +1520,13 @@
        accumulatedShortLossCount++;
        log.info("[Gate] 空仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
                gridId, accumulatedShortLossCount, sameGrid ? "(同网格)" : "");
        // 埋点: STOP_LOSS_TRIGGERED
        emitStats("STOP_LOSS_TRIGGERED", mapOf(
                "direction", "short",
                "gridId", gridId,
                "lossCount", accumulatedShortLossCount
        ));
        int newEntryGridId = gridId - 1;
        GridElement newEntryGrid = GridElement.findById(newEntryGridId);
@@ -1960,8 +2095,12 @@
    public void setMarkPrice(BigDecimal markPrice) { this.markPrice = markPrice; }
    /** @return 策略是否处于活跃状态(非 STOPPED 且非 WAITING_KLINE) */
    public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; }
    /** @return 当前已完成轮数 */
    public int getCurrentRound() { return currentRound; }
    /** @return 累计已实现盈亏(平仓推送驱动累加) */
    public BigDecimal getCumulativePnl() { return cumulativePnl; }
    /** @return 初始本金 */
    public BigDecimal getInitialPrincipal() { return initialPrincipal; }
    /** @return 当前未实现盈亏(每根 K 线实时计算) */
    public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
    /** @return Gate 用户 ID(用于私有频道订阅 payload) */