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