开仓信号详细告警 - BUG 完整因果链分析
开仓信号详细告警 - BUG 完整因果链分析
分析日期: 2026-02-16
分析方法: 静态代码分析 + 调用链追踪 + 状态机建模
🔴 BUG #1: 告警发送失败时不降级(Critical)
完整因果链
输入触发
↓
用户开仓成功
↓
orchestrator.on_entry_signal() 第498-499行
↓
orchestrator._send_entry_alert() 第753-793行
↓
[分支1: ENABLE_SIGNAL_DETAIL_ALERT=True]
↓
format_signal_alert() 成功生成详细告警
↓
alert_sender.send() 第38-72行
↓
[限流检查] 第55-57行
↓
if priority != "high" and not self._check_rate_limit(pair_name):
return AlertStatus.THROTTLED ← 🔥 返回THROTTLED状态
↓
orchestrator 第780-786行
↓
self._alert_sender.send(...) ← ❌ 没有接收返回值
return ← ❌ 直接返回,不降级
↓
❌ 用户完全收不到任何通知
详细调用路径
路径: on_entry_signal → _send_entry_alert → alert_sender.send → [限流] → THROTTLED → orchestrator忽略 → 用户无通知
关键代码位置:
-
入口 -
orchestrator.py:498-499# 发送开仓告警 self._send_entry_alert( signal, multi_period_result, position, order_result, adaptive_z, ) -
告警发送 -
orchestrator.py:780-786priority = "high" if abs(adaptive_z) > self._config.strategy_adaptive_threshold * 1.4 else "normal" self._alert_sender.send( # ❌ 没有接收返回值 title=title, content=content, pair_name=signal.symbol, priority=priority, ) return # ❌ 直接返回,不检查是否成功 -
限流逻辑 -
alert_sender.py:55-57if priority != "high" and not self._check_rate_limit(pair_name): logger.info(f"告警被限流: {pair_name}") return AlertStatus.THROTTLED # ← 返回THROTTLED -
限流检查 -
alert_sender.py:74-82def _check_rate_limit(self, pair_name: str) -> bool: now = time.time() history = self._history[pair_name] # 清理过期 while history and now - history[0] > self._rate_window: history.popleft() return len(history) < self._rate_max # ← 超过rate_max则返回False
状态变化
| 时间 | 状态 | 告警队列 | 用户感知 |
|---|---|---|---|
| T0 | 开仓成功 | [] | - |
| T1 | 第1次告警 | [告警1] | ✅ 收到 |
| T2 | 第2次告警 | [告警1, 告警2] | ✅ 收到 |
| T3 | 第3次告警 | [告警1, 告警2, ❌拒绝] | ❌ 未收到 |
| T4 | orchestrator返回 | - | ❌ 无降级 |
出错点定位
文件: src/trading/orchestrator.py
行号: 780-786
错误代码:
self._alert_sender.send(...) # ❌ 问题1: 没有接收返回值
return # ❌ 问题2: 直接返回,不检查成功与否
根本原因
设计缺陷: 告警系统与降级机制之间缺少状态反馈
具体原因:
- 接口设计不一致:
AlertSender.send()返回AlertStatus枚举,但调用方不检查 - 降级条件错误: 降级仅在
format_signal_alert()抛异常时触发,而限流/去重/发送失败时返回错误枚举而非异常 - 控制流缺陷:
return语句在成功/失败路径上没有区分
触发条件
场景1: 限流触发
# alert_sender.py 配置
rate_limit_window = 300 # 5分钟
rate_limit_max = 2 # 最多2条
# 触发条件: 5分钟内同一币对发送超过2条normal优先级告警
场景2: 去重触发
# alert_sender.py 配置
dedup_window = 60 # 60秒
# 触发条件: 60秒内发送了内容完全相同的告警
场景3: 飞书API失败
# alert_sender.py:66-72
try:
sender_colourful(content=content, title=title)
except Exception as e:
return AlertStatus.FAILED # 网络异常、webhook失败等
影响范围
严重程度: 🔴 Critical
影响场景:
- 高频交易: 5分钟内开仓3次以上
- 多币对交易: 多个币对同时触发开仓
- 网络抖动: 飞书API临时故障
- 系统异常: 队列满、内存不足等
实际影响:
- ❌ 用户完全收不到开仓通知
- ❌ 无法及时调整仓位
- ❌ 资金管理风险增加
🟠 BUG #2: sections与builder对应关系错位(High)
完整因果链
输入触发
↓
format_signal_alert() 被调用
↓
_format_full() 第135-190行
↓
构建10个区块的builders列表 第159-170行
↓
遍历builders: for name, builder, args in builders (第172行)
↓
调用 section = builder(*args) 第174行
↓
[分支1: builder返回空字符串或None]
↓
if section: 第175行
↓
条件为False,不执行append ← 🔥 跳过append
↓
sections数组长度 < 10 ← ❌ 长度不足
sections索引与builder不对应 ← ❌ 索引错位
↓
超长保护检查 第184-188行
↓
if len(content) > _MAX_CONTENT_LENGTH:
core = [sections[1], sections[7], sections[8], sections[9]] ← ❌ 索引错位
↓
❌ 截断后的内容不是预期的核心区块
详细调用路径
路径: format_signal_alert → _format_full → builders遍历 → if section判断 → sections.append → 超长截断
关键代码位置:
-
builders定义 -
signal_alert_formatter.py:159-170builders = [ ("基本信息", _section_basic_info, (signal,)), # 期望: sections[0] ("信号概览", _section_signal_overview, (...)), # 期望: sections[1] ("多周期 Z-score 验证", _section_zscore_validation, (...)), # 期望: sections[2] ("多周期相关性分析", _section_correlation, (...)), # 期望: sections[3] ("协整检验统计", _section_cointegration, (...)), # 期望: sections[4] ("协整健康监控", _section_health_monitor, (...)), # 期望: sections[5] ("窗口对比", _section_window_comparison, (...)), # 期望: sections[6] ("风险评估", _section_risk_assessment, (risk,)), # 期望: sections[7] ("交易建议", _section_recommendation, (risk,)), # 期望: sections[8] ("执行信息", _section_execution_info, (...)), # 期望: sections[9] ] -
区块构建循环 -
signal_alert_formatter.py:172-179for name, builder, args in builders: try: section = builder(*args) if section: # ❌ 如果section为空字符串/None,不会append sections.append(section) except Exception as e: logger.warning(f"告警区块 [{name}] 生成失败: {e}") sections.append(f"**{name}**: ⚠️ 数据缺失") # ✅ 异常时会append占位符 -
超长截断 -
signal_alert_formatter.py:184-188if len(content) > _MAX_CONTENT_LENGTH: logger.warning(f"告警内容超长 ({len(content)} > {_MAX_CONTENT_LENGTH}),截断") # 保留核心区块:信号概览 + 风险评估 + 执行信息 core = [sections[1], sections[7], sections[8], sections[9]] if len(sections) >= 10 else sections[:4] # ❌ 假设sections[1]是"信号概览",sections[7]是"风险评估" # ❌ 但如果之前有builder返回空,索引就错位了
状态变化示例
正常情况 (所有builder都返回真值):
sections = [
"基本信息", # sections[0]
"信号概览", # sections[1] ✅
"Z-score验证", # sections[2]
"相关性分析", # sections[3]
"协整统计", # sections[4]
"健康监控", # sections[5]
"窗口对比", # sections[6]
"风险评估", # sections[7] ✅
"交易建议", # sections[8] ✅
"执行信息", # sections[9] ✅
]
# 截断: core = [sections[1], sections[7], sections[8], sections[9]]
# 结果: ["信号概览", "风险评估", "交易建议", "执行信息"] ✅ 正确
异常情况 (第3个builder返回空字符串):
# _section_correlation 返回 "" (空字符串)
sections = [
"基本信息", # sections[0]
"信号概览", # sections[1] ✅
"Z-score验证", # sections[2]
# ❌ 相关性分析被跳过,没有append
"协整统计", # sections[3] ← 错位!原本应该是sections[4]
"健康监控", # sections[4] ← 错位!原本应该是sections[5]
"窗口对比", # sections[5] ← 错位!原本应该是sections[6]
"风险评估", # sections[6] ← 错位!原本应该是sections[7]
"交易建议", # sections[7] ← 错位!原本应该是sections[8]
"执行信息", # sections[8] ← 错位!原本应该是sections[9]
]
# len(sections) = 9 < 10
# 截断: core = sections[:4]
# 结果: ["基本信息", "信号概览", "Z-score验证", "协整统计"] ❌ 不是预期的核心区块
出错点定位
文件: src/utils/monitoring/signal_alert_formatter.py
行号: 175-176
错误代码:
if section: # ❌ 空字符串会导致条件为False
sections.append(section)
问题: Python的真值判断中,空字符串 "" 会被判断为 False
根本原因
设计缺陷: sections数组与builders数组的对应关系不是强制的
具体原因:
- 条件append: 使用
if section:而不是无条件append - 空字符串判断: 空字符串被Python判断为falsy值
- 索引硬编码: 截断逻辑使用硬编码索引
[1], [7], [8], [9] - 异常处理不一致:
- 抛异常时: 会append占位符
"**{name}**: ⚠️ 数据缺失" - 返回空字符串时: 不会append,导致索引错位
- 抛异常时: 会append占位符
触发条件
条件1: builder返回空字符串
def _section_correlation(mpr: dict) -> str:
details = mpr.get("details", {})
rows: list[str] = []
for period_key, window_label in _WINDOW_LABELS.items():
if period_key in details:
corr = details[period_key].get("correlation", 0)
rows.append(f"| {period_key[0]} | {corr:.3f} | {window_label} |")
if not rows:
return "**🔗 多周期相关性分析**: 无数据" # ✅ 返回字符串
# 但如果某些情况下返回 "",就会触发bug
条件2: 内容超长触发截断
if len(content) > _MAX_CONTENT_LENGTH: # 25000字符
# 触发截断逻辑
影响范围
严重程度: 🟠 High
影响:
- ✅ 不会导致程序崩溃(有
else sections[:4]保护) - ❌ 截断后展示的内容不是预期的核心区块
- ❌ 用户可能看到次要信息而缺少关键信息
实际场景:
- 25000字符限制很宽松,实际很少触发
- 但一旦触发且有builder返回空,展示内容就会错乱
🟡 BUG #3: _section_correlation的tuple/string key不兼容(Medium)
完整因果链
输入触发
↓
multi_period_result 经过JSON序列化/反序列化
↓
dict的key从tuple变为string
例: ("5m", "7d") → "('5m', '7d')"
↓
_format_full() 调用 _section_correlation(mpr)
↓
_section_correlation() 第257-274行
↓
遍历 _WINDOW_LABELS.items() 第261行
↓
period_key = ("5m", "7d") ← tuple类型
↓
if period_key in details: 第262行
↓
检查: ("5m", "7d") in {"('5m', '7d')": {...}} ← 🔥 key类型不匹配
↓
条件为False ← ❌ 无法匹配
↓
不会append到rows ← ❌ 所有周期都匹配失败
↓
if not rows: 第266行
↓
return "**🔗 多周期相关性分析**: 无数据" ← ❌ 即使有数据也显示"无数据"
详细调用路径
路径: JSON反序列化 → multi_period_result → _section_correlation → tuple key查找 → 匹配失败 → 显示"无数据"
关键代码位置:
-
期望的数据结构 (内存中直接传递)
multi_period_result = { "details": { ("5m", "7d"): { # ✅ tuple key "correlation": 0.85, "zscore": 2.5, ... }, ("1h", "30d"): {...}, ("4h", "60d"): {...}, } } -
JSON序列化后的数据结构
# JSON.stringify 后 { "details": { "('5m', '7d')": { # ❌ string key "correlation": 0.85, "zscore": 2.5, ... }, "('1h', '30d')": {...}, "('4h', '60d')": {...}, } } -
问题代码 -
signal_alert_formatter.py:261-264for period_key, window_label in _WINDOW_LABELS.items(): if period_key in details: # ❌ tuple in dict with string keys corr = details[period_key].get("correlation", 0) rows.append(f"| {period_key[0]} | {corr:.3f} | {window_label} |") -
正确的处理方式 (其他区块已使用)
# _section_health_monitor 第312行 period_data = _get_period_data(details, ("4h", "60d")) # _get_period_data 实现 第29-47行 def _get_period_data(details: dict, period: tuple) -> dict: # 先尝试 tuple key data = details.get(period) if data: return data # 再尝试 string key str_key = str(period) return details.get(str_key, {})
状态变化
| 数据来源 | key类型 | 匹配结果 | 显示结果 |
|---|---|---|---|
| 内存直接传递 | tuple | ✅ 匹配成功 | ✅ 显示数据 |
| JSON反序列化 | string | ❌ 匹配失败 | ❌ "无数据" |
出错点定位
文件: src/utils/monitoring/signal_alert_formatter.py
行号: 262
错误代码:
if period_key in details: # ❌ 没有使用_get_period_data()
corr = details[period_key].get("correlation", 0)
对比: 同文件的其他区块
# _section_health_monitor 第312行 ✅
period_data = _get_period_data(details, ("4h", "60d"))
# _section_window_comparison 第346行 ✅
period_data = _get_period_data(details, ("4h", "60d"))
根本原因
代码不一致: 没有使用统一的数据访问辅助函数
具体原因:
- 缺少辅助函数调用:
_section_correlation直接访问dict,而不是用_get_period_data() - Python类型限制: JSON序列化后,tuple key会自动转换为string
- 代码审查遗漏: 其他区块都使用了辅助函数,唯独这个区块遗漏
触发条件
当前代码中: 不会触发
# orchestrator.py:499
self._send_entry_alert(
signal, multi_period_result, # ← 直接传递内存中的dict
position, order_result, adaptive_z,
)
# multi_period_result 是从analyze_pair_advanced()返回的dict
# 没有经过JSON序列化
未来可能触发的场景:
# 场景1: 从API接收数据
response = requests.get("/api/multi_period")
multi_period_result = response.json() # ← JSON反序列化
format_signal_alert(..., multi_period_result, ...)
# 场景2: 从数据库读取
result = db.query("SELECT data FROM signals WHERE id = ?")
multi_period_result = json.loads(result["data"]) # ← JSON反序列化
format_signal_alert(..., multi_period_result, ...)
# 场景3: 从消息队列接收
message = redis.rpop("signal_queue")
multi_period_result = json.loads(message) # ← JSON反序列化
format_signal_alert(..., multi_period_result, ...)
影响范围
严重程度: 🟡 Medium
当前影响: 无(数据不经过JSON序列化)
潜在影响:
- 如果未来引入API/数据库/消息队列,会导致相关性区块永远显示"无数据"
- 用户会误以为系统没有相关性数据
🟡 BUG #4: 去重使用Python内置hash()不稳定(Medium)
完整因果链
输入触发
↓
alert_sender.send() 被调用 第38-72行
↓
计算content_hash 第60行
↓
content_hash = hash(content) ← 🔥 使用Python内置hash()
↓
[Python hash随机化机制]
↓
进程1: hash("相同内容") = 1234567890123456789
进程2: hash("相同内容") = 9876543210987654321 ← ❌ 不同的hash值
↓
去重检查 第61-63行
↓
if self._is_duplicate(content_hash):
return AlertStatus.THROTTLED
↓
[场景1: 单进程内]
✅ 去重有效(相同内容生成相同hash)
[场景2: 进程重启后]
❌ 去重失效(相同内容生成不同hash)
[场景3: 多进程部署]
❌ 各进程去重不同步(各自的hash值不同)
详细调用路径
路径: alert_sender.send → hash(content) → _is_duplicate → _recent_hashes
关键代码位置:
-
hash计算 -
alert_sender.py:60content_hash = hash(content) # ❌ 使用Python内置hash() -
去重检查 -
alert_sender.py:84-93def _is_duplicate(self, content_hash: int) -> bool: now = time.time() # 清理过期 if len(self._recent_hashes) > 100: expired = [k for k, t in self._recent_hashes.items() if now - t > self._dedup_window] for k in expired: del self._recent_hashes[k] return content_hash in self._recent_hashes and now - self._recent_hashes[content_hash] < self._dedup_window -
记录hash -
alert_sender.py:95-98def _record(self, pair_name: str, content_hash: int): now = time.time() self._history[pair_name].append(now) self._recent_hashes[content_hash] = now # ← 存储hash值和时间戳
Python hash随机化机制
原理: Python 3.3+ 默认启用 PYTHONHASHSEED 随机化
# 进程1
>>> hash("test content")
-1234567890123456789
# 进程2 (重启后)
>>> hash("test content")
9876543210987654321 # ❌ 不同!
# 禁用随机化
$ PYTHONHASHSEED=0 python
>>> hash("test content")
5678901234567890123 # 固定值
$ PYTHONHASHSEED=0 python # 再次运行
>>> hash("test content")
5678901234567890123 # ✅ 相同
状态变化
场景1: 单进程运行
# T0: 第1次发送
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 1234567890123456789
_recent_hashes[1234567890123456789] = T0
# T1: 10秒后重复发送
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 1234567890123456789 (相同)
if 1234567890123456789 in _recent_hashes: # ✅ True
return AlertStatus.THROTTLED # ✅ 正确去重
场景2: 进程重启
# 进程1 (T0-T60)
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 1234567890123456789
_recent_hashes[1234567890123456789] = T30
# ── 进程重启 ──
# 进程2 (T70)
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 9876543210987654321 (不同!)
if 9876543210987654321 in _recent_hashes: # ❌ False (_recent_hashes已清空)
return AlertStatus.THROTTLED
# ✅ 不会触发去重(但T70距T30已经40秒,在60秒窗口内,理论上应该去重)
实际影响: 由于去重窗口只有60秒,而进程重启通常不会这么频繁,所以实际影响不大
场景3: 多进程部署
# 进程A
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 1234567890123456789
_recent_hashes[1234567890123456789] = T0
# 进程B (同时运行)
content = "开仓成功 BTC/USDC"
hash_value = hash(content) # = 9876543210987654321 (不同!)
if 9876543210987654321 in _recent_hashes: # ❌ False (进程B的_recent_hashes是空的)
return AlertStatus.THROTTLED
# ❌ 进程B会重复发送
出错点定位
文件: src/utils/monitoring/alert_sender.py
行号: 60
错误代码:
content_hash = hash(content) # ❌ 不稳定
根本原因
安全机制: Python的hash随机化是为了防止hash碰撞攻击
具体原因:
- Python 3.3+默认行为:
PYTHONHASHSEED默认为随机值 - 进程隔离: 每个进程有不同的hash seed
- 内存状态:
_recent_hashes是实例变量,进程重启后清空
为什么使用hash():
- 性能好: O(1)
- 方便: 直接返回int
- 但不稳定: 跨进程/重启后不一致
触发条件
条件1: 进程重启
# T0: 启动进程
python main.py
# T30: 发送告警 "开仓成功 BTC"
# hash("开仓成功 BTC") = 1234567890
# T40: 进程崩溃/重启
kill -9 <pid>
python main.py
# T50: 再次发送 "开仓成功 BTC"
# hash("开仓成功 BTC") = 9876543210 (不同)
# ❌ 不会被去重(虽然T50-T30=20秒 < 60秒窗口)
条件2: 多进程部署
# 进程1
python main.py --port 8001 &
# 进程2
python main.py --port 8002 &
# 同时收到相同信号
# 进程1: hash(content) = 1234567890
# 进程2: hash(content) = 9876543210
# ❌ 两个进程都会发送告警
影响范围
严重程度: 🟡 Medium
实际影响:
- ✅ 单进程短期内去重有效
- ❌ 进程重启后去重失效(但影响小,因为窗口只有60秒)
- ❌ 多进程部署时去重不同步
实际场景:
# 当前部署方式
# orchestrator.py 是单进程运行
# 所以实际影响很小
# 但如果未来:
# - 改为多进程部署(如gunicorn -w 4)
# - 或使用分布式架构(多台服务器)
# 就会出现去重失效问题
🟠 BUG #5: 异步队列满时静默丢弃(High)
完整因果链
输入触发
↓
alert_sender.send() 调用 sender_colourful()
↓
sender_colourful() 第89-121行
↓
尝试放入队列 第115-119行
↓
_send_queue.put_nowait((url, headers, payload))
↓
[队列状态检查]
↓
if len(_send_queue) >= maxsize: ← 🔥 队列已满
raise queue.Full
↓
except queue.Full: 第117行
↓
logger.warning('lark 告警队列已满,丢弃本次告警') ← ⚠️ 仅warning级别
return None ← 🔥 静默失败
↓
alert_sender.send() 第67行
↓
sender_colourful(content=content, title=title) ← 返回None
↓
没有检查返回值 ← ❌ 忽略None
↓
self._record(pair_name, content_hash) ← ❌ 继续执行
return AlertStatus.SUCCESS ← ❌ 错误!实际未发送
↓
orchestrator 收到 SUCCESS ← ❌ 误判成功
↓
不会降级到简短通知 ← ❌ 用户无通知
详细调用路径
路径: alert_sender.send → sender_colourful → put_nowait → queue.Full → return None → 忽略返回值 → 误判成功
关键代码位置:
-
队列定义 -
lark_bot.py:15_send_queue: queue.Queue = queue.Queue(maxsize=200) # ← 最大200条 -
入队操作 -
lark_bot.py:115-119try: _send_queue.put_nowait((url, headers, payload)) # ← 非阻塞put except queue.Full: logger.warning('lark 告警队列已满,丢弃本次告警') # ⚠️ 仅warning return None # 🔥 静默失败 -
调用方 -
alert_sender.py:66-72try: sender_colourful(content=content, title=title) # ← 没有接收返回值 self._record(pair_name, content_hash) return AlertStatus.SUCCESS # ← ❌ 即使队列满也返回SUCCESS except Exception as e: logger.error(f"告警发送失败: {pair_name} | {e}") return AlertStatus.FAILED -
队列消费 -
lark_bot.py:21-52def _send_worker(): """后台守护线程:消费告警队列并同步发送""" while not _shutdown_event.is_set(): try: url, headers, payload = _send_queue.get(timeout=2) except queue.Empty: continue # 发送逻辑(带重试) for attempt in range(LARK_MAX_RETRIES): try: response = requests.post(url, headers=headers, data=payload, timeout=LARK_REQUEST_TIMEOUT) # ... except Exception as e: # ...
队列状态变化
正常情况:
# 队列状态: [告警1, 告警2, ..., 告警50] (50/200)
# 新告警到达
_send_queue.put_nowait(new_alert)
# 队列状态: [告警1, 告警2, ..., 告警50, new_alert] (51/200)
# ✅ 成功入队
队列满情况:
# 队列状态: [告警1, 告警2, ..., 告警200] (200/200) ← 已满
# 新告警到达
_send_queue.put_nowait(new_alert) # ← 抛出queue.Full异常
# except queue.Full:
# return None ← 🔥 静默丢弃
# 队列状态: [告警1, 告警2, ..., 告警200] (200/200) ← 未变化
# ❌ 新告警被丢弃
队列满的触发条件
计算公式:
队列增长速度 = 告警产生速度 - 队列消费速度
# 假设:
告警产生速度 = 5条/秒
队列消费速度 = 0.5条/秒 (2秒/条)
队列增长速度 = 5 - 0.5 = 4.5条/秒
队列满时间 = 200条 / 4.5条/秒 ≈ 44秒
场景1: 高频告警
# 每秒产生5条告警
# 飞书API响应时间2秒/条
# 队列处理速度: 0.5条/秒
# T0: 队列空
# T10: 队列45条 (5*10 - 0.5*10 = 45)
# T20: 队列90条
# T30: 队列135条
# T40: 队列180条
# T44: 队列200条 ← 队列满
# T44+: 新告警全部丢弃
场景2: 飞书API故障
# 飞书API返回500错误或超时
# 队列消费速度 = 0 (重试也失败)
# T0: 队列空,告警速度1条/秒
# T200: 队列满 (1*200 = 200)
# T200+: 新告警全部丢弃
出错点定位
文件1: src/utils/monitoring/lark_bot.py
行号: 117-119
错误代码:
except queue.Full:
logger.warning('...') # ⚠️ 仅warning,应该是error
return None # 🔥 应该抛出异常,让调用方感知
文件2: src/utils/monitoring/alert_sender.py
行号: 67
错误代码:
sender_colourful(content=content, title=title) # ❌ 没有检查返回值
根本原因
接口设计缺陷: 失败时返回None而非抛出异常
具体原因:
- 返回值忽略:
sender_colourful()返回"queued"或None,但调用方不检查 - 异常处理不当: 队列满时返回None而非抛异常
- 状态误判:
alert_sender.send()即使队列满也返回AlertStatus.SUCCESS
设计问题:
# 当前设计
def sender_colourful(...):
try:
_send_queue.put_nowait(...)
return "queued" # ← 成功
except queue.Full:
return None # ← 失败,但调用方无法区分
# 应该的设计
def sender_colourful(...):
try:
_send_queue.put_nowait(...)
return "queued"
except queue.Full:
raise RuntimeError("Alert queue full") # ← 抛出异常
影响范围
严重程度: 🟠 High
影响场景:
- 高频告警: 每秒5条以上
- 飞书API故障: 临时不可用
- 网络抖动: 请求超时增加
实际影响:
- ❌ 告警被静默丢弃
- ❌ 调用方无法感知失败
- ❌ 不会触发降级机制
- ❌ 用户完全收不到通知
影响链路:
队列满
↓
sender_colourful() 返回 None
↓
alert_sender.send() 忽略返回值
↓
返回 AlertStatus.SUCCESS (错误!)
↓
orchestrator 认为发送成功
↓
不降级到简短通知
↓
用户无通知
📊 BUG优先级总结
| BUG | 严重程度 | 触发概率 | 影响范围 | 修复优先级 |
|---|---|---|---|---|
| #1 告警发送失败不降级 | 🔴 Critical | 高 | 所有开仓 | P0 |
| #5 队列满静默丢弃 | 🟠 High | 中 | 高频场景 | P1 |
| #2 sections索引错位 | 🟠 High | 低 | 超长告警 | P1 |
| #3 tuple/string key不兼容 | 🟡 Medium | 低 | 未来扩展 | P2 |
| #4 hash()不稳定 | 🟡 Medium | 低 | 多进程 | P2 |
🔧 修复建议
P0: BUG #1 (1小时)
# orchestrator.py:780-786
status = self._alert_sender.send(...)
if status != AlertStatus.SUCCESS:
logger.warning(f"详细告警失败({status.value}), 降级")
else:
return # 只有成功才返回
# 降级到简短通知
self._send_simple_entry_notification(...)
P1: BUG #5 (1小时)
# lark_bot.py:117-119
except queue.Full:
logger.error('lark 告警队列已满')
raise RuntimeError("Alert queue full")
# alert_sender.py:67
try:
sender_colourful(...)
except RuntimeError as e:
return AlertStatus.FAILED
P1: BUG #2 (30分钟)
# signal_alert_formatter.py:175-179
for name, builder, args in builders:
try:
section = builder(*args)
sections.append(section if section else f"**{name}**: ⚠️ 数据缺失")
except Exception as e:
sections.append(f"**{name}**: ⚠️ 数据缺失")
P2: BUG #3 (30分钟)
# signal_alert_formatter.py:262
# 改用辅助函数
for period_key, window_label in _WINDOW_LABELS.items():
pd = _get_period_data(details, period_key) # ✅
if pd:
corr = pd.get("correlation", 0)
rows.append(...)
P2: BUG #4 (30分钟)
# alert_sender.py:60
import hashlib
content_hash = hashlib.md5(content.encode('utf-8')).hexdigest()
报告生成时间: 2026-02-16
分析工具: Claude Code SuperClaude
分析方法: 静态代码分析 + 调用链追踪 + 状态机建模