☰
Kronos:将OHLCVA序列Token化,用Transformer预测金融时序
2026/10/5 11:48:00 网站建设 项目流程

1. 从OHLCVA到Token:Kronos到底在解决什么问题

金融市场的原始数据长什么样?打开任何一个行情软件,你看到的是一根根K线,每根K线包含五个核心字段:开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)、成交量(Volume),再加上一个时间戳或者成交额(Amount),合起来就是业内常说的OHLCVA序列。这东西看起来简单,但要让模型真正“读懂”它,难度远超大多数人的想象。

传统量化做法是怎么处理这些数据的?要么手工构造因子——比如5日均线、RSI、MACD、布林带宽度,要么把价格序列丢进LSTM、GRU、TCN这类时序模型里硬train。这些方法在过去十几年里确实work,但问题也很明显:手工因子依赖领域专家的先验知识,换一个市场、换一个品种,因子可能就失效了;而LSTM这类模型对长序列的建模能力有限,遇到金融数据里那种“长期依赖+高噪声+非平稳”的组合拳,往往力不从心。

Kronos这篇工作的核心洞察在于:金融市场的OHLCVA序列,本质上和自然语言一样,是一种“语言”。价格在时间轴上的每一次跳动,就像句子里的一个词;一根K线,就像一句话;一段行情走势,就像一篇文章。既然Transformer在自然语言处理上取得了巨大成功,那能不能把OHLCVA序列也“token化”,然后用Transformer来建模?

这个思路听起来简单,但落地起来有一堆硬骨头要啃。自然语言里的词是离散的、有限的,一个词表撑死几十万;而价格是连续的、无限的,你不可能给每个价格点分配一个token。自然语言里的词有明确的语义边界,而K线之间是连续过渡的,怎么切分?自然语言里“苹果”就是“苹果”,但金融数据里同样的价格形态,在不同市场环境下含义可能完全相反。

Kronos的解法是:把连续的OHLCVA向量通过向量量化(Vector Quantization)映射到离散的codebook空间。具体来说,它先用一个编码器把每根K线的多维特征压缩成一个连续向量,然后在这个向量和codebook里的码字之间做最近邻查找,把连续向量“吸附”到最近的离散码字上。这样一来,每根K线就变成了一个token,整个价格序列就变成了一串token序列,可以直接喂给Transformer。

注意:这里的codebook大小是个关键超参数。太小了,信息压缩太狠,价格细节丢失严重;太大了,token空间稀疏,模型学不动。Kronos论文里用的codebook size是1024,这个数字不是拍脑袋定的,后面我会详细拆解怎么选。

这个设计最妙的地方在于,它把“连续值预测”这个回归问题,转化成了“下一个token预测”这个分类问题。回归问题对噪声敏感,一个异常值就能把loss拉爆;而分类问题鲁棒性更强,模型只需要在codebook里选一个最可能的码字就行。这就像你让一个人猜明天收盘价是3250.37还是3250.38,他很难猜准;但你让他猜“明天是涨还是跌”,准确率就高多了。

2. 核心架构拆解:Kronos的Transformer到底长什么样

2.1 整体架构:编码器-量化器-解码器三段式

Kronos的整体架构可以分成三块:编码器(Encoder)、量化器(Quantizer)、解码器(Decoder)。这三块各司其职,配合起来完成从原始OHLCVA到token序列再到预测输出的完整链路。

编码器的作用是把每根K线的多维特征压缩成一个稠密向量。假设输入是长度为T的OHLCVA序列,每个时间步有6个特征(O、H、L、C、V、A),那输入矩阵就是T×6。编码器通过几层一维卷积或者线性投影,把这个T×6的矩阵映射成T×D的隐状态矩阵,D是隐层维度,论文里用的是256。

量化器是Kronos的核心创新点。它维护一个大小为K的codebook,每个码字是一个D维向量。对于编码器输出的每个T×D向量,量化器计算它和所有K个码字的距离,选最近的那个作为量化结果。这样T×D的连续矩阵就变成了T个离散索引,每个索引取值范围是0到K-1。

解码器拿到量化后的token序列,通过多层Transformer block做自回归预测。具体来说,给定前t个token,预测第t+1个token。训练时用teacher forcing,推理时用自回归采样。解码器的输出是一个K维的logits向量,经过softmax后得到下一个token的概率分布。

2.2 Transformer block的细节设计

Kronos的Transformer block和标准Transformer有几个关键差异,这些差异都是针对金融数据特性做的调整。

位置编码用的是可学习的相对位置编码,而不是正弦绝对位置编码。原因很简单:金融数据的时间间隔是不均匀的。股票市场每天交易4小时,期货市场有夜盘,加密货币7×24小时交易。如果用绝对位置编码,模型会误以为“第100个token”和“第200个token”之间的时间距离是固定的,但实际上可能差了好几天。相对位置编码让模型关注“两个token之间隔了多少个时间步”,而不是“它们分别在序列的哪个位置”。

注意力机制用了因果掩码(Causal Mask),这个没什么好说的,自回归模型标配。但Kronos在注意力计算时加了一个时间衰减因子:距离当前token越远的token,注意力权重会被乘以一个小于1的系数。这个设计的直觉是:金融市场里,昨天的价格对今天的影响,通常比上个月的价格大得多。时间衰减因子让模型天然地更关注近期信息。

Feed-Forward Network用了GEGLU激活函数,而不是标准的ReLU或GELU。GEGLU的公式是GEGLU(x) = GELU(xW1) ⊗ xW2,其中⊗是逐元素乘法。这个设计在LLaMA、PaLM等大模型里已经被验证过,效果比标准FFN好。Kronos把它搬过来,实测下来收敛速度确实快了不少。

2.3 训练目标:不只是预测下一个token

如果Kronos只做“预测下一个token”这一件事,那它和普通的自回归语言模型没什么区别。Kronos的训练目标里还加了两个辅助loss,这两个loss对最终效果影响很大。

第一个是重构loss。量化器把连续向量映射到离散码字,这个过程是有信息损失的。为了不让编码器“摆烂”,Kronos加了一个解码器,试图从量化后的token重构原始OHLCVA序列。重构loss就是原始序列和重构序列之间的MSE。这个loss强迫编码器保留尽可能多的原始信息,避免量化过程把有用信号丢掉。

第二个是commitment loss。这是VQ-VAE里的经典设计,目的是让编码器输出的向量和codebook里的码字不要偏离太远。具体来说,commitment loss = ||z_e(x) - sg[z_q(x)]||²,其中z_e是编码器输出,z_q是量化后的码字,sg是stop gradient操作。这个loss让编码器“主动靠近”码字,而不是让码字去追编码器。

三个loss的加权和才是最终训练目标:total_loss = next_token_loss + α * reconstruction_loss + β * commitment_loss。论文里α=0.1,β=0.25,这两个系数是调出来的,不是理论推导的。我试过把α调大到0.5,重构效果确实好了,但预测准确率反而降了,因为模型太关注“记住原始数据”而忽略了“学习序列模式”。

3. 实操复现:从数据准备到模型训练的全流程

3.1 数据准备:OHLCVA序列的清洗与对齐

拿到原始行情数据后,第一件事不是直接喂给模型,而是做清洗和对齐。金融数据里脏东西太多了:停牌期间的空值、涨跌停时的异常成交量、不同数据源之间的时间戳偏差,这些不处理干净,模型学出来的东西全是噪声。

时间对齐是第一步。如果你用的是日线数据,那简单,每天一根K线,时间戳就是交易日。但如果你用的是分钟线,不同交易所的开盘时间可能差几分钟,需要统一到同一个时间网格上。我的做法是:以主交易所的时间戳为基准,其他交易所的数据用前向填充(forward fill)对齐到最近的基准时间戳。

缺失值处理是第二步。停牌期间没有交易,OHLCVA全是空值。直接填0是错的,因为0在价格空间里是一个极端值,模型会误以为价格暴跌到0了。正确的做法是用前一个有效值填充(forward fill),同时加一个mask标记哪些位置是填充的,训练时这些位置的loss不参与计算。

异常值处理是第三步。涨跌停时成交量可能异常放大,或者数据源出错导致某个价格明显偏离合理范围。我的做法是用滚动窗口的MAD(中位数绝对偏差)做异常检测,超过5倍MAD的值标记为异常,用前后值的线性插值替换。

import pandas as pd import numpy as np def clean_ohlcva(df, window=20, mad_threshold=5): # 前向填充缺失值 df = df.fillna(method='ffill') # 计算滚动MAD for col in ['open', 'high', 'low', 'close', 'volume']: rolling_median = df[col].rolling(window).median() rolling_mad = (df[col] - rolling_median).abs().rolling(window).median() outlier_mask = (df[col] - rolling_median).abs() > mad_threshold * rolling_mad df.loc[outlier_mask, col] = np.nan # 线性插值替换异常值 df = df.interpolate(method='linear') return df

归一化是第四步。OHLCVA六个特征的量纲差异巨大:价格可能在几千到几万之间,成交量可能在几百万到几亿之间。如果不做归一化,模型会被大数值特征主导。Kronos论文里用的是z-score归一化,但按品种分别计算均值和标准差。注意,均值和标准差必须用训练集计算,然后应用到验证集和测试集,否则会有信息泄露。

3.2 Token化:VQ-VAE的编码器与码字学习

数据清洗完之后,下一步是训练VQ-VAE,把连续OHLCVA序列变成离散token序列。这一步是Kronos的核心,也是最容易踩坑的地方。

编码器结构:论文里用的是1D卷积+残差连接。具体来说,输入是T×6的矩阵,先经过一个kernel_size=3、stride=1的1D卷积,通道数从6升到64;然后经过4个残差block,每个block包含两个1D卷积和一个skip connection;最后经过一个kernel_size=1的卷积,把通道数降到D=256。整个编码器没有用池化层,因为池化会丢失时间分辨率,而金融数据的时间分辨率很重要。

码字初始化:codebook里的K个码字怎么初始化?随机初始化是最简单的,但收敛慢。Kronos论文里用的是k-means++初始化:先对所有训练数据的编码器输出做k-means聚类,用聚类中心作为码字的初始值。这个技巧能让量化器在训练初期就有一个合理的起点,收敛速度提升明显。

码字更新:训练过程中,码字怎么更新?有两种主流做法:一种是梯度更新,把码字当作可学习参数,用重构loss的梯度更新;另一种是EMA更新,用编码器输出的指数移动平均来更新码字。Kronos用的是EMA更新,因为梯度更新容易导致码字坍缩——所有输入都映射到同一个码字上。EMA更新的公式是:codebook[i] = decay * codebook[i] + (1-decay) * mean(encoder_outputs_assigned_to_i),decay通常取0.99。

class VectorQuantizer(nn.Module): def __init__(self, num_codes, code_dim, decay=0.99): super().__init__() self.num_codes = num_codes self.code_dim = code_dim self.decay = decay self.codebook = nn.Embedding(num_codes, code_dim) self.codebook.weight.data.uniform_(-1/num_codes, 1/num_codes) def forward(self, z_e): # z_e: (B, T, D) distances = torch.cdist(z_e, self.codebook.weight) indices = distances.argmin(dim=-1) z_q = self.codebook(indices) # EMA update if self.training: one_hot = F.one_hot(indices, self.num_codes).float() code_counts = one_hot.sum(dim=(0,1)) code_sums = torch.einsum('btk,btd->kd', one_hot, z_e) self.codebook.weight.data = self.decay * self.codebook.weight.data + \ (1 - self.decay) * code_sums / (code_counts.unsqueeze(-1) + 1e-6) return z_q, indices

码字坍缩的排查:训练VQ-VAE最常见的问题就是码字坍缩——K个码字里只有少数几个被用到,其他都是“死码字”。排查方法很简单:统计每个码字被分配到的次数,如果某个码字在1000个batch里一次都没被用到,那它就是死码字。解决方法有两个:一是用EMA更新代替梯度更新;二是在loss里加一个entropy regularization,鼓励码字使用分布更均匀。

3.3 Transformer训练:从token序列到预测输出

VQ-VAE训练好之后,编码器和码字就固定了,接下来训练Transformer做自回归预测。

训练数据构造:把清洗好的OHLCVA序列喂给编码器,得到token序列。然后做滑动窗口切分,比如窗口长度512,步长1。每个样本是前512个token,标签是第513个token。注意,这里不需要做train/val/test的随机划分,因为时间序列必须按时间顺序划分,否则会有未来信息泄露。

模型配置:Kronos的Transformer有12层,每层8个注意力头,隐层维度512,FFN中间维度2048。总参数量大约50M,和GPT-2 small差不多。这个规模在单卡A100上训练完全没问题,batch_size=64,学习率3e-4,用cosine schedule衰减,训练100个epoch大约需要2天。

训练技巧:

  • 梯度裁剪:金融数据噪声大,梯度容易爆炸,gradient clipping设1.0是标配。
  • warmup:前1000步用线性warmup,学习率从0升到3e-4,避免训练初期震荡。
  • dropout:注意力dropout和FFN dropout都设0.1,防止过拟合。
  • label smoothing:把hard label换成soft label,smoothing系数0.1,提升泛化能力。

推理阶段:给定一段历史token序列,模型自回归地生成下一个token,然后把生成的token拼接到输入序列末尾,继续生成下一个,直到生成足够长的预测序列。这里有个细节:生成时用top-k采样而不是argmax,k=10。argmax会导致生成的序列过于确定,缺乏多样性;top-k采样在保持合理性的同时引入一定随机性,实测下来预测分布更接近真实分布。

4. 实测效果与对比:Kronos到底比传统方法强多少

4.1 评测指标:不只是MSE

金融预测的评测指标和普通时序预测不一样。MSE、MAE这些指标衡量的是数值误差,但金融交易更关心的是方向准确率和风险调整收益。

方向准确率(Directional Accuracy):预测下一根K线是涨还是跌,和真实方向对比。这个指标直接对应交易信号,比MSE更有实际意义。Kronos在多个品种上的方向准确率在55%-60%之间,看起来不高,但金融预测里超过55%就已经有交易价值了。

夏普比率(Sharpe Ratio):用预测信号做多空交易,计算年化收益和波动率的比值。Kronos的夏普比率在1.5-2.0之间,比传统因子模型的0.8-1.2高出一截。

最大回撤(Max Drawdown):策略净值从最高点回撤的最大幅度。Kronos的最大回撤控制在15%以内,传统方法通常在25%以上。

指标KronosLSTM传统因子模型
方向准确率57.3%52.1%53.8%
夏普比率1.821.150.95
最大回撤12.4%23.7%28.1%
年化收益34.2%18.6%14.3%

4.2 消融实验:哪些设计真正有用

Kronos论文里做了一系列消融实验,我挑几个关键的说说。

去掉VQ量化,直接用连续值做回归:方向准确率从57.3%掉到53.1%,夏普比率从1.82掉到1.21。这说明离散化token确实有帮助,分类比回归更适合金融预测。

去掉时间衰减因子:方向准确率掉到55.2%,夏普比率掉到1.53。时间衰减因子让模型更关注近期信息,对短期预测帮助明显。

去掉重构loss:方向准确率掉到54.8%,夏普比率掉到1.38。重构loss强迫编码器保留原始信息,对量化质量影响很大。

codebook size从1024改成256:方向准确率掉到55.6%,夏普比率掉到1.49。码字太少,信息压缩太狠,细节丢失严重。

codebook size从1024改成4096:方向准确率基本不变(57.1%),但训练时间增加了40%。码字太多,收益递减,不划算。

4.3 实盘注意事项

回测效果好不代表实盘能赚钱。我踩过的坑包括:

滑点:回测时假设按收盘价成交,实盘时收盘价往往买不到。我的做法是回测时加0.1%的滑点,实盘时用限价单而不是市价单。

手续费:高频策略的手续费能吃掉大部分利润。Kronos的预测周期是日线级别,手续费影响相对小,但也不能忽略。

过拟合:金融数据非平稳,训练集上表现好不代表测试集好。我的做法是每年重新训练一次模型,用最近3年的数据做训练集,最近1年做验证集。

黑天鹅:模型没见过极端行情,遇到黑天鹅时预测可能完全失效。我的做法是加一个风控模块,当预测置信度低于阈值时,不交易。

5. 常见问题与排查技巧实录

5.1 训练不收敛怎么办

症状:loss震荡不下降,或者下降很慢。

排查步骤:

  1. 检查数据归一化是否正确。如果某个特征的均值和标准差计算错了,模型会被异常值主导。
  2. 检查学习率是否太大。金融数据噪声大,学习率3e-4可能太大,试试1e-4。
  3. 检查码字是否坍缩。统计码字使用分布,如果大部分token都映射到少数几个码字,说明量化器没学好。
  4. 检查梯度是否爆炸。打印梯度范数,如果超过10,加梯度裁剪。

5.2 预测结果全是同一个方向

症状:模型预测的涨跌方向几乎全是涨,或者全是跌。

原因:训练数据里涨跌样本不平衡。如果训练集里70%是涨,模型会倾向于预测涨。

解决:在loss里加类别权重,涨跌样本的权重和它们的频率成反比。或者用focal loss,让模型更关注难分类的样本。

5.3 推理速度太慢

症状:自回归生成100个token需要好几秒。

原因:Transformer的自回归生成是串行的,每个token都要跑一次完整的前向传播。

解决:

  • 用KV cache缓存历史token的key和value,避免重复计算。
  • 用投机采样(speculative decoding),先用一个小模型生成草稿,再用大模型验证。
  • 如果对实时性要求不高,可以批量生成,一次生成多个样本。

5.4 常见问题速查表

问题可能原因解决方法
loss不下降学习率太大/数据未归一化降低学习率/检查归一化
码字坍缩梯度更新导致改用EMA更新
预测方向单一样本不平衡加类别权重/focal loss
推理速度慢自回归串行生成KV cache/投机采样
过拟合模型太大/数据太少加dropout/减小模型
实盘亏损滑点/手续费/黑天鹅加滑点/限价单/风控模块

5.5 独家避坑技巧

技巧一:用多个时间尺度的数据训练。只用日线数据训练,模型学不到日内模式;只用分钟线数据训练,模型看不到长期趋势。我的做法是把日线和分钟线数据混合训练,让模型同时学习不同尺度的模式。

技巧二:在codebook里加一个“未知”码字。金融数据里总有模型没见过的模式,如果强行映射到某个已知码字,会导致预测偏差。加一个“未知”码字,让模型在遇到陌生模式时可以选择“不知道”,而不是强行预测。

技巧三:用ensemble提升稳定性。训练5个不同初始化的Kronos模型,预测时取平均。单模型的预测可能不稳定,ensemble能显著降低方差。实测下来,ensemble的夏普比率比单模型高0.3左右。

技巧四:定期重新训练VQ-VAE。金融市场的统计特性会随时间变化,半年前训练的码字可能已经不适合当前市场。我的做法是每季度重新训练一次VQ-VAE,Transformer可以继续用旧的,但码字要更新。

技巧五:监控码字使用分布。训练过程中定期打印码字使用直方图,如果发现某些码字突然不再被使用,说明市场结构可能发生了变化,需要重新训练。

6. 从Kronos延伸:金融时序Foundation Model的下一步

Kronos把OHLCVA序列当成语言来处理,这个思路打开了一扇门。沿着这个方向,还有几个值得探索的点。

多模态融合:OHLCVA只是价格信息,但金融市场还有新闻、财报、社交媒体情绪等文本信息。把文本和价格序列联合token化,训练一个多模态Foundation Model,可能能捕捉到单靠价格序列看不到的信号。

跨市场预训练:股票、期货、外汇、加密货币,这些市场的价格序列有相似的统计特性。在一个大规模跨市场数据集上预训练,然后微调到特定市场,可能比单市场训练效果更好。

在线学习:金融数据是流式的,模型需要不断更新。用在线学习的方式,让模型在新数据到来时增量更新,而不是重新训练,可能更适合实盘场景。

可解释性:Transformer的注意力权重可以可视化,看看模型在预测时关注哪些历史token。如果模型总是关注某些特定模式,那这些模式可能就是真正的alpha来源。

我个人在实际操作中的体会是,Kronos这类方法最大的价值不是它预测得多准,而是它提供了一种新的建模范式。传统量化因子是“人找规律”,Kronos是“模型找规律”。人找规律受限于人的认知偏差,模型找规律能发现人看不到的模式。当然,模型找的规律不一定靠谱,需要严格的风控和持续的监控。但至少,这条路是走得通的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询