| | |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | @Slf4j |
| | |
| | | WAITING_KLINE, OPENING, ACTIVE, STOPPED |
| | | } |
| | | |
| | | private final String accountName; |
| | | private final OkxConfig config; |
| | | private final OkxTradeExecutor executor; |
| | | private final String accountName; |
| | | |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | |
| | | private volatile BigDecimal longPositionSize = BigDecimal.ZERO; |
| | | private volatile BigDecimal shortPositionSize = BigDecimal.ZERO; |
| | | |
| | | private volatile WebSocketClient wsClient; |
| | | |
| | | public OkxGridTradeService(OkxConfig config, String accountName) { |
| | | this.config = config; |
| | | this.accountName = accountName; |
| | | this.executor = new OkxTradeExecutor(config, accountName); |
| | | this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName); |
| | | } |
| | | |
| | | public void setWebSocketClient(WebSocketClient wsClient) { |
| | | this.wsClient = wsClient; |
| | | this.executor.setWebSocketClient(wsClient); |
| | | } |
| | | |
| | | public void startGrid() { |
| | | if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) { |
| | | log.warn("[{}] 策略已在运行中, state:{}", accountName, state); |
| | | log.warn("[{}] 策略已在运行中, state:{}", config.getContract(), state); |
| | | return; |
| | | } |
| | | state = StrategyState.WAITING_KLINE; |
| | |
| | | shortActive = false; |
| | | shortPriceQueue.clear(); |
| | | longPriceQueue.clear(); |
| | | log.info("[{}] 网格策略已启动", accountName); |
| | | log.info("[{}] 网格策略已启动", config.getContract()); |
| | | } |
| | | |
| | | public void stopGrid() { |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllPriceTriggeredOrders(); |
| | | executor.shutdown(); |
| | | log.info("[{}] 策略已停止, 累计盈亏: {}", accountName, cumulativePnl); |
| | | log.info("[{}] 策略已停止, 累计盈亏: {}", config.getContract(), cumulativePnl); |
| | | } |
| | | |
| | | public void onKline(BigDecimal closePrice) { |
| | | if (wsClient == null || !wsClient.isOpen()) { |
| | | return; |
| | | } |
| | | lastKlinePrice = closePrice; |
| | | updateUnrealizedPnl(); |
| | | if (state == StrategyState.STOPPED) { |
| | |
| | | |
| | | if (state == StrategyState.WAITING_KLINE) { |
| | | state = StrategyState.OPENING; |
| | | log.info("[{}] 首根K线到达,开基底仓位...", accountName); |
| | | executor.openLong(wsClient, () -> log.info("[{}] 基底多单已发送", accountName)); |
| | | executor.openShort(wsClient, () -> log.info("[{}] 基底空单已发送", accountName)); |
| | | log.info("[{}] 首根K线到达,开基底仓位...", config.getContract()); |
| | | executor.openLong(config.getQuantity(), () -> { |
| | | log.info("[{}] 基底多单已提交", config.getContract()); |
| | | }, null); |
| | | executor.openShort(config.getQuantity(), () -> { |
| | | log.info("[{}] 基底空单已提交", config.getContract()); |
| | | }, null); |
| | | return; |
| | | } |
| | | |
| | |
| | | longPositionSize = size; |
| | | longBaseEntryPrice = entryPrice; |
| | | baseLongOpened = true; |
| | | log.info("[{}] 基底多成交价: {}", accountName, longBaseEntryPrice); |
| | | log.info("[{}] 基底多成交价: {}", config.getContract(), longBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (size.compareTo(longPositionSize) > 0) { |
| | | longPositionSize = size; |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.add(config.getGridRate())).setScale(1, RoundingMode.HALF_UP); |
| | | executor.placeTakeProfit(wsClient, OkxEnums.POSSIDE_LONG, tpPrice, config.getQuantity()); |
| | | log.info("[{}] 多单止盈已设, entry:{}, tp:{}", accountName, entryPrice, tpPrice); |
| | | 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; |
| | | } |
| | |
| | | shortPositionSize = size; |
| | | shortBaseEntryPrice = entryPrice; |
| | | baseShortOpened = true; |
| | | log.info("[{}] 基底空成交价: {}", accountName, shortBaseEntryPrice); |
| | | log.info("[{}] 基底空成交价: {}", config.getContract(), shortBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else if (size.compareTo(shortPositionSize) > 0) { |
| | | shortPositionSize = size; |
| | | BigDecimal tpPrice = entryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate())).setScale(1, RoundingMode.HALF_UP); |
| | | executor.placeTakeProfit(wsClient, OkxEnums.POSSIDE_SHORT, tpPrice, config.getQuantity()); |
| | | log.info("[{}] 空单止盈已设, entry:{}, tp:{}", accountName, entryPrice, tpPrice); |
| | | 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; |
| | | } |
| | |
| | | return; |
| | | } |
| | | cumulativePnl = cumulativePnl.add(pnl); |
| | | log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", accountName, pnl, posSide, cumulativePnl); |
| | | log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", config.getContract(), pnl, posSide, cumulativePnl); |
| | | |
| | | if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) { |
| | | log.info("[{}] 已达止盈目标 {}→已停止", accountName, cumulativePnl); |
| | | log.info("[{}] 已达止盈目标 {}→已停止", config.getContract(), cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) { |
| | | log.info("[{}] 已达亏损上限 {}→已停止", accountName, cumulativePnl); |
| | | log.info("[{}] 已达亏损上限 {}→已停止", config.getContract(), cumulativePnl); |
| | | state = StrategyState.STOPPED; |
| | | } |
| | | } |
| | |
| | | generateLongQueue(); |
| | | state = StrategyState.ACTIVE; |
| | | log.info("[{}] 网格队列已生成, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活", |
| | | accountName, |
| | | config.getContract(), |
| | | 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 step = config.getGridRate(); |
| | | for (int i = 1; i <= config.getGridQueueSize(); i++) { |
| | | shortPriceQueue.add(shortBaseEntryPrice.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); |
| | | BigDecimal fixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal prev = shortBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(prev); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[{}] 空队列:{}", accountName, shortPriceQueue); |
| | | log.info("[{}] 空队列:{} 步长:{}", config.getContract(), shortPriceQueue, fixedStep); |
| | | } |
| | | |
| | | private void generateLongQueue() { |
| | | longPriceQueue.clear(); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= config.getGridQueueSize(); i++) { |
| | | longPriceQueue.add(longBaseEntryPrice.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); |
| | | BigDecimal fixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal prev = longBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.add(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(prev); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[{}] 多队列:{}", accountName, longPriceQueue); |
| | | Collections.sort(longPriceQueue); |
| | | log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep); |
| | | } |
| | | |
| | | /** |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部以固定步长(shortBasePrice × gridRate)递减补充新元素</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,以多仓固定步长递减加入新元素</li> |
| | | * <li>保证金检查 → 安全则开空一次</li> |
| | | * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li> |
| | | * </ol> |
| | | * |
| | | * <h3>多仓队列转移过滤</h3> |
| | |
| | | } |
| | | } |
| | | } |
| | | log.info("[{}] 原空队列:{}", config.getContract(), shortPriceQueue); |
| | | if (matched.isEmpty()) { |
| | | log.info("[{}] 空仓队列未触发, 当前价:{}", config.getContract(), currentPrice); |
| | | return; |
| | | } |
| | | log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", accountName, matched.size(), currentPrice); |
| | | 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, "空"); |
| | | |
| | | transferBetweenQueues(longPriceQueue, matched, matched.get(matched.size() - 1), |
| | | longFixedStep, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过空单开仓", accountName); |
| | | log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract()); |
| | | } else { |
| | | executor.openShort(wsClient, null); |
| | | 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.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) { |
| | | executor.openLong(wsClient, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", accountName, currentPrice); |
| | | } |
| | | } |
| | | |
| | | synchronized (shortPriceQueue) { |
| | | shortPriceQueue.removeAll(matched); |
| | | BigDecimal min = shortPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : shortPriceQueue.get(shortPriceQueue.size() - 1); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | min = min.multiply(BigDecimal.ONE.subtract(step)).setScale(1, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(min); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | } |
| | | |
| | | synchronized (longPriceQueue) { |
| | | BigDecimal first = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(0); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP); |
| | | if (longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(longEntryPrice).abs().compareTo(longEntryPrice.multiply(step)) < 0) { |
| | | continue; |
| | | } |
| | | longPriceQueue.add(elem); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | while (longPriceQueue.size() > config.getGridQueueSize()) { |
| | | longPriceQueue.remove(longPriceQueue.size() - 1); |
| | | && currentPrice.compareTo(longEntryPrice.subtract(longFixedStep)) < 0) { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |
| | | } |
| | | } |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部以固定步长(longBasePrice × gridRate)递增补充新元素</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,以空仓固定步长递增加入新元素</li> |
| | | * <li>保证金检查 → 安全则开多一次</li> |
| | | * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li> |
| | | * </ol> |
| | | * |
| | | * <h3>空仓队列转移过滤</h3> |
| | |
| | | } |
| | | } |
| | | } |
| | | log.info("[{}] 原多队列:{}", config.getContract(), longPriceQueue); |
| | | if (matched.isEmpty()) { |
| | | log.info("[{}] 多仓队列未触发, 当前价:{}", config.getContract(), currentPrice); |
| | | return; |
| | | } |
| | | log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", accountName, matched.size(), 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, "多"); |
| | | |
| | | transferBetweenQueues(shortPriceQueue, matched, matched.get(0), |
| | | shortFixedStep, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过多单开仓", accountName); |
| | | log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract()); |
| | | } else { |
| | | executor.openLong(wsClient, null); |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice.multiply(BigDecimal.ONE.add(config.getGridRate()))) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice.add(shortFixedStep)) > 0 |
| | | && currentPrice.compareTo(longEntryPrice) < 0) { |
| | | executor.openShort(wsClient, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", accountName, currentPrice); |
| | | } |
| | | } |
| | | |
| | | synchronized (longPriceQueue) { |
| | | longPriceQueue.removeAll(matched); |
| | | BigDecimal max = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(longPriceQueue.size() - 1); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | max = max.multiply(BigDecimal.ONE.add(step)).setScale(1, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(max); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | } |
| | | |
| | | synchronized (shortPriceQueue) { |
| | | BigDecimal first = shortPriceQueue.isEmpty() ? matched.get(0) : shortPriceQueue.get(0); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP); |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(shortEntryPrice).abs().compareTo(shortEntryPrice.multiply(step)) < 0) { |
| | | continue; |
| | | } |
| | | shortPriceQueue.add(elem); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | while (shortPriceQueue.size() > config.getGridQueueSize()) { |
| | | shortPriceQueue.remove(shortPriceQueue.size() - 1); |
| | | executor.openShort(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 保证金安全阀检查。 |
| | | * |
| | | * <p>当前版本通过 wsHandler 推送的账户/持仓数据间接判断保证金状态。 |
| | | * 后续可通过 OKX REST API 实时查询保证金占用比例,超 marginRatioLimit 时拒绝开仓。 |
| | | * |
| | | * @return true=安全可开仓 / false=保证金超限跳过开仓 |
| | | */ |
| | | private void replenishOwnQueue(List<BigDecimal> queue, List<BigDecimal> matched, BigDecimal fixedStep, |
| | | boolean isShort, String label) { |
| | | synchronized (queue) { |
| | | queue.removeAll(matched); |
| | | BigDecimal tail = queue.isEmpty() ? matched.get(matched.size() - 1) : queue.get(queue.size() - 1); |
| | | Comparator<BigDecimal> comparator = isShort ? (a, b) -> b.compareTo(a) : BigDecimal::compareTo; |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | tail = isShort |
| | | ? tail.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP) |
| | | : tail.add(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | queue.add(tail); |
| | | log.info("[{}] {}队列增加:{}", config.getContract(), label, tail); |
| | | } |
| | | queue.sort(comparator); |
| | | log.info("[{}] 现{}队列:{}", config.getContract(), label, queue); |
| | | } |
| | | } |
| | | |
| | | private void transferBetweenQueues(List<BigDecimal> targetQueue, List<BigDecimal> matched, |
| | | BigDecimal firstFallback, BigDecimal fixedStep, boolean isShort, |
| | | BigDecimal filterEntryPrice, Comparator<BigDecimal> comparator, |
| | | String label, BigDecimal currentPrice) { |
| | | synchronized (targetQueue) { |
| | | BigDecimal first = targetQueue.isEmpty() ? firstFallback : targetQueue.get(0); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal offset = fixedStep.multiply(BigDecimal.valueOf(i)); |
| | | BigDecimal elem = isShort |
| | | ? first.add(offset).setScale(1, RoundingMode.HALF_UP) |
| | | : first.subtract(offset).setScale(1, RoundingMode.HALF_UP); |
| | | if (filterEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(filterEntryPrice).abs().compareTo(filterEntryPrice.multiply(config.getGridRate())) < 0) { |
| | | log.info("[{}] {}队列跳过(price≈entry):{}", config.getContract(), label, elem); |
| | | continue; |
| | | } |
| | | targetQueue.add(elem); |
| | | log.info("[{}] {}队列增加:{}", config.getContract(), label, elem); |
| | | } |
| | | targetQueue.sort(comparator); |
| | | while (targetQueue.size() > config.getGridQueueSize()) { |
| | | targetQueue.remove(targetQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现{}队列:{}", config.getContract(), label, targetQueue); |
| | | } |
| | | } |
| | | |
| | | private boolean isMarginSafe() { |
| | | return true; |
| | | } |
| | |
| | | shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price)); |
| | | } |
| | | unrealizedPnl = longPnl.add(shortPnl); |
| | | log.info("[{}] 未实现盈亏: {}", config.getContract(), unrealizedPnl); |
| | | } |
| | | |
| | | public BigDecimal getLastKlinePrice() { return lastKlinePrice; } |