Type/to search
2
Follow
490
Followers
如果你学得够慢,那么你什么也不用学——TabFM 预测框架应用于量化领域的实践
Discussions
Created 2026-07-10 13:06:29  Updated 2026-07-13 16:06:22
 0
 26

img

最近,Google Research 发布了面向表格分类与回归任务的基础模型 TabFM

它试图简化传统表格机器学习中的建模流程。研究者不再需要针对每个数据集重新完成复杂的模型训练、参数搜索和特征工程,而是向模型提供一批带标签的历史样本,再输入一条新的待预测数据,由模型根据上下文理解特征与标签之间的关系,并完成分类或回归预测(Google Research)。

看到这个框架时,我首先想到的是量化交易。

量化研究本质上一直在制造表格。无论是 K 线、成交量、资金费率、基差,还是持仓量和订单流数据,最终都会被整理成一行行结构化样本,再交给统计模型或机器学习模型处理。

这次实践采用了一种非常直接的方案:

不计算 RSI,不计算 MACD,不构造均线,也不提前设计复杂因子,只将一段经过尺度统一的 OHLCV 数据组成时间窗口,再交给 TabFM 判断下一根 K 线更可能上涨、横盘还是下跌。

这种方式与均线、RSI、ATR 等传统指标并不冲突。传统指标同样需要先规定一个时间窗口,再使用固定公式从窗口中提取信息。区别在于,过去是研究者先决定模型应该观察均值、涨跌强弱还是波动幅度;现在则只提供经过标准化处理的时间窗口,由基础模型自行寻找其中可能有用的组合关系。

这正是 TabFM 在量化研究中最值得关注的地方。

img


一、把时间窗口变成一行数据

假设我们规定一个长度为 window 的 K 线窗口,每根 K 线包含以下五个字段:

  • Open
  • High
  • Low
  • Close
  • Volume

一条样本使用目标 K 线之前的 window 根已完成 K 线,标签则是紧接着该窗口出现的目标 K 线方向。

准备预测下一根 K 线时,最新输入由当前已经确认完成的若干根 K 线组成。在构造数据之前,需要确认行情接口返回的最后一根 K 线是否已经完成;如果最后一根仍在形成,就应先将其排除,避免把未完成数据误认为完整样本。

用作模型上下文的历史样本不断向前平移。每向过去移动一根 K 线,就可以得到一条新的历史样本。

样本顺序输入窗口标签
最新样本最近一个完整历史窗口紧接窗口之后的已知 K 线
次新样本整体向前移动一根 K 线对应的下一根已知 K 线
更早样本继续向历史方向移动对应的下一根已知 K 线

历史样本按照从最新到最旧的顺序排列,每一行内部同样遵循最近数据优先的规则:

  • lag_1 表示距离目标最近的一根 K 线;
  • lag_2 表示目标之前的第二根 K 线;
  • lag_window 表示窗口中距离目标最远的一根 K 线。

字段生成代码如下:

python
def make_feature_columns(window): columns = [] for lag in range(1, window + 1): prefix = "lag_{}".format(lag) columns.extend([ prefix + "_open", prefix + "_high", prefix + "_low", prefix + "_close", prefix + "_volume", ]) return columns

如果窗口长度为 15,每行包含 75 个字段;如果窗口长度为 24,每行则包含 120 个字段。

这里的窗口长度、K 线周期以及历史样本数量都只是参数,并不存在固定答案。在正式研究中,可以通过滚动回测或参数搜索,比较不同参数组合在样本外数据中的表现。

img

二、为什么不直接输入绝对价格

虽然本次实践使用的是原始 OHLCV 数据,但并不意味着可以将绝对价格原封不动地交给模型。

以 BTC 为例,当价格从 30,000 上涨到 100,000 后,整个价格尺度已经发生了明显变化。如果直接输入绝对价格,模型可能会把价格所处的水平误认为一种稳定规律,而不是关注窗口内部真正有意义的相对结构。

因此,每一行样本都使用距离预测目标最近的收盘价作为锚点。Open、High、Low 和 Close 被转换为相对锚点的变化比例,成交量则除以当前窗口的平均成交量。

python
def make_raw_kline_row( bars, target_idx, window, ): anchor_close = float( bars[target_idx - 1]["Close"] ) if anchor_close <= 0: anchor_close = 1.0 volumes = [ float( bars[target_idx - lag].get( "Volume", 0.0, ) ) for lag in range(1, window + 1) ] mean_volume = ( sum(volumes) / len(volumes) if volumes else 1.0 ) if mean_volume <= 0: mean_volume = 1.0 row = {} for lag in range(1, window + 1): bar = bars[target_idx - lag] prefix = "lag_{}".format(lag) row[prefix + "_open"] = ( float(bar["Open"]) - anchor_close ) / anchor_close row[prefix + "_high"] = ( float(bar["High"]) - anchor_close ) / anchor_close row[prefix + "_low"] = ( float(bar["Low"]) - anchor_close ) / anchor_close row[prefix + "_close"] = ( float(bar["Close"]) - anchor_close ) / anchor_close row[prefix + "_volume"] = ( float(bar.get("Volume", 0.0)) / mean_volume ) return row

经过这样的处理,不同时间阶段的数据被统一到了相近的尺度中。

例如,lag_5_high = 0.012 表示目标之前的第五根 K 线最高价比当前锚点高约 1.2%;lag_3_volume = 1.6 则表示目标之前的第三根 K 线成交量约为窗口平均成交量的 1.6 倍。

这里没有计算均线、RSI、ATR 等传统技术指标,只是将原始 OHLCV 数据展开为滞后窗口,并统一不同时间阶段的价格与成交量尺度,使模型更容易比较不同市场环境中的相对关系。


三、模型需要预测什么

本次实践将预测任务定义为三分类:

  • up
  • flat
  • down

标签表示目标 K 线收盘价相对于前一根收盘价的变化方向。

python
def make_next_bar_label( bars, target_idx, threshold, ): previous_close = float( bars[target_idx - 1]["Close"] ) target_close = float( bars[target_idx]["Close"] ) if previous_close <= 0: return "flat" target_return = ( target_close / previous_close - 1.0 ) if target_return > threshold: return "up" if target_return < -threshold: return "down" return "flat"

其中,target_return 表示目标 K 线收盘价相对于前一根收盘价的收益率。当收益率高于 threshold 时,标签为 up;低于负阈值时,标签为 down;介于两者之间时,则归类为 flat

threshold 的作用,是区分真正具有方向意义的价格变化与普通噪声。这个参数同样不存在统一答案,需要结合交易品种、K 线周期、平均波动、手续费、滑点和实际执行成本进行选择。

阈值过低,会使大量微小波动被标记为上涨或下跌;阈值过高,则可能使大部分样本都落入 flat。因此,标签分布本身也应该成为参数研究和回测验证的一部分。


四、历史样本是怎样生成的

历史数据从最新一条已经知道结果的样本开始,逐步向过去移动。

python
def build_dataset( bars, window, train_rows, return_threshold, ): minimum_bars = window + train_rows if len(bars) < minimum_bars: raise ValueError( "K线数量不足,至少需要 {} 根".format( minimum_bars ) ) rows = [] labels = [] latest_known_target_idx = len(bars) - 1 for offset in range(train_rows): target_idx = ( latest_known_target_idx - offset ) rows.append( make_raw_kline_row( bars=bars, target_idx=target_idx, window=window, ) ) labels.append( make_next_bar_label( bars=bars, target_idx=target_idx, threshold=return_threshold, ) ) latest = make_raw_kline_row( bars=bars, target_idx=len(bars), window=window, ) return rows, labels, latest

最终得到三部分数据:

  • rows:已经知道结果的历史时间窗口;
  • labels:每个历史窗口之后那根 K 线的实际方向;
  • latest:当前最新时间窗口,用于预测下一根尚未出现的 K 线。

其中,rows[0] 是最新历史样本,索引越大,样本越旧。latest 则只使用当前已经确认完成的 K 线,不包含任何未来数据。

这种构造方式的关键在于,输入窗口与预测目标必须严格对齐。每条历史样本只能使用目标 K 线之前的数据,不能在特征中包含目标 K 线本身,更不能使用目标之后的信息。

还需要注意,build_dataset() 默认传入的 bars 已经排除了未完成 K 线。如果行情接口会返回正在形成的当前 K 线,应在调用该函数之前先进行处理。


五、这和均线、RSI 的区别在哪里

img

假设我们观察同一个长度的时间窗口,传统做法通常会先人工定义指标。

例如,均线可以写成:

python
ma = sum(close_list[-window:]) / window

RSI 会计算窗口内上涨幅度与下跌幅度的平均强度,ATR 会计算真实波幅,动量指标则会比较不同时间位置上的价格变化。

这些方法具有一个共同特点:

研究者先决定什么关系可能有效,再使用固定公式将整个时间窗口压缩为一个或少量指标。

TabFM 的思路不同。我们仍然定义时间窗口,但不再提前规定模型必须观察哪一种关系。模型可以直接读取窗口内全部经过尺度统一的 OHLCV 字段,尝试从中识别收盘价是否连续抬高、高点和低点是否同步上移、K 线实体是否扩大、上下影线结构是否变化、成交量是否放大,以及波动是否处于收缩或扩张状态。

除此之外,模型还可能发现不同时间位置之间的组合关系。例如,某种价格结构可能只有在成交量同步放大时才有意义,也可能只有在短期波动收缩之后才具有预测价值。

传统指标属于人工特征。本次方法则是将经过尺度统一的 OHLCV 时间窗口交给表格基础模型,让模型自行寻找可能有效的特征组合。

这并不意味着研究者什么都不用做。时间窗口、数据周期、标签定义、样本数量和验证方式仍然需要人工确定。真正被减少的,是提前穷举大量指标和特征组合的工作。


六、它不是 TimesFM

TabFM 并不是专门的时间序列模型。

Google 的 TimesFM 才是直接读取连续时间序列,并预测未来序列的基础模型(Google Research)。

TabFM 接收的是固定列结构的表格。在本次数据中,时间关系并不是由模型天然识别,而是通过字段位置人工表达:

  • lag_1 永远代表最近一根 K 线;
  • lag_2 永远代表前两根 K 线;
  • lag_window 永远代表窗口中最早的一根 K 线。

因此,更准确的说法是:

我们先使用人工时间窗口,将时序问题转换为表格分类问题,再使用 TabFM 完成预测。

这种处理方式与传统机器学习中的滞后特征方法相似。不同的是,TabFM 是一个表格基础模型,可以将历史特征和标签样本作为上下文,理解当前表格任务,而不需要针对当前数据集重新训练一套完整模型参数(Google Research)。


七、调用 TabFM

完成数据整理之后,真正调用模型的代码并不复杂。

python
import numpy as np import pandas as pd from tabfm import TabFMClassifier from tabfm import tabfm_v1_0_0_jax model = tabfm_v1_0_0_jax.load( model_type="classification" ) classifier = TabFMClassifier( model=model, random_state=42, ) rows, labels, latest = build_dataset( bars=bars, window=window, train_rows=train_rows, return_threshold=return_threshold, ) x_train = pd.DataFrame(rows) y_train = np.asarray(labels) x_test = pd.DataFrame( [latest], columns=x_train.columns, ) classifier.fit( x_train, y_train, ) probabilities = ( classifier.predict_proba(x_test)[0] )

预测完成后,可以将类别和概率整理为字典:

python
probs = { str(class_name): float(probability) for class_name, probability in zip( classifier.classes_, probabilities, ) } for class_name in ( "up", "flat", "down", ): probs.setdefault( class_name, 0.0, ) label = max( probs, key=probs.get, ) confidence = probs[label]

最终可以得到下一根 K 线上涨、横盘和下跌的概率,并选择概率最高的类别作为当前预测结果。

需要注意的是,TabFM 的 fit() 与传统机器学习中的完整参数训练不同。它主要完成当前数据的编码、尺度处理以及上下文准备,并不会针对当前数据集重新训练 TabFM 的预训练参数。历史特征与标签随后作为上下文参与最新样本的预测(GitHub)。

img


八、安装注意事项

TabFM 当前要求 Python 3.11 或更高版本。本次实践使用的是 Python 3.11 环境。

bash
conda create -n py311 python=3.11 -y conda activate py311

随后下载项目并安装 JAX 后端:

bash
git clone https://github.com/google-research/tabfm.git cd tabfm python -m pip install \ --upgrade pip setuptools wheel python -m pip install -e '.[jax]'

第一次加载模型时,程序会自动下载预训练权重:

python
model = tabfm_v1_0_0_jax.load( model_type="classification" )

本次实践只加载 JAX 分类检查点,实际下载量约为 6GB,因此运行环境需要预留足够的磁盘空间、内存和首次初始化时间。

此外,预训练权重当前使用 TabFM 非商业许可。如果计划用于生产环境、商业系统或收费服务,需要在正式使用前确认许可范围(Hugging Face)。

在发明者量化本地托管者中,还需要确认策略实际使用的 Python 解释器,就是安装了 TabFM 的 Python 3.11 或更高版本环境。

python
import sys Log( "Python executable: " + sys.executable ) Log( "Python version: " + sys.version )

如果终端中能够正常导入 TabFM,而策略运行时仍然提示找不到模块,通常不是安装失败,而是终端与托管者使用了不同的 Python 环境。


九、为什么目前只适合低频预测

TabFM 当前并不是一个轻量模型。第一次运行通常会涉及模型权重读取、JAX 初始化、XLA 编译以及 CPU 或加速设备推理,整体速度明显慢于 LightGBM、XGBoost 和普通线性模型。因此,它暂时不适合逐 Tick、秒级或盘口级预测,也不适合在每次行情更新时重新执行推理。

更现实的方式,是将模型作为低频预测模块使用:策略启动时执行一次预测,之后按照固定周期更新;仓位管理、止损、订单状态检查和交易执行,则继续由普通程序持续完成。

预测频率最好与 K 线周期保持一致。例如,使用小时 K 线时,可以在每根小时 K 线完成后预测下一根;使用 15 分钟 K 线时,则在每根 15 分钟 K 线完成后更新。

当前完整示例为了控制推理频率,采用策略启动时执行一次,之后每个自然小时更新一次的方式。模型只负责提供低频方向判断,实际交易流程仍然由普通策略程序管理。


十、这次实践真正说明了什么

首先需要明确:这次实验并不能证明 TabFM 具备稳定预测市场的能力。当前方案尚未经过完整的样本外检验,也没有与传统技术指标、LightGBM、XGBoost 或专用时间序列模型进行系统比较。因此,它更适合作为一次方法验证,而不是一套已经被证明有效的交易模型。

这次实践真正值得关注的,并不是某一次预测结果,而是量化研究流程正在发生的变化。

传统方法面对一段 K 线数据时,通常会先进行人工特征设计,例如计算均线、RSI、ATR、动量或波动率,再将这些指标交给模型。换句话说,研究者需要先判断哪些市场关系可能有效,然后再用公式对原始数据进行压缩。TabFM 提供了另一种思路:保留时间窗口内较完整的价格与成交量结构,让基础模型直接观察不同时间位置之间的关系,并自行寻找可能有效的特征组合。

当然,这并不意味着研究者可以完全退出建模过程。时间窗口应该覆盖多长的历史、使用什么级别的 K 线、预测上涨下跌还是未来收益率、标签阈值如何划分、提供多少历史样本,以及预测结果能否覆盖手续费和滑点,这些问题仍然需要由人来定义和验证。

真正被弱化的,是过去大量依赖人工完成的特征工程与模型训练流程。因此,“如果你学得够慢,那么你什么也不用学”,并不是说量化知识已经不再重要,而是说研究工作的重心正在发生变化。

过去,研究者往往需要先学习大量指标、模型和参数调优方法,才能开始一次预测实验;现在,则可以先从更核心的问题出发:预测什么、数据如何组织、结果如何验证,以及预测是否具有真实的交易价值。至于时间窗口内部究竟应该提取哪些特征,其中一部分工作可以交给基础模型完成。

模型可能会越来越通用,调用代码也可能越来越短,但决定研究价值的仍然不是模型本身,而是问题定义、数据结构、验证方法与交易执行。

策略源码TabFM 谷歌预测框架策略


Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)