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 盈利回收循环策略 — 永远持有多空双向底仓,盈利兑现 + 利润反向加仓。 * *

核心思想

*
    *
  1. 永远持有多空双向底仓
  2. *
  3. 盈利的一边不断兑现利润
  4. *
  5. 用部分利润强化亏损的一边
  6. *
  7. 利用市场波动不断放大利润
  8. *
* *

策略流程(状态机)

*
 *   INIT → 双开底仓 → MONITOR
 *     ↓
 *   盈利达到50% → 平50%盈利仓 → 利润拆分 → 反向加仓 → 继续监控
 *     ↓
 *   账户盈利5% → 全部平仓 → 重新双开
 * 
* *

数学模型

* * *

风控

* * * @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; } }