| | |
| | | private volatile BigDecimal lastKlinePrice; |
| | | private volatile BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | private volatile BigDecimal unrealizedPnl = BigDecimal.ZERO; |
| | | private volatile BigDecimal longRealizedPnl = BigDecimal.ZERO; |
| | | private volatile BigDecimal shortRealizedPnl = BigDecimal.ZERO; |
| | | private volatile BigDecimal longEntryPrice = BigDecimal.ZERO; |
| | | private volatile BigDecimal shortEntryPrice = BigDecimal.ZERO; |
| | | private volatile BigDecimal longPositionSize = BigDecimal.ZERO; |
| | |
| | | public OkxGridTradeService(OkxConfig config, String accountName) { |
| | | this.config = config; |
| | | this.accountName = accountName; |
| | | this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName); |
| | | this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName, config.getInstIdCode()); |
| | | } |
| | | |
| | | public void setWebSocketClient(WebSocketClient wsClient) { |
| | |
| | | state = StrategyState.WAITING_KLINE; |
| | | cumulativePnl = BigDecimal.ZERO; |
| | | unrealizedPnl = BigDecimal.ZERO; |
| | | longRealizedPnl = BigDecimal.ZERO; |
| | | shortRealizedPnl = BigDecimal.ZERO; |
| | | longEntryPrice = BigDecimal.ZERO; |
| | | shortEntryPrice = BigDecimal.ZERO; |
| | | longPositionSize = BigDecimal.ZERO; |
| | |
| | | shortActive = false; |
| | | shortPriceQueue.clear(); |
| | | longPriceQueue.clear(); |
| | | config.setStep(null); |
| | | log.info("[{}] 网格策略已启动", config.getContract()); |
| | | } |
| | | |
| | |
| | | processLongGrid(closePrice); |
| | | } |
| | | |
| | | public void onPositionUpdate(String posSide, BigDecimal size, BigDecimal entryPrice) { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | public void onPositionUpdate(String posSide, BigDecimal size, BigDecimal entryPrice, BigDecimal realizedPnl) { |
| | | if (state == StrategyState.STOPPED) { |
| | | return; |
| | | } |
| | | |
| | | boolean hasPosition = size.compareTo(BigDecimal.ZERO) > 0; |
| | | |
| | | if (OkxEnums.POSSIDE_LONG.equals(posSide)) { |
| | | longRealizedPnl = realizedPnl; |
| | | if (hasPosition) { |
| | | longActive = true; |
| | | longEntryPrice = entryPrice; |
| | | if (!baseLongOpened) { |
| | | longPositionSize = size; |
| | | longBaseEntryPrice = entryPrice; |
| | | baseLongOpened = true; |
| | | log.info("[{}] 基底多成交价: {}", config.getContract(), longBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (size.compareTo(longPositionSize) > 0) { |
| | | longPositionSize = size; |
| | | if (longPriceQueue.isEmpty()) { |
| | | log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = longPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | | longPositionSize = size; |
| | | } |
| | | longPositionSize = size; |
| | | } else { |
| | | longActive = false; |
| | | longPositionSize = BigDecimal.ZERO; |
| | | longRealizedPnl = BigDecimal.ZERO; |
| | | } |
| | | } else if (OkxEnums.POSSIDE_SHORT.equals(posSide)) { |
| | | shortRealizedPnl = realizedPnl; |
| | | if (hasPosition) { |
| | | shortActive = true; |
| | | shortEntryPrice = entryPrice; |
| | | if (!baseShortOpened) { |
| | | shortPositionSize = size; |
| | | shortBaseEntryPrice = entryPrice; |
| | | baseShortOpened = true; |
| | | log.info("[{}] 基底空成交价: {}", config.getContract(), shortBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (size.compareTo(shortPositionSize) > 0) { |
| | | shortPositionSize = size; |
| | | if (shortPriceQueue.isEmpty()) { |
| | | log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = shortPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | | shortPositionSize = size; |
| | | } |
| | | shortPositionSize = size; |
| | | } else { |
| | | shortActive = false; |
| | | shortPositionSize = BigDecimal.ZERO; |
| | | shortRealizedPnl = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | } |
| | | |
| | | public void onOrderFilled(String posSide, BigDecimal fillSz, BigDecimal pnl) { |
| | | if (state == StrategyState.STOPPED) { |
| | | cumulativePnl = longRealizedPnl.add(shortRealizedPnl); |
| | | log.info("[{}] 持仓更新, 多已实现盈亏:{}, 空已实现盈亏:{}, 累计:{}", |
| | | config.getContract(), longRealizedPnl, shortRealizedPnl, cumulativePnl); |
| | | |
| | | if (state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", config.getContract(), pnl, posSide, cumulativePnl); |
| | | |
| | | if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { |
| | | log.info("[{}] 已达止盈目标 {}→已停止", config.getContract(), cumulativePnl); |
| | |
| | | } |
| | | } |
| | | |
| | | public void onOrderFilled(String posSide, BigDecimal fillSz, BigDecimal avgPx, BigDecimal pnl) { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | | } |
| | | |
| | | log.info("[{}] 订单成交, 方向:{}, 数量:{}, 成交价:{}, 盈亏:{}", |
| | | config.getContract(), posSide, fillSz, avgPx, pnl); |
| | | |
| | | if (OkxEnums.POSSIDE_LONG.equals(posSide)) { |
| | | if (!baseLongOpened) { |
| | | longBaseEntryPrice = avgPx; |
| | | baseLongOpened = true; |
| | | log.info("[{}] 基底多成交价: {}", config.getContract(), longBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (fillSz.compareTo(BigDecimal.ZERO) > 0) { |
| | | if (longPriceQueue.isEmpty()) { |
| | | log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = longPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | log.info("[{}] 多单止盈已设, 成交价:{}, tp:{}, size:{}", |
| | | config.getContract(), avgPx, tpPrice, config.getQuantity()); |
| | | } |
| | | } |
| | | } else if (OkxEnums.POSSIDE_SHORT.equals(posSide)) { |
| | | if (!baseShortOpened) { |
| | | shortBaseEntryPrice = avgPx; |
| | | baseShortOpened = true; |
| | | log.info("[{}] 基底空成交价: {}", config.getContract(), shortBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (fillSz.compareTo(BigDecimal.ZERO) > 0) { |
| | | if (shortPriceQueue.isEmpty()) { |
| | | log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = shortPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | log.info("[{}] 空单止盈已设, 成交价:{}, tp:{}, size:{}", |
| | | config.getContract(), avgPx, tpPrice, config.getQuantity()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void tryGenerateQueues() { |
| | | if (baseLongOpened && baseShortOpened) { |
| | | BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | config.setStep(step); |
| | | generateShortQueue(); |
| | | generateLongQueue(); |
| | | state = StrategyState.ACTIVE; |
| | | log.info("[{}] 网格队列已生成, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活", |
| | | config.getContract(), |
| | | log.info("[{}] 网格队列已生成, 步长:{}, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活", |
| | | config.getContract(), step, |
| | | shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(0), |
| | | shortPriceQueue.isEmpty() ? "N/A" : shortPriceQueue.get(shortPriceQueue.size() - 1), |
| | | longPriceQueue.isEmpty() ? "N/A" : longPriceQueue.get(0), |
| | |
| | | |
| | | private void generateShortQueue() { |
| | | shortPriceQueue.clear(); |
| | | BigDecimal fixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal step = config.getStep(); |
| | | BigDecimal prev = shortBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | prev = prev.subtract(step).setScale(1, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(prev); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[{}] 空队列:{} 步长:{}", config.getContract(), shortPriceQueue, fixedStep); |
| | | log.info("[{}] 空队列:{} 步长:{}", config.getContract(), shortPriceQueue, step); |
| | | } |
| | | |
| | | private void generateLongQueue() { |
| | | longPriceQueue.clear(); |
| | | BigDecimal fixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal step = config.getStep(); |
| | | BigDecimal prev = longBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.add(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | prev = prev.add(step).setScale(1, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(prev); |
| | | } |
| | | Collections.sort(longPriceQueue); |
| | | log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep); |
| | | log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, step); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); |
| | | |
| | | BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | replenishOwnQueue(shortPriceQueue, matched, shortFixedStep, true, "空"); |
| | | BigDecimal step = config.getStep(); |
| | | replenishOwnQueue(shortPriceQueue, matched, step, true, "空"); |
| | | |
| | | transferBetweenQueues(longPriceQueue, matched, matched.get(matched.size() - 1), |
| | | longFixedStep, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice); |
| | | step, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract()); |
| | | } else { |
| | | executor.openShort(config.getQuantity(), null, null); |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(longEntryPrice.subtract(longFixedStep)) < 0) { |
| | | if (shortPositionSize.compareTo(config.getMaxPosSize()) < 0){ |
| | | executor.openShort(config.getQuantity(), null, null); |
| | | } |
| | | if (currentPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(longEntryPrice) < 0 |
| | | && shortPositionSize.compareTo(config.getMaxPosSize()) < 0) { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |
| | |
| | | } |
| | | log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); |
| | | |
| | | BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | replenishOwnQueue(longPriceQueue, matched, longFixedStep, false, "多"); |
| | | BigDecimal step = config.getStep(); |
| | | replenishOwnQueue(longPriceQueue, matched, step, false, "多"); |
| | | |
| | | transferBetweenQueues(shortPriceQueue, matched, matched.get(0), |
| | | shortFixedStep, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice); |
| | | step, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract()); |
| | | } else { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | if (longPositionSize.compareTo(config.getMaxPosSize()) < 0) { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | } |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice.add(shortFixedStep)) > 0 |
| | | && currentPrice.compareTo(longEntryPrice) < 0) { |
| | | && currentPrice.compareTo(shortEntryPrice.add(step)) > 0 |
| | | && currentPrice.compareTo(longEntryPrice) < 0 |
| | | && shortPositionSize.compareTo(config.getMaxPosSize()) < 0) { |
| | | executor.openShort(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |