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 盈利回收循环策略 — 永远持有多空双向底仓,盈利兑现 + 利润反向加仓。
*
*
核心思想
*
* - 永远持有多空双向底仓
* - 盈利的一边不断兑现利润
* - 用部分利润强化亏损的一边
* - 利用市场波动不断放大利润
*
*
* 策略流程(状态机)
*
* INIT → 双开底仓 → MONITOR
* ↓
* 盈利达到50% → 平50%盈利仓 → 利润拆分 → 反向加仓 → 继续监控
* ↓
* 账户盈利5% → 全部平仓 → 重新双开
*
*
* 数学模型
*
* - ROI = 未实现盈亏 / 该方向保证金
* - 触发条件:ROI ≥ profitTriggerRatio (默认50%)
* - 平仓张数:floor(positionSize / 2)
* - 收益 A = 已实现利润
* - 保证金 B = A × reinvestRatio (默认50%)
* - 单张保证金 = contractMultiplier × entryPrice / leverage
* - 补仓张数 = max(baseQty, floor(B / 单张保证金))
*
*
* 风控
*
* - 风控1:反向仓位倍数上限 maxPositionMultiplier(默认10x)
* - 风控2:保证金占比检查(全局亏损 maxLoss 阈值)
* - 风控3:权益增长 equityRestartRatio(默认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; }
}