开仓信号详细告警 - 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忽略 → 用户无通知

关键代码位置:

  1. 入口 - orchestrator.py:498-499

    # 发送开仓告警
    self._send_entry_alert(
        signal, multi_period_result, position, order_result, adaptive_z,
    )
    
  2. 告警发送 - orchestrator.py:780-786

    priority = "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  # ❌ 直接返回,不检查是否成功
    
  3. 限流逻辑 - alert_sender.py:55-57

    if priority != "high" and not self._check_rate_limit(pair_name):
        logger.info(f"告警被限流: {pair_name}")
        return AlertStatus.THROTTLED  # ← 返回THROTTLED
    
  4. 限流检查 - alert_sender.py:74-82

    def _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: 直接返回,不检查成功与否

根本原因

设计缺陷: 告警系统与降级机制之间缺少状态反馈

具体原因:

  1. 接口设计不一致: AlertSender.send() 返回 AlertStatus 枚举,但调用方不检查
  2. 降级条件错误: 降级仅在 format_signal_alert() 抛异常时触发,而限流/去重/发送失败时返回错误枚举而非异常
  3. 控制流缺陷: 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 → 超长截断

关键代码位置:

  1. builders定义 - signal_alert_formatter.py:159-170

    builders = [
        ("基本信息", _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]
    ]
    
  2. 区块构建循环 - signal_alert_formatter.py:172-179

    for 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占位符
    
  3. 超长截断 - signal_alert_formatter.py:184-188

    if 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数组的对应关系不是强制的

具体原因:

  1. 条件append: 使用 if section: 而不是无条件append
  2. 空字符串判断: 空字符串被Python判断为falsy值
  3. 索引硬编码: 截断逻辑使用硬编码索引 [1], [7], [8], [9]
  4. 异常处理不一致:
    • 抛异常时: 会append占位符 "**{name}**: ⚠️ 数据缺失"
    • 返回空字符串时: 不会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查找 → 匹配失败 → 显示"无数据"

关键代码位置:

  1. 期望的数据结构 (内存中直接传递)

    multi_period_result = {
        "details": {
            ("5m", "7d"): {      # ✅ tuple key
                "correlation": 0.85,
                "zscore": 2.5,
                ...
            },
            ("1h", "30d"): {...},
            ("4h", "60d"): {...},
        }
    }
    
  2. JSON序列化后的数据结构

    # JSON.stringify 后
    {
        "details": {
            "('5m', '7d')": {    # ❌ string key
                "correlation": 0.85,
                "zscore": 2.5,
                ...
            },
            "('1h', '30d')": {...},
            "('4h', '60d')": {...},
        }
    }
    
  3. 问题代码 - signal_alert_formatter.py:261-264

    for 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} |")
    
  4. 正确的处理方式 (其他区块已使用)

    # _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"))

根本原因

代码不一致: 没有使用统一的数据访问辅助函数

具体原因:

  1. 缺少辅助函数调用: _section_correlation 直接访问dict,而不是用 _get_period_data()
  2. Python类型限制: JSON序列化后,tuple key会自动转换为string
  3. 代码审查遗漏: 其他区块都使用了辅助函数,唯独这个区块遗漏

触发条件

当前代码中: 不会触发

# 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

关键代码位置:

  1. hash计算 - alert_sender.py:60

    content_hash = hash(content)  # ❌ 使用Python内置hash()
    
  2. 去重检查 - alert_sender.py:84-93

    def _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
    
  3. 记录hash - alert_sender.py:95-98

    def _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碰撞攻击

具体原因:

  1. Python 3.3+默认行为: PYTHONHASHSEED 默认为随机值
  2. 进程隔离: 每个进程有不同的hash seed
  3. 内存状态: _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 → 忽略返回值 → 误判成功

关键代码位置:

  1. 队列定义 - lark_bot.py:15

    _send_queue: queue.Queue = queue.Queue(maxsize=200)  # ← 最大200条
    
  2. 入队操作 - lark_bot.py:115-119

    try:
        _send_queue.put_nowait((url, headers, payload))  # ← 非阻塞put
    except queue.Full:
        logger.warning('lark 告警队列已满,丢弃本次告警')  # ⚠️ 仅warning
        return None  # 🔥 静默失败
    
  3. 调用方 - alert_sender.py:66-72

    try:
        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
    
  4. 队列消费 - lark_bot.py:21-52

    def _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而非抛出异常

具体原因:

  1. 返回值忽略: sender_colourful() 返回 "queued"None,但调用方不检查
  2. 异常处理不当: 队列满时返回None而非抛异常
  3. 状态误判: 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
分析方法: 静态代码分析 + 调用链追踪 + 状态机建模

Read more

跑步的技巧(滚动落地)

“滚动落地(rolling contact / rolling foot strike)”不是一种教条式的“脚法”,而是一种 让冲击沿着整只脚、整条后链逐级传递的落地机制。 它的核心不是“你先用哪儿着地”,而是: 你的脚落地之后,冲击是不是像轮子一样滚过去,而不是像锤子一样砸下去。 这就是滚动落地的本质。 一、什么叫“滚动落地”? 你可以把它理解成两种完全不同的落地方式: 1. 砸地(撞击式) 脚像锤子一样拍到地上: * 要么后跟先砸 * 要么前掌先戳 * 冲击集中在一个点 * 一个结构瞬间吃掉大部分载荷 结果就是: * 后跟砸 → 膝盖难受 * 前掌戳 → 前脚掌磨烂 * 都不是长跑友好模式 这叫 撞击式着地(impact strike)。 2. 滚地(滚动式) 脚像轮胎一样“滚”过地面: * 不是某一点硬砸 * 而是外侧中足先轻触 * 再向前滚到前掌 * 最后从大脚趾蹬离

By SHI XIAOLONG

AMI的优越性

世界模型(World Models)的具体例子 如下,我按类型分类,便于理解。每类都附带实际实现、演示效果和应用场景。 1. Yann LeCun / Meta 的 JEPA 系列(最直接对应“世界模型”概念) 这些是 LeCun 主张的非生成式抽象预测世界模型代表。 * I-JEPA(Image JEPA,2023) 输入一张图像,模型把不同区域(context 和 target)编码成抽象表示,然后预测 target 的表示(不在像素级别重建)。 例子:给定一张遮挡了部分物体的图片,模型能预测“被遮挡物体的大致位置和属性”,构建对物体持久性和空间关系的理解。 这是一个“原始世界模型”,能学习物理常识(如物体不会凭空消失)。 * V-JEPA / V-JEPA 2(Video JEPA,

By SHI XIAOLONG

什么是:“世界模型(World Models)”

世界模型(World Models) 是人工智能领域的一个核心概念,尤其在 Yann LeCun 等研究者推动的下一代 AI 架构中占据中心位置。它指的是 AI 系统在内部构建的对现实世界的抽象模拟或内部表示,让机器能够像人类或动物一样“理解”物理世界、预测未来、规划行动。 简单比喻 想象你闭上眼睛也能“看到”房间里的物体会如何移动、碰撞或掉落——这就是你大脑里的世界模型。AI 的世界模型就是类似的“数字孪生”(digital twin)或“内部模拟器”:它不是简单记住数据,而是学习世界的动态、因果关系和物理直觉(如重力、物体持久性、遮挡、因果等)。 为什么需要世界模型? 当前主流的大型语言模型(LLM) 擅长处理文本(统计模式预测),但存在根本局限: * 缺乏对物理世界的真正理解 → 容易“幻觉”、无法可靠规划。 * 样本效率低 → 人类/

By SHI XIAOLONG

K线周期可配置化设计方案

K线周期可配置化设计方案 1. 背景与目标 当前 Beta 套利策略的 K 线周期硬编码为 "1h",分散在多个文件中。需要: 1. 将 K 线周期从 1h 改为 2h 2. 提取为环境变量 BETA_ARB_KLINE_INTERVAL,使其可在 .env 中配置 2. 影响范围分析 2.1 需要修改的文件(共 6 个) 文件 硬编码位置 修改内容 src/trading/config.py BetaArbConfig dataclass 新增 kline_interval 字段,

By SHI XIAOLONG