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 网格交易策略引擎 — 多空对冲网格。
|
*
|
* <h3>策略原理</h3>
|
* 对齐 Gate 版本逻辑:以空仓基底入场价为价格基准,向上/向下各生成价格网格队列。
|
* 价格触发网格层级时挂条件单,成交后自动挂止盈单。
|
*
|
* <h3>完整生命周期</h3>
|
* <pre>
|
* startGrid() → WAITING_KLINE
|
* ↓
|
* onKline(首根K线) → OPENING → 异步市价双开基底(开多+开空)
|
* ↓
|
* onPositionUpdate() → 基底成交 → baseLongOpened && baseShortOpened
|
* ↓
|
* tryGenerateQueues() → generateShortQueue()+generateLongQueue()+updateGridElements()
|
* → 挂基座止损单 + state=ACTIVE
|
* ↓
|
* ACTIVE 状态每根K线:
|
* processShortGrid() + processLongGrid() → 匹配队列 → 挂条件单 → 订单成交后挂止盈
|
* ↓
|
* onPositionClose() → cumulativePnl 累加 → 检查停止条件
|
* </pre>
|
*
|
* <h3>OKX vs Gate 差异</h3>
|
* <ul>
|
* <li>OKX 使用 side(buy/sell)+posSide(long/short) 下单,而非 size 正负</li>
|
* <li>条件单使用 algo order (ordType=conditional)</li>
|
* <li>止盈止损通过 algo order 实现</li>
|
* </ul>
|
*
|
* @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<BigDecimal> shortPriceQueue = Collections.synchronizedList(new ArrayList<>());
|
/** 多仓价格队列,升序排列(小→大) */
|
private final List<BigDecimal> longPriceQueue = Collections.synchronizedList(new ArrayList<>());
|
|
/** 当前多仓条件单映射:algoId → 止盈价格 */
|
private final Map<String, BigDecimal> currentLongOrderIds = Collections.synchronizedMap(new LinkedHashMap<>());
|
/** 当前空仓条件单映射:algoId → 止盈价格 */
|
private final Map<String, BigDecimal> 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<OkxGridElement> 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; }
|
}
|