From 04063bcb7b9e9d8e0242c1313f54ccc1b71f0b6e Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Thu, 25 Jun 2026 22:46:56 +0800
Subject: [PATCH] fix(gateApi): 调整网格交易参数配置

---
 src/main/java/com/xcong/excoin/modules/okxApi/OkxProfitRecycleStrategy.java |  548 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 548 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/okxApi/OkxProfitRecycleStrategy.java b/src/main/java/com/xcong/excoin/modules/okxApi/OkxProfitRecycleStrategy.java
new file mode 100644
index 0000000..a30a95f
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxApi/OkxProfitRecycleStrategy.java
@@ -0,0 +1,548 @@
+package com.xcong.excoin.modules.okxApi;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.xcong.excoin.utils.dingtalk.DingTalkUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * OKX 盈利回收循环策略 — 永远持有多空双向底仓,盈利兑现 + 利润反向加仓。
+ *
+ * <h3>核心思想</h3>
+ * <ol>
+ *   <li>永远持有多空双向底仓</li>
+ *   <li>盈利的一边不断兑现利润</li>
+ *   <li>用部分利润强化亏损的一边</li>
+ *   <li>利用市场波动不断放大利润</li>
+ * </ol>
+ *
+ * <h3>策略流程(状态机)</h3>
+ * <pre>
+ *   INIT → 双开底仓 → MONITOR
+ *     ↓
+ *   盈利达到50% → 平50%盈利仓 → 利润拆分 → 反向加仓 → 继续监控
+ *     ↓
+ *   账户盈利5% → 全部平仓 → 重新双开
+ * </pre>
+ *
+ * <h3>数学模型</h3>
+ * <ul>
+ *   <li>ROI = 未实现盈亏 / 该方向保证金</li>
+ *   <li>触发条件:ROI ≥ profitTriggerRatio (默认50%)</li>
+ *   <li>平仓张数:floor(positionSize / 2)</li>
+ *   <li>收益 A = 已实现利润</li>
+ *   <li>保证金 B = A × reinvestRatio (默认50%)</li>
+ *   <li>单张保证金 = contractMultiplier × entryPrice / leverage</li>
+ *   <li>补仓张数 = max(baseQty, floor(B / 单张保证金))</li>
+ * </ul>
+ *
+ * <h3>风控</h3>
+ * <ul>
+ *   <li>风控1:反向仓位倍数上限 maxPositionMultiplier(默认10x)</li>
+ *   <li>风控2:保证金占比检查(全局亏损 maxLoss 阈值)</li>
+ *   <li>风控3:权益增长 equityRestartRatio(默认5%)全平重置</li>
+ * </ul>
+ *
+ * @author Administrator
+ */
+@Slf4j
+public class OkxProfitRecycleStrategy implements IOkxStrategy {
+
+    public enum StrategyState {
+        WAITING_KLINE,
+        OPENING,
+        ACTIVE,
+        RESTARTING,
+        STOPPED
+    }
+
+    private final OkxConfig config;
+    private final OkxTradeExecutor executor;
+
+    private volatile StrategyState state = StrategyState.WAITING_KLINE;
+
+    // ---- 价格 ----
+    private volatile BigDecimal lastPrice = BigDecimal.ZERO;
+    private volatile BigDecimal markPrice = BigDecimal.ZERO;
+
+    // ---- 持仓 ----
+    private volatile BigDecimal longPositionSize = BigDecimal.ZERO;
+    private volatile BigDecimal longEntryPrice = BigDecimal.ZERO;
+    private volatile BigDecimal shortPositionSize = BigDecimal.ZERO;
+    private volatile BigDecimal shortEntryPrice = BigDecimal.ZERO;
+
+    // ---- 盈亏 ----
+    private volatile BigDecimal cumulativePnl = BigDecimal.ZERO;
+    private volatile BigDecimal initialPrincipal = BigDecimal.ZERO;
+    private volatile boolean rebalancing = false;
+    /** 循环计数器(每完成一次"平仓+反向加仓"计数+1) */
+    private volatile int cycleCount = 0;
+    /** 重置计数器(每完成一次权益重置计数+1) */
+    private volatile int restartCount = 0;
+
+    private volatile OkxKlineWebSocketClient wsClient;
+
+    public OkxProfitRecycleStrategy(OkxConfig config) {
+        this.config = config;
+        this.executor = new OkxTradeExecutor(config);
+    }
+
+    // ==================== 生命周期 ====================
+
+    public void init() {
+        try {
+            refreshInitialPrincipal();
+            JSONObject posModeBody = new JSONObject();
+            posModeBody.put("posMode", config.getPositionMode());
+            executorPost("/api/v5/account/set-position-mode", posModeBody.toJSONString());
+            log.info("[ProfitRecycle] 持仓模式: {}", config.getPositionMode());
+
+            JSONObject levBody = new JSONObject();
+            levBody.put("instId", config.getContract());
+            levBody.put("lever", config.getLeverage());
+            levBody.put("mgnMode", config.getMarginMode());
+            executorPost("/api/v5/account/set-leverage", levBody.toJSONString());
+            log.info("[ProfitRecycle] 杠杆: {}x {}", config.getLeverage(), config.getMarginMode());
+
+            executor.cancelAllPriceTriggeredOrders();
+            closeExistingPositions();
+            log.info("[ProfitRecycle] 初始化完成, 本金: {} USDT, 合约: {}, 基础张数: {}",
+                    initialPrincipal, config.getContract(), config.getBaseQuantity());
+        } catch (Exception e) {
+            log.error("[ProfitRecycle] 初始化失败", e);
+        }
+    }
+
+    public void startStrategy() {
+        if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) {
+            log.warn("[ProfitRecycle] 策略已在运行中, state:{}", state);
+            return;
+        }
+        resetState();
+        refreshInitialPrincipal();
+        log.info("[ProfitRecycle] ✅ 策略启动 — 本金: {} USDT | 基础仓位: {}张 | 杠杆: {}x | "
+                        + "触发ROI: {}% | 再投资: {}% | 仓位上限: {}x | 重置阈值: {}%",
+                initialPrincipal, config.getBaseQuantity(), config.getLeverage(),
+                config.getProfitTriggerRatio().multiply(new BigDecimal("100")),
+                config.getReinvestRatio().multiply(new BigDecimal("100")),
+                config.getMaxPositionMultiplier(),
+                config.getEquityRestartRatio().multiply(new BigDecimal("100")));
+    }
+
+    public void stopStrategy() {
+        state = StrategyState.STOPPED;
+        executor.cancelAllPriceTriggeredOrders();
+        closeExistingPositions();
+        executor.shutdown();
+        log.info("[ProfitRecycle] ⏹ 策略停止 — 累计盈亏: {} | 循环: {} | 重置: {}",
+                cumulativePnl, cycleCount, restartCount);
+    }
+
+    @Override
+    public boolean isStrategyActive() {
+        return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE;
+    }
+
+    private void resetState() {
+        state = StrategyState.WAITING_KLINE;
+        cumulativePnl = BigDecimal.ZERO;
+        lastPrice = BigDecimal.ZERO;
+        markPrice = BigDecimal.ZERO;
+        longPositionSize = BigDecimal.ZERO;
+        longEntryPrice = BigDecimal.ZERO;
+        shortPositionSize = BigDecimal.ZERO;
+        shortEntryPrice = BigDecimal.ZERO;
+        rebalancing = false;
+        cycleCount = 0;
+    }
+
+    // ==================== WS 回调 ====================
+
+    @Override
+    public void onKline(BigDecimal closePrice) {
+        lastPrice = closePrice;
+
+        if (state == StrategyState.WAITING_KLINE) {
+            if (wsClient == null || !wsClient.areAllSubscribed()) return;
+            state = StrategyState.OPENING;
+            final String size = config.getBaseQuantity();
+            log.info("[ProfitRecycle] 🚀 首根K线到达,多空双开各{}张", size);
+            executor.openLong(size, ordId -> log.info("[ProfitRecycle] 多仓已提交 ordId:{}", ordId), null);
+            executor.openShort(size, ordId -> log.info("[ProfitRecycle] 空仓已提交 ordId:{}", ordId), null);
+            return;
+        }
+
+        if (state == StrategyState.ACTIVE) {
+            checkAndRecycle();
+        }
+    }
+
+    @Override
+    public void setMarkPrice(BigDecimal markPrice) {
+        this.markPrice = markPrice;
+    }
+
+    @Override
+    public void onPositionUpdate(String contract, Direction direction,
+                                 BigDecimal size, BigDecimal entryPrice) {
+        if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) return;
+
+        boolean hasPos = size.abs().compareTo(BigDecimal.ZERO) > 0;
+        boolean isLong = (direction == Direction.LONG);
+
+        if (state == StrategyState.OPENING || state == StrategyState.RESTARTING) {
+            if (isLong && hasPos) {
+                longPositionSize = size;
+                longEntryPrice = entryPrice;
+                log.info("[ProfitRecycle] 多仓确认: {}张 @ {}", size, entryPrice);
+                tryActivate();
+            } else if (!isLong && hasPos) {
+                shortPositionSize = size.abs();
+                shortEntryPrice = entryPrice;
+                log.info("[ProfitRecycle] 空仓确认: {}张 @ {}", size.abs(), entryPrice);
+                tryActivate();
+            }
+        }
+
+        if (state == StrategyState.ACTIVE) {
+            if (isLong) {
+                longPositionSize = hasPos ? size : BigDecimal.ZERO;
+                longEntryPrice = hasPos ? entryPrice : BigDecimal.ZERO;
+            } else {
+                shortPositionSize = hasPos ? size.abs() : BigDecimal.ZERO;
+                shortEntryPrice = hasPos ? entryPrice : BigDecimal.ZERO;
+            }
+        }
+    }
+
+    @Override
+    public void onAutoOrder(String orderId, String status, String reason,
+                            String orderType, String tradeId) {
+        log.debug("[ProfitRecycle] 条件单: id={} status={}", orderId, status);
+    }
+
+    // ==================== 核心循环 ====================
+
+    private void checkAndRecycle() {
+        if (rebalancing) return;
+
+        final BigDecimal price = resolvePrice();
+        if (price.compareTo(BigDecimal.ZERO) <= 0) return;
+
+        final BigDecimal mult = config.getContractMultiplier();
+        final BigDecimal lev = new BigDecimal(config.getLeverage());
+        final BigDecimal trigger = config.getProfitTriggerRatio();
+
+        // 检查多仓
+        if (longPositionSize.compareTo(BigDecimal.ZERO) > 0
+                && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal pnl = longPositionSize.multiply(mult).multiply(price.subtract(longEntryPrice));
+            BigDecimal margin = calcMargin(longPositionSize, longEntryPrice);
+            BigDecimal roi = margin.compareTo(BigDecimal.ZERO) > 0
+                    ? pnl.divide(margin, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+
+            if (pnl.compareTo(margin.multiply(trigger)) >= 0) {
+                log.info("[ProfitRecycle] 🔔 多头触发 | pnl={} margin={} ROI={}%", pnl, margin, roi.multiply(hundred()));
+                executeRecycle(Direction.LONG, longPositionSize, pnl, longEntryPrice, price);
+                return;
+            }
+        }
+
+        // 检查空仓
+        if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0
+                && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal pnl = shortPositionSize.multiply(mult).multiply(shortEntryPrice.subtract(price));
+            BigDecimal margin = calcMargin(shortPositionSize, shortEntryPrice);
+            BigDecimal roi = margin.compareTo(BigDecimal.ZERO) > 0
+                    ? pnl.divide(margin, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+
+            if (pnl.compareTo(margin.multiply(trigger)) >= 0) {
+                log.info("[ProfitRecycle] 🔔 空头触发 | pnl={} margin={} ROI={}%", pnl, margin, roi.multiply(hundred()));
+                executeRecycle(Direction.SHORT, shortPositionSize, pnl, shortEntryPrice, price);
+            }
+        }
+    }
+
+    /**
+     * 执行一次完整的盈利回收操作:
+     * 平50%盈利仓 → 利润A → B=A×50% → 计算反方向张数 → 开反向仓
+     */
+    private void executeRecycle(Direction profitableSide,
+                                 BigDecimal posSize, BigDecimal totalPnl,
+                                 BigDecimal entryPrice, BigDecimal price) {
+        rebalancing = true;
+        final boolean isLongProfit = (profitableSide == Direction.LONG);
+        final BigDecimal mult = config.getContractMultiplier();
+        final BigDecimal lev = new BigDecimal(config.getLeverage());
+        final String label = isLongProfit ? "多头" : "空头";
+
+        // ---- Step 1: 平50%仓位 ----
+        final int closeQty = Math.max(posSize.divide(two(), 0, RoundingMode.DOWN).intValue(), 1);
+        final BigDecimal priceDiff = isLongProfit ? price.subtract(entryPrice) : entryPrice.subtract(price);
+        final BigDecimal profitA = new BigDecimal(closeQty).multiply(mult).multiply(priceDiff);
+
+        log.info("[ProfitRecycle] ═══ {} 触发回收 ═══", label);
+        log.info("[ProfitRecycle] Step1 平仓: {}张(总{}张) → 预期利润A = {} USDT", closeQty, posSize, profitA);
+
+        // ---- Step 2: B = A × reinvestRatio ----
+        final BigDecimal reinvestRatio = config.getReinvestRatio();
+        final BigDecimal marginB = profitA.multiply(reinvestRatio);
+        log.info("[ProfitRecycle] Step2 拆分: B = {} × {} = {} USDT", profitA, reinvestRatio, marginB);
+
+        // ---- Step 3: 计算反方向张数 (单张保证金法) ----
+        // 单张保证金 = contractMultiplier × entryPrice / leverage
+        final BigDecimal perContractMargin = mult.multiply(entryPrice).divide(lev, 8, RoundingMode.HALF_UP);
+        final int rawQty = marginB.divide(perContractMargin, 0, RoundingMode.DOWN).intValue();
+        final int baseQty = Integer.parseInt(config.getBaseQuantity());
+
+        // 风控1: 反向仓位倍数上限
+        final BigDecimal oppositeSize = isLongProfit ? shortPositionSize : longPositionSize;
+        final int maxQty = baseQty * config.getMaxPositionMultiplier();
+        final int availableSlots = maxQty - oppositeSize.intValue();
+
+        int newQty = Math.max(rawQty, baseQty); // 至少开基础仓位
+        if (availableSlots <= 0) {
+            log.warn("[ProfitRecycle] 🛑 风控1触发: 反方向已达上限 {}张, 跳过加仓", maxQty);
+            rebalancing = false;
+            return;
+        }
+        if (newQty > availableSlots) {
+            newQty = availableSlots;
+            log.info("[ProfitRecycle] 风控1限制: 调整张数 {} → {}", Math.max(rawQty, baseQty), newQty);
+        }
+        final String openSize = String.valueOf(newQty);
+
+        log.info("[ProfitRecycle] Step3 补仓: 单张保证金={} | raw={} | base={} | max={} | final={}张",
+                perContractMargin, rawQty, baseQty, maxQty, openSize);
+
+        // ---- Step 4: 执行平仓 → 回调中开反方向 ----
+        final String closePosSide = isLongProfit ? "long" : "short";
+
+        executor.marketClosePosition(String.valueOf(closeQty), closePosSide,
+                () -> {
+                    cumulativePnl = cumulativePnl.add(profitA);
+                    cycleCount++;
+                    updatePositionAfterClose(profitableSide, closeQty);
+
+                    log.info("[ProfitRecycle] ✅ 平仓完成 | 累计盈亏: {} | 第{}次循环",
+                            cumulativePnl, cycleCount);
+
+                    // 开反方向
+                    if (!isLongProfit) {
+                        executor.openLong(openSize,
+                                ordId -> log.info("[ProfitRecycle] 反方向开多{}张 ok ordId:{}", openSize, ordId),
+                                () -> log.error("[ProfitRecycle] ❌ 反方向开多{}张失败", openSize));
+                    } else {
+                        executor.openShort(openSize,
+                                ordId -> log.info("[ProfitRecycle] 反方向开空{}张 ok ordId:{}", openSize, ordId),
+                                () -> log.error("[ProfitRecycle] ❌ 反方向开空{}张失败", openSize));
+                    }
+
+                    // 当前仓位状态
+                    log.info("[ProfitRecycle] 当前仓位: LONG={}张 SHORT={}张",
+                            longPositionSize, shortPositionSize);
+
+                    // 风控检查
+                    if (checkLossStop()) return;
+                    if (checkEquityRestart()) return;
+                    rebalancing = false;
+                },
+                () -> {
+                    log.error("[ProfitRecycle] ❌ 平仓失败 direction:{}", closePosSide);
+                    rebalancing = false;
+                });
+    }
+
+    // ==================== 风控 ====================
+
+    /** 风控2: 全局亏损超限 → 停止策略 */
+    private boolean checkLossStop() {
+        BigDecimal totalPnl = cumulativePnl.add(calcUnrealizedPnl());
+        if (config.getMaxLoss() != null && config.getMaxLoss().compareTo(BigDecimal.ZERO) > 0
+                && totalPnl.compareTo(config.getMaxLoss().negate()) <= 0) {
+            String msg = StrUtil.format("[ProfitRecycle] 🛑 亏损超限! 合计:{} 已实现:{}",
+                    totalPnl, cumulativePnl);
+            log.warn(msg);
+            DingTalkUtils.getDefault().sendActionCard("风险提醒", msg, config.getApiKey(), "");
+            stopStrategy();
+            return true;
+        }
+        return false;
+    }
+
+    /** 风控3: 账户权益增长≥5% → 全平重置 */
+    private boolean checkEquityRestart() {
+        BigDecimal equity = fetchCurrentEquity();
+        if (equity.compareTo(BigDecimal.ZERO) <= 0) return false;
+
+        BigDecimal threshold = initialPrincipal.multiply(
+                BigDecimal.ONE.add(config.getEquityRestartRatio()));
+        if (equity.compareTo(threshold) >= 0) {
+            restartCount++;
+            log.info("[ProfitRecycle] 🔄 权益重置触发! 当前权益: {} ≥ 目标: {} (初始: {} + {}%) | 第{}次重置",
+                    equity, threshold, initialPrincipal,
+                    config.getEquityRestartRatio().multiply(hundred()), restartCount);
+            executeRestart();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 执行权益重置:全平 → 取消所有订单 → 刷新本金 → 重新双开底仓
+     */
+    private void executeRestart() {
+        rebalancing = true;
+        state = StrategyState.RESTARTING;
+        log.info("[ProfitRecycle] 开始重置: 平所有仓位...");
+
+        executor.cancelAllPriceTriggeredOrders();
+        // 全平多空
+        if (longPositionSize.compareTo(BigDecimal.ZERO) > 0) {
+            executor.marketClosePosition(longPositionSize.toPlainString(), "long", null, null);
+        }
+        if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0) {
+            executor.marketClosePosition(shortPositionSize.toPlainString(), "short", null, null);
+        }
+
+        // 延迟后重新开仓
+        executor.submitTask(() -> {
+            try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+            refreshInitialPrincipal();
+            resetState();
+            state = StrategyState.OPENING;
+            final String size = config.getBaseQuantity();
+            log.info("[ProfitRecycle] 🔄 重置开仓: 多空各{}张, 新本金: {} USDT", size, initialPrincipal);
+            executor.openLong(size, ordId -> log.info("[ProfitRecycle] 重置多仓 ordId:{}", ordId), null);
+            executor.openShort(size, ordId -> log.info("[ProfitRecycle] 重置空仓 ordId:{}", ordId), null);
+            rebalancing = false;
+            // 等待 WS 仓位确认后 tryActivate()
+        });
+    }
+
+    // ==================== 辅助 ====================
+
+    private BigDecimal calcMargin(BigDecimal size, BigDecimal entry) {
+        return size.multiply(config.getContractMultiplier())
+                .multiply(entry)
+                .divide(new BigDecimal(config.getLeverage()), 8, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal calcUnrealizedPnl() {
+        BigDecimal price = resolvePrice();
+        if (price.compareTo(BigDecimal.ZERO) <= 0) return BigDecimal.ZERO;
+        BigDecimal m = config.getContractMultiplier();
+        BigDecimal lp = BigDecimal.ZERO, sp = BigDecimal.ZERO;
+        if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0)
+            lp = longPositionSize.multiply(m).multiply(price.subtract(longEntryPrice));
+        if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0)
+            sp = shortPositionSize.multiply(m).multiply(shortEntryPrice.subtract(price));
+        return lp.add(sp);
+    }
+
+    private BigDecimal resolvePrice() {
+        return (config.getUnrealizedPnlPriceMode() == OkxConfig.PnLPriceMode.MARK_PRICE
+                && markPrice.compareTo(BigDecimal.ZERO) > 0) ? markPrice : lastPrice;
+    }
+
+    private BigDecimal fetchCurrentEquity() {
+        try {
+            JSONObject account = executorGet("/api/v5/account/balance");
+            JSONArray details = account.getJSONArray("data").getJSONObject(0).getJSONArray("details");
+            if (details != null) for (int i = 0; i < details.size(); i++) {
+                if ("USDT".equals(details.getJSONObject(i).getString("ccy")))
+                    return details.getJSONObject(i).getBigDecimal("eq");
+            }
+        } catch (Exception e) { log.warn("[ProfitRecycle] 获取权益失败", e); }
+        return BigDecimal.ZERO;
+    }
+
+    private void refreshInitialPrincipal() {
+        BigDecimal eq = fetchCurrentEquity();
+        if (eq.compareTo(BigDecimal.ZERO) > 0) this.initialPrincipal = eq;
+    }
+
+    private void updatePositionAfterClose(Direction side, int closed) {
+        if (side == Direction.LONG) {
+            longPositionSize = longPositionSize.subtract(new BigDecimal(closed));
+            if (longPositionSize.compareTo(BigDecimal.ZERO) <= 0) {
+                longPositionSize = BigDecimal.ZERO;
+                longEntryPrice = BigDecimal.ZERO;
+            }
+        } else {
+            shortPositionSize = shortPositionSize.subtract(new BigDecimal(closed));
+            if (shortPositionSize.compareTo(BigDecimal.ZERO) <= 0) {
+                shortPositionSize = BigDecimal.ZERO;
+                shortEntryPrice = BigDecimal.ZERO;
+            }
+        }
+    }
+
+    private void tryActivate() {
+        if ((state == StrategyState.OPENING || state == StrategyState.RESTARTING)
+                && longPositionSize.compareTo(BigDecimal.ZERO) > 0
+                && shortPositionSize.compareTo(BigDecimal.ZERO) > 0) {
+            state = StrategyState.ACTIVE;
+            log.info("[ProfitRecycle] ═══ 策略激活 ═══");
+            log.info("[ProfitRecycle] LONG: {}张 @ {} | SHORT: {}张 @ {} | "
+                            + "多保证金: {} | 空保证金: {}",
+                    longPositionSize, longEntryPrice, shortPositionSize, shortEntryPrice,
+                    calcMargin(longPositionSize, longEntryPrice),
+                    calcMargin(shortPositionSize, shortEntryPrice));
+        }
+    }
+
+    private void closeExistingPositions() {
+        try {
+            JSONObject resp = executorGet("/api/v5/account/positions?instType=SWAP");
+            JSONArray data = resp.getJSONArray("data");
+            if (data == null || data.isEmpty()) return;
+            for (int i = 0; i < data.size(); i++) {
+                JSONObject pos = data.getJSONObject(i);
+                if (!config.getContract().equals(pos.getString("instId"))) continue;
+                String posVal = pos.getString("pos");
+                if (posVal == null || "0".equals(posVal)) continue;
+                String posSide = pos.getString("posSide");
+                String side = "long".equals(posSide) ? "sell" : "buy";
+                JSONObject body = new JSONObject();
+                body.put("instId", config.getContract());
+                body.put("tdMode", "cross");
+                body.put("side", side);
+                body.put("posSide", posSide);
+                body.put("ordType", "market");
+                body.put("sz", posVal);
+                executorPost("/api/v5/trade/order", body.toJSONString());
+                log.info("[ProfitRecycle] 平旧仓 posSide:{} sz:{}", posSide, posVal);
+            }
+        } catch (Exception e) { log.warn("[ProfitRecycle] 平仓异常", e); }
+    }
+
+    // ==================== REST 快捷 ====================
+
+    private JSONObject executorGet(String path) throws Exception { return executor.okGet(path); }
+
+    private JSONObject executorPost(String path, String body) throws Exception { return executor.okPost(path, body); }
+
+    private static BigDecimal two() { return new BigDecimal("2"); }
+
+    private static BigDecimal hundred() { return new BigDecimal("100"); }
+
+    // ==================== Getters ====================
+
+    public BigDecimal getLastKlinePrice() { return lastPrice; }
+    public BigDecimal getCumulativePnl() { return cumulativePnl; }
+    public StrategyState getState() { return state; }
+    public int getCycleCount() { return cycleCount; }
+    public int getRestartCount() { return restartCount; }
+    public BigDecimal getInitialPrincipal() { return initialPrincipal; }
+    public BigDecimal getLongPositionSize() { return longPositionSize; }
+    public BigDecimal getShortPositionSize() { return shortPositionSize; }
+    public BigDecimal getLongEntryPrice() { return longEntryPrice; }
+    public BigDecimal getShortEntryPrice() { return shortEntryPrice; }
+    @Override
+    public void setWsClient(OkxKlineWebSocketClient wsClient) { this.wsClient = wsClient; }
+}

--
Gitblit v1.9.1