package com.xcong.excoin.modules.okxNewPrice; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; /** * 止损管理器 — 负责基底止损单挂载、止损触发后的逐步缩进、以及止损追挂。 * *

止损策略(对齐 Gate 版本)

*
    *
  1. 基底开仓后挂多仓止损(id=-2~-11)和空仓止损(id=2~11),每格 1 倍 quantity
  2. *
  3. 条件单成交后以成交量为粒度追挂止损(extend 系列)
  4. *
  5. 止损触发后向基底方向缩进 1 格,数量由价格差 / 步长决定,N>2 时先取消旧挂单
  6. *
* *

线程安全

* 本类通过 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(); } }