启动数据自愈系统BUG分析3
数据自愈系统严重 BUG — 完整因果链分析
分析日期: 2026-02-15
涉及文件:src/utils/data_healing/orchestrator.py,continuity_checker.py,config.py,src/services/realtime_kline_service_base.py
概览
在 orchestrator.py、continuity_checker.py、config.py 以及调用方 realtime_kline_service_base.py 中发现了 3 个严重 BUG,它们协同作用,使得数据自愈系统在启动时基本无法正确工作。
BUG #1(致命):时间间隔配置不匹配 — 连续性检查完全失效
因果链
| 阶段 | 详情 |
|---|---|
| 输入 | _run_data_healing() 调用 DataHealingOrchestrator(repair_timeframe='4h'),然后 heal_and_prepare(required_count=144) |
| 状态变化 | 编排器以 4h 周期 加载 K 线和计算 zscore,每条记录间隔 240 分钟 |
| 调用路径 | heal_and_prepare() → _load_zscore_history() → checker.check_continuity() |
| 出错点 | continuity_checker.py:31: self.expected_interval = timedelta(minutes=EXPECTED_INTERVAL_MINUTES) = 5 分钟 |
| 根因 | config.py:10 硬编码 EXPECTED_INTERVAL_MINUTES = 5,而实际运行用 4h (240 分钟) 间隔的数据 |
具体推演
数据库中的 zscore_4h 记录时间间隔:240 分钟
连续性检查器的预期间隔:5 分钟 + 1 分钟容差 = 6 分钟
实际间隔 240min > 预期间隔 6min → 每对相邻记录之间被判定存在 ~47 个"缺失"时间点
后果链:
- 连续性检查永远返回
is_continuous=False— 即使数据完全连续 - 生成大量虚假缺失时间点(间隔 5 分钟),如
[08:05, 08:10, 08:15, ..., 12:00] - 这些 5 分钟间隔的时间点被传入
RepairExecutor.repair() repair()尝试用timeframe='4h'的 K 线计算这些 5 分钟点的 zscore → 窗口提取失败(因为 4h K 线无法精确对齐到 5 分钟点位)- 修复返回
repaired_count=0→ 认为"修复无进展",提前退出循环 - 最终质量评估基于错误的
missing_count→ 数据质量被严重低估
⚠️ CAUTION: 这意味着 即使数据库中有完美连续的 4h zscore 数据,系统也会判定为"不连续"并尝试无效修复,最终可能以
degraded或failed状态启动。
BUG #2(严重):缺少数据新鲜度检查 — 过时数据误判为"就绪"
因果链
| 阶段 | 详情 |
|---|---|
| 输入 | 系统停机 N 小时后重启,heal_and_prepare() 开始执行 |
| 状态变化 | _load_zscore_history() 从 DB 加载数据,最新记录时间为 N 小时前 |
| 调用路径 | heal_and_prepare() → check_continuity() → 判断 is_continuous=True(假设 BUG#1 不存在)→ L210: if is_continuous and len(records) >= required_count: break |
| 出错点 | orchestrator.py:210: 仅检查数量和内部连续性,不检查数据是否新鲜 |
| 根因 | 没有将 最新记录时间 与 当前时间 进行比较 |
具体推演
场景:系统停机了 12 小时后重启
数据库中有 144 条连续记录,最新的 kline_time = 12 小时前
检查结果:
len(records)=144 >= required_count=144 ✅
is_continuous=True ✅
→ 直接 break,返回 status='ready' ❌
后果:
- 系统以
ready状态启动,使用 12 小时前的过时数据 - 在等待新的 WebSocket 数据填充之前的这段时间,策略引擎基于严重过时的 zscore 做出交易决策
- 可能导致错误的开仓/平仓操作
⚠️ WARNING: 这个 BUG 在长时间停机(如维护、部署)后重启时尤其危险。
BUG #3(严重):时间窗口与数据量需求严重不匹配 — 4h 数据永远加载不足
因果链
| 阶段 | 详情 |
|---|---|
| 输入 | heal_and_prepare(required_count=144) → _load_zscore_history(144) |
| 状态变化 | 函数尝试在 [12h, 24h, 48h] 的时间范围内查找 144 条 zscore_4h 记录 |
| 调用路径 | orchestrator.py:372: time_ranges = [12, 24, 48] |
| 出错点 | 144 条 × 4 小时间隔 = 576 小时 (24 天) 的数据,但最大查询窗口只有 48 小时 |
| 根因 | required_count=144 是按 5 分钟间隔设计的(144×5min = 12h),但 repair_timeframe='4h' 意味着需要 144×4h = 576h |
具体推演
required_count = 144
repair_timeframe = 4h → 每条记录间隔 4 小时
理论需要数据跨度: 144 × 4h = 576h = 24天
实际最大查询窗口: 48h
48h 内最多能有: 48/4 = 12 条 4h 数据
后果链:
_load_zscore_history遍历所有时间范围[12h, 24h, 48h]- 48h 窗口内只能获取最多 12 条记录
len(rows)=12 < required_count=144→ 条件rows and len(rows) >= required_count永远为False- 最终
records = rows(最多 12 条),永远无法满足 144 条的需求 - 后续连续性检查在只有 12 条记录情况下计算完整度 =
12/144 = 8.3%→ F 级 _determine_status返回failed(因为8.3% < 60%)
⚠️ CAUTION: 即使数据库中有完整的 24 天 4h zscore 数据,系统也无法加载到足够数量,导致自愈必然失败。
BUG 联合效应
三个 BUG 形成"死亡三角",使得数据自愈系统在某些情况下完全失效:
graph TD
A["系统启动<br>repair_timeframe='4h'<br>required_count=144"] --> B["_load_zscore_history()"]
B --> C{"48h 窗口内<br>能找到 ≥144 条?"}
C -->|"不可能(最多12条)"| D["BUG #3<br>数据量永远不足"]
D --> E["传入 ≤12 条记录"]
E --> F["check_continuity()"]
F --> G{"每对记录间隔<br>240min vs 5min+1min?"}
G -->|"240 >> 6"| H["BUG #1<br>生成大量虚假缺失点"]
H --> I["repair() 尝试按5min<br>时间点修复4h数据"]
I --> J["修复失败<br>repaired_count=0"]
J --> K["质量评估: F级<br>completeness ≈ 8%"]
K --> L["status = 'failed'"]
style D fill:#ff6b6b,color:#fff
style H fill:#ff6b6b,color:#fff
style L fill:#ff6b6b,color:#fff
即使假设修复了 BUG #1 和 #3,BUG #2 仍然存在:
graph TD
A["假设 BUG#1 #3 已修复<br>数据加载正确"] --> B{"数据内部连续?"}
B -->|"Yes"| C{"数据量 ≥ 144?"}
C -->|"Yes"| D["直接 break ✅"]
D --> E["返回 status='ready'"]
E --> F{"最新记录是否新鲜?"}
F -->|"未检查!"| G["BUG #2<br>可能使用 N 小时前的旧数据"]
G --> H["策略引擎基于过时数据交易"]
style G fill:#ff6b6b,color:#fff
style H fill:#ff6b6b,color:#fff
修复建议
BUG #1 修复方向
EXPECTED_INTERVAL_MINUTES 不应硬编码为 5,应该从 repair_timeframe 动态推导:
# config.py 中删除硬编码的 EXPECTED_INTERVAL_MINUTES = 5
# 改为在 orchestrator 初始化时动态计算
TIMEFRAME_TO_MINUTES = {'5m': 5, '1h': 60, '4h': 240}
self.expected_interval_minutes = TIMEFRAME_TO_MINUTES[repair_timeframe]
BUG #2 修复方向
在 heal_and_prepare() 的连续性判断后增加新鲜度检查:
if is_continuous and len(records) >= required_count:
# 新增:检查数据是否新鲜
latest_time = max(r['kline_time'] for r in records)
now = self._get_db_now()
staleness = (now - latest_time).total_seconds() / 60
if staleness > expected_interval_minutes * 2:
# 数据过时,生成缺失时间点并继续修复
...
BUG #3 修复方向
_load_zscore_history 的时间窗口应根据 required_count 和实际间隔动态计算:
# 根据实际时间间隔计算所需时间跨度
needed_hours = (required_count * self.expected_interval_minutes) / 60
# 加 20% 余量
time_ranges = [needed_hours * 1.2]