TabPFN 模型实践:分钟频选股与收益率预测

量化研究里,股票收益率预测通常被当作回归问题来做。由于股票因子任务本身信号弱、噪声大,模型不一定能精准预测绝对收益率,但只要预测结果能在同一时间截面上稳定区分表现更好和更差的股票,就已经具备选股价值。

问题是,怎样才能又快又低成本地拿到这样一个稳定的排序信号?TabPFN 为这种弱信号、高噪声的表格任务提供了一种快速建模思路。

一、什么是 TabPFN 模型

TabPFN(Tabular Prior-data Fitted Network)是一种专为中小规模表格数据设计的基础模型,凭借预训练能力,可以在较少调参的情况下快速得到一个可用的基线;如果目标任务与预训练分布差异较大,还可以进一步 fine-tuning,使模型更贴近具体数据分布。

股票因子预测里,每一行样本对应的是某只股票在某个时间点上的一组结构化因子特征------这类结构清晰、规模适中的表格数据,正好落在 TabPFN 的适用范围内,也让我们可以更快验证因子信号是否具有实际的选股价值。

二、DolphinDB × TabPFN 分钟频选股预测方案

本文以分钟频股票因子预测未来 30 分钟收益率为例,结合 DolphinDB 完成股票池筛选、因子宽表转换、价格数据合并和回测验证,再在 Python 中使用 TabPFN 进行建模和预测,最终基于预测结果构建行业内 TopN 选股策略。

该方案整体流程如下:

其中,DolphinDB 的作用主要体现在数据的存储、转换和回测执行;Python 侧则主要完成因子筛选、数据预处理、TabPFN 训练以及预测结果生成。

三、案例实践

本文以申万二级行业"普钢"为例,行业代码为 801044.SI,共包含 23 只股票。目标是利用分钟频因子预测未来 30 分钟收益率,并据此构建行业内的截面 TopN 选股策略。

3.1 数据准备:行业股票池、因子和价格数据

首先,将行业成分数据、分钟频因子数据和分钟收盘价数据导入 DolphinDB。本文假设三类数据已经分别存储在 DolphinDB 的分布式表中:
•行业成分表:记录股票所属申万行业;
•分钟频因子表:记录每只股票在每个分钟截面的因子值;
•分钟价格表:记录每只股票的分钟收盘价。

下文的示例代码均为 Python 代码,测试前请确保安装了 DolphinDB Python API。

第一步是根据行业成分表筛选出目标行业的股票池。这里封装了成了一个函数便于调用,可以根据自己的表字段对 SQL 语句进行修改:

复制代码
1 def fetch_code_list_from_ddb(session):
2     script = f"""
3         exec code
4         from {INDUSTRY_TABLE}
5         where l2_code == "{INDUSTRY_CODE}"
6     """
7     code_df = session.run(script)
8     return code_df.tolist()

随后,根据股票池读取对应的分钟频因子和分钟价格数据;然后将价格表和因子宽表进行连接,形成模型输入数据。参考代码如下:

复制代码
 1 script = f"""
 2     // 查询因子数据
 3     factor = select factorvalue
 4     from {FACTOR_TABLE}
 5     where code in {code_filter}
 6       and trade_date >= {start_time.strftime('%Y.%m.%d')}
 7       and trade_date < {end_time.strftime('%Y.%m.%d')}
 8       and trade_time >= 09:30:00
 9       and trade_time <= 15:00:00
10     pivot by code, trade_date, trade_time, factorname
11 
12     // 查询价格数据
13     price = select code, trade_date, trade_time, close
14             from {PRICE_TABLE}
15             where code in {code_filter}
16               and trade_date >= {start_time.strftime('%Y.%m.%d')}
17               and trade_date < {end_time.strftime('%Y.%m.%d')}
18               and trade_time >= 09:30:00
19               and trade_time <= 15:00:00
20   
21     // 拼接价格表和因子宽表
22     lj(price, factor, `code`trade_date`trade_time)
23 """
24 chunk_df = session.run(script)

这里需要特别说明的是,分钟频因子表通常采用窄表结构,即一行只记录一个股票、一个时间点、一个因子名和对应因子值。例如:

trade_date trade_time factor_name factor_value
000709.SZ 2026.01.05 10:12:00 AR 31.57
000709.SZ 2026.01.05 10:12:00 ASI 0.35
000709.SZ 2026.01.05 10:13:00 ASI -0.019

这种结构适合因子持续扩展和动态更新,因为新增因子时只需要增加新的 factor_name 记录,不需要频繁修改表结构。但对于机器学习模型而言,输入数据通常要求每一行对应一个样本、每一列对应一个特征。

因此,在建模前需要使用 DolphinDB 的 pivot by 操作将窄表转换为宽表,使每一行对应某只股票在某个时间点的一组完整因子向量。这样处理之后,原始的分钟因子、行业股票池和价格行情就被整理成了适合建模的表格数据。模型输入表结构大致如下:

code trade_date trade_time close AR ASI ASIT
000709.SZ 2026.01.05 10:12:00 4.52 31.58 0.35 0.39
000709.SZ 2026.01.05 10:13:00 4.53 31.58 -0.019 0.34

3.2 标签定义:未来 30 分钟收益率

本文的预测目标定义为未来 30 分钟收益率,公式如下:

复制代码
1 return_30min = close(t + 30min) / close(t) - 1

其中,close(t) 表示当前分钟收盘价,close(t+30min) 表示同一交易日内 30 分钟后的收盘价。为了避免跨日收益率计算带来的干扰,样本仅保留 14:30 及之前的分钟数据。

这个标签定义方式比较适合分钟频选股任务,因为它既保留了短周期预测的特征,又能和后续每 30 分钟调仓的回测逻辑保持一致。换句话说,模型预测的目标和交易执行的节奏是匹配的,这一点很重要。

本案例直接使用 DolphinDB 现有因子模块构造 335 个因子,包括 MyTT 常见指标、WorldQuant 101 Alpha 因子指标库中的因子和国泰君安 191 Alpha 因子库中的因子。

如果把这些因子直接全部输入模型会带来两个问题:一是输入维度过高,增加训练和推理成本;二是因子中包含大量冗余和噪声信息,可能反而影响模型表现。因此在训练前需要先做一轮相对系统的因子筛选。

本文采用的筛选流程主要分三步:
•第一步,基于训练集去除缺失率大于 0.3 的因子,这一步可以先清掉明显不稳定的特征。
•第二步,计算单因子 RankIC 和 ICIR,优先保留绝对值排名靠前、方向一致性较好的因子。
•第三步,对高度相关的因子进行去冗余处理,若两两相关系数绝对值超过 0.9,则保留 RankIC 更好的那个。

经过这一轮筛选后,最终从 335 个因子中保留了 45 个因子。对于这类金融表格任务来说,因子筛选不是简单的"降维",更重要的是控制噪声和冗余,让模型更容易学习到稳定的横截面信息。

3.4 Base Model 预测

本文首先使用 TabPFNRegressor 作为 base model 进行预测,不做额外微调。

复制代码
1 model = TabPFNRegressor()
2 model.fit(X_train, y_train)
3 pred = model.predict(X_test)

Base Model 的价值在于"快"和"稳"。它可以帮助我们快速判断这个任务是否适合 TabPFN,预测信号是否具备初步排序能力。如果连基线都跑不出来,后面的 fine-tuning 也没有必要;如果基线本身已经有不错的 RankIC,那么继续微调就有可能进一步提升效果。

需要注意的是,TabPFN 每次调用 predict 都会重新计算训练集上下文,因此应尽量减少调用次数、一次性预测完整个测试集,而非拆成多次小批量调用;若测试集样本量过大导致显存不足,可再将其拆分为若干较大的分块分别预测。

3.5 Fine-Tuning 设置

在 Base Model 的基础上,本文进一步使用 FinetunedTabPFNRegressor 对模型进行微调。Fine-Tuning 的思路是先加载预训练 checkpoint,再在目标行业的数据上继续优化参数,使模型更适应当前行业的因子分布和收益率特征。

复制代码
 1 model = FinetunedTabPFNRegressor(
 2     device=DEVICE,
 3     epochs=EPOCHS,
 4     learning_rate=LEARNING_RATE,
 5     n_finetune_ctx_plus_query_samples=N_FINETUNE_CTX_PLUS_QUERY_SAMPLES,
 6 )
 7 
 8 model.fit(
 9     X_train,
10     y_train,
11     output_dir=OUTPUT_DIR,
12 )

从实操角度看,fine-tuning 时主要关注几个参数:学习率、训练轮数、早停策略、每个训练 step 的样本规模,以及预测阶段的集成次数。其中 N_FINETUNE_CTX_PLUS_QUERY_SAMPLES 用来控制每次训练输入的 context 和 query 总样本数,如果显存不足,可以适当调小。本文实验中将其设置为 5000。

如果希望使用微调后的权重进行预测,也可以在 TabPFNRegressor 中通过 model_path 指定路径,直接加载训练好的模型进行推理。

3.6 评价指标:为什么重点看 RankIC

对于股票预测任务来说,单纯使用 MSE 或 R² 并不能完整反映模型效果。因为选股场景真正关心的不是"预测值和真实值差多少",而是"模型能不能把未来更强的股票排在前面"。因此,本文重点使用 RankIC 作为评价指标。

RankIC 本质上是预测值与真实收益率之间的 Spearman 秩相关系数,衡量的是模型在每个时间截面的排序能力。除此之外,还会统计 RankIC 的均值、标准差以及 ICIR。其中,Mean RankIC 反映整体排序能力,RankIC Std 反映波动程度,ICIR 则衡量排序能力的稳定性。

从量化选股角度看,RankIC 往往比均方误差 Mean Squared Error(MSE)更有解释力。因为策略最终依赖的是横截面排序,而不是点预测的绝对值。

四、实验结果与回测验证

4.1 预测结果

本文分别对 Base Model 和 Fine-tuned Model 做了预测评估,结果如下:

模型 MSE Mean RankIC RankIC Std ICIR
Base Model 0.44341278 0.0164 0.311661 0.210042 1.483805
Fine-tuned Model 0.45892538 -0.0180 0.343308 0.203209 1.689436

从结果来看,Fine-tuning 后 MSE 有所上升、R² 由正转负,点预测精度不升反降;但 RankIC 和 ICIR 却进一步提升。这说明在这个任务中,模型的点预测精度并不是最关键的,真正有价值的是它对股票横截面排序能力的增强。

这个结果其实很符合金融预测任务的特点。由于收益率本身噪声较大,模型很难在绝对值层面预测得非常精确,但只要它能稳定识别出相对更强的股票,就已经足够支持选股策略构建。

4.2 回测设计

为了进一步验证预测结果是否真的具有选股价值,本文使用 DolphinDB Backtest 插件构建了一个行业内 TopN 策略。具体做法是每 30 分钟调仓一次,在每个调仓时点对行业内股票按预测收益率降序排序,选取预测收益率最高的前 3 只股票等权买入,并持有 30 分钟。基准组合则设置为行业等权组合,即在每个调仓时点对行业内所有股票等权配置。这样可以比较模型选股策略是否真的跑赢了行业基准。

策略核心逻辑(伪代码)如下:

复制代码
 1 // 1. 策略状态初始化:设置调仓参数
 2 def initialize(mutable context){
 3     context["rebalanceN"] = 3        // 每次调仓持有 3 只股票
 4     context["buyThreshold"] = 0      // 预测收益率过滤阈值
 5 }
 6 
 7 // 2. 每次收到行情时触发的调仓逻辑
 8 def onBar(mutable context, msg, indicator){
 9     // 若当前行情没有预测信号,直接跳过(即每 30 分钟才真正触发一次调仓)
10     // 按预测收益率降序排序,选出前 rebalanceN 只
11     // 卖出:当前持仓中不在 TopN 名单内的标的清仓
12     // 买入:TopN 标的按总资产等权配置,逐只提交买单
13 }
14 
15 // 3. 注册回调、创建回测引擎,喂入行情数据驱动整个回测流程
16 callbacks = dict(STRING, ANY)
17 callbacks["initialize"] = initialize
18 callbacks["onBar"] = onBar
19 callbacks["finalize"] = finalize
20 
21 engine = Backtest::createBacktester(engineName, config, callbacks)
22 Backtest::appendQuotationMsg(engine, data)

本文主要展示的是研究流程和回测结果,回测中已考虑手续费和印花税成本,但滑点、T+1 约束以及市场冲击成本等因素尚未纳入,真实交易中这些因素会进一步影响实盘表现,最终结果可能与回测存在差异。

4.3 回测结果

回测结果如下:

组合 总收益率 最大回撤 超额收益
行业等权组合 -0.85% 4.14% -
TabPFN 基准模型选股策略 1.76% 1.89% 2.61%
TabPFN 微调后选股策略 6.88% 1.14% 7.73%

结果比较明确: TabPFN 基准模型构建的选股策略已经能够跑赢行业等权组合,而经过 Fine-Tuning 之后,策略收益进一步提升,最大回撤也有所下降。

如果从超额收益角度看,基准模型相对行业等权组合的超额收益为 2.61%,微调后策略的超额收益提升至 7.73%。这说明在本次测试区间内,TabPFN 对行业内股票未来 30 分钟收益率具有一定的截面排序能力,模型预测信号可以转化为实际可用的选股逻辑。

五、方案价值

从结果上看,这套方案的价值并不只是"能做预测",而是把分钟频因子处理、表格模型预测和策略回测串成了一个完整闭环。

首先,TabPFN 提供了一种很适合表格数据的快速建模方式,特别适合作为量化研究中的 baseline。对于因子较多、噪声较强的行业数据,它可以在较少调参的情况下快速输出结果,帮助研究者判断任务是否具有可建模性。

其次,DolphinDB 在这套流程里承担了数据准备和回测的核心角色。分钟行情、行业成分股、因子宽表转换、预测结果落表和策略回测都可以在同一个系统内完成,这对高频金融研究来说非常重要,因为数据处理效率往往直接决定研究迭代速度。

最后,从实验结果来看,这套方法不仅在 RankIC 上有提升,也在行业内 TopN 回测中体现出了一定的超额收益能力。对于量化研究而言,这比单纯的回归指标更有说服力,因为它说明模型信号不仅"看起来有用",而且"在策略层面也能落地"。

六、结语

如果你的研究场景也是分钟频因子、行业轮动或者横截面选股,这套"DolphinDB + TabPFN + Backtest"的组合可以提供一条现成的实现路径------用 TabPFN 快速验证因子信号是否具备选股价值,用 DolphinDB 完成数据处理和策略回测的闭环,把模型预测接到交易验证上。

点击 http://dolphindb.com/downloads/tutorials/script/TabPFN%E6%A8%A1%E5%9E%8B.zip 获取完整代码!