package com.xcong.excoin.modules.okxNewPrice;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。
*
*
止损策略(对齐 Gate 版本)
*
* - 基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity
* - 条件单成交后以成交量为粒度追挂止损(extend 系列)
* - 止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N>2 时先取消旧挂单
*
*
* 线程安全
* 本类通过 executor 的异步回调串行化下单操作;对 OkxGridElement 的读写依赖调用方保证有序。
*
* @author Administrator
*/
@Slf4j
public class StopLossManager {
private final OkxConfig config;
private final OkxTradeExecutor executor;
public StopLossManager(OkxConfig config, OkxTradeExecutor executor) {
this.config = config;
this.executor = executor;
}
// ========== 基底止损挂载 ==========
/**
* 在基底开仓完成后挂载初始止损单。
* 多仓止损: id=-2 到 -11,每格 quantity 张,方向 sell/long
* 空仓止损: id=2 到 11,每格 quantity 张,方向 buy/short
*/
public void setupBaseStopLosses() {
// 挂多仓止损 (id=-2 到 -11)
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)
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");
}
// ========== 止损触发处理 ==========
/**
* 多仓止损触发处理。
* 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
* 若 N > 2,先取消上一步的旧挂单。
*/
public void handleLongStopLossTriggered(OkxGridElement gridElement, BigDecimal longEntryPrice) {
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 -> {
clearLongEntryState(cancelGrid);
log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId);
});
}
}
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs();
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);
}
/**
* 空仓止损触发处理。
* 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。
* 若 N > 2,先取消上一步的旧挂单。
*/
public void handleShortStopLossTriggered(OkxGridElement gridElement, BigDecimal shortEntryPrice) {
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 -> {
clearShortEntryState(cancelGrid);
log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId);
});
}
}
BigDecimal triggerPrice = newEntryGrid.getGridPrice();
BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs();
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);
}
// ========== 止损追挂 ==========
/**
* 多仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。
*/
public void extendLongStopLoss(int filledQty) {
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);
});
}
}
/**
* 空仓追挂止损:按 quantity 粒度拆分为 count 个止损单,从当前最远止损格再往外延伸。
*/
public 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);
});
}
}
// ========== 辅助:条件单创建 & 状态回写 ==========
/**
* 预置标志位后发送条件入单请求,成功/失败均通过回调写入 GridElement 状态。
*/
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) {
setLongEntryState(gridElement, orderId, true);
} else {
setShortEntryState(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);
}
);
}
// ---- GridElement 状态修改(package-private,OkxGridTradeService 也可调用) ----
void setLongEntryState(OkxGridElement gridElement, String entryId, boolean flag) {
OkxTraderParam tp = gridElement.getLongTraderParam();
tp.setEntryOrderId(entryId);
tp.setEntryOrderPlaced(flag);
gridElement.setHasLongOrder(flag);
gridElement.setLongOrderId(entryId);
OkxGridElement.refreshIndices();
}
void setShortEntryState(OkxGridElement gridElement, String entryId, boolean flag) {
OkxTraderParam tp = gridElement.getShortTraderParam();
tp.setEntryOrderId(entryId);
tp.setEntryOrderPlaced(flag);
gridElement.setHasShortOrder(flag);
gridElement.setShortOrderId(entryId);
OkxGridElement.refreshIndices();
}
void clearLongEntryState(OkxGridElement gridElement) {
setLongEntryState(gridElement, null, false);
}
void clearShortEntryState(OkxGridElement gridElement) {
setShortEntryState(gridElement, null, false);
}
void setLongTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) {
OkxTraderParam tp = gridElement.getLongTraderParam();
tp.setTakeProfitOrderId(profitId);
tp.setTakeProfitPlaced(flag);
gridElement.setLongTakeProfitOrderId(profitId);
OkxGridElement.refreshIndices();
}
void setShortTakeProfitState(OkxGridElement gridElement, String profitId, boolean flag) {
OkxTraderParam tp = gridElement.getShortTraderParam();
tp.setTakeProfitOrderId(profitId);
tp.setTakeProfitPlaced(flag);
gridElement.setShortTakeProfitOrderId(profitId);
OkxGridElement.refreshIndices();
}
}