常见报错的自检方式
由hxgre创建,最终由hxgre 被浏览 17 用户
本文为 BigAlpha 2026 全球高校联赛 · AI 因子挖掘赛道的自检指引。
为防止验证集 / 测试集数据泄露,评估系统在参赛者提交代码后不会返回详细报错日志。这导致大家遇到报错时往往不知从何查起。本文整理了两类最常见的报错,并提供可在本地跑通的自检模板,帮助你在提交前把代码调通。
目录
一、评估系统是怎么跑你代码的
理解报错前,先理解评估系统对你 main() 函数做了什么。
你提交的代码,核心是一个 main(datasources, start_date, end_date) 函数。评估时,平台做的事情非常简单——替换这三个入参,然后调用你的函数:
datasources:把bar1m(分钟盘口量价)和financial(财务数据)这两个数据源,替换成验证集 / 测试集对应的数据表(真实表名不公开);start_date/end_date:替换成验证集 / 测试集的评估时间段(真实区间不公开)。
# 你本地自测时(训练集)
datasources = {'bar1m': 'bigalpha_2026_stock_bar1m', 'financial': 'bigalpha_2026_financial'}
start_date = '2019-01-01 00:00:00'
end_date = '2023-12-31 23:59:59'
# 评估系统实际运行时(仅示意,真实表名与区间不公开)
factor_data = main(
{'bar1m': '<验证集分钟表>', 'financial': '<验证集财务表>'},
'<验证集开始时间>',
'<验证集结束时间>',
)
哪些表会被替换,哪些不会
这是很多人踩坑的地方,务必分清:
-
只有
datasources里的bar1m和financial会被替换。 这两个数据源在公榜 / 私榜阶段指向的物理表不同,所以必须从datasources里取表名,绝对不能硬编码,否则评估系统换了表你却还在查旧表,直接报错或数据为空。bar1m = datasources["bar1m"] # ✅ 正确:从入参取表名 financial = datasources["financial"] # ✅ 同理 sql = f"SELECT ... FROM {bar1m}" # ❌ 错误:硬编码分钟表/财务表,公榜/私榜切换时必然出问题 sql = "SELECT ... FROM bigalpha_2026_stock_bar1m" -
其余数据表在各阶段都不替换,直接写表名即可。 例如股票池
bigalpha_2026_instruments、暴露度bigalpha_2026_exposure、因子库bigalpha_2026_factorlib等,它们在所有阶段都是同一张表,不需要、也不应该放进datasources。# ✅ 这些辅助数据表直接查,无需替换 stk_pool = dai.query("SELECT date, instrument FROM bigalpha_2026_instruments", ...).df()
一句话总结:分钟表、财务表从 datasources 取;其他辅助表直接写死。
验证集数据会向前多取一段
这一点非常重要,也是报错的高频原因:
为了让时序因子能在评估区间的开头几天正常计算,评估系统提供给你的验证集数据表,其实际数据的起始时间会比
start_date更早一段。但传给你main()的start_date/end_date仍然是评估区间本身。
举例说明:假设验证区间是 2024 年 1 月到 2024 年 10 月,那么数据表里其实从 2023 年 10 月就有数据了(向前多备了约 3 个月)。这段"提前量"是留给你算时序因子用的缓冲。
但是——评估系统不会替你去用这段缓冲。 你要在 main() 内部自己把查询窗口往前扩,才能吃到这段提前量;否则时序算子(lag / lead / 滚动窗口等)在区间开头会因为缺少历史数据而算出 NaN,最终触发覆盖度校验失败。
下面的自检一就是这个坑的完整复现。
二、自检一:公榜验证集数据的模拟检验
你会看到的报错
DataValidationError: 覆盖度检查失败:因子 factor 单日缺失率 > 40%
评估系统会检查每个交易日因子的覆盖度。如果某些交易日因子缺失率过高(NaN 太多),就会判定为覆盖度不足并报错。时序因子在评估区间开头几天大面积为 NaN,是导致这个报错最常见的原因。
用 bigalpha_2026_stock_bar1m_selftest 模拟评估
我们准备了一张自检数据表 bigalpha_2026_stock_bar1m_selftest,你可以把它当成"验证集数据"来模拟评估系统的运行流程:
- 模拟评估区间:2024 年 1 月 ~ 2024 年 10 月
- 数据实际起始:2023 年 10 月(即向前多取了约一个季度作为时序缓冲)
把 main() 的入参换成下面这样在本地跑一遍,就能提前发现覆盖度问题:
datasources = {'bar1m': 'bigalpha_2026_stock_bar1m_selftest'}
start_date = '2024-01-01 00:00:00' # 模拟验证集开始
end_date = '2024-10-31 23:59:59' # 模拟验证集结束
factor_data = main(datasources, start_date, end_date)
错误示范:没有向前扩窗
下面是一个典型的时序因子(N 日价格反转),它没有在内部往前扩查询窗口:
def main(datasources, start_date, end_date):
import pandas as pd
import dai
bar1m = datasources["bar1m"]
N = 5 # 反转窗口(交易日)
sql = f"""
WITH cte_daily AS (
SELECT
instrument,
strftime(date, '%Y-%m-%d') AS trading_day,
last(close ORDER BY date) AS close
FROM {bar1m}
WHERE close > 0
GROUP BY instrument, strftime(date, '%Y-%m-%d')
),
cte_lag AS (
SELECT
*,
lag(close, {N}) OVER (PARTITION BY instrument ORDER BY trading_day) AS prev_close
FROM cte_daily
)
SELECT
CAST(trading_day AS DATETIME) AS date,
instrument,
-1 * (close / prev_close - 1) AS factor
FROM cte_lag
"""
# ❌ 问题所在:filters 的下界就是 start_date,没有留缓冲
df = dai.query(
sql,
filters={'date': [start_date, end_date]},
compression=True,
).df()
stk_pool = dai.query(
"SELECT date, instrument FROM bigalpha_2026_instruments",
filters={'date': [start_date, end_date]},
).df()
df = pd.merge(df, stk_pool, how='inner', on=['date', 'instrument'])
return df
跑完检查 2024-01-02(评估区间第一天)的因子值,会发现开头几天几乎全是 NaN:
factor_data[factor_data['date'] == '2024-01-02']
# date instrument factor
# 0 2024-01-02 002911.SZ NaN ← 算 lag(close, 5) 需要往前 5 个交易日,
# ... 但 query 从 2024-01-01 才开始取数,
# 区间开头没有历史,因此全为 NaN
原因:lag(close, 5) 要用到 5 个交易日前的收盘价。当查询窗口从 2024-01-01 才开始时,2024-01-02 前面根本没有数据可供回溯,于是这几天的因子只能是 NaN。区间开头一批 NaN 累积起来,就触发了覆盖度 / 缺失率报错。
正确做法:在 main() 内部向前扩查询窗口
修复的核心是:查询数据时把下界往前推足够多的自然日(覆盖你时序算子需要的历史长度),算完因子后再用 start_date 把输出裁回评估区间。
def main(datasources, start_date, end_date):
import pandas as pd
import dai
bar1m = datasources["bar1m"]
N = 5 # 反转窗口(交易日)
# ✅ 关键:查询下界往前多取一段。N 个交易日需要更多自然日覆盖(含周末/节假日),
# 这里给足缓冲。经验上按交易日数的 2~3 倍估算自然日,宁多勿少。
BUFFER_DAYS = 20 # 对 N=5 的反转因子,20 个自然日足够覆盖 5 个交易日的历史
query_start_date = (pd.to_datetime(start_date) - pd.Timedelta(days=BUFFER_DAYS))
sql = f"""
WITH cte_daily AS (
SELECT
instrument,
strftime(date, '%Y-%m-%d') AS trading_day,
last(close ORDER BY date) AS close
FROM {bar1m}
WHERE close > 0
GROUP BY instrument, strftime(date, '%Y-%m-%d')
),
cte_lag AS (
SELECT
*,
lag(close, {N}) OVER (PARTITION BY instrument ORDER BY trading_day) AS prev_close
FROM cte_daily
)
SELECT
CAST(trading_day AS DATETIME) AS date,
instrument,
-1 * (close / prev_close - 1) AS factor
FROM cte_lag
"""
# 用扩过的下界查询,让 lag 在评估区间开头也能吃到历史数据
df = dai.query(
sql,
filters={'date': [query_start_date.strftime('%Y-%m-%d %H:%M:%S'), end_date]},
compression=True,
).df()
# ✅ 算完因子后,把输出裁回真正的评估区间 [start_date, end_date]
df = df[(df['date'] >= pd.to_datetime(start_date)) & (df['date'] <= pd.to_datetime(end_date))]
stk_pool = dai.query(
"SELECT date, instrument FROM bigalpha_2026_instruments",
filters={'date': [start_date, end_date]},
).df()
df = pd.merge(df, stk_pool, how='inner', on=['date', 'instrument'])
return df
自检要点
- 缓冲要给足:
BUFFER_DAYS要能覆盖你时序算子回溯的最长历史。注意lag(close, N)里的N是交易日,换算成自然日时要考虑周末和节假日,建议按交易日数的 2~3 倍取自然日,宁多勿少。滚动窗口、多周期指标(如 MACD、20 日均线)同理,取最长的那个窗口来定缓冲。 - 算完一定要裁回区间:向前扩窗只是为了给时序算子喂历史,最终输出必须严格落在
[start_date, end_date]内,不要把缓冲期的行也返回出去。 - 用 selftest 表跑一遍开头几天:重点看
start_date当天及往后几天的因子是否还有大面积NaN。区间开头能算出值,覆盖度校验基本就能过。
完整可运行示例见:
比赛页面->代码->factor_selftest
https://bigquant.com/codesharev3/247fabcb-59b5-4ff9-b800-6808e1a3a281
三、自检二:疑似未来函数的报错
未来函数是怎么被检出的
未来函数(look-ahead bias)指的是:在计算 t 日因子时,用到了 t 日盘后或 t 日之后才能知道的数据。我们新增了一套自动检测机制,原理如下:
我们额外提供一张数据表
bigalpha_2026_stock_bar1m_ahead,它与自检表bigalpha_2026_stock_bar1m_selftest数据完全一致,唯一区别是时间截止到 2024 年 1 月 31 日(即"砍掉"了 1 月 31 日之后的所有数据)。用两张表分别跑
main(),构造区间都设为 2024-01-01 ~ 2024-01-31:
- 用
selftest表跑(后面还有 2~10 月的数据)→ 得到df_full- 用
ahead表跑(1 月 31 日之后什么都没有)→ 得到df_cut如果因子没有未来函数,那么 2024-01-31 及之前每一天的因子值,两份结果应当逐格完全一致——因为算这些天的因子本就不该用到 1 月 31 日之后的数据,砍不砍掉后面都不影响。
反之,如果因子偷看了未来,
df_cut里靠近 1 月 31 日的那几天就会和df_full对不上,典型症状就是最后一天(2024-01-31)变成了NaN——因为它依赖的"未来数据"在 ahead 表里被砍掉了。
未来函数错误示范
下面这个因子直接把"明日收益率"当成"今日因子",是最典型的未来函数:
def main(datasources, start_date, end_date):
import pandas as pd
import dai
bar1m = datasources["bar1m"]
# 用了未来函数(lead),需要向后多取几天才能算出区间末尾的"明日收益"
LOOKAHEAD_DAYS = 7
query_end_date = pd.to_datetime(end_date) + pd.Timedelta(days=LOOKAHEAD_DAYS)
sql = f"""
WITH cte_daily AS (
SELECT
instrument,
strftime(date, '%Y-%m-%d') AS trading_day,
last(close ORDER BY date) AS close
FROM {bar1m}
WHERE close > 0
GROUP BY instrument, strftime(date, '%Y-%m-%d')
),
cte_lead AS (
SELECT
*,
-- ❌ 未来函数:lead 取到"下一交易日"的收盘价
lead(close, 1) OVER (PARTITION BY instrument ORDER BY trading_day) AS next_close
FROM cte_daily
)
SELECT
CAST(trading_day AS DATETIME) AS date,
instrument,
(next_close / close - 1) AS factor -- ❌ 用明日收益率作为今日因子
FROM cte_lead
WHERE next_close IS NOT NULL
"""
df = dai.query(
sql,
filters={'date': [start_date, query_end_date.strftime('%Y-%m-%d %H:%M:%S')]},
compression=True,
).df()
df = df[(df['date'] >= pd.to_datetime(start_date)) & (df['date'] <= pd.to_datetime(end_date))]
stk_pool = dai.query(
"SELECT date, instrument FROM bigalpha_2026_instruments",
filters={'date': [start_date, end_date]},
).df()
df = pd.merge(df, stk_pool, how='inner', on=['date', 'instrument'])
return df
本地自检代码
用配套的 check_lookahead 工具比对两份结果。你只需要用两张表分别跑同一个 main(),构造区间都是 2024-01-01 ~ 2024-01-31:
import pandas as pd
from lookahead_selfcheck import check_lookahead
start_date = '2024-01-01 00:00:00'
end_date = '2024-01-31 23:59:59'
# 全窗:用 selftest 表(1 月 31 日之后还有数据)
df_full = main({'bar1m': 'bigalpha_2026_stock_bar1m_selftest'}, start_date, end_date)
# 截断窗:用 ahead 表(数据截止到 1 月 31 日,之后什么都没有)
df_cut = main({'bar1m': 'bigalpha_2026_stock_bar1m_ahead'}, start_date, end_date)
# 只比对 date <= cutoff 的格子;有差异会打印出最早差异日期和明细
check_lookahead(df_full, df_cut, cutoff='2024-01-31 23:59:59')
存在未来函数时的输出(示意):
[未来函数自检] 发现差异:cutoff=2024-01-31 前 X/Y 个格子不一致,
最早差异出现在 2024-01-31,说明算 2024-01-31 这天的因子值时用到了 cutoff 之后才有的数据。
date instrument factor_full factor_cut abs_dev
2024-01-31 000012.SZ 0.014733 NaN NaN ← 全窗能算出,截断窗变 NaN
...
factor_cut 那一列在最后一天变成 NaN,正是未来函数的铁证:ahead 表砍掉了 2 月的数据,lead 拿不到"明日收盘价",所以 1 月 31 日算不出来。
通过时的输出:
[未来函数自检] 通过:cutoff=2024-01-31 前共 N 个格子全部一致,未发现未来函数。
完整可运行示例见:
比赛页面->代码->factor_ahead
https://bigquant.com/codesharev3/632aa428-b1b6-4958-8e10-572296caf2da
⚠️ 没用未来函数,也可能报差异——检查这些坑
注意:check_lookahead 报出差异,不一定代表你真的用了未来函数。 有些看似无关的写法也会让两份数据对不上。如果你确信因子逻辑里没有 lead、没有引用未来日期,请重点排查下面这些"算子用法不当"导致的差异:
坑 1:last() / first() 没有指定排序
这是最常见的误报来源。在 DAI 里,last(close) 如果不带 ORDER BY,返回的是"分组内最后一条",而不是"时间上最后一条"。两张表底层数据的物理存储顺序可能不同,于是:
-- ❌ 错误:没有 ORDER BY,last 取到的可能不是当天最后一分钟的 close
last(close) AS close
-- ✅ 正确:显式按 date 排序,确保取到当天最后一分钟的收盘价
last(close ORDER BY date) AS close
同一个交易日,两张表可能因为行顺序不同,last(close) 取到了不同分钟的价格,导致因子值对不上——这跟未来函数无关,是算子没用对。first() / arg_max / arg_min 等聚合同理,凡是"取某一条"的聚合,都要明确排序依据。
坑 2:聚合 / 窗口函数缺少确定性排序
row_number()、nth_value() 等窗口函数,以及任何依赖"第几条"的逻辑,都必须有明确的 ORDER BY。否则同一份逻辑数据在不同表上可能得到不同结果。
坑 3:浮点误差被误判
check_lookahead 默认容差为 rtol=1e-5, atol=1e-8。正常的浮点计算误差不会触发差异。如果你的差异 abs_dev 极小(比如 1e-12 量级)却仍被报出来,可以适当放宽容差;但如果差异是整整一列 NaN 或量级明显的数值,那就不是浮点问题,要回到坑 1、坑 2 去查。
自检要点
- 先确认因子逻辑里没有任何引用未来日期的算子(
lead、t+n的 join、用未来财报回填等)。 - 再逐一排查
last()/first()/ 窗口函数是否都带了确定性ORDER BY——这是"没用未来函数却报差异"的头号原因。 - 差异明细里
factor_cut在最后一天成片NaN→ 大概率是真未来函数;差异散布在中间日期且数值零星不同 → 大概率是排序 / 算子问题。
四、自检数据表速查
| 数据表 | 用途 | 数据区间 | 备注 |
|---|---|---|---|
bigalpha_2026_stock_bar1m |
训练集分钟量价,本地开发用 | 2019 ~ 2023 | 对应 datasources['bar1m'],会被替换 |
bigalpha_2026_financial |
训练集财务数据,本地开发用 | 2019 ~ 2023 | 对应 datasources['financial'],会被替换 |
bigalpha_2026_stock_bar1m_selftest |
模拟验证集分钟量价,做覆盖度 / 缺失率自检 | 数据从 2023-10 起,模拟评估区间 2024-01 ~ 2024-10 | 已向前多取一季度作缓冲,但要自己在 main() 里扩窗才吃得到 |
bigalpha_2026_stock_bar1m_ahead |
未来函数自检的"截断表" | 与 selftest 一致,但截止到 2024-01-31 | 与 selftest 配对,用 check_lookahead 比对 |
bigalpha_2026_instruments |
中证 1000 历史成分股(股票池) | 2019 至今 | 直接查,不替换、不入 datasources |
bigalpha_2026_exposure |
风险暴露度数据 | 2019 至今 | 直接查,不替换、不入 datasources |
bigalpha_2026_factorlib |
平台因子库,评估回归用 | 与评估区间一致 | 直接查,本地评估时读取 |
提交前自检清单
- [ ] 分钟表、财务表的表名全部来自
datasources['bar1m']/datasources['financial'],没有硬编码;其余辅助表直接写表名。 - [ ] 时序因子在
main()内部向前扩了查询窗口,且算完后裁回[start_date, end_date]。 - [ ] 用
bigalpha_2026_stock_bar1m_selftest跑一遍,评估区间开头几天无大面积NaN(覆盖度自检)。 - [ ] 用
selftest+ahead两张表跑check_lookahead,结果为**"通过"**(未来函数自检)。 - [ ] 所有
last()/first()/ 窗口函数都带了确定性ORDER BY。 - [ ] 因子输出为三列
['date', 'instrument', 'factor'],且不含inf/-inf。
\