From 5bda3635131c0fd6fc8af5ca9d7f4392f803a056 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Thu, 06 Aug 2026 16:00:53 +0800
Subject: [PATCH] fix(gate): 修复网格交易止损计数和同网格重复触发问题
---
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java | 6 -
GateGridTradeService_Audit.md | 185 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 187 insertions(+), 4 deletions(-)
diff --git a/GateGridTradeService_Audit.md b/GateGridTradeService_Audit.md
new file mode 100644
index 0000000..611635b
--- /dev/null
+++ b/GateGridTradeService_Audit.md
@@ -0,0 +1,185 @@
+# GateGridTradeService 代码审计报告
+
+> 审计日期:2026-08-03
+> 文件:`src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java`
+> 审查范围:全部 2147 行,含交叉验证 `GridElement` / `TraderParam` / `GateConfig` / `GateTradeExecutor`
+
+---
+
+## 一、总体评估
+
+代码结构清晰,多空双方向网格交易的主流程(K线驱动 → 条件单挂单 → 成交回调 → 止损追单 → 级联查表)完整且自洽。发现 **4 个 Bug**,其中 **1 个 Critical** 直接影响止损级联计数准确性。
+
+---
+
+## 二、BUG 详细分析
+
+### Bug #1 🔴 Critical:sameGrid 阻止同网格多个止损单触发
+
+**位置**
+
+- `handleLongStopLossTriggered` — 行 1283 ~ 1287
+- `handleShortStopLossTriggered` — 行 1494 ~ 1498
+
+**问题代码**
+
+```java
+boolean sameGrid = (gridId == lastLongStopLossGridId);
+if (sameGrid) {
+ log.info("[Gate] 多仓止损触发 同网格, 忽略");
+ return;
+}
+lastLongStopLossGridId = gridId;
+accumulatedLongLossCount++;
+```
+
+**根因**
+
+`extendLongStopLoss` / `extendShortStopLoss` 中当 `gridCount == 1` 时,所有止损单挂在**同一个 gridId**:
+
+```java
+// extendLongStopLoss,行 1759-1761
+if (gridCount == 1) {
+ orders[0] = totalOrders; // 多张止损单 → 同一个 gridId
+}
+// 行 1789:gridId - 2 - 0 = gridId - 2,同一个位置
+```
+
+以 `baseQty=2, qty=2, filledQty=4` 为例:
+- `gridCount = 2/2 = 1`,`totalOrders = 4/2 = 2`
+- **2 单止损全部挂在 gridId-2**
+- 第 1 单触发 → `lastLongStopLossGridId = gridId`,count = 1
+- 第 2 单触发 → `sameGrid = true` → **被丢弃**,count 少 1
+
+**为什么可以用 `removeXxxStopLossOrderId` 替代**
+
+`handleLongStopLossTriggered` 第一步就是 `gridElement.removeLongStopLossOrderId(orderId)`,从列表中删除该 orderId。`refreshIndices()` 会重建全局索引,确保同一 orderId 不会二次匹配。
+
+> 重复推送的防线:若 WS 对**同一订单**推送两次,第二次 `findByLongStopLossOrderId` 找不到(已从索引清除),直接落到后续匹配逻辑,无副作用。
+
+`sameGrid` 是**按 gridId 去重**,粒度太粗,误杀了**不同 orderId 但同一个 gridId** 的正常多单触发。
+
+**修复方案**
+
+```java
+private void handleLongStopLossTriggered(GridElement gridElement, String orderId) {
+ // 用 remove 返回值判断是否重复推送,代替 sameGrid
+ if (!gridElement.removeLongStopLossOrderId(orderId)) {
+ log.info("[Gate] 多仓止损重复推送 orderId:{}, 忽略", orderId);
+ return;
+ }
+ int gridId = gridElement.getId();
+ lastLongStopLossGridId = gridId;
+ accumulatedLongLossCount++;
+ log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}", gridId, accumulatedLongLossCount);
+
+ int newEntryGridId = gridId + 1;
+ // ... 后续追单逻辑不变 ...
+}
+```
+
+`handleShortStopLossTriggered` 同理(`gridId - 1`)。
+
+**影响范围**
+
+- `accumulatedLongLossCount` / `accumulatedShortLossCount` **少计数**
+- `STOP_LOSS_RULES` 级联规则表推进不到应有行数
+- 对手止盈/己方止盈缺失,策略漏掉对冲机会
+- 网格数越少 (`baseQty/qty` 小),问题越严重(所有止损同格)
+
+---
+
+### Bug #2 🟡 Low:三处死代码
+
+| 方法 | 行号 | 功能 | 调用情况 |
+|---|---|---|---|
+| `checkLastTakeProfitAndRestart()` | 1549 | 止盈全部清空后检查跨度重启 | 全项目检索 → **0 处调用** |
+| `placeOpponentTakeProfit()` | 2125 | 旧版对手止盈挂单逻辑 | 全项目检索 → **0 处调用** |
+| `cancelAllInitialTakeProfits()` | 1719 | 取消初始化阶段止盈单 | 全项目检索 → **0 处调用** |
+
+`checkLastTakeProfitAndRestart` 本身逻辑完整(跨度检查 → 清理条件单 → 平仓 → 重启),可能是某个版本的入口未被接上。
+
+**影响**:仅代码整洁度,无功能影响。若跨度重启功能需要保留,需在合适回调点接入(如止盈触发后)。
+
+---
+
+### Bug #3 🟡 Low-Medium:整数截断使小 maxPos 下多行规则不可区分
+
+**位置**:行 1389、1391(`checkLongStopShortProfit`)及镜像 1445、1447(`checkShortStopLongProfit`)
+
+```java
+int fullTpPctNum = maxPos * (100 - fullTpPct) / 100; // Java int 除法截断
+int thresholdPosNum = maxPos * (100 - thresholdPct) / 100;
+```
+
+**以 `maxPos = 4` 为例**
+
+| STOP_LOSS_RULES 行 | fullTpPct | fullTpPctNum | 触发条件 `oppPos > N` |
+|---|---|---|---|
+| 第 3 行 (times=3) | 30 | `4*70/100 = 2` | oppPos ≥ 3 |
+| 第 6 行 (times=6) | 40 | `4*60/100 = 2` | oppPos ≥ 3 ⚠️ 与第3行相同 |
+| 第 9 行 (times=9) | 50 | `4*50/100 = 2` | oppPos ≥ 3 ⚠️ 与第3/6行相同 |
+
+前三行规则对于对手止盈**完全不可区分**,实际触发门槛都是 `maxPos` 的 75%。
+
+同样 `thresholdPosNum` 也有精度损失:
+
+| thresholdPct | thresholdPosNum (`maxPos=4`) | 实际百分比 |
+|---|---|---|
+| 40 | `4*60/100 = 2` | excess = selfPos - 2 |
+| 50 | `4*50/100 = 2` | 同上 ⚠️ |
+| 60 | `4*40/100 = 1` | excess = selfPos - 1 |
+
+**影响**
+
+- `maxPos < 10`:规则表精度差,多行等价
+- `maxPos ≥ 20`:每 5% 差 1 张,基本可区分
+- 如需保证小 `maxPos` 精度,可改用:
+ ```java
+ int fullTpPctNum = (int) Math.ceil(maxPos * (100.0 - fullTpPct) / 100.0);
+ ```
+
+---
+
+### Bug #4 🟢 Very Minor:perOrderQty 整数截断(已有兜底)
+
+**位置**:行 1410 ~ 1413(`checkLongStopShortProfit`)及镜像 1466 ~ 1469
+
+```java
+int perOrderQty = maxPos * DEFAULT_STOP_LOSS_PERCENT / 100; // DEFAULT_STOP_LOSS_PERCENT = 5
+if (perOrderQty <= 0) {
+ perOrderQty = 1; // 兜底
+}
+```
+
+`maxPos = 4` → `4 * 5 / 100 = 0` → 兜底为 1。
+
+**影响**:几乎无。`perOrderQty` 从设计的 "maxPos 的 5%" 降级为固定 1 张,但兜底保证了逻辑不崩溃。`maxPos ≥ 20` 后恢复正常。
+
+---
+
+## 三、审查确认正确的模块 ✅
+
+以下模块经逐行审查,确认无 Bug:
+
+| 模块 | 验证点 | 结论 |
+|---|---|---|
+| `checkLongStopShortProfit` | 对手(空)盈止位 `gridId`,己方(多)盈止位 `0+2,4,6...` | ✅ 正确 |
+| `checkShortStopLongProfit` | 对手(多)盈止位 `gridId`,己方(空)盈止位 `0-2,-4,-6...` | ✅ 正确 |
+| `cancelAllXxxTakeProfitsAndStopLosses` | 先清后建:取消 → extend → checkXxxProfit 重挂 | ✅ 无遗漏 |
+| `onAutoOrder` 入口匹配 | 止损单 → 入口单 的顺序匹配 | ✅ 正确 |
+| `extendStopLossInProgress` 防重入 | 同格多成交 → pendingReExtend → 重挂 | ✅ 正确 |
+| `processShortGrid/processLongGrid` | 动量方向、队列匹配、下行守卫 | ✅ 正确 |
+| `checkProfitAndReset` | 估算手续费、轮次控制 | ✅ 正确 |
+| `onKline` 单边归零触发 | `longActive==false` 时触发 `processShortGrid` | ✅ 正确 |
+
+---
+
+## 四、优先级总结
+
+| 优先级 | Bug | 修复难度 | 建议 |
+|---|---|---|---|
+| 🔴 P0 | #1 sameGrid 丢止损 | 低(删 3 行加 4 行) | **立即修复** |
+| 🟡 P2 | #2 死代码 | — | 清理或恢复接入 |
+| 🟡 P3 | #3 小 maxPos 精度 | 中(需改计算逻辑) | maxPos ≥ 10 可暂缓 |
+| 🟢 P4 | #4 perOrderQty 截断 | — | 已有兜底,不需处理 |
diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
index 5ce118d..5e271ec 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -676,7 +676,7 @@
log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
-
+ accumulatedShortLossCount++;
checkShortStopLongProfit(accumulatedShortLossCount, shortGridElement.getId() + 2);
}
@@ -708,7 +708,7 @@
// placeExcessTakeProfit(posSize, true);
log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize);
-
+ accumulatedLongLossCount++;
checkLongStopShortProfit(accumulatedLongLossCount, longGridElement.getId() -2);
}
@@ -1287,7 +1287,6 @@
}
lastLongStopLossGridId = gridId;
- accumulatedLongLossCount++;
log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
gridId, accumulatedLongLossCount, sameGrid ? "(同网格)" : "");
int newEntryGridId = gridId + 1;
@@ -1497,7 +1496,6 @@
return;
}
lastShortStopLossGridId = gridId;
- accumulatedShortLossCount++;
log.info("[Gate] 空仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单",
gridId, accumulatedShortLossCount, sameGrid ? "(同网格)" : "");
int newEntryGridId = gridId - 1;
--
Gitblit v1.9.1