"开仓信号详细告警"模块 Bug 分析报告 main

"开仓信号详细告警"模块完整因果链分析

📋 分析目标

建立从输入到BUG的完整因果链:输入 → 状态变化 → 调用路径 → 出错点 → 根因


1️⃣ 输入(Input)

触发条件

# orchestrator.py:515-533
open_result = self._position_manager.open_position(signal, adaptive_z=adaptive_z)

if open_result:
    position, order_result = open_result
    # ...保存信号、更新统计...

    # 发送开仓告警(触发点)
    self._send_entry_alert(
        signal,                    # 交易信号对象
        multi_period_result,       # 多周期分析结果 ← 关键输入
        position,                  # 仓位信息
        order_result,              # 订单结果
        adaptive_z,                # 自适应Z值
    )

关键输入数据结构

multi_period_result:

{
    "zscore_list": [2.2, 2.5, 2.75],  # 多周期Z-score
    "details": {
        ("4h", "60d"): {
            "correlation": 0.83,
            "cointegration_old": {"passed": True, "adf_pvalue": 0.001},
            "health_monitor": {
                "long_window": {
                    "health_score": 77.0,
                    "hurst": 0.35  # ← Hurst指数(趋势性指标)
                },
                "short_window": {
                    "health_score": 80.0,
                    "hurst": 0.38
                }
            }
        },
        # ...其他周期数据...
    }
}

Hurst 指数语义:

  • < 0.4: 均值回归特性(适合配对交易)
  • 0.4-0.6: 随机游走
  • > 0.6: 趋势性强(破坏配对交易基础)

2️⃣ 状态变化(State Changes)

阶段1:风险评估入口

文件: orchestrator.py:892-934

def _send_entry_alert(self, signal, multi_period_result, ...):
    """发送开仓告警(详细告警 or 简短通知)"""
    if ENABLE_SIGNAL_DETAIL_ALERT:
        try:
            # 调用格式化器
            title, content = format_signal_alert(
                signal=signal,
                multi_period_result=multi_period_result,  # ← 传递多周期数据
                ...
            )

阶段2:格式化器调用风险评估

文件: signal_alert_formatter.py:67-128

def format_signal_alert(..., multi_period_result, ...):
    try:
        if level == AlertLevel.FULL:
            return _format_full(
                signal, multi_period_result, ...  # ← 继续传递
            )

文件: signal_alert_formatter.py (内部调用)

def _format_full(...):
    # ...构建告警内容...
    risk_assessment = evaluate_risk(multi_period_result)  # ← 调用风险评估

阶段3:风险评估计算(关键状态变化

文件: risk_evaluator.py:67-95

def evaluate_risk(multi_period_result: dict) -> RiskAssessment:
    """执行多维度风险评估"""
    details = multi_period_result.get("details", {})
    zscore_list = multi_period_result.get("zscore_list", [])

    trend = _calc_trend_risk(details)          # ← 维度1:趋势风险
    coint = _calc_cointegration_score(details) # 维度2:协整质量
    corr = _calc_correlation_score(details)    # 维度3:相关性
    zscore = _calc_zscore_consistency(zscore_list)  # 维度4:Z-score一致性
    health = _calc_health_score(details)       # 维度5:协整健康度

    return _calc_overall(trend, coint, corr, zscore, health)  # ← 综合评分

状态变化3.1:趋势风险计算

文件: risk_evaluator.py:101-116

def _calc_trend_risk(details: dict) -> dict:
    """趋势风险评估 — score 越高风险越大"""  # ← 关键注释!
    hurst = _extract_hurst(details)  # 从 health_monitor 提取 hurst 指数

    # 状态转换:hurst → risk score
    if hurst < 0.4:  # 均值回归
        score, level = 20.0, "低风险-均值回归"
        detail = "均值回归特性,适合配对交易"
    elif hurst < 0.6:  # 随机游走
        score, level = 50.0, "中风险-随机游走"
        detail = "接近随机游走,回归不明显"
    else:  # 趋势性强
        score = min(20 + (hurst - 0.6) * 200, 100.0)  # 线性增长
        level = "高风险-趋势性强"
        detail = "趋势性强,可能破坏配对交易基础"

    # 返回状态:score 越高 = 风险越大
    return {"score": score, "level": level, "reason": f"Hurst={hurst:.3f}", "detail": detail}

状态示例:

  • 输入: hurst = 0.35
  • 输出: {"score": 20.0, "level": "低风险-均值回归", ...}
  • 语义: score=20 表示低风险

状态变化3.2:综合评分计算(第一次反转

文件: risk_evaluator.py:300-357

def _calc_overall(trend, coint, corr, zscore, health) -> RiskAssessment:
    """加权计算综合评分并生成评级"""

    # ⚠️ 关键状态转换:趋势风险反转为趋势质量
    weighted = (
        (100 - trend["score"]) * RISK_WEIGHTS["trend_risk"]  # ← 第1次反转(仅用于加权计算)
        + coint["score"] * RISK_WEIGHTS["cointegration"]     # 其他维度不反转
        + corr["score"] * RISK_WEIGHTS["correlation"]
        + zscore["score"] * RISK_WEIGHTS["zscore_consistency"]
        + health["score"] * RISK_WEIGHTS["health_monitor"]
    )

    # 计算综合评级
    if weighted >= 75:
        risk_level, rec, action = "低风险", "积极", "可按标准仓位执行"
    elif weighted >= 55:
        risk_level, rec, action = "中风险", "谨慎", "建议减半仓位或观望"
    else:
        risk_level, rec, action = "高风险", "警惕", "建议极小仓位试探或放弃"

    # ⚠️ 关键:返回的 details 中保存的是原始 trend["score"](未反转)
    return RiskAssessment(
        overall_score=round(weighted, 1),
        risk_level=risk_level,
        # ...
        details={
            "trend_risk": trend,  # ← 原始风险分(score=20表示低风险)
            "cointegration": coint,
            "correlation": corr,
            "zscore_consistency": zscore,
            "health_monitor": health,
        },
    )

状态变化示例:

# 输入
trend = {"score": 20.0, "level": "低风险-均值回归"}
coint = {"score": 75.0, "level": "优秀"}
# ...

# 计算
weighted = (100 - 20) * 0.25 + 75 * 0.25 + ... = 20 + 18.75 + ... ≈ 72.5
# weighted 中使用了反转后的值(80),但 details 中保存的是原始值(20)

# 输出
RiskAssessment(
    overall_score=72.5,
    risk_level="中风险",
    details={
        "trend_risk": {"score": 20.0, ...},  # ← 仍是原始风险分
        ...
    }
)

阶段4:格式化展示(第二次反转,BUG触发点

文件: signal_alert_formatter.py:370-394

def _section_risk_assessment(risk: RiskAssessment) -> str:
    d = risk.details
    rows: list[str] = []

    if "trend_risk" in d:
        # ⚠️ BUG触发点:第2次反转
        rows.append(f"| 趋势风险 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")
        #                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        #                        100 - 20 = 80(第2次反转)

    if "cointegration" in d:
        rows.append(f"| 协整质量 | {d['cointegration']['score']:.0f} | ...")  # 不反转

    if "correlation" in d:
        rows.append(f"| 相关性 | {d['correlation']['score']:.0f} | ...")  # 不反转
    # ...其他维度都不反转...

状态变化示例:

# 输入(从 risk.details 获取)
d['trend_risk']['score'] = 20.0  # 原始风险分(低风险)

# 计算
display_score = 100 - 20.0 = 80.0  # 第2次反转

# 输出(用户看到的)
"| 趋势风险 | 80 | 低风险-均值回归 | 25% |"

语义混淆:

  • 列名: "趋势风险"
  • 分数: 80(高分)
  • 等级: "低风险-均值回归"
  • 用户理解: "趋势风险得分80,风险很高?但等级说是低风险?"
  • 实际含义: 80 = 趋势安全性分(100 - 20),不是风险分

3️⃣ 完整调用路径(Call Path)

【入口】orchestrator.py
    ↓
  L530: self._send_entry_alert(signal, multi_period_result, ...)
    ↓
  L905: title, content = format_signal_alert(..., multi_period_result, ...)
    ↓
【格式化】signal_alert_formatter.py
    ↓
  L89: return _format_full(...)
    ↓
  内部: risk = evaluate_risk(multi_period_result)  # 调用风险评估
    ↓
【风险评估】risk_evaluator.py
    ↓
  L80: trend = _calc_trend_risk(details)
    ↓
  L102-116: 计算趋势风险分
    └─ 输出: {"score": 20.0}  ← score 越高风险越大
    ↓
  L305-306: weighted = (100 - trend["score"]) * 0.25  ← 第1次反转(仅用于加权)
    ↓
  L350-357: 返回 RiskAssessment
    └─ details["trend_risk"] = {"score": 20.0}  ← 仍保存原始风险分
    ↓
【格式化展示】signal_alert_formatter.py
    ↓
  L370: _section_risk_assessment(risk)
    ↓
  L375: f"趋势风险 | {100 - d['trend_risk']['score']}"  ← 第2次反转
    └─ 计算: 100 - 20 = 80
    ↓
【用户看到】
    "| 趋势风险 | 80 | 低风险-均值回归 | 25% |"
    ^^^^^^^^^  ^^   ^^^^^^^^^^^^^^^^^
    列名含义   分数   等级说明
    风险?     很高   但是低风险?  ← 语义混淆

4️⃣ 出错点(Error Point)

主要出错点

位置: signal_alert_formatter.py:375

rows.append(f"| 趋势风险 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")
#                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                        双重反转的第2次,导致语义混淆

错误假设

代码作者在编写 _section_risk_assessment() 时的错误假设:

假设: risk.details["trend_risk"]["score"] 已经是反转后的"趋势质量分"(越高越好)

实际: risk.details["trend_risk"]["score"] 仍是原始"趋势风险分"(越高风险越大)

为什么会有这个假设?

设计不一致导致的认知偏差:

  1. 其他4个维度的定义都是"质量分"(越高越好):

    • 协整质量: score 高 = 协整好
    • 相关性: score 高 = 相关性强
    • Z-score一致性: score 高 = 一致性好
    • 协整健康: score 高 = 健康好
  2. 只有趋势风险是"风险分"(越高风险越大):

    • 趋势风险: score 高 = 风险大
  3. _calc_overall() 中的反转只用于加权计算:

    weighted = (100 - trend["score"]) * 0.25  # 反转是为了统一方向
    

    但返回的 details["trend_risk"] 仍是原始值

  4. 展示逻辑的作者认为应该把所有维度统一为"质量分"展示,所以又做了一次反转


5️⃣ 根因(Root Cause)

根本原因分类

1. 设计层面:语义不一致

问题: 风险评估体系中混用了两种相反的语义:

维度 score 语义 展示处理
趋势风险 越高风险越大(风险分) 反转展示
协整质量 越高质量越好(质量分) 直接展示
相关性 越高质量越好(质量分) 直接展示
Z-score一致性 越高质量越好(质量分) 直接展示
协整健康 越高质量越好(质量分) 直接展示

根因: 5个维度中,4个用"质量分",1个用"风险分",造成设计不一致

2. 实现层面:双重反转错误

问题: 两处代码对 trend_risk["score"] 都做了反转:

  1. 第1次反转 (risk_evaluator.py:305-306):

    weighted = (100 - trend["score"]) * 0.25
    
    • 目的: 统一方向用于加权计算
    • 副作用: 给人错觉以为 details 中的 score 也被反转了
  2. 第2次反转 (signal_alert_formatter.py:375):

    f"趋势风险 | {100 - d['trend_risk']['score']}"
    
    • 目的: 统一展示为"质量分"
    • 问题: details 中的 score 其实是原始风险分,再反转就错了

根因: 第1次反转没有修改 details 中的值,第2次反转基于错误假设

3. 沟通层面:列名与语义不匹配

问题: 展示时的列名和分数语义不一致:

| 趋势风险 | 80 | 低风险-均值回归 | 25% |
  ^^^^^^^   ^^   ^^^^^^^^^^^^^^^
  风险?    高分  但是低风险?
  • 列名叫"趋势风险"(暗示分数越高风险越大)
  • 分数是 80(高分)
  • 等级是"风险-均值回归"
  • 结果: 用户困惑 — 到底是高风险还是低风险?

根因: 列名使用了"风险",但分数已经反转为"安全性"


6️⃣ 影响分析(Impact Analysis)

用户视角

场景1:均值回归信号(低风险)

输入: hurst = 0.35 → 原始风险分 = 20
展示: "| 趋势风险 | 80 | 低风险-均值回归 |"
用户: "趋势风险80分,很高啊?但等级说低风险?矛盾!"

场景2:趋势性强信号(高风险)

输入: hurst = 0.75 → 原始风险分 = 50
展示: "| 趋势风险 | 50 | 高风险-趋势性强 |"
用户: "趋势风险50分,中等?但等级说高风险?矛盾!"

决策影响

用户可能的误判:

  • 看到"趋势风险 80"认为风险很高,放弃本来应该积极执行的均值回归信号
  • 看到"趋势风险 50"认为风险中等,对高风险信号掉以轻心

实际不影响系统功能:

  • 开仓执行逻辑不依赖这个展示值
  • 综合评分(overall_score)计算是正确的(第1次反转是对的)
  • 只是展示给用户的信息有误导性

7️⃣ 修复方案(Solution)

方案1:不反转,直接展示原始风险分 ✅ 推荐

修改: signal_alert_formatter.py:375

# 修改前
rows.append(f"| 趋势风险 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")

# 修改后
rows.append(f"| 趋势风险 | {d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")

效果:

| 趋势风险 | 20 | 低风险-均值回归 | 25% |
  • 列名"趋势风险"+ 低分(20) + "低风险" = 语义一致 ✅

优点:

  • ✅ 最小改动(只删除 100 -
  • ✅ 语义一致(分数和列名匹配)
  • ✅ 与 risk_evaluator.py 中的定义一致

缺点:

  • ⚠️ 与其他4个维度的方向不一致(其他都是"越高越好")

方案2:改列名为"趋势安全性"

修改: signal_alert_formatter.py:375

# 修改前
rows.append(f"| 趋势风险 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")

# 修改后
rows.append(f"| 趋势安全性 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")
#            ^^^^^^^^

效果:

| 趋势安全性 | 80 | 低风险-均值回归 | 25% |
  • 列名"趋势安全性"+ 高分(80) + "低风险" = 语义一致 ✅

优点:

  • ✅ 语义一致(分数和列名匹配)
  • ✅ 与其他4个维度方向一致(都是"越高越好")
  • ✅ 用户理解更直观(安全性80 = 很安全)

缺点:

  • ⚠️ 需要修改列名(可能影响用户习惯)
  • ⚠️ "低风险-均值回归"这个等级文本仍有小冲突(安全性高但叫"低风险"?)

方案3:在 details 中保存反转后的值(最彻底)

修改: risk_evaluator.py:350-357

def _calc_overall(...):
    # 计算加权分数
    weighted = (100 - trend["score"]) * RISK_WEIGHTS["trend_risk"] + ...

    # ⚠️ 关键:修改 trend 字典中的 score 为反转后的值
    trend_inverted = trend.copy()
    trend_inverted["score"] = 100 - trend["score"]  # 反转后保存
    trend_inverted["level"] = trend["level"].replace("风险", "安全性")  # 修改等级文本

    return RiskAssessment(
        overall_score=round(weighted, 1),
        # ...
        details={
            "trend_risk": trend_inverted,  # ← 保存反转后的值
            # ...
        },
    )

效果:

# risk.details["trend_risk"]["score"] = 80(已反转)
# signal_alert_formatter.py:375 直接使用(不反转)
rows.append(f"| 趋势安全性 | {d['trend_risk']['score']:.0f} | ...")

优点:

  • ✅ 彻底解决问题(统一语义)
  • ✅ 展示代码不需要特殊处理
  • ✅ 所有维度都是"质量分"(越高越好)

缺点:

  • ⚠️ 改动较大(需要同时修改等级文本)
  • ⚠️ 可能影响其他使用 risk.details 的代码

方案对比

方案 改动量 语义一致性 用户体验 推荐度
方案1:不反转 最小 中等(风险分逆向) 需要理解"风险越低越好" ⭐⭐⭐⭐
方案2:改列名 最佳(安全性越高越好) ⭐⭐⭐⭐⭐
方案3:details反转 最高 最佳 ⭐⭐⭐

建议: 方案2(改列名)+ 方案1(不反转的简化版)

# 最终推荐代码
rows.append(f"| 趋势安全性 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")

8️⃣ 测试验证(Test Verification)

测试场景

# 场景1:低风险信号(均值回归)
input_hurst = 0.35
expected_score = 20.0
expected_display = "| 趋势安全性 | 80 | 低风险-均值回归 | 25% |"

# 场景2:中风险信号(随机游走)
input_hurst = 0.50
expected_score = 50.0
expected_display = "| 趋势安全性 | 50 | 中风险-随机游走 | 25% |"

# 场景3:高风险信号(趋势性强)
input_hurst = 0.75
expected_score = 50.0  # min(20 + (0.75 - 0.6) * 200, 100) = 50
expected_display = "| 趋势安全性 | 50 | 高风险-趋势性强 | 25% |"

📊 总结

因果链全景图

【输入】
hurst = 0.35 (均值回归特性)
    ↓
【状态1】risk_evaluator._calc_trend_risk()
score = 20.0 (低风险)
    ↓
【状态2】risk_evaluator._calc_overall()
weighted = (100 - 20) * 0.25 = 20 (第1次反转,用于加权)
details["trend_risk"]["score"] = 20.0 (仍保存原始值)
    ↓
【状态3】signal_alert_formatter._section_risk_assessment()
display = 100 - 20 = 80 (第2次反转,语义错误)
    ↓
【输出】
"| 趋势风险 | 80 | 低风险-均值回归 | 25% |"
         列名"风险" + 高分80 + "低风险" = 语义混淆 ❌
    ↓
【用户理解】
"趋势风险80分?很高啊?但为什么说低风险?矛盾!"

关键节点

  1. 输入: hurst = 0.35 → 均值回归特性
  2. 计算: score = 20.0 → 原始风险分(低风险)
  3. 第1次反转: (100 - 20) * 0.25 → 仅用于加权计算
  4. 第2次反转: 100 - 20 = 80BUG触发点
  5. 输出: "趋势风险 | 80" → 语义混淆

根因总结

层面 根因 解决方向
设计 5个维度中4个用"质量分",1个用"风险分",语义不统一 统一为"质量分"
实现 双重反转:第1次用于加权但未修改details,第2次基于错误假设 删除第2次反转或修改列名
沟通 列名"趋势风险"与反转后的"趋势安全性"语义不符 改列名或改分数

影响评估

  • 系统功能: ✅ 不影响(综合评分计算正确)
  • 用户决策: ⚠️ 可能误导(分数与直觉相反)
  • 严重性: 🟡 中高(不会崩溃,但影响判断)

推荐修复

方案2:改列名 + 保持反转

# signal_alert_formatter.py:375
rows.append(f"| 趋势安全性 | {100 - d['trend_risk']['score']:.0f} | {d['trend_risk']['level']} | 25% |")
#            ^^^^^^^^  ← 列名改为"安全性",分数保持反转

理由:

  • ✅ 改动最小(只改4个字)
  • ✅ 语义一致(安全性越高越好)
  • ✅ 与其他维度方向一致
  • ✅ 用户体验最佳

报告完成时间: 2026-02-16
分析深度: 完整因果链(输入→状态→路径→出错→根因)
修复建议: 方案2(改列名为"趋势安全性")

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