Administrator
2026-06-05 2fede14ef1191ecd8738af4be3808c087131d8a5
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridTradeService.java
@@ -57,6 +57,7 @@
    private final OkxConfig config;
    private final OkxTradeExecutor executor;
    private final OKXAccount okxAccount;
    private final StopLossManager stopLossManager;
    private volatile StrategyState state = StrategyState.WAITING_KLINE;
@@ -105,6 +106,7 @@
        this.config = config;
        this.okxAccount = okxAccount;
        this.executor = new OkxTradeExecutor(okxAccount, config.getInstId(), config.getTdMode());
        this.stopLossManager = new StopLossManager(config, executor);
    }
    // ---- 初始化 ----
@@ -417,32 +419,32 @@
            return;
        }
        // 匹配止损单
        // 匹配止损单 → 委托 StopLossManager
        OkxGridElement byLongStopLoss = OkxGridElement.findByLongStopLossOrderId(algoId);
        if (byLongStopLoss != null) {
            handleLongStopLossTriggered(byLongStopLoss);
            stopLossManager.handleLongStopLossTriggered(byLongStopLoss, longEntryPrice);
            return;
        }
        OkxGridElement byShortStopLoss = OkxGridElement.findByShortStopLossOrderId(algoId);
        if (byShortStopLoss != null) {
            handleShortStopLossTriggered(byShortStopLoss);
            stopLossManager.handleShortStopLossTriggered(byShortStopLoss, shortEntryPrice);
            return;
        }
        // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 + 挂止盈单
        // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损
        OkxGridElement shortGridElement = OkxGridElement.findByShortOrderId(algoId);
        if (shortGridElement != null && shortGridElement.isHasShortOrder()) {
            int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity());
            shortEntryTraderIdParam(shortGridElement, null, false);
            extendShortStopLoss(filledQty);
            stopLossManager.clearShortEntryState(shortGridElement);
            stopLossManager.extendShortStopLoss(filledQty);
            log.info("[OKX] 空单成交 gridId:{}, qty:{}, 追挂止损", shortGridElement.getId(), filledQty);
            return;
        }
        OkxGridElement longGridElement = OkxGridElement.findByLongOrderId(algoId);
        if (longGridElement != null && longGridElement.isHasLongOrder()) {
            int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity());
            longEntryTraderIdParam(longGridElement, null, false);
            extendLongStopLoss(filledQty);
            stopLossManager.clearLongEntryState(longGridElement);
            stopLossManager.extendLongStopLoss(filledQty);
            log.info("[OKX] 多单成交 gridId:{}, qty:{}, 追挂止损", longGridElement.getId(), filledQty);
            return;
        }
@@ -465,9 +467,18 @@
                return;
            }
            generateShortQueue();
            generateLongQueue();
            updateGridElements();
            // 委托 GridQueueBuilder 构建价格队列 + GridElements
            List<BigDecimal> tmpShort = GridQueueBuilder.buildShortQueue(config, shortBaseEntryPrice);
            List<BigDecimal> tmpLong = GridQueueBuilder.buildLongQueue(config, shortBaseEntryPrice);
            synchronized (shortPriceQueue) {
                shortPriceQueue.clear();
                shortPriceQueue.addAll(tmpShort);
            }
            synchronized (longPriceQueue) {
                longPriceQueue.clear();
                longPriceQueue.addAll(tmpLong);
            }
            GridQueueBuilder.buildGridElements(config, shortPriceQueue, longPriceQueue, shortBaseEntryPrice);
            // 标记基座挂单
            OkxGridElement baseGridElement = OkxGridElement.findById(0);
@@ -476,336 +487,14 @@
            baseGridElement.setShortOrderId(baseShortTp.getEntryOrderId());
            baseGridElement.setHasShortOrder(true);
            // 挂多仓止损 (id=-2 到 -11),每格 quantity 张
            for (int id = -2; id >= -11; id--) {
                OkxGridElement elem = OkxGridElement.findById(id);
                if (elem == null) continue;
                BigDecimal triggerPrice = elem.getGridPrice();
                int finalId = id;
                executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
                        profitId -> {
                            elem.setLongStopLossOrderId(profitId);
                            OkxGridElement.refreshIndices();
                            log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
                                    finalId, triggerPrice, config.getQuantity(), profitId);
                        });
            }
            // 挂空仓止损 (id=2 到 11),每格 quantity 张
            for (int id = 2; id <= 11; id++) {
                OkxGridElement elem = OkxGridElement.findById(id);
                if (elem == null) continue;
                BigDecimal triggerPrice = elem.getGridPrice();
                int finalId = id;
                executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
                        profitId -> {
                            elem.setShortStopLossOrderId(profitId);
                            OkxGridElement.refreshIndices();
                            log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
                                    finalId, triggerPrice, config.getQuantity(), profitId);
                        });
            }
            log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11");
            // 委托 StopLossManager 挂止损单
            stopLossManager.setupBaseStopLosses();
            state = StrategyState.ACTIVE;
        }
    }
    private void generateShortQueue() {
        shortPriceQueue.clear();
        int prec = config.getPriceScale();
        BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).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);
            elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP);
            if (elem.compareTo(BigDecimal.ZERO) <= 0) break;
        }
        shortPriceQueue.sort((a, b) -> b.compareTo(a));
        log.info("[OKX] 空队列:{}", shortPriceQueue);
    }
    private void generateLongQueue() {
        longPriceQueue.clear();
        int prec = config.getPriceScale();
        BigDecimal step = config.getStep();
        BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP);
        for (int i = 0; i < config.getGridQueueSize(); i++) {
            longPriceQueue.add(elem);
            elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP);
        }
        longPriceQueue.sort(BigDecimal::compareTo);
        log.info("[OKX] 多队列:{}", longPriceQueue);
    }
    private void updateGridElements() {
        List<OkxGridElement> elements = new ArrayList<>();
        int shortSize = shortPriceQueue.size();
        int longSize = longPriceQueue.size();
        int prec = config.getPriceScale();
        BigDecimal step = config.getStep();
        String qty = config.getQuantity();
        // 空仓队列: id=-1, -2, ...
        for (int i = 0; i < shortSize; i++) {
            int id = -(i + 1);
            Integer upId = (i == 0) ? 0 : id + 1;
            Integer downId = (i == shortSize - 1) ? null : id - 1;
            BigDecimal price = shortPriceQueue.get(i);
            OkxTraderParam longParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.LONG)
                    .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            OkxTraderParam shortParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.SHORT)
                    .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId)
                    .longTraderParam(longParam).shortTraderParam(shortParam).build());
        }
        // 位置 0: 基底价格
        {
            BigDecimal price = shortBaseEntryPrice;
            OkxTraderParam longParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.LONG)
                    .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            OkxTraderParam shortParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.SHORT)
                    .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            elements.add(OkxGridElement.builder().id(0).gridPrice(price)
                    .upId(shortSize > 0 ? 1 : null).downId(longSize > 0 ? -1 : null)
                    .longTraderParam(longParam).shortTraderParam(shortParam).build());
        }
        // 多仓队列: id=1, 2, ...
        for (int i = 0; i < longSize; i++) {
            int id = i + 1;
            Integer downId = (i == 0) ? 0 : id - 1;
            Integer upId = (i == longSize - 1) ? null : id + 1;
            BigDecimal price = longPriceQueue.get(i);
            OkxTraderParam longParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.LONG)
                    .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            OkxTraderParam shortParam = OkxTraderParam.builder()
                    .direction(OkxTraderParam.Direction.SHORT)
                    .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
            elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId)
                    .longTraderParam(longParam).shortTraderParam(shortParam).build());
        }
        config.setGridElements(elements);
        log.info("[OKX] 网格元素列表已构建, 共{}个元素", elements.size());
    }
    // ---- 止损触发处理 ----
    /**
     * 多仓止损触发处理(Gate 模式逐步缩进)。
     * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
     * 若 N>2,先取消上一步的旧挂单。
     */
    private void handleLongStopLossTriggered(OkxGridElement gridElement) {
        int gridId = gridElement.getId();
        int N = Math.abs(gridId);
        gridElement.setLongStopLossOrderId(null);
        log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId);
        int newEntryGridId = -(N - 1);
        OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
        if (newEntryGrid == null) {
            OkxGridElement.refreshIndices();
            log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
            return;
        }
        if (N > 2) {
            int cancelGridId = -(N - 2);
            OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
            if (cancelGrid != null && cancelGrid.isHasLongOrder()) {
                executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> {
                    longEntryTraderIdParam(cancelGrid, null, false);
                    log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId);
                });
            }
        }
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs();
        // 精度补偿:步长被setScale截断,priceDiff/step可能产生1.99998→Down截断为1的问题
        BigDecimal epsilon = new BigDecimal("0.00000001");
        int count = priceDiff.add(epsilon).divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
        count = Math.max(1, count);
        int entryQty = count * Integer.parseInt(config.getQuantity());
        String size = String.valueOf(entryQty);
        log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(价差:{},步长:{},count:{},qty:{})",
                gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity());
        newEntryGrid.getLongTraderParam().setQuantity(size);
        placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size);
    }
    /**
     * 空仓止损触发处理(Gate 模式逐步缩进)。
     * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
     * 若 N>2,先取消上一步的旧挂单。
     */
    private void handleShortStopLossTriggered(OkxGridElement gridElement) {
        int gridId = gridElement.getId();
        int N = gridId;
        gridElement.setShortStopLossOrderId(null);
        log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId);
        int newEntryGridId = N - 1;
        OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
        if (newEntryGrid == null) {
            OkxGridElement.refreshIndices();
            log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
            return;
        }
        if (N > 2) {
            int cancelGridId = N - 2;
            OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
            if (cancelGrid != null && cancelGrid.isHasShortOrder()) {
                executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> {
                    shortEntryTraderIdParam(cancelGrid, null, false);
                    log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId);
                });
            }
        }
        BigDecimal triggerPrice = newEntryGrid.getGridPrice();
        BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs();
        // 精度补偿:步长被setScale截断,priceDiff/step可能产生1.99998→Down截断为1的问题
        BigDecimal epsilon = new BigDecimal("0.00000001");
        int count = priceDiff.add(epsilon).divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
        count = Math.max(1, count);
        int entryQty = count * Integer.parseInt(config.getQuantity());
        String size = String.valueOf(entryQty);
        log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(价差:{},步长:{},count:{},qty:{})",
                gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity());
        newEntryGrid.getShortTraderParam().setQuantity(size);
        placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size);
    }
    private void extendLongStopLoss(int filledQty) {
        // filledQty 为本次新增止损张数 = count * quantity, 需要按 quantity 为粒度拆分为 count 个止损单
        int qty = Integer.parseInt(config.getQuantity());
        int stopLossCount = filledQty / qty;
        int furthestSlId = 0;
        for (OkxGridElement e : config.getGridElements()) {
            if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) {
                furthestSlId = e.getId();
            }
        }
        if (furthestSlId == 0) furthestSlId = -11;
        log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
        for (int i = 0; i < stopLossCount; i++) {
            int newSlId = furthestSlId - i - 1;
            OkxGridElement elem = OkxGridElement.findById(newSlId);
            if (elem == null) continue;
            BigDecimal triggerPrice = elem.getGridPrice();
            int finalSlId = newSlId;
            executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
                    profitId -> {
                        elem.setLongStopLossOrderId(profitId);
                        OkxGridElement.refreshIndices();
                        log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
                    });
        }
    }
    private void extendShortStopLoss(int filledQty) {
        int qty = Integer.parseInt(config.getQuantity());
        int stopLossCount = filledQty / qty;
        int furthestSlId = 0;
        for (OkxGridElement e : config.getGridElements()) {
            if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) {
                furthestSlId = e.getId();
            }
        }
        if (furthestSlId == 0) furthestSlId = 11;
        log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
        for (int i = 0; i < stopLossCount; i++) {
            int newSlId = furthestSlId + i + 1;
            OkxGridElement elem = OkxGridElement.findById(newSlId);
            if (elem == null) continue;
            BigDecimal triggerPrice = elem.getGridPrice();
            int finalSlId = newSlId;
            executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
                    profitId -> {
                        elem.setShortStopLossOrderId(profitId);
                        OkxGridElement.refreshIndices();
                        log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
                    });
        }
    }
    // ---- 辅助方法 ----
    private void longTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) {
        OkxTraderParam tp = baseElement.getLongTraderParam();
        tp.setTakeProfitOrderId(profitId);
        tp.setTakeProfitPlaced(flag);
        baseElement.setLongTakeProfitOrderId(profitId);
        OkxGridElement.refreshIndices();
    }
    private void shortTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) {
        OkxTraderParam tp = baseElement.getShortTraderParam();
        tp.setTakeProfitOrderId(profitId);
        tp.setTakeProfitPlaced(flag);
        baseElement.setShortTakeProfitOrderId(profitId);
        OkxGridElement.refreshIndices();
    }
    private void longEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) {
        OkxTraderParam tp = baseElement.getLongTraderParam();
        tp.setEntryOrderId(entryId);
        tp.setEntryOrderPlaced(flag);
        baseElement.setHasLongOrder(flag);
        baseElement.setLongOrderId(entryId);
        OkxGridElement.refreshIndices();
    }
    private void shortEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) {
        OkxTraderParam tp = baseElement.getShortTraderParam();
        tp.setEntryOrderId(entryId);
        tp.setEntryOrderPlaced(flag);
        baseElement.setHasShortOrder(flag);
        baseElement.setShortOrderId(entryId);
        OkxGridElement.refreshIndices();
    }
    private void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong,
                                             BigDecimal triggerPrice, String size) {
        if (isLong) {
            gridElement.setHasLongOrder(true);
        } else {
            gridElement.setHasShortOrder(true);
        }
        String side = isLong ? "buy" : "sell";
        String posSide = isLong ? "long" : "short";
        executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, size,
                orderId -> {
                    if (isLong) {
                        longEntryTraderIdParam(gridElement, orderId, true);
                    } else {
                        shortEntryTraderIdParam(gridElement, orderId, true);
                    }
                },
                () -> {
                    if (isLong) {
                        gridElement.setHasLongOrder(false);
                        gridElement.setLongOrderId(null);
                    } else {
                        gridElement.setHasShortOrder(false);
                        gridElement.setShortOrderId(null);
                    }
                    OkxGridElement.refreshIndices();
                    log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong);
                }
        );
    }
    // ---- 盈亏计算 ----
    private void updateUnrealizedPnl() {
        if (lastKlinePrice == null || lastKlinePrice.compareTo(BigDecimal.ZERO) == 0) return;