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 |  171 ++++++++++++++++++++++++++------------------------------
 1 files changed, 80 insertions(+), 91 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
index 75b1296..8de17ad 100644
--- a/src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxGridTradeService.java
@@ -8,6 +8,7 @@
 import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 @Slf4j
@@ -17,9 +18,7 @@
         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;
 
@@ -45,6 +44,7 @@
 
     public OkxGridTradeService(OkxConfig config, String accountName) {
         this.config = config;
+        this.accountName = accountName;
         this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName);
     }
 
@@ -129,7 +129,7 @@
                         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 {
@@ -155,7 +155,7 @@
                         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 {
@@ -200,22 +200,26 @@
 
     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);
     }
 
     /**
@@ -228,8 +232,8 @@
      * <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>
@@ -255,38 +259,12 @@
         }
         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());
@@ -296,7 +274,7 @@
                     && 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);
             }
@@ -313,8 +291,8 @@
      * <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>
@@ -340,38 +318,12 @@
         }
         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());
@@ -380,7 +332,7 @@
             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);
@@ -388,14 +340,51 @@
         }
     }
 
-    /**
-     * 保证金安全阀检查。
-     *
-     * <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;
     }
@@ -423,5 +412,5 @@
     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; }
 }

--
Gitblit v1.9.1