| | |
| | | private final OkxConfig config; |
| | | private final OkxTradeExecutor executor; |
| | | private final OKXAccount okxAccount; |
| | | private final StopLossManager stopLossManager; |
| | | |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | |
| | | this.config = config; |
| | | this.okxAccount = okxAccount; |
| | | this.executor = new OkxTradeExecutor(okxAccount, config.getInstId(), config.getTdMode()); |
| | | this.stopLossManager = new StopLossManager(config, executor); |
| | | } |
| | | |
| | | // ---- 初始化 ---- |
| | |
| | | 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; |
| | | } |
| | |
| | | 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); |
| | |
| | | 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; |