| src/main/java/com/xcong/excoin/modules/okxNewPrice/GridQueueBuilder.java | ●●●●● patch | view | raw | blame | history | |
| src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxGridTradeService.java | ●●●●● patch | view | raw | blame | history | |
| src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java | ●●●●● patch | view | raw | blame | history | |
| src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/utils/RequestBuilder.java | ●●●●● patch | view | raw | blame | history |
src/main/java/com/xcong/excoin/modules/okxNewPrice/GridQueueBuilder.java
New file @@ -0,0 +1,152 @@ package com.xcong.excoin.modules.okxNewPrice; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; /** * 网格价格队列构建器 — 从基底入场价生成双向价格队列和 OkxGridElement 列表。 * * <h3>职责</h3> * <ul> * <li>根据基底空头入场价,向下递减排空仓队列</li> * <li>向上递增排多仓队列</li> * <li>整合两队列为 OkxGridElement 列表并注入 {@link OkxConfig}</li> * </ul> * * <h3>线程安全</h3> * 本类为纯函数式(除 config.setStep / config.setGridElements 副作用外), * 每次调用返回新构建的对象,不持有可变状态。 * * @author Administrator */ @Slf4j public final class GridQueueBuilder { private GridQueueBuilder() { /* utility class */ } /** * 从基底入场价向下递减生成空仓价格队列,降序排列(大→小)。 * * @param config 全局配置(step 会被计算并写入) * @param basePrice 空仓基底入场价 * @return 空仓价格队列,按降序排列 */ public static List<BigDecimal> buildShortQueue(OkxConfig config, BigDecimal basePrice) { int prec = config.getPriceScale(); BigDecimal step = basePrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP); config.setStep(step); // 其它方法依赖此 step List<BigDecimal> queue = new ArrayList<>(); BigDecimal elem = basePrice.subtract(step).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { queue.add(elem); elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP); if (elem.compareTo(BigDecimal.ZERO) <= 0) break; } queue.sort((a, b) -> b.compareTo(a)); // 降序 log.info("[OKX] 空队列:{}", queue); return queue; } /** * 从基底入场价向上递增生成多仓价格队列,升序排列(小→大)。 * * @param config 全局配置(使用已设置的 step) * @param basePrice 空仓基底入场价(对齐 Gate 版本,多仓队列也以此为基准) * @return 多仓价格队列,按升序排列 */ public static List<BigDecimal> buildLongQueue(OkxConfig config, BigDecimal basePrice) { int prec = config.getPriceScale(); BigDecimal step = config.getStep(); List<BigDecimal> queue = new ArrayList<>(); BigDecimal elem = basePrice.add(step).setScale(prec, RoundingMode.HALF_UP); for (int i = 0; i < config.getGridQueueSize(); i++) { queue.add(elem); elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP); } queue.sort(BigDecimal::compareTo); // 升序 log.info("[OKX] 多队列:{}", queue); return queue; } /** * 将空/多仓队列整合为 OkxGridElement 列表。 * <ul> * <li>空仓网格: id = -1, -2, -3 …</li> * <li>基底网格: id = 0(短仓入场价位置)</li> * <li>多仓网格: id = +1, +2, +3 …</li> * </ul> * 每个网格元素包含两个方向的 {@link OkxTraderParam}(入场价 / 止盈价 / 数量)。 * * @param config 全局配置 * @param shortQueue 空仓价格队列 * @param longQueue 多仓价格队列 * @param basePrice 空仓基底入场价 * @return 构建好的网格元素列表(已注入 config) */ public static List<OkxGridElement> buildGridElements(OkxConfig config, List<BigDecimal> shortQueue, List<BigDecimal> longQueue, BigDecimal basePrice) { List<OkxGridElement> elements = new ArrayList<>(); int shortSize = shortQueue.size(); int longSize = longQueue.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 = shortQueue.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()); } // ---- 基底网格: id = 0 ---- { BigDecimal price = basePrice; 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 = longQueue.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()); return elements; } } 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; src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
New file @@ -0,0 +1,302 @@ package com.xcong.excoin.modules.okxNewPrice; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; /** * 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。 * * <h3>止损策略(对齐 Gate 版本)</h3> * <ol> * <li>基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity</li> * <li>条件单成交后以成交量为粒度追挂止损(extend 系列)</li> * <li>止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N>2 时先取消旧挂单</li> * </ol> * * <h3>线程安全</h3> * 本类通过 executor 的异步回调串行化下单操作;对 OkxGridElement 的读写依赖调用方保证有序。 * * @author Administrator */ @Slf4j public class StopLossManager { private final OkxConfig config; private final OkxTradeExecutor executor; public StopLossManager(OkxConfig config, OkxTradeExecutor executor) { this.config = config; this.executor = executor; } // ========== 基底止损挂载 ========== /** * 在基底开仓完成后挂载初始止损单。 * 多仓止损: id=-2 到 -11,每格 quantity 张,方向 sell/long * 空仓止损: id=2 到 11,每格 quantity 张,方向 buy/short */ public void setupBaseStopLosses() { // 挂多仓止损 (id=-2 到 -11) 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) 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"); } // ========== 止损触发处理 ========== /** * 多仓止损触发处理。 * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N > 2,先取消上一步的旧挂单。 */ public void handleLongStopLossTriggered(OkxGridElement gridElement, BigDecimal longEntryPrice) { 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 -> { clearLongEntryState(cancelGrid); log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs(); 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); } /** * 空仓止损触发处理。 * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 * 若 N > 2,先取消上一步的旧挂单。 */ public void handleShortStopLossTriggered(OkxGridElement gridElement, BigDecimal shortEntryPrice) { 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 -> { clearShortEntryState(cancelGrid); log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId); }); } } BigDecimal triggerPrice = newEntryGrid.getGridPrice(); BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs(); 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); } // ========== 止损追挂 ========== /** * 多仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。 */ public void extendLongStopLoss(int filledQty) { 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); }); } } /** * 空仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。 */ public 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); }); } } // ========== 辅助:条件单创建 & 状态回写 ========== /** * 预置标志位后发送条件入单请求,成功/失败均通过回调写入 GridElement 状态。 */ 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) { setLongEntryState(gridElement, orderId, true); } else { setShortEntryState(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); } ); } // ---- GridElement 状态修改(package-private,OkxGridTradeService 也可调用) ---- void setLongEntryState(OkxGridElement gridElement, String entryId, boolean flag) { OkxTraderParam tp = gridElement.getLongTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); gridElement.setHasLongOrder(flag); gridElement.setLongOrderId(entryId); OkxGridElement.refreshIndices(); } void setShortEntryState(OkxGridElement gridElement, String entryId, boolean flag) { OkxTraderParam tp = gridElement.getShortTraderParam(); tp.setEntryOrderId(entryId); tp.setEntryOrderPlaced(flag); gridElement.setHasShortOrder(flag); gridElement.setShortOrderId(entryId); OkxGridElement.refreshIndices(); } void clearLongEntryState(OkxGridElement gridElement) { setLongEntryState(gridElement, null, false); } void clearShortEntryState(OkxGridElement gridElement) { setShortEntryState(gridElement, null, false); } void setLongTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) { OkxTraderParam tp = gridElement.getLongTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); gridElement.setLongTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } void setShortTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) { OkxTraderParam tp = gridElement.getShortTraderParam(); tp.setTakeProfitOrderId(profitId); tp.setTakeProfitPlaced(flag); gridElement.setShortTakeProfitOrderId(profitId); OkxGridElement.refreshIndices(); } } src/main/java/com/xcong/excoin/modules/okxNewPrice/okxpi/config/utils/RequestBuilder.java
@@ -96,7 +96,6 @@ .get() .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) .addHeader("OK-ACCESS-PASSPHRASE", passphrase) @@ -112,7 +111,6 @@ .put(RequestBody.create(JSON_TYPE, "")) .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) .addHeader("OK-ACCESS-PASSPHRASE", passphrase) @@ -127,7 +125,6 @@ .url(fullUrl) .delete() .addHeader("Content-Type", "application/x-www-form-urlencoded") .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-KEY", apiKey) .addHeader("OK-ACCESS-SIGN", sign) .addHeader("OK-ACCESS-TIMESTAMP", timeStamp)