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