From 6a51f45e6a00b65a9e7b0b0707b453c11311f3ef Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Mon, 11 May 2026 22:38:13 +0800
Subject: [PATCH] feat(okxApi): 添加仓位模式配置和REST客户端功能
---
src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java | 416 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 416 insertions(+), 0 deletions(-)
diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java b/src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
new file mode 100644
index 0000000..8de17ad
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
@@ -0,0 +1,416 @@
+package com.xcong.excoin.modules.okxApi;
+
+import com.xcong.excoin.modules.okxApi.enums.OkxEnums;
+import lombok.extern.slf4j.Slf4j;
+import org.java_websocket.client.WebSocketClient;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+@Slf4j
+public class OkxGridTradeService {
+
+ public enum StrategyState {
+ WAITING_KLINE, OPENING, ACTIVE, STOPPED
+ }
+
+ private final String accountName;
+ private final OkxConfig config;
+ private final OkxTradeExecutor executor;
+
+ private volatile StrategyState state = StrategyState.WAITING_KLINE;
+
+ private final List<BigDecimal> shortPriceQueue = Collections.synchronizedList(new ArrayList<>());
+ private final List<BigDecimal> longPriceQueue = Collections.synchronizedList(new ArrayList<>());
+
+ private BigDecimal shortBaseEntryPrice;
+ private BigDecimal longBaseEntryPrice;
+ private volatile boolean baseLongOpened = false;
+ private volatile boolean baseShortOpened = false;
+ private volatile boolean shortActive = false;
+ private volatile boolean longActive = false;
+
+ private volatile BigDecimal lastKlinePrice;
+ private volatile BigDecimal cumulativePnl = BigDecimal.ZERO;
+ private volatile BigDecimal unrealizedPnl = BigDecimal.ZERO;
+ private volatile BigDecimal longEntryPrice = BigDecimal.ZERO;
+ private volatile BigDecimal shortEntryPrice = BigDecimal.ZERO;
+ private volatile BigDecimal longPositionSize = BigDecimal.ZERO;
+ private volatile BigDecimal shortPositionSize = BigDecimal.ZERO;
+
+ public OkxGridTradeService(OkxConfig config, String accountName) {
+ this.config = config;
+ this.accountName = accountName;
+ this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName);
+ }
+
+ public void setWebSocketClient(WebSocketClient wsClient) {
+ this.executor.setWebSocketClient(wsClient);
+ }
+
+ public void startGrid() {
+ if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) {
+ log.warn("[{}] 策略已在运行中, state:{}", config.getContract(), state);
+ return;
+ }
+ state = StrategyState.WAITING_KLINE;
+ cumulativePnl = BigDecimal.ZERO;
+ unrealizedPnl = BigDecimal.ZERO;
+ longEntryPrice = BigDecimal.ZERO;
+ shortEntryPrice = BigDecimal.ZERO;
+ longPositionSize = BigDecimal.ZERO;
+ shortPositionSize = BigDecimal.ZERO;
+ baseLongOpened = false;
+ baseShortOpened = false;
+ longActive = false;
+ shortActive = false;
+ shortPriceQueue.clear();
+ longPriceQueue.clear();
+ log.info("[{}] 网格策略已启动", config.getContract());
+ }
+
+ public void stopGrid() {
+ state = StrategyState.STOPPED;
+ executor.cancelAllPriceTriggeredOrders();
+ executor.shutdown();
+ log.info("[{}] 策略已停止, 累计盈亏: {}", config.getContract(), cumulativePnl);
+ }
+
+ public void onKline(BigDecimal closePrice) {
+ lastKlinePrice = closePrice;
+ updateUnrealizedPnl();
+ if (state == StrategyState.STOPPED) {
+ return;
+ }
+
+ if (state == StrategyState.WAITING_KLINE) {
+ state = StrategyState.OPENING;
+ log.info("[{}] 首根K线到达,开基底仓位...", config.getContract());
+ executor.openLong(config.getQuantity(), () -> {
+ log.info("[{}] 基底多单已提交", config.getContract());
+ }, null);
+ executor.openShort(config.getQuantity(), () -> {
+ log.info("[{}] 基底空单已提交", config.getContract());
+ }, null);
+ return;
+ }
+
+ if (state != StrategyState.ACTIVE) {
+ return;
+ }
+ processShortGrid(closePrice);
+ processLongGrid(closePrice);
+ }
+
+ public void onPositionUpdate(String posSide, BigDecimal size, BigDecimal entryPrice) {
+ if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) {
+ return;
+ }
+
+ boolean hasPosition = size.compareTo(BigDecimal.ZERO) > 0;
+
+ if (OkxEnums.POSSIDE_LONG.equals(posSide)) {
+ 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;
+ }
+ } else {
+ longActive = false;
+ longPositionSize = BigDecimal.ZERO;
+ }
+ } else if (OkxEnums.POSSIDE_SHORT.equals(posSide)) {
+ 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;
+ }
+ } else {
+ shortActive = false;
+ shortPositionSize = BigDecimal.ZERO;
+ }
+ }
+ }
+
+ public void onOrderFilled(String posSide, BigDecimal fillSz, BigDecimal pnl) {
+ if (state == StrategyState.STOPPED) {
+ return;
+ }
+ cumulativePnl = cumulativePnl.add(pnl);
+ log.info("[{}] 订单成交盈亏: {}, 方向:{}, 累计:{}", config.getContract(), pnl, posSide, cumulativePnl);
+
+ if (cumulativePnl.compareTo(config.getOverallTp()) >= 0) {
+ log.info("[{}] 已达止盈目标 {}→已停止", config.getContract(), cumulativePnl);
+ state = StrategyState.STOPPED;
+ } else if (cumulativePnl.compareTo(config.getMaxLoss().negate()) <= 0) {
+ log.info("[{}] 已达亏损上限 {}→已停止", config.getContract(), cumulativePnl);
+ state = StrategyState.STOPPED;
+ }
+ }
+
+ private void tryGenerateQueues() {
+ if (baseLongOpened && baseShortOpened) {
+ generateShortQueue();
+ generateLongQueue();
+ state = StrategyState.ACTIVE;
+ log.info("[{}] 网格队列已生成, 空队首:{} → 尾:{}, 多队首:{} → 尾:{}, 已激活",
+ 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),
+ longPriceQueue.isEmpty() ? "N/A" : longPriceQueue.get(longPriceQueue.size() - 1));
+ }
+ }
+
+ private void generateShortQueue() {
+ shortPriceQueue.clear();
+ 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, fixedStep);
+ }
+
+ private void generateLongQueue() {
+ longPriceQueue.clear();
+ 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);
+ }
+ Collections.sort(longPriceQueue);
+ log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep);
+ }
+
+ /**
+ * 空仓网格处理(价格跌破空仓队列中的高价)。
+ *
+ * <h3>匹配规则</h3>
+ * 遍历空仓队列(降序),收集所有大于当前价的元素为 matched。
+ * 队列为降序排列,一旦遇 price ≤ currentPrice 即停止遍历。
+ *
+ * <h3>执行流程</h3>
+ * <ol>
+ * <li>匹配队列元素 → 为空则直接返回</li>
+ * <li>空仓队列:移除 matched 元素,尾部以固定步长(shortBasePrice × gridRate)递减补充新元素</li>
+ * <li>多仓队列:以多仓队列首元素(最小价)为种子,以多仓固定步长递减加入新元素</li>
+ * <li>保证金检查 → 安全则开空一次</li>
+ * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li>
+ * </ol>
+ *
+ * <h3>多仓队列转移过滤</h3>
+ * 新增元素若与多仓持仓均价差距小于 gridRate,则跳过该元素(避免在持仓成本附近生成无效网格线)。
+ */
+ private void processShortGrid(BigDecimal currentPrice) {
+ List<BigDecimal> matched = new ArrayList<>();
+ synchronized (shortPriceQueue) {
+ for (BigDecimal p : shortPriceQueue) {
+ if (p.compareTo(currentPrice) > 0) {
+ matched.add(p);
+ } else {
+ break;
+ }
+ }
+ }
+ log.info("[{}] 原空队列:{}", config.getContract(), shortPriceQueue);
+ if (matched.isEmpty()) {
+ log.info("[{}] 空仓队列未触发, 当前价:{}", config.getContract(), currentPrice);
+ return;
+ }
+ 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("[{}] 保证金超限,跳过空单开仓", 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) {
+ executor.openLong(config.getQuantity(), null, null);
+ log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice);
+ }
+ }
+ }
+
+ /**
+ * 多仓网格处理(价格涨破多仓队列中的低价)。
+ *
+ * <h3>匹配规则</h3>
+ * 遍历多仓队列(升序),收集所有小于当前价的元素为 matched。
+ * 队列为升序排列,一旦遇 price ≥ currentPrice 即停止遍历。
+ *
+ * <h3>执行流程</h3>
+ * <ol>
+ * <li>匹配队列元素 → 为空则直接返回</li>
+ * <li>多仓队列:移除 matched 元素,尾部以固定步长(longBasePrice × gridRate)递增补充新元素</li>
+ * <li>空仓队列:以空仓队列首元素(最高价)为种子,以空仓固定步长递增加入新元素</li>
+ * <li>保证金检查 → 安全则开多一次</li>
+ * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li>
+ * </ol>
+ *
+ * <h3>空仓队列转移过滤</h3>
+ * 新增元素若与空仓持仓均价差距小于 gridRate,则跳过该元素(避免在持仓成本附近生成无效网格线)。
+ */
+ private void processLongGrid(BigDecimal currentPrice) {
+ List<BigDecimal> matched = new ArrayList<>();
+ synchronized (longPriceQueue) {
+ for (BigDecimal p : longPriceQueue) {
+ if (p.compareTo(currentPrice) < 0) {
+ matched.add(p);
+ } else {
+ break;
+ }
+ }
+ }
+ log.info("[{}] 原多队列:{}", config.getContract(), longPriceQueue);
+ if (matched.isEmpty()) {
+ log.info("[{}] 多仓队列未触发, 当前价:{}", config.getContract(), currentPrice);
+ return;
+ }
+ 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("[{}] 保证金超限,跳过多单开仓", config.getContract());
+ } else {
+ 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) {
+ executor.openShort(config.getQuantity(), null, null);
+ log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice);
+ }
+ }
+ }
+
+ 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;
+ }
+
+ private void updateUnrealizedPnl() {
+ BigDecimal price = lastKlinePrice;
+ if (price == null || price.compareTo(BigDecimal.ZERO) == 0) {
+ return;
+ }
+ BigDecimal multiplier = config.getContractMultiplier();
+ BigDecimal longPnl = BigDecimal.ZERO;
+ BigDecimal shortPnl = BigDecimal.ZERO;
+ if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
+ longPnl = longPositionSize.multiply(multiplier).multiply(price.subtract(longEntryPrice));
+ }
+ if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
+ shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(price));
+ }
+ unrealizedPnl = longPnl.add(shortPnl);
+ log.info("[{}] 未实现盈亏: {}", config.getContract(), unrealizedPnl);
+ }
+
+ public BigDecimal getLastKlinePrice() { return lastKlinePrice; }
+ public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; }
+ public BigDecimal getCumulativePnl() { return cumulativePnl; }
+ public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
+ public StrategyState getState() { return state; }
+ public String getAccountName() { return accountName; }
+}
--
Gitblit v1.9.1