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