package com.xcong.excoin.modules.okxNewPrice;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod;
import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants;
import com.xcong.excoin.utils.dingtalk.DingTalkUtils;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* OKX 网格交易策略引擎 — 多空对冲网格。
*
*
策略原理
* 对齐 Gate 版本逻辑:以空仓基底入场价为价格基准,向上/向下各生成价格网格队列。
* 价格触发网格层级时挂条件单,成交后自动挂止盈单。
*
* 完整生命周期
*
* startGrid() → WAITING_KLINE
* ↓
* onKline(首根K线) → OPENING → 异步市价双开基底(开多+开空)
* ↓
* onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened
* ↓
* tryGenerateQueues() → generateShortQueue()+generateLongQueue()+updateGridElements()
* → 挂基座止损单 + state=ACTIVE
* ↓
* ACTIVE 状态每根K线:
* processShortGrid() + processLongGrid() → 匹配队列 → 挂条件单 → 订单成交后挂止盈
* ↓
* onPositionClose() → cumulativePnl 累加 → 检查停止条件
*
*
* OKX vs Gate 差异
*
* - OKX 使用 side(buy/sell)+posSide(long/short) 下单,而非 size 正负
* - 条件单使用 algo order (ordType=conditional)
* - 止盈止损通过 algo order 实现
*
*
* @author Administrator
*/
@Slf4j
public class OkxGridTradeService {
public enum StrategyState {
WAITING_KLINE, OPENING, ACTIVE, STOPPED
}
private final OkxConfig config;
private final OkxTradeExecutor executor;
private final OKXAccount okxAccount;
private volatile StrategyState state = StrategyState.WAITING_KLINE;
/** 空仓价格队列,降序排列(大→小) */
private final List shortPriceQueue = Collections.synchronizedList(new ArrayList<>());
/** 多仓价格队列,升序排列(小→大) */
private final List longPriceQueue = Collections.synchronizedList(new ArrayList<>());
/** 当前多仓条件单映射:algoId → 止盈价格 */
private final Map currentLongOrderIds = Collections.synchronizedMap(new LinkedHashMap<>());
/** 当前空仓条件单映射:algoId → 止盈价格 */
private final Map currentShortOrderIds = Collections.synchronizedMap(new LinkedHashMap<>());
/** 基底空头入场价 */
private BigDecimal shortBaseEntryPrice;
/** 基底多头入场价 */
private BigDecimal longBaseEntryPrice;
/** 基底多头是否已开 */
private volatile boolean baseLongOpened = false;
/** 基底空头是否已开 */
private volatile boolean baseShortOpened = false;
private volatile boolean shortActive = false;
private volatile boolean longActive = false;
private volatile BigDecimal lastKlinePrice;
private volatile BigDecimal cumulativePnl = BigDecimal.ZERO;
private volatile BigDecimal unrealizedPnl = BigDecimal.ZERO;
private volatile BigDecimal longEntryPrice = BigDecimal.ZERO;
private volatile BigDecimal shortEntryPrice = BigDecimal.ZERO;
private volatile BigDecimal longPositionSize = BigDecimal.ZERO;
private volatile BigDecimal shortPositionSize = BigDecimal.ZERO;
private volatile BigDecimal initialPrincipal = BigDecimal.ZERO;
public OkxGridTradeService(OkxConfig config, OKXAccount okxAccount) {
this.config = config;
this.okxAccount = okxAccount;
this.executor = new OkxTradeExecutor(okxAccount, config.getInstId(), config.getTdMode());
}
// ---- 初始化 ----
/**
* 初始化策略环境:获取账户 → 清旧条件单 → 平已有仓位 → 设杠杆。
*/
public void init() {
try {
// 1. 查询账户获取初始本金(仅取 USDT 合约账户余额)
String balanceResp = executor.getBalance();
if (balanceResp != null) {
JSONObject json = JSON.parseObject(balanceResp);
if ("0".equals(json.getString("code"))) {
JSONArray data = json.getJSONArray("data");
if (data != null && !data.isEmpty()) {
JSONObject accountData = data.getJSONObject(0);
JSONArray details = accountData.getJSONArray("details");
if (details != null) {
for (int i = 0; i < details.size(); i++) {
JSONObject detail = details.getJSONObject(i);
if ("USDT".equals(detail.getString("ccy"))) {
String eq = detail.getString("eq");
if (eq != null) {
this.initialPrincipal = new BigDecimal(eq);
log.info("[OKX] 初始本金(USDT合约): {} USDT", initialPrincipal);
}
break;
}
}
}
}
}
}
// 2. 清除旧条件单
executor.cancelAllAlgoOrders();
log.info("[OKX] 旧条件单已清除");
// 3. 平掉已有仓位
closeExistingPositions();
// 4. 设置杠杆
executor.setLeverage(config.getLeverage());
log.info("[OKX] 初始化完成, 合约:{}, 杠杆:{}x", config.getInstId(), config.getLeverage());
} catch (Exception e) {
log.error("[OKX] 初始化失败", e);
}
}
/**
* 平掉当前合约的所有已有仓位。
*/
private void closeExistingPositions() {
try {
String posResp = executor.getPositions();
if (posResp == null) return;
JSONObject json = JSON.parseObject(posResp);
if (!"0".equals(json.getString("code"))) return;
JSONArray data = json.getJSONArray("data");
if (data == null || data.isEmpty()) {
log.info("[OKX] 无已有仓位");
return;
}
for (int i = 0; i < data.size(); i++) {
JSONObject pos = data.getJSONObject(i);
String instId = pos.getString("instId");
if (!config.getInstId().equals(instId)) continue;
String posSide = pos.getString("posSide");
String posSz = pos.getString("pos");
if (posSz == null || "0".equals(posSz)) continue;
String side = "long".equals(posSide) ? "sell" : "buy";
executor.marketClose(side, posSide, posSz);
log.info("[OKX] 平已有仓位, posSide:{}, sz:{}, side:{}", posSide, posSz, side);
}
} catch (Exception e) {
log.warn("[OKX] 平仓位异常", e);
}
}
// ---- 启动/停止 ----
public void startGrid() {
if (state != StrategyState.WAITING_KLINE && state != StrategyState.STOPPED) {
log.warn("[OKX] 策略已在运行中, state:{}", state);
return;
}
state = StrategyState.WAITING_KLINE;
cumulativePnl = BigDecimal.ZERO;
unrealizedPnl = BigDecimal.ZERO;
longEntryPrice = BigDecimal.ZERO;
shortEntryPrice = BigDecimal.ZERO;
longPositionSize = BigDecimal.ZERO;
shortPositionSize = BigDecimal.ZERO;
baseLongOpened = false;
baseShortOpened = false;
longActive = false;
shortActive = false;
shortPriceQueue.clear();
longPriceQueue.clear();
currentLongOrderIds.clear();
currentShortOrderIds.clear();
// 每次重启重新获取当前本金,确保盈亏对比基准正确
refreshInitialPrincipal();
log.info("[OKX] 网格策略已启动, 当前本金: {} USDT", initialPrincipal);
}
/**
* 重新获取当前账户权益作为初始本金。
*/
private void refreshInitialPrincipal() {
try {
String balanceResp = executor.getBalance();
if (balanceResp != null) {
JSONObject json = JSON.parseObject(balanceResp);
if ("0".equals(json.getString("code"))) {
JSONArray data = json.getJSONArray("data");
if (data != null && !data.isEmpty()) {
JSONObject accountData = data.getJSONObject(0);
JSONArray details = accountData.getJSONArray("details");
if (details != null) {
for (int i = 0; i < details.size(); i++) {
JSONObject detail = details.getJSONObject(i);
if ("USDT".equals(detail.getString("ccy"))) {
String eq = detail.getString("eq");
if (eq != null) {
this.initialPrincipal = new BigDecimal(eq);
}
break;
}
}
}
}
}
}
} catch (Exception e) {
log.warn("[OKX] 获取初始化本金失败,使用旧值: {}", initialPrincipal);
}
}
public void stopGrid() {
state = StrategyState.STOPPED;
executor.cancelAllAlgoOrders();
executor.shutdown();
log.info("[OKX] 策略已停止, 累计盈亏: {}", cumulativePnl);
}
// ---- K线回调 ----
public void onKline(BigDecimal closePrice) {
lastKlinePrice = closePrice;
updateUnrealizedPnl();
if (state == StrategyState.STOPPED) {
executor.cancelAllAlgoOrders();
closeExistingPositions();
BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl);
log.info("[OKX] 已实现:{}, 未实现:{}, 合计:{}", cumulativePnl, unrealizedPnl, totalPnl);
startGrid();
return;
}
if (state == StrategyState.WAITING_KLINE) {
state = StrategyState.OPENING;
log.info("[OKX] 首根K线到达,开基底仓位 多空各{}张...", config.getBaseQuantity());
executor.openLong(config.getBaseQuantity(), (orderId) -> {
OkxTraderParam baseLongTp = OkxTraderParam.builder().entryOrderId(orderId).build();
config.setBaseLongTraderParam(baseLongTp);
}, null);
executor.openShort(config.getBaseQuantity(), (orderId) -> {
OkxTraderParam baseShortTp = OkxTraderParam.builder().entryOrderId(orderId).build();
config.setBaseShortTraderParam(baseShortTp);
}, null);
return;
}
if (state != StrategyState.ACTIVE) {
return;
}
checkProfitAndReset();
}
// ---- 仓位推送回调 ----
public void onPositionUpdate(String instId, String posSide, BigDecimal posSize, BigDecimal avgPx) {
if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) {
return;
}
boolean hasPosition = posSize.compareTo(BigDecimal.ZERO) > 0;
if ("long".equals(posSide)) {
if (hasPosition) {
longActive = true;
longEntryPrice = avgPx;
if (!baseLongOpened) {
longPositionSize = posSize;
longBaseEntryPrice = avgPx;
baseLongOpened = true;
log.info("[OKX] 基底多成交价: {}", longBaseEntryPrice);
tryGenerateQueues();
} else {
longPositionSize = posSize;
}
} else {
if (longActive && state == StrategyState.ACTIVE) {
log.info("[OKX] 多仓持仓归零,重置策略");
handlePositionZeroAndReset("多仓");
}
longActive = false;
longPositionSize = BigDecimal.ZERO;
}
} else if ("short".equals(posSide)) {
if (hasPosition) {
shortActive = true;
shortEntryPrice = avgPx;
if (!baseShortOpened) {
shortPositionSize = posSize;
shortBaseEntryPrice = avgPx;
baseShortOpened = true;
log.info("[OKX] 基底空成交价: {}", shortBaseEntryPrice);
tryGenerateQueues();
} else {
shortPositionSize = posSize;
}
} else {
if (shortActive && state == StrategyState.ACTIVE) {
log.info("[OKX] 空仓持仓归零,重置策略");
handlePositionZeroAndReset("空仓");
}
shortActive = false;
shortPositionSize = BigDecimal.ZERO;
}
}
}
// ---- 平仓推送回调 ----
public void onPositionClose(String side, BigDecimal pnl) {
if (state == StrategyState.STOPPED) {
return;
}
cumulativePnl = cumulativePnl.add(pnl);
updateUnrealizedPnl();
BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl);
log.info("[OKX] 已实现:{}, 未实现:{}, 合计:{}", cumulativePnl, unrealizedPnl, totalPnl);
if (totalPnl.compareTo(config.getMaxLoss().negate()) <= 0) {
String logMsg = StrUtil.format("[OKX] 已达亏损风险值(合计{}), 已实现:{}, 未实现:{}",
totalPnl, cumulativePnl, unrealizedPnl);
log.info(logMsg);
DingTalkUtils.getDefault().sendActionCard("风险提醒", logMsg, config.getApiKey(), "");
}
}
// ---- 订单/条件单推送回调 ----
public void onOrderUpdate(String algoId, String state, String ordType) {
if (!"filled".equals(state) && !"canceled".equals(state)) {
return;
}
// 匹配止损单
OkxGridElement byLongStopLoss = OkxGridElement.findByLongStopLossOrderId(algoId);
if (byLongStopLoss != null) {
handleLongStopLossTriggered(byLongStopLoss);
return;
}
OkxGridElement byShortStopLoss = OkxGridElement.findByShortStopLossOrderId(algoId);
if (byShortStopLoss != null) {
handleShortStopLossTriggered(byShortStopLoss);
return;
}
// 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 + 挂止盈单
OkxGridElement shortGridElement = OkxGridElement.findByShortOrderId(algoId);
if (shortGridElement != null && shortGridElement.isHasShortOrder()) {
int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity());
shortEntryTraderIdParam(shortGridElement, null, false);
extendShortStopLoss(filledQty);
log.info("[OKX] 空单成交 gridId:{}, qty:{}, 追挂止损", shortGridElement.getId(), filledQty);
return;
}
OkxGridElement longGridElement = OkxGridElement.findByLongOrderId(algoId);
if (longGridElement != null && longGridElement.isHasLongOrder()) {
int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity());
longEntryTraderIdParam(longGridElement, null, false);
extendLongStopLoss(filledQty);
log.info("[OKX] 多单成交 gridId:{}, qty:{}, 追挂止损", longGridElement.getId(), filledQty);
return;
}
}
// ---- 网格队列处理 ----
private void tryGenerateQueues() {
if (baseLongOpened && baseShortOpened) {
generateShortQueue();
generateLongQueue();
updateGridElements();
// 标记基座挂单
OkxGridElement baseGridElement = OkxGridElement.findById(0);
OkxTraderParam baseLongTp = config.getBaseLongTraderParam();
baseGridElement.setLongOrderId(baseLongTp.getEntryOrderId());
baseGridElement.setHasLongOrder(true);
OkxTraderParam baseShortTp = config.getBaseShortTraderParam();
baseGridElement.setShortOrderId(baseShortTp.getEntryOrderId());
baseGridElement.setHasShortOrder(true);
// 挂多仓止损 (id=-2 到 -11),每格 quantity 张
for (int id = -2; id >= -11; id--) {
OkxGridElement elem = OkxGridElement.findById(id);
if (elem == null) continue;
BigDecimal triggerPrice = elem.getGridPrice();
int finalId = id;
executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
profitId -> {
elem.setLongStopLossOrderId(profitId);
OkxGridElement.refreshIndices();
log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
finalId, triggerPrice, config.getQuantity(), profitId);
});
}
// 挂空仓止损 (id=2 到 11),每格 quantity 张
for (int id = 2; id <= 11; id++) {
OkxGridElement elem = OkxGridElement.findById(id);
if (elem == null) continue;
BigDecimal triggerPrice = elem.getGridPrice();
int finalId = id;
executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
profitId -> {
elem.setShortStopLossOrderId(profitId);
OkxGridElement.refreshIndices();
log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, qty:{}, stopLossId:{}",
finalId, triggerPrice, config.getQuantity(), profitId);
});
}
log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11");
state = StrategyState.ACTIVE;
}
}
private void generateShortQueue() {
shortPriceQueue.clear();
int prec = config.getPriceScale();
BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP);
config.setStep(step);
BigDecimal elem = shortBaseEntryPrice.subtract(step).setScale(prec, RoundingMode.HALF_UP);
for (int i = 0; i < config.getGridQueueSize(); i++) {
shortPriceQueue.add(elem);
elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP);
if (elem.compareTo(BigDecimal.ZERO) <= 0) break;
}
shortPriceQueue.sort((a, b) -> b.compareTo(a));
log.info("[OKX] 空队列:{}", shortPriceQueue);
}
private void generateLongQueue() {
longPriceQueue.clear();
int prec = config.getPriceScale();
BigDecimal step = config.getStep();
BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP);
for (int i = 0; i < config.getGridQueueSize(); i++) {
longPriceQueue.add(elem);
elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP);
}
longPriceQueue.sort(BigDecimal::compareTo);
log.info("[OKX] 多队列:{}", longPriceQueue);
}
private void updateGridElements() {
List elements = new ArrayList<>();
int shortSize = shortPriceQueue.size();
int longSize = longPriceQueue.size();
int prec = config.getPriceScale();
BigDecimal step = config.getStep();
String qty = config.getQuantity();
// 空仓队列: id=-1, -2, ...
for (int i = 0; i < shortSize; i++) {
int id = -(i + 1);
Integer upId = (i == 0) ? 0 : id + 1;
Integer downId = (i == shortSize - 1) ? null : id - 1;
BigDecimal price = shortPriceQueue.get(i);
OkxTraderParam longParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.LONG)
.entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
OkxTraderParam shortParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.SHORT)
.entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId)
.longTraderParam(longParam).shortTraderParam(shortParam).build());
}
// 位置 0: 基底价格
{
BigDecimal price = shortBaseEntryPrice;
OkxTraderParam longParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.LONG)
.entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
OkxTraderParam shortParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.SHORT)
.entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
elements.add(OkxGridElement.builder().id(0).gridPrice(price)
.upId(shortSize > 0 ? 1 : null).downId(longSize > 0 ? -1 : null)
.longTraderParam(longParam).shortTraderParam(shortParam).build());
}
// 多仓队列: id=1, 2, ...
for (int i = 0; i < longSize; i++) {
int id = i + 1;
Integer downId = (i == 0) ? 0 : id - 1;
Integer upId = (i == longSize - 1) ? null : id + 1;
BigDecimal price = longPriceQueue.get(i);
OkxTraderParam longParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.LONG)
.entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
OkxTraderParam shortParam = OkxTraderParam.builder()
.direction(OkxTraderParam.Direction.SHORT)
.entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build();
elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId)
.longTraderParam(longParam).shortTraderParam(shortParam).build());
}
config.setGridElements(elements);
log.info("[OKX] 网格元素列表已构建, 共{}个元素", elements.size());
}
// ---- 止损触发处理 ----
/**
* 多仓止损触发处理(Gate 模式逐步缩进)。
* 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
* 若 N>2,先取消上一步的旧挂单。
*/
private void handleLongStopLossTriggered(OkxGridElement gridElement) {
int gridId = gridElement.getId();
int N = Math.abs(gridId);
gridElement.setLongStopLossOrderId(null);
log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId);
int newEntryGridId = -(N - 1);
OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
if (newEntryGrid == null) {
OkxGridElement.refreshIndices();
log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
return;
}
if (N > 2) {
int cancelGridId = -(N - 2);
OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
if (cancelGrid != null && cancelGrid.isHasLongOrder()) {
executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> {
longEntryTraderIdParam(cancelGrid, null, false);
log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId);
});
}
}
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs();
// 精度补偿:步长被setScale截断,priceDiff/step可能产生1.99998→Down截断为1的问题
BigDecimal epsilon = new BigDecimal("0.00000001");
int count = priceDiff.add(epsilon).divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
count = Math.max(1, count);
int entryQty = count * Integer.parseInt(config.getQuantity());
String size = String.valueOf(entryQty);
log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(价差:{},步长:{},count:{},qty:{})",
gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity());
newEntryGrid.getLongTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size);
}
/**
* 空仓止损触发处理(Gate 模式逐步缩进)。
* 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
* 若 N>2,先取消上一步的旧挂单。
*/
private void handleShortStopLossTriggered(OkxGridElement gridElement) {
int gridId = gridElement.getId();
int N = gridId;
gridElement.setShortStopLossOrderId(null);
log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId);
int newEntryGridId = N - 1;
OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId);
if (newEntryGrid == null) {
OkxGridElement.refreshIndices();
log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId);
return;
}
if (N > 2) {
int cancelGridId = N - 2;
OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId);
if (cancelGrid != null && cancelGrid.isHasShortOrder()) {
executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> {
shortEntryTraderIdParam(cancelGrid, null, false);
log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId);
});
}
}
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs();
// 精度补偿:步长被setScale截断,priceDiff/step可能产生1.99998→Down截断为1的问题
BigDecimal epsilon = new BigDecimal("0.00000001");
int count = priceDiff.add(epsilon).divide(config.getStep(), 0, RoundingMode.DOWN).intValue();
count = Math.max(1, count);
int entryQty = count * Integer.parseInt(config.getQuantity());
String size = String.valueOf(entryQty);
log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(价差:{},步长:{},count:{},qty:{})",
gridId, newEntryGridId, entryQty, priceDiff, config.getStep(), count, config.getQuantity());
newEntryGrid.getShortTraderParam().setQuantity(size);
placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size);
}
private void extendLongStopLoss(int filledQty) {
// filledQty 为本次新增止损张数 = count * quantity, 需要按 quantity 为粒度拆分为 count 个止损单
int qty = Integer.parseInt(config.getQuantity());
int stopLossCount = filledQty / qty;
int furthestSlId = 0;
for (OkxGridElement e : config.getGridElements()) {
if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) {
furthestSlId = e.getId();
}
}
if (furthestSlId == 0) furthestSlId = -11;
log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
for (int i = 0; i < stopLossCount; i++) {
int newSlId = furthestSlId - i - 1;
OkxGridElement elem = OkxGridElement.findById(newSlId);
if (elem == null) continue;
BigDecimal triggerPrice = elem.getGridPrice();
int finalSlId = newSlId;
executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", config.getQuantity(),
profitId -> {
elem.setLongStopLossOrderId(profitId);
OkxGridElement.refreshIndices();
log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
});
}
}
private void extendShortStopLoss(int filledQty) {
int qty = Integer.parseInt(config.getQuantity());
int stopLossCount = filledQty / qty;
int furthestSlId = 0;
for (OkxGridElement e : config.getGridElements()) {
if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) {
furthestSlId = e.getId();
}
}
if (furthestSlId == 0) furthestSlId = 11;
log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}单, 每单{}张", furthestSlId, stopLossCount, qty);
for (int i = 0; i < stopLossCount; i++) {
int newSlId = furthestSlId + i + 1;
OkxGridElement elem = OkxGridElement.findById(newSlId);
if (elem == null) continue;
BigDecimal triggerPrice = elem.getGridPrice();
int finalSlId = newSlId;
executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", config.getQuantity(),
profitId -> {
elem.setShortStopLossOrderId(profitId);
OkxGridElement.refreshIndices();
log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId);
});
}
}
// ---- 辅助方法 ----
private void longTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) {
OkxTraderParam tp = baseElement.getLongTraderParam();
tp.setTakeProfitOrderId(profitId);
tp.setTakeProfitPlaced(flag);
baseElement.setLongTakeProfitOrderId(profitId);
OkxGridElement.refreshIndices();
}
private void shortTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) {
OkxTraderParam tp = baseElement.getShortTraderParam();
tp.setTakeProfitOrderId(profitId);
tp.setTakeProfitPlaced(flag);
baseElement.setShortTakeProfitOrderId(profitId);
OkxGridElement.refreshIndices();
}
private void longEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) {
OkxTraderParam tp = baseElement.getLongTraderParam();
tp.setEntryOrderId(entryId);
tp.setEntryOrderPlaced(flag);
baseElement.setHasLongOrder(flag);
baseElement.setLongOrderId(entryId);
OkxGridElement.refreshIndices();
}
private void shortEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) {
OkxTraderParam tp = baseElement.getShortTraderParam();
tp.setEntryOrderId(entryId);
tp.setEntryOrderPlaced(flag);
baseElement.setHasShortOrder(flag);
baseElement.setShortOrderId(entryId);
OkxGridElement.refreshIndices();
}
private void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong,
BigDecimal triggerPrice, String size) {
if (isLong) {
gridElement.setHasLongOrder(true);
} else {
gridElement.setHasShortOrder(true);
}
String side = isLong ? "buy" : "sell";
String posSide = isLong ? "long" : "short";
executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, size,
orderId -> {
if (isLong) {
longEntryTraderIdParam(gridElement, orderId, true);
} else {
shortEntryTraderIdParam(gridElement, orderId, true);
}
},
() -> {
if (isLong) {
gridElement.setHasLongOrder(false);
gridElement.setLongOrderId(null);
} else {
gridElement.setHasShortOrder(false);
gridElement.setShortOrderId(null);
}
OkxGridElement.refreshIndices();
log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong);
}
);
}
private void updateUnrealizedPnl() {
if (lastKlinePrice == null || lastKlinePrice.compareTo(BigDecimal.ZERO) == 0) return;
BigDecimal multiplier = config.getCtVal();
BigDecimal longPnl = BigDecimal.ZERO;
BigDecimal shortPnl = BigDecimal.ZERO;
if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
longPnl = longPositionSize.multiply(multiplier).multiply(lastKlinePrice.subtract(longEntryPrice));
}
if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) {
shortPnl = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice.subtract(lastKlinePrice));
}
unrealizedPnl = longPnl.add(shortPnl);
log.info("[OKX] 未实现盈亏: {}", unrealizedPnl);
}
private boolean isMarginSafe() {
try {
String balanceResp = executor.getBalance();
if (balanceResp == null) return true;
JSONObject json = JSON.parseObject(balanceResp);
if (!"0".equals(json.getString("code"))) return true;
JSONArray data = json.getJSONArray("data");
if (data == null || data.isEmpty()) return true;
JSONObject detail = data.getJSONObject(0);
String imr = detail.getString("imr");
if (imr == null) return true;
BigDecimal margin = new BigDecimal(imr);
if (initialPrincipal.compareTo(BigDecimal.ZERO) == 0) return true;
BigDecimal ratio = margin.divide(initialPrincipal, 4, RoundingMode.HALF_UP);
log.debug("[OKX] 保证金比例: {}/{}={}", margin, initialPrincipal, ratio);
return ratio.compareTo(config.getMarginRatioLimit()) < 0;
} catch (Exception e) {
log.warn("[OKX] 查保证金失败,默认放行", e);
return true;
}
}
private void checkProfitAndReset() {
try {
String balanceResp = executor.getBalance();
if (balanceResp == null) return;
JSONObject json = JSON.parseObject(balanceResp);
if (!"0".equals(json.getString("code"))) return;
JSONArray data = json.getJSONArray("data");
if (data == null || data.isEmpty()) return;
JSONObject detail = data.getJSONObject(0);
String upl = detail.getString("upl");
String availEq = detail.getString("availEq");
if (upl == null || availEq == null) return;
BigDecimal unrealisedPnl = new BigDecimal(upl);
BigDecimal available = new BigDecimal(availEq);
BigDecimal totalEquity = unrealisedPnl.add(available);
BigDecimal target = initialPrincipal.add(config.getExpectedProfit());
log.info("[OKX] 盈亏检查 upl:{}, availEq:{}, 合计:{}, 目标:{}", unrealisedPnl, available, totalEquity, target);
if (totalEquity.compareTo(target) > 0) {
log.info("[OKX] 盈亏达标({}>{}),重置策略", totalEquity, target);
state = StrategyState.STOPPED;
closeExistingPositions();
executor.cancelAllAlgoOrders();
startGrid();
}
} catch (Exception e) {
log.warn("[OKX] 盈亏检查失败", e);
}
}
private void handlePositionZeroAndReset(String direction) {
state = StrategyState.STOPPED;
executor.cancelAllAlgoOrders();
closeExistingPositions();
startGrid();
}
// ---- getters ----
public BigDecimal getLastKlinePrice() { return lastKlinePrice; }
public boolean isStrategyActive() { return state != StrategyState.STOPPED && state != StrategyState.WAITING_KLINE; }
public BigDecimal getCumulativePnl() { return cumulativePnl; }
public BigDecimal getUnrealizedPnl() { return unrealizedPnl; }
public StrategyState getState() { return state; }
}