📋 期货AI基金经理 · 工作日报 (交易日 2026-10-20_TLM失控开仓事故与修复) ⏭ 跳到最新(2026-11-12)

📅 全部日报已保留 · 交易时段每小时更新 · 休息日无日报 · 夜盘21:00后归下一交易日 · → 切到股票AI基金经理日报
历史日报: 2026-11-122026-11-112026-11-102026-11-092026-11-062026-11-052026-11-042026-11-032026-11-022026-10-302026-10-292026-10-282026-10-272026-10-262026-10-232026-10-222026-10-212026-10-20_TLM失控开仓事故与修复2026-10-20_IFOS同类隐患排查2026-10-202026-10-192026-10-16_TL优化复核2026-10-162026-10-15_未完成项推进_铜重建_配对口径_通道前视闭环_AG冻结溯源2026-10-15_方案B_主力连续序列重做与H10复验2026-10-152026-10-14_策略迭代评估_zadj近期亏损诊断2026-10-14_未完成项2_菜油费率敏感度2026-10-14_数据重建与zadj基线重算2026-10-14_zadj_walkforward复核2026-10-14_TL国债复刻与WF复核2026-10-142026-10-132026-10-122026-10-092026-10-082026-09-302026-09-292026-09-282026-09-242026-09-232026-09-222026-09-212026-09-182026-09-172026-09-162026-09-152026-09-142026-09-112026-09-102026-09-092026-09-082026-09-072026-09-042026-09-03

# 2026-10-20 TLM 失控开仓事故与修复

> 触发: 10:03 告警邮件「⚠️ 交易系统告警 10-20 10:03 —— ⛔ 委托被服务器拒绝×6: 开仓保证金不足」
> 处置: 停服务 → 平裸仓 → 改代码 → 单测 → 重启 → 冷却期后复验
> 状态: **已修复并重启**;损失已实现,账户归零持仓

---

## 一、结论摘要

| 项 | 内容 |
|---|---|
| 事件 | `tq-strategy-tlm`(30年国债执行质量测量版)在 2 秒内连开 **22 手** TL2612 空单 |
| 直接表现 | 6 次「开仓保证金不足」被拒 → 触发告警邮件 |
| **真实风险** | 不止"下单被拒":**20 手裸空未被策略状态跟踪**(策略以为只持 2 手) |
| 已实现损失 | **-25,832 元**(权益 1,000,745.64 → 974,913.64),含 20 手平仓价差 -24,000 与手续费 |
| 处置 | 10:07 停服务 → 10:08 人工平掉 20 手 @118.20 → 修 5 处 → 10:09 重启 |
| 当前状态 | 持仓 0、保证金 0、风险度 0;服务 active;冷却期至 10:35 |

---

## 二、时间线(2026-10-20)

| 时刻 | 事件 |
|---|---|
| 10:02:00 | TLM 正常:`G1:0 G2:0 SW2:+0 价=118.030 挂单4 权益=1,000,746`(4 张被动单:2 组×双向) |
| 10:03:02 | 上轨 2 张 SELL 被动单在 **118.080** 成交 → 进入"待确认(需价格穿过 118.080 才认账,90s 窗口)" |
| 10:03:02–10:03:04 | **连续 22 笔 `TL2612 卖 OPEN @118.08` 成交**(账本 trades_history.csv 可查,订单号 TLM1_/TLM2_ 递增) |
| 10:03:04 | 开始出现「开仓保证金不足」被拒,累计 **6 次** → 告警邮件 |
| 10:04:54 / 10:05:10 | 策略按自己的状态只平掉 2 手(分组2 移动止盈 -700、分组1 硬止损 -1,000)→ 仍剩 **20 手裸空** |
| 10:06:19 | 告警巡检发出邮件(alert_watch) |
| 10:07 | 人工确认:`margin=823,900 / float_profit=-28,000 / risk_ratio=0.85`,服务已停 |
| 10:08:20 | 人工平掉 20 手 @118.20(BUY CLOSETODAY),撤遗留挂单 0 张,账户归零 |
| 10:09:29 | 修复后代码重启,启动对账通过(实际净持仓=状态预期=0) |
| 10:09–10:10 | 观察:`挂单0`(两组处于 30 分钟止损冷却),无新委托、无被拒 |

---

## 三、根因(代码级)

`strategy_tl_measure.py` 的被动挂单循环:

```
poll_fills() # 成交 → 从 slots 移出,放进 self.pending(待确认)
check_confirm() # 只有"价格真穿过挂单价"才认账 → 才写 st[g]['dir']
...
# 挂被动单
if st[g]['dir'] != 0: continue # ← 只看状态,不看 pending
pf.sync(...) # ← 于是每轮循环又挂一张同价单
```

**在"成交→确认"窗口内,该组仍被视为空仓**,而当时行情正停在轨值 118.08 上:

- 挂单 → 立即成交 → 进 pending → 下一轮再挂 → 再成交 …… 每轮 ~100ms,2 秒滚到 22 手;
- 直到保证金耗尽被拒 6 次才被动停下(**拒单反而救了这个账户**);
- 确认逻辑随后才把 `st[g]['dir']` 置为 -1,但**只记录 1 手/组**,于是 22 手中的 20 手成为状态外的裸仓;
- 出场逻辑只按状态平 2 手,剩余 20 手无人管理 → 若未及时人工干预,风险度 85%、浮亏继续扩大。

附加缺陷:
1. 下单被拒后 order 变 `FINISHED` 但 `volume_left>0`,原实现永久留在 `slots` → "挂单N"虚高、该侧不再重挂;
2. 无净持仓上限,无可用资金预检 → 只能靠服务器拒单兜底;
3. 进程停止/崩溃后不核对账户实际持仓,裸仓无人认领。

---

## 四、修复清单(已提交)

| # | 修复 | 位置 |
|---|---|---|
| 1 | **待确认组视为已占用**:`busy_groups()` —— 有 pending 的组不再挂新单,并 `cancel_group()` 撤掉同组反向单(防双向都成交) | 挂单主循环 |
| 2 | **净持仓硬上限** `MAX_NET_LOTS=2`:持仓达标即**撤掉所有未成交挂单**并停止开新仓(日志打印 🛑) | 挂单主循环 |
| 3 | **可用资金预检** `MARGIN_BUF=2.0`:可用 < 单手预估保证金×2 则暂停开仓 → 从源头消灭"保证金不足"被拒 | 挂单主循环 |
| 4 | **被拒/已撤槽位清理** `prune_dead()`:每个循环清理并打印告警(拒单不再静默) | 主循环开头 |
| 5 | **启动对账**:实际净持仓 ≠ `g1.dir+g2.dir` → 强制平到空仓并重置状态(防裸仓遗留) | `main()` 启动段 |

告警侧同时加固(`alert_watch.py`):同一原因 **10 分钟内被拒 ≥3 次** 时,邮件文案升级为
「高度疑似策略重复下单/失控 → 立即停服务并核查裸仓」,不再只说"某笔风控动作未执行"。

---

## 五、验证

1. **单元测试** `test_tl_runaway_fix.py`(3 场景全通过):
- 成交进入待确认后 → `busy_groups={'g1','g2'}`,**不再产生新委托**;
- 被拒委托槽位 → 被 `prune_dead` 清理;
- 常量断言 `MAX_NET_LOTS=2`、`MARGIN_BUF=2.0`。
2. **失败场景复现** `test_tl_runaway_sim.py`(300 轮,每轮都挂单):
- A 价格停在轨值 118.08 → 开仓 **2 笔 / 2 手**(修复前同场景 22 笔);
- B 价格两轨间来回甩 → 开仓 **2 笔 / 2 手**;
- 断言"总持仓 ≤ 2 手"通过。
3. **静态检查**:`py_compile` 通过;5 处修复点全部命中;`alert_watch.py` 升级文案已用伪造 journal 验证。
4. **启动对账**:两次重启均无对账告警(实际净持仓=状态预期=0),`G1:0 G2:0 挂单0 权益=974,914`。
5. **冷却期后实盘复验(已完成)**:10:35 止损冷却结束后重新挂单,观察到:
- `挂单2`(SW2=+1 只允许做多 → 每组 1 张,共 2 张)—— **远低于上限 4 张**;
- 4 分钟内 9 次"下单成功 / 3 次撤单成功" = 轨值移动后的正常撤挂,**无雪球式重复开仓**;
- **零"开仓保证金不足"被拒**;
- 账户 `balance=974,913.64 / 保证金占用 82,396(=2张挂单冻结) / 无持仓 / risk_ratio=0`。

---

## 六、待办 / 建议

- [x] ~~冷却期后确认:重新挂单时最多 4 张(2 组×双向),成交后最多 2 手,无拒单~~ → 已复验(挂单2、零拒单)
- [ ] 该策略本身**未通过验证(前推观察)**,本次事故不改变此结论;是否继续跑由用户决定
- [ ] 账户 60 与 `tq-strategy-ifos` 共用保证金:TLM 占用变小后 IF 才有开仓空间(当前可用 89.3 万)
- [ ] 同类"被动挂单"逻辑若复制到其它策略,需一并检查 pending 期间的状态标记
- [ ] 人工平仓单在账本里归到"其他"台位(订单号无 TLM 前缀),不影响资金账,仅归因显示

## 七、相关文件

| 文件 | 说明 |
|---|---|
| `strategy_tl_measure.py` | 已修复(备份 `strategy_tl_measure.py.bak-20261020-runaway`) |
| `alert_watch.py` | 告警文案升级:同原因被拒≥3次 → 提示"疑似策略失控" |
| `test_tl_runaway_fix.py` | 单元测试:待确认不挂单 / 拒单清槽 / 常量断言 |
| `test_tl_runaway_sim.py` | 失败场景复现:300 轮每轮挂单,总持仓 ≤2 手 |
| `emergency_flatten_tl.py` | 应急处置脚本(平裸仓+撤遗留单,本次使用一次) |