常见报错的自检方式

由hxgre创建,最终由hxgre 被浏览 17 用户

本文为 BigAlpha 2026 全球高校联赛 · AI 因子挖掘赛道的自检指引。

为防止验证集 / 测试集数据泄露,评估系统在参赛者提交代码后不会返回详细报错日志。这导致大家遇到报错时往往不知从何查起。本文整理了两类最常见的报错,并提供可在本地跑通的自检模板,帮助你在提交前把代码调通。

目录

  1. 评估系统是怎么跑你代码的
  2. 自检一:公榜验证集数据的模拟检验(覆盖度 / 缺失率报错)
  3. 自检二:疑似未来函数的报错
  4. 自检数据表速查

一、评估系统是怎么跑你代码的

理解报错前,先理解评估系统对你 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 里的 bar1mfinancial 会被替换。 这两个数据源在公榜 / 私榜阶段指向的物理表不同,所以必须从 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 去查。

自检要点

  • 先确认因子逻辑里没有任何引用未来日期的算子(leadt+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

\

{link}