package com.xcong.excoin.modules.okxNewPrice;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import java.math.BigDecimal;
|
|
/**
|
* 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。
|
*
|
* <h3>止损策略(对齐 Gate 版本)</h3>
|
* <ol>
|
* <li>基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity</li>
|
* <li>条件单成交后以成交量为粒度追挂止损(extend 系列)</li>
|
* <li>止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N>2 时先取消旧挂单</li>
|
* </ol>
|
*
|
* <h3>线程安全</h3>
|
* 本类通过 executor 的异步回调串行化下单操作;对 OkxGridElement 的读写依赖调用方保证有序。
|
*
|
* @author Administrator
|
*/
|
@Slf4j
|
public class StopLossManager {
|
|
private final OkxConfig config;
|
private final OkxTradeExecutor executor;
|
|
/** 多仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */
|
private volatile int longEntryQty = 1;
|
/** 空仓挂单张数计数器:止损触发时递增后再使用;挂单成交后重置为1 */
|
private volatile int shortEntryQty = 1;
|
|
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();
|
longEntryQty++;
|
int entryQty = longEntryQty * Integer.parseInt(config.getQuantity());
|
String size = String.valueOf(entryQty);
|
log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单(计数器:{}, qty:{})",
|
gridId, newEntryGridId, entryQty, longEntryQty, 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();
|
shortEntryQty++;
|
int entryQty = shortEntryQty * Integer.parseInt(config.getQuantity());
|
String size = String.valueOf(entryQty);
|
log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单(计数器:{}, qty:{})",
|
gridId, newEntryGridId, entryQty, shortEntryQty, 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();
|
}
|
|
// ========== 计数器管理 ==========
|
|
/** 重置多仓挂单张数计数器(挂单成交后调用) */
|
public void resetLongEntryQty() { longEntryQty = 1; }
|
|
/** 重置空仓挂单张数计数器(挂单成交后调用) */
|
public void resetShortEntryQty() { shortEntryQty = 1; }
|
|
/** 重置全部挂单张数计数器(startGrid时调用) */
|
public void resetAllEntryQuantities() { longEntryQty = 1; shortEntryQty = 1; }
|
}
|