| | |
| | | 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 static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position"; |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | private final String accountName; |
| | | private final OkxConfig config; |
| | | private final OkxTradeExecutor executor; |
| | | |
| | |
| | | |
| | | public OkxGridTradeService(OkxConfig config, String accountName) { |
| | | this.config = config; |
| | | this.accountName = accountName; |
| | | this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName); |
| | | } |
| | | |
| | |
| | | log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = longPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | |
| | | log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = shortPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | |
| | | |
| | | 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("[{}] 空队列:{}", config.getContract(), 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("[{}] 多队列:{}", config.getContract(), longPriceQueue); |
| | | Collections.sort(longPriceQueue); |
| | | log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep); |
| | | } |
| | | |
| | | /** |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部以固定步长(shortBasePrice × gridRate)递减补充新元素</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,以多仓固定步长递减加入新元素</li> |
| | | * <li>保证金检查 → 安全则开空一次</li> |
| | | * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li> |
| | | * </ol> |
| | |
| | | } |
| | | log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), 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); |
| | | log.info("[{}] 空队列增加:{}", config.getContract(), min); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue); |
| | | } |
| | | 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, "空"); |
| | | |
| | | 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) { |
| | | log.info("[{}] 多队列跳过(price≈longEntry):{}", config.getContract(), elem); |
| | | continue; |
| | | } |
| | | longPriceQueue.add(elem); |
| | | log.info("[{}] 多队列增加:{}", config.getContract(), elem); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | while (longPriceQueue.size() > config.getGridQueueSize()) { |
| | | longPriceQueue.remove(longPriceQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue); |
| | | } |
| | | transferBetweenQueues(longPriceQueue, matched, matched.get(matched.size() - 1), |
| | | longFixedStep, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract()); |
| | |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) { |
| | | && currentPrice.compareTo(longEntryPrice.subtract(longFixedStep)) < 0) { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部以固定步长(longBasePrice × gridRate)递增补充新元素</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,以空仓固定步长递增加入新元素</li> |
| | | * <li>保证金检查 → 安全则开多一次</li> |
| | | * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li> |
| | | * </ol> |
| | |
| | | } |
| | | log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), 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); |
| | | log.info("[{}] 多队列增加:{}", config.getContract(), max); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue); |
| | | } |
| | | 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, "多"); |
| | | |
| | | 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) { |
| | | log.info("[{}] 空队列跳过(price≈shortEntry):{}", config.getContract(), elem); |
| | | continue; |
| | | } |
| | | shortPriceQueue.add(elem); |
| | | log.info("[{}] 空队列增加:{}", config.getContract(), elem); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | while (shortPriceQueue.size() > config.getGridQueueSize()) { |
| | | shortPriceQueue.remove(shortPriceQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue); |
| | | } |
| | | transferBetweenQueues(shortPriceQueue, matched, matched.get(0), |
| | | shortFixedStep, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract()); |
| | |
| | | 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(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; |
| | | } |
| | |
| | | public BigDecimal getCumulativePnl() { return cumulativePnl; } |
| | | public BigDecimal getUnrealizedPnl() { return unrealizedPnl; } |
| | | public StrategyState getState() { return state; } |
| | | public String getAccountName() { return config.getContract(); } |
| | | public String getAccountName() { return accountName; } |
| | | } |