最近,Google Research 发布了面向表格分类与回归任务的基础模型 TabFM。
它试图简化传统表格机器学习中的建模流程。研究者不再需要针对每个数据集重新完成复杂的模型训练、参数搜索和特征工程,而是向模型提供一批带标签的历史样本,再输入一条新的待预测数据,由模型根据上下文理解特征与标签之间的关系,并完成分类或回归预测(Google Research)。
看到这个框架时,我首先想到的是量化交易。
量化研究本质上一直在制造表格。无论是 K 线、成交量、资金费率、基差,还是持仓量和订单流数据,最终都会被整理成一行行结构化样本,再交给统计模型或机器学习模型处理。
这次实践采用了一种非常直接的方案:
不计算 RSI,不计算 MACD,不构造均线,也不提前设计复杂因子,只将一段经过尺度统一的 OHLCV 数据组成时间窗口,再交给 TabFM 判断下一根 K 线更可能上涨、横盘还是下跌。
这种方式与均线、RSI、ATR 等传统指标并不冲突。传统指标同样需要先规定一个时间窗口,再使用固定公式从窗口中提取信息。区别在于,过去是研究者先决定模型应该观察均值、涨跌强弱还是波动幅度;现在则只提供经过标准化处理的时间窗口,由基础模型自行寻找其中可能有用的组合关系。
这正是 TabFM 在量化研究中最值得关注的地方。
一、把时间窗口变成一行数据
假设我们规定一个长度为 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 线周期以及历史样本数量都只是参数,并不存在固定答案。在正式研究中,可以通过滚动回测或参数搜索,比较不同参数组合在样本外数据中的表现。
二、为什么不直接输入绝对价格
虽然本次实践使用的是原始 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 数据展开为滞后窗口,并统一不同时间阶段的价格与成交量尺度,使模型更容易比较不同市场环境中的相对关系。
三、模型需要预测什么
本次实践将预测任务定义为三分类:
upflatdown
标签表示目标 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 的区别在哪里
假设我们观察同一个长度的时间窗口,传统做法通常会先人工定义指标。
例如,均线可以写成:
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)。
八、安装注意事项
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 谷歌预测框架策略
- 1




