Administrator
2026-08-04 39119e045e77c693d1231948141d38c2c726c312
fix(gate): 修复网格交易止损计数和同网格重复触发问题

- 修复空单成交时累积亏损计数增加的逻辑错误
- 修复多单成交时累积亏损计数增加的逻辑错误
- 移除止损触发时的重复计数累加,避免计数不准确
- 使用orderId去重替代gridId去重,解决同网格多个止损单被忽略的问题
- 修复多空双边止损触发的重复推送判断机制
- 添加详细的代码审计报告文档
1 files added
1 files modified
191 ■■■■■ changed files
GateGridTradeService_Audit.md 185 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java 6 ●●●●● patch | view | raw | blame | history
GateGridTradeService_Audit.md
New file
@@ -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 截断 | — | 已有兜底,不需处理 |
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;