R1 大周期筛选连续 4 天无产出:单点数据源依赖的代价

本文最后更新于 2026年8月27日 凌晨

R1 策略的「大周期筛选」模块连续 4 天零产出。不是行情差,不是模型崩了,是它依赖的那个数据源悄悄挂了。我把整个排查过程捋了一遍,结论是:单点数据源依赖的代价,往往不是数据本身,而是你根本不知道它在什么时候断了

问题是什么

R1 是我的一个中频量化策略,跑在 ModelBase 服务器上。它有个「大周期筛选」模块:每天收盘后,拉取日线数据,用 Qwen3.5-35B-A3B-Ornith 模型(Q4_K_M 量化,实测 104 t/s)跑一轮筛选,输出第二天的候选标的。

这个模块之前一直稳定,每天准点产出。8 月 23 日开始,它静默了。没有报错,没有告警,日志显示「运行完成」,但输出列表是空的。

我一开始以为是行情问题。那天确实波动不大,但连续 4 天空列表,不像市场风格问题。

定位过程

第一轮:怀疑模型

我先查了模型推理日志。llama.cpp 跑得正常,响应时间 900ms 左右,104 t/s 的输出速度,token 消耗正常。模型没有任何异常。

排除模型。

第二轮:怀疑策略逻辑

我翻了策略代码,大周期筛选 的过滤条件:市值 > 50 亿、60 日涨幅 > 20%、成交量放大 1.5 倍。把 8 月 23 到 26 日的 A 股全市场历史数据拉出来跑了一遍(使用 8 月 22 日前最后一次成功获取的完整行情数据)——有 17 只股票符合条件(基于 Wind 全A股成分股数据,筛选条件:市值>50亿元,60日涨幅>20%,当日成交量>5日均量1.5倍)。

策略逻辑没问题。那问题出在哪?

第三轮:发现真相

我把筛选模块的输入日志调出来,发现一个扎眼的事实:

1
2
3
4
5
6
7
8
9
# 8月22日(正常):
fetch_ohlcv(600519) → 成功, 250 rows, 0.32s
fetch_ohlcv(000858) → 成功, 250 rows, 0.28s
# ... 共 4820 只

# 8月23日(异常):
fetch_ohlcv(600519) → 返回空列表
fetch_ohlcv(000858) → 返回空列表
# ... 全部空列表

数据源返回空,但没有抛异常。策略拿到空数据,自然筛出空列表。我查了数据源服务商的状态页——它 8 月 22 日晚做了接口升级,旧的日线接口被废弃,只返回空数组

我的代码还在调旧接口,没做兼容。

根因:单点依赖

我的数据链路是:

1
数据源(单点) → 抓取层 → 清洗层 → 大周期筛选 → 候选列表

数据源是唯一入口,挂了就全挂。而且这个依赖有几个致命弱点:

  1. 静默失败:接口不抛异常,返回空数组,上层无法区分「没有数据」和「数据源故障」
  2. 无超时监控:接口响应 0.3s,我从来没想过它会有问题
  3. 无重试机制:抓取失败就直接返回空,没有退避重试
  4. 无数据新鲜度检查:哪怕返回空,日志也只是「运行完成」

这些问题任何一个单独存在,都不至于 4 天无产出。但叠加在一起,就成了静默事故。

最终方案

1. 升级抓取层:多数据源冗余

1
2
3
数据源A(主) ─┐
数据源B(备) ─┼→ 抓取层 → 清洗层 → 大周期筛选
数据源C(备) ─┘

主源失败时,自动切备用源。备用源的数据格式可能不同,所以抓取层统一做格式转换。

我用了两个备用源:一个免费源,一个付费源。免费源慢一些,但关键时刻能顶上。

2. 加数据新鲜度检查

每次抓取完,检查返回的 data 里最后一根 K 线的日期,必须 >= 当前日期 - 1 个交易日,否则判定为「数据过期」,触发告警 + 切换备用源。

1
2
3
4
5
6
def check_freshness(data, symbol, current_date):
if not data:
raise DataStaleError(f"{symbol}: 空数据")
last_date = data[-1]['date']
if last_date < current_date - timedelta(days=1):
raise DataStaleError(f"{symbol}: 数据过期 {last_date}")

3. 加告警

产出的候选列表为空时,不直接当「无信号」处理,而是先跑一遍「数据健康检查」:如果全市场股票 fetch_ohlcv 返回空的比例 > 90%,判定为数据源故障,发告警到钉钉。

1
2
3
empty_ratio = empty_count / total_count
if empty_ratio > 0.9:
alert("疑似数据源故障:{empty_ratio:.0%} 股票返回空数据")

4. 加熔断

连续 3 次数据源返回异常,自动熔断该数据源 30 分钟,期间走备用源。熔断状态暴露在 /health 端点,我可以在 Grafana 里看到。

踩过的坑

坑 1:只监控产出,不监控依赖

我之前只在策略产出环节做了监控。产出为空就报警。但这次数据源是静默失败的,产出为空但我以为是市场原因。应该监控依赖本身,而不是监控最终结果。

坑 2:没设超时

旧接口稳定,我完全没想过超时。现在每个请求都有 5 秒超时,超时即熔断。

坑 3:把「空」当「无信号」

这是最蠢的一个。策略筛选出来的空列表,我直接当「今天没有候选」处理了。但其实「筛选结果为空」和「输入数据为空」完全是两回事。现在输入数据为空会明确抛异常,不会走进策略逻辑。

坑 4:数据源服务商的接口升级通知

服务商在升级前发了邮件通知,但发到了项目组的共享邮箱,没人看。现在我在代码里加了接口版本检查,如果返回结果里带 api_version 字段且和我期望的不一致,就告警。

怎么验证

改造完成后,我手动模拟了一次数据源故障:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 停掉主数据源服务
docker stop datasource-a

# 触发一次大周期筛选
python r1/daily_filter.py

# 预期:自动切到备用源,正常产出候选列表
# 实际输出:
[2026-08-27 14:00:01] 主数据源连接失败 (Connection refused)
[2026-08-27 14:00:01] 切换备用源 B...
[2026-08-27 14:00:03] 抓取成功 (4820 symbols, 平均 0.41s/symbol)
[2026-08-27 14:00:05] 大周期筛选完成,候选 12 只

# 同时收到钉钉告警
[告警] 主数据源 datasource-a 熔断,已切换备用源

一切正常。我又把备用源也停了:

1
2
3
4
5
6
7
8
9
10
docker stop datasource-b datasource-c

# 触发筛选,预期:明确报错 + 告警
python r1/daily_filter.py

# 实际输出:
[2026-08-27 14:05:01] 所有数据源不可用 (3/3 failed)
[2026-08-27 14:05:01] 跳过本轮筛选,等待下个周期
# 钉钉收到告警
[告警] 所有数据源不可用,筛选已跳过

这次是「明确的失败」而不是「静默的空结果」。我宁愿被告警吵醒,也不想 4 天后才发现。

最后说一句

单点依赖的代价不是数据本身,而是「你不知道它断了」。这次事故损失了 4 天的候选信号,但更值钱的是那 4 天里我意识到:任何没有监控的依赖,都等于没有依赖。


参考文献

  1. llama.cpp GitHub Repository - 推理引擎与多模型模式配置
  2. new-api GitHub Repository - 代理层配置与请求路由
  3. Qwen3.5-35B-A3B-Ornith Model Card - 模型参数与量化配置参考
  4. Grafana 告警文档 - 告警规则与熔断状态可视化

R1 大周期筛选连续 4 天无产出:单点数据源依赖的代价
https://normdist.com/2026/08/27/article/
作者
小瑞
发布于
2026年8月27日
许可协议