资讯详情

资讯详情

Kronos:将OHLCVA序列Token化,用Transformer预测金融时序

1. 从OHLCVA到TokenKronos到底在解决什么问题金融市场的原始数据长什么样打开任何一个行情软件你看到的是一根根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预测第t1个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, window20, mad_threshold5): # 前向填充缺失值 df df.fillna(methodffill) # 计算滚动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(methodlinear) return df归一化是第四步。OHLCVA六个特征的量纲差异巨大价格可能在几千到几万之间成交量可能在几百万到几亿之间。如果不做归一化模型会被大数值特征主导。Kronos论文里用的是z-score归一化但按品种分别计算均值和标准差。注意均值和标准差必须用训练集计算然后应用到验证集和测试集否则会有信息泄露。3.2 Token化VQ-VAE的编码器与码字学习数据清洗完之后下一步是训练VQ-VAE把连续OHLCVA序列变成离散token序列。这一步是Kronos的核心也是最容易踩坑的地方。编码器结构论文里用的是1D卷积残差连接。具体来说输入是T×6的矩阵先经过一个kernel_size3、stride1的1D卷积通道数从6升到64然后经过4个残差block每个block包含两个1D卷积和一个skip connection最后经过一个kernel_size1的卷积把通道数降到D256。整个编码器没有用池化层因为池化会丢失时间分辨率而金融数据的时间分辨率很重要。码字初始化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, decay0.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个注意力头隐层维度512FFN中间维度2048。总参数量大约50M和GPT-2 small差不多。这个规模在单卡A100上训练完全没问题batch_size64学习率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 labelsmoothing系数0.1提升泛化能力。推理阶段给定一段历史token序列模型自回归地生成下一个token然后把生成的token拼接到输入序列末尾继续生成下一个直到生成足够长的预测序列。这里有个细节生成时用top-k采样而不是argmaxk10。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震荡不下降或者下降很慢。排查步骤检查数据归一化是否正确。如果某个特征的均值和标准差计算错了模型会被异常值主导。检查学习率是否太大。金融数据噪声大学习率3e-4可能太大试试1e-4。检查码字是否坍缩。统计码字使用分布如果大部分token都映射到少数几个码字说明量化器没学好。检查梯度是否爆炸。打印梯度范数如果超过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-VAETransformer可以继续用旧的但码字要更新。技巧五监控码字使用分布。训练过程中定期打印码字使用直方图如果发现某些码字突然不再被使用说明市场结构可能发生了变化需要重新训练。6. 从Kronos延伸金融时序Foundation Model的下一步Kronos把OHLCVA序列当成语言来处理这个思路打开了一扇门。沿着这个方向还有几个值得探索的点。多模态融合OHLCVA只是价格信息但金融市场还有新闻、财报、社交媒体情绪等文本信息。把文本和价格序列联合token化训练一个多模态Foundation Model可能能捕捉到单靠价格序列看不到的信号。跨市场预训练股票、期货、外汇、加密货币这些市场的价格序列有相似的统计特性。在一个大规模跨市场数据集上预训练然后微调到特定市场可能比单市场训练效果更好。在线学习金融数据是流式的模型需要不断更新。用在线学习的方式让模型在新数据到来时增量更新而不是重新训练可能更适合实盘场景。可解释性Transformer的注意力权重可以可视化看看模型在预测时关注哪些历史token。如果模型总是关注某些特定模式那这些模式可能就是真正的alpha来源。我个人在实际操作中的体会是Kronos这类方法最大的价值不是它预测得多准而是它提供了一种新的建模范式。传统量化因子是“人找规律”Kronos是“模型找规律”。人找规律受限于人的认知偏差模型找规律能发现人看不到的模式。当然模型找的规律不一定靠谱需要严格的风控和持续的监控。但至少这条路是走得通的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →