From 1b55621d4dcf3b4ee6b9c4beb81ad69e5b7a5856 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Fri, 05 Jun 2026 17:58:08 +0800
Subject: [PATCH] refactor(okxNewPrice): 优化止损管理器的挂单数量计算逻辑
---
src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java | 311 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 311 insertions(+), 0 deletions(-)
diff --git a/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java b/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
new file mode 100644
index 0000000..b534a98
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/modules/okxNewPrice/StopLossManager.java
@@ -0,0 +1,311 @@
+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; }
+}
--
Gitblit v1.9.1