main分支开仓告警BUG
"开仓信号详细告警"模块 Bug 分析报告
分析范围
| 文件 | 职责 |
|---|---|
| [signal_alert_formatter.py](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py) | 告警内容格式化(三级降级) |
| [risk_evaluator.py](file:///Users/test/Downloads/Trading-in-websocket/src/utils/analysis/risk_evaluator.py) | 5维度加权风险评估 |
| [alert_sender.py](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/alert_sender.py) | 告警发送(限流 + 去重) |
| [orchestrator.py](file:///Users/test/Downloads/Trading-in-websocket/src/trading/orchestrator.py) | 调用入口 _send_entry_alert |
🔴 严重 BUG
BUG #1:_section_health_monitor 缺少空 dict 防护 — 必崩
[!CAUTION]
严重性:高 — 生产环境中当(4h, 60d)周期数据缺失时,函数必定抛出异常。
[signal_alert_formatter.py:313-314](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L313-L314):
period_data = _get_period_data(details, ("4h", "60d"))
monitor = period_data.get("health_monitor", {}) # ← period_data 可能是 {}
_get_period_data 在找不到 key 时返回 {},.get("health_monitor", {}) 返回 {},后续检查 if not monitor 会进入早期返回分支。这一段本身不会崩。
但对比同一文件中 [_section_window_comparison:346-348](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L346-L348),逻辑完全相同 — 同样安全。
✅ 结论:经再次审查,此处实际没有 bug,有防护,不会崩。撤销此项。
BUG #1(更正):_section_risk_assessment 趋势风险得分展示语义反转
[!WARNING]
严重性:中高 — 用户看到的分数与其直觉理解相反,可能导致交易决策失误。
[signal_alert_formatter.py:375](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L375):
rows.append(f"| 趋势风险 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")
trend_risk['score']在risk_evaluator.py中的定义是:score 越高 → 风险越大(见 [L102](file:///Users/test/Downloads/Trading-in-websocket/src/utils/analysis/risk_evaluator.py#L102))- 告警展示时对 trend_risk 做了
100 - score反转(变成"质量分"),但表头列名是"趋势风险" - 其他 4 个维度(协整质量、相关性、Z-score 一致性、协整健康)的 score 本身就是"质量分"(越高越好),直接展示无反转
结果:5 行表格中,4 个维度的"得分"列含义是"越高越好",但"趋势风险"行的含义也被反转为"越高越好"(反转后),跟列头"趋势风险"的字面含义相矛盾。用户看到 趋势风险 | 80 会以为风险很高,实际是 100 - 20 = 80,代表风险很 低。
建议:将行名改为"趋势安全性"或者不做反转直接展示原始风险分。
🟡 中等 BUG
BUG #2:alert_sender 限流后被去重遗漏记录 — 可能丢失告警
[alert_sender.py:59-67](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/alert_sender.py#L59-L67):
# 1. 限流
if priority != "high" and not self._check_rate_limit(pair_name):
return AlertStatus.THROTTLED # ← 限流时直接返回
# 2. 去重
content_hash = hashlib.md5(content.encode()).hexdigest()
if self._is_duplicate(content_hash):
return AlertStatus.THROTTLED # ← 去重时直接返回
被限流的消息没有被记录到 _recent_hashes 中。这意味着:
- 第一条消息被限流 → 没记录 hash
- 限流窗口过后,相同内容的消息不会被去重 → 可能重复发送
但在实际场景中,由于每次开仓的 position_id 和 signal_id 不同,内容 hash 几乎不会重复,所以影响有限。
BUG #3:_format_full 超长截断时索引越界风险
[signal_alert_formatter.py:186](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L186):
core = [sections[1], sections[7], sections[8], sections[9]] if len(sections) >= 10 else sections[:4]
builders 列表总是有 10 个元素,所以 sections 在不出异常时长度固定为 10,条件 len(sections) >= 10 总为真。但如果某个 builder 抛异常后被跳过(虽然当前代码用了 try/except 不会跳过),或者未来修改了 builders 列表,这里的硬编码索引就会出问题。当前不会触发,但代码脆弱。
BUG #4:_section_zscore_validation — 符号一致性判断逻辑有缺陷
[signal_alert_formatter.py:236](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L236):
majority_sign = sum(1 if z > 0 else -1 for z in non_zero) if non_zero else 0
majority_sign 是一个整数(例如 3、1、-1、-3),而非严格的多数符号标志。
[L241](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L241):
sign_ok = "✅" if (majority_sign > 0 and z > 0) or (majority_sign < 0 and z < 0) or z == 0 else "⚠️"
边界情况:当 majority_sign == 0(例如 zscore_list = [1.0, -1.0, 0],non_zero = [1.0, -1.0],sum = 0),所有非零 zscore 都会被标记为 ⚠️,即使这种分歧本身就是中性信号的正常状态。这并不是严重 bug,但展示效果有误导性。
🟢 设计层面的注意事项(非 BUG,但值得关注)
注意 #1:降级链的递归调用可能掩盖真实错误
[signal_alert_formatter.py:102-120](file:///Users/test/Downloads/Trading-in-websocket/src/utils/monitoring/signal_alert_formatter.py#L102-L120):
except Exception as e:
if level == AlertLevel.FULL:
return format_signal_alert(..., level=AlertLevel.SIMPLIFIED)
elif level == AlertLevel.SIMPLIFIED:
return format_signal_alert(..., level=AlertLevel.BASIC)
三级降级是递归调用 format_signal_alert 本身。如果 传入参数本身有问题(例如 signal 对象属性缺失),降级到 SIMPLIFIED 和 BASIC 时仍然会访问相同的属性,可能连续三级都失败,最终落入 L121 的最终兜底。三级降级机制对格式化逻辑内部的错误有效,但对入参数据错误无效。
不过这并不是严重 bug,因为最终兜底(L123-L127)只访问了 signal.symbol、signal.direction、signal.zscore_4h 这三个核心属性,在 PairTradeSignal 构造时已保证存在。
注意 #2:evaluate_risk 的异常吞没
[risk_evaluator.py:87-95](file:///Users/test/Downloads/Trading-in-websocket/src/utils/analysis/risk_evaluator.py#L87-L95):
风险评估异常时返回默认"中性评估"(50 分、中风险),不会阻断告警发送。但 用户无法从告警中看出这是默认值而非真实评估结果,可能误导交易决策。
总结
| # | 严重性 | 描述 | 文件 |
|---|---|---|---|
| 1 | 🔴 中高 | 趋势风险得分展示语义反转,用户看到的分数与直觉理解相反 | signal_alert_formatter.py:375 |
| 2 | 🟡 低 | 限流消息未记录 hash,理论上可能在限流窗口过后重复发送 | alert_sender.py:59-67 |
| 3 | 🟡 低 | 超长截断使用硬编码索引,代码脆弱 | signal_alert_formatter.py:186 |
| 4 | 🟡 低 | zscore 符号一致性 majority_sign==0 时展示误导 | signal_alert_formatter.py:236 |
[!IMPORTANT]
整体评估:该模块不存在会导致资金损失或系统崩溃的严重 BUG。三级降级机制 + 区块级 try/except 的防御性编程做得比较完善,即使格式化某个区块失败也只会显示"数据缺失",不会影响开仓执行本身。最值得关注的是 BUG #1(趋势风险得分语义反转),虽然不影响系统功能,但可能影响用户对风险的判断。