☰
FaceCat-Kronos:从时间序列预测到量化信号的完整实践
2026/10/10 5:01:48 网站建设 项目流程

简介:FaceCat-Kronos是一套基于清华大学Kronos框架与深度神经网络的金融量化预测工具,由花卷猫量化团队整理发布,适合量化投资研究者、高频策略开发人员以及金融工程方向的学习者。压缩包内置50个文件,整体约19.43MB,主要包含23个Python源码脚本、10张界面与回测截图、8个备份文件以及JSON/Markdown配置说明;源码、模型与文档分层存放,查阅与二次开发都比较方便。该资源已有565人学习下载。其中提供了可运行的训练与预测示例,覆盖K线预测、回测模式、分时图、五档行情等应用场景,并附带Kronos小型模型与分词器组件,有助于快速复现基于深度学习的市场波动识别流程,理解做市商行为建模、特征工程及时间序列趋势预判的具体实现。对于希望掌握Kronos架构在量化领域落地方法的读者,这份材料具备较强的参考与迁移价值。

1. FaceCat-Kronos 究竟做了什么:从时间序列预测到可直接下单的量化信号

做量化的人都知道一条血泪经验:大部分 AI 模型拿来做金融预测,翻车点往往不在模型本身,而在“预测出来之后怎么办”。很多团队花几个月训出一个 LSTM 或 Transformer,预测曲线画得漂亮,真放进回测却亏得稳定。FaceCat-Kronos 这个工具方向,讨巧的地方在于它不是一个模型调参的玩具,而是一整套基于 Kronos 时间序列框架的、从数据清洗到预测信号再到回测验证的量化 AI 闭环。Kronos 的长处是它保留了带解释性的注意力权重和概率区间输出,FaceCat-Kronos 把它接到金融数据上,解决的是“预测结果可解释、置信区间可量化、信号能落地”这三个实际投研问题。

这套工具适合三类人:自己写策略但总是被数据预处理和序列对齐拖住的人;想用深度学习模型做多因子或择时、但不想从零维护样例行数据管线的人;以及要参加量化比赛或做内部研究、需要一个快速验证思路的框架底座的人。读完这篇,你能搭起一个最小可用的 FaceCat-Kronos 项目,知道参数怎么设、坑在哪、回测结果怎么判断是真的还是过拟合。

2. 从 Kronos 到 FaceCat-Kronos:框架选型理由与整体数据管线

2.1 Kronos 凭什么适合做金融序列预测:概率输出、可解释注意力、非平稳适应

如果你手头有一列价格序列,直接套用普通 Transformer 做预测,最容易出的问题有两个:第一,金融数据远非平稳,今天和明天的均值漂移可能比模型学到的规律本身还大;第二,Transformer 的全局注意力会导致它对最近一段突变反应过激,尤其是当你用滚动窗口训练时。Kronos 的核心是 TFT(Temporal Fusion Transformer)的延续,它在结构上做了几件对金融数据非常有用的事:变量选择网络(Variable Selection Network)会为每个时间步自动评估哪些特征更重要,这等于内置了一份特征重要性的动态报告;分位数回归输出不是预测一个点,而是给出 10%、50%、90% 分位区间,这个区间在金融里可以直接被翻译成“最坏情况下的亏损范围”;它的循环编码层对局部时间模式做了一次预压缩,再进注意力层,防止注意力被单日暴涨暴跌牵着走。

我在选型时对比过 Informer、Autoformer 这类纯 Transformer 变形,它们在竞赛数据上指标好看,但落地到日频和分钟频金融序列时,分位数输出的价值没有 Kronos 系框架明显。FaceCat-Kronos 的定位就是接住 Kronos 的这些能力,再把金融数据常见的脏问题在进模型之前解决掉。

2.2 FaceCat-Kronos 目录结构与数据模块实现:一份可照抄的工程骨架

常见做法是把整个项目拆成五个模块:数据采集与清洗、特征构造、模型训练、回测验证、信号接口。我一般这样组织目录:

facecat-kronos/ ├── configs/ │ ├── kronos_config.yaml │ └── symbols.yaml ├── data/ │ ├── raw/ │ ├── processed/ │ └── cache/ ├── src/ │ ├── data_pipeline/ │ │ ├── loader.py │ │ ├── cleaner.py │ │ └── aligner.py │ ├── features/ │ │ ├── technical.py │ │ └── macro.py │ ├── train/ │ │ ├── train_kronos.py │ │ └── evaluate.py │ └── backtest/ │ ├── engine.py │ └── metrics.py └── logs/

这个结构的关键不是目录名,而是强制你区分 raw 和 processed。很多第一次做 AI 量化的人把原始行情和特征文件混在一起,回测时一改特征就要重跑全部数据,非常浪费时间。下面是最小数据加载脚本,逻辑是先读原始 CSV,做基本清洗后缓存成 parquet。

# src/data_pipeline/loader.py import pandas as pd import numpy as np from pathlib import Path def load_kline_csv(path: str, freq: str = "1D") -> pd.DataFrame: df = pd.read_csv(path, parse_dates=["datetime"]) df = df.sort_values("datetime").reset_index(drop=True) # 统一列名,不同数据源经常叫 Close 或 close df.columns = [c.lower() for c in df.columns] df = df[["datetime", "open", "high", "low", "close", "volume"]] # 去重与排序是金融数据最基础也最容易被跳过的步骤 df = df.drop_duplicates(subset=["datetime"]) return df def cache_to_parquet(df: pd.DataFrame, output_path: str): Path(output_path).parent.mkdir(parents=True, exist_ok=True) df.to_parquet(output_path, index=False)

这段脚本的逻辑很直白,但有一个细节值得注意:排序必须在去重之前?不对,更稳的做法是先去重再排序,因为不同数据源可能存在同一时间戳多行记录的情况,先去重后排序才能保证时间顺序。参数freq目前只作注释用,因为真实情况中,日线和分钟线的清洗规则完全不同。分钟线要处理夜盘和集合竞价时段,如果直接按1D重采样,夜盘的跳空会被错误地算进日线开盘价里。

2.3 金融数据对齐的隐蔽陷阱:交易时段、时区、停牌

这是 FaceCat-Kronos 数据管线的第一个坑,也是新手最容易翻车的地方。如果你同时输入中国期货主连数据和外盘指数数据,两者交易时间不一样,直接按时间戳合并会有大量空行。我一般会写一个专门的 aligner,把不同品种的时间索引统一到目标品种的交易日历上,空白的值用前向填充加标志位,而不是用均值插值。因为金融数据不能假设中间缺失值是均匀分布的,停牌期间你看到的均值,往往来自完全不同的市场情绪环境。

# src/data_pipeline/aligner.py def align_to_calendar(df: pd.DataFrame, calendar: pd.DatetimeIndex) -> pd.DataFrame: df = df.reindex(calendar) # 前向填充只适合“停牌延续上一交易日状态”的场景 df = df.fillna(method="ffill") # 不要用 mean 或 interpolate # 标记缺失来源,模型可以学到“这个特征今天其实是停牌” df["is_filled"] = df["close"].isna().astype(int) return df

这段代码的逻辑说明:reindex后出现的 NaN 先用前向填充,保留一个is_filled标志位给模型,让模型知道某些特征是“继承”来的而非真实交易的。参数上没有太多可调空间,但原理层面要理解:停牌、涨跌停、流动性枯竭都会产生无效值,如果直接删掉这些时序行,你回测的资金曲线会比实际平滑很多,因为最坏的那些日子被你删了。

这一章的重点并不在于把脚本写得多复杂,而是要建立一条规矩:任何原始数据进入模型之前,必须经过“加载 → 统一列名与时区 → 对齐交易日历 → 标记填充位 → 缓存”这五步。FaceCat-Kronos 的后续所有模块都建立在这套干净的格式上。

3. 特征工程与标签设计:量化预测能不能赚钱,一半看这里

3.1 该用哪些特征:价量因子、技术指标、外部状态

很多人一上来就堆几十个技术指标,MACD、KDJ、RSI、BOLL 一起来,Kronos 的变量选择网络确实能扛住,但你得知道它扛的原理是什么:变量选择网络会对无关特征赋予接近零的权重,所以多塞几个无害。代价是训练变慢,且变量选择权重本身可能不稳定,尤其当特征之间存在多重共线性时。我的习惯是先放一组基础特征,再放两组行为特征,再放一组状态特征。

基础特征包括:对数收益率(日频 5 天、10 天、20 天)、最高最低价相对收盘价的距离、成交量相对 20 日均量的倍数。行为特征包括:连续上涨/下跌天数、波动率突破(当前已实现波动率是否超过过去 60 天的 95 分位)、量价背离信号。状态特征包括:当日是月初还是月末、距上次宏观数据发布日的天数、市场整体情绪指标。我不放任何需要未来数据才能计算的指标,这是底线。

# src/features/technical.py import pandas as pd import numpy as np def add_technical_features(df: pd.DataFrame, window: int = 20) -> pd.DataFrame: # 对数收益率是更平稳的分布形态 df["log_ret_1"] = np.log(df["close"] / df["close"].shift(1)) df["log_ret_5"] = df["log_ret_1"].rolling(5).sum() df["log_ret_20"] = df["log_ret_1"].rolling(20).sum() # 波动率突破是状态特征,报出“市场是否进入异常期” df["volatility_20"] = df["log_ret_1"].rolling(window).std() df["vol_breakout"] = (df["volatility_20"] > df["volatility_20"].rolling(60).quantile(0.95)).astype(int) # 量能倍数:当日成交量相对过去20日均量的比例 df["vol_ratio"] = df["volume"] / df["volume"].rolling(window).mean() return df

参数说明:window=20对应一个月交易日的周期感官,对日频策略比较通用。rolling(60).quantile(0.95)这个写法要注意,它是拿滚动窗口内的分位数做动态阈值,而不是拿全样本做固定阈值。如果拿全样本算 95 分位,等于用了未来数据,训练出来的模型在实盘里会明显退化。

3.2 标签设计决定预测的性质:回归点预测还是分位数区间

在 FaceCat-Kronos 里,预测目标不是一个固定的“下一日收盘价”,而是未来 5 个交易日收益的分位数分布。我选择的标签是这样构造的:用未来 5 日的收盘价相对当前收盘价计算对数收益,然后把它作为回归目标。Kronos 在训练时通过分位数损失函数(pinball loss)学会输出多个分位点,这两个能力刚好咬合在一起。

# 标签构造示例 def create_labels(df: pd.DataFrame, horizon: int = 5) -> pd.DataFrame: # 未来收益的 log,注意 shift 的方向不能搞反 df["target_fwd_ret"] = np.log( df["close"].shift(-horizon) / df["close"] ) # 去掉最后 horizon 行,因为它们的标签是空值 df = df.iloc[:-horizon] return df

这段的坑集中在shift(-horizon)上,新手常写成shift(horizon),那样标签就会变成过去收益,但训练时又不会立刻报错,因为张量形状是对的,只是学的东西反了。另一个细节是,最后 5 行的target_fwd_ret是 NaN,如果不去除它们,pytorch 训练会直接报错或静默跳过,两种行为都会污染你的 baseline。

3.3 时间序列训练集划分:绝对不能用随机 shuffle

这是我在量化 AI 里见过最贵的一课。普通机器学习任务里,随机划分训练集和测试集是标准操作,但时间序列不行。FaceCat-Kronos 的默认做法是用“时间块”划分:前 70% 的时间段做训练,接下来 15% 做验证,最后 15% 做测试。而且验证集必须从训练集之后的时间开始,因为金融模型一旦看到了未来哪怕一条样本,它的回测结果就会虚高。

def split_time_series(df, train_ratio=0.7, val_ratio=0.15): n = len(df) train_end = int(n * train_ratio) val_end = int(n * (train_ratio + val_ratio)) train = df.iloc[:train_end] val = df.iloc[train_end:val_end] test = df.iloc[val_end:] return train, val, test

这里没有太多参数要解释,但有一个值得强调的工程细节:切分完之后,记得检查训练集和验证集之间是否存在同一根 K 线的标签重叠。因为标签是用未来 5 日收益构造的,训练集最后 5 条样本的标签会延伸到验证集的时间范围里,造成轻微的数据泄漏。我一般会从训练集末尾多切掉horizon行,再做切分,成本极低但能避免不少解释不清的过拟合迹象。

4. 用 Kronos 训练金融预测模型:配置、命令与三个必调参数

4.1 最小训练脚本:从 CSV 到分位数预测只要一个入口

FaceCat-Kronos 把训练流程封装成一个入口脚本,内部按“加载特征 → 构造时序数据集 → 配置 Kronos 模型 → 训练 → 保存 checkpoint”的顺序执行。下面是一份可以直接抄来改的脚本骨架。

# src/train/train_kronos.py import yaml import torch from kronos import KronosModel # 假设安装后的包名,按你的环境调整 from src.data_pipeline.loader import load_kline_csv, cache_to_parquet from src.features.technical import add_technical_features from src.features.label import create_labels def main(config_path: str): with open(config_path, "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) df = load_kline_csv(cfg["data"]["path"]) df = add_technical_features(df, window=cfg["features"]["window"]) df = create_labels(df, horizon=cfg["label"]["horizon"]) # 关键:特征列顺序固定下来,训练和推理必须一致 feature_cols = cfg["features"]["columns"] target_col = cfg["label"]["target_col"] model = KronosModel( context_len=cfg["model"]["context_len"], forecast_len=cfg["label"]["horizon"], hidden_size=cfg["model"]["hidden_size"], quantiles=[0.1, 0.5, 0.9], ) trainer = KronosTrainer( model=model, train_batch_size=cfg["train"]["batch_size"], val_batch_size=cfg["train"]["val_batch_size"], max_epochs=cfg["train"]["max_epochs"], learning_rate=cfg["train"]["learning_rate"], ) trainer.fit(train_df, val_df) torch.save(model.state_dict(), cfg["train"]["save_path"]) if __name__ == "__main__": import sys main(sys.argv[1])

这段代码的逻辑顺序是固定的:先加载,再特征,再标签,然后初始化模型和 trainer。quantiles=[0.1, 0.5, 0.9]是 Kronos 系模型的核心,意思是模型会输出三条预测曲线,分别对应悲观、中性、乐观场景,你在回测里可以用 0.5 分位做方向信号,用 0.1 和 0.9 的间距做仓位调节的置信信号。

4.2 配置文件里的参数怎么设:三个直接影响盈亏的参数

FaceCat-Kronos 的配置集中在 YAML 里,下面是一份我常用的初始配置,适用于日频股票/期货指数类数据。

data: path: "./data/processed/rb_main.parquet" freq: "1D" features: window: 20 columns: ["log_ret_1", "log_ret_5", "log_ret_20", "volatility_20", "vol_breakout", "vol_ratio", "high_low_pos", "macro_state", "is_filled"] label: horizon: 5 target_col: "target_fwd_ret" model: context_len: 60 hidden_size: 64 num_layers: 2 dropout: 0.2 train: batch_size: 64 val_batch_size: 256 max_epochs: 50 learning_rate: 0.001

参数说明从最重要的三个开始。第一是context_len,即模型每次“回头看”多少根 K 线。60 对应日频 3 个月左右,这基本是中期趋势的常见窗口;如果做分钟频,可以砍到 30 或 15,但不能再短,否则注意力模块发挥不出作用。第二是horizon,预测未来几根 K 线,5 日是个平衡点,太短(1 日)噪声大,太长(20 日)标签之间重叠严重导致回测虚高。第三是learning_rate,金融数据量通常不大,0.001 是 Transformer 系的安全起点,太大会直接发散,太小则容易卡在局部最优上,典型的失败现象是训练 loss 下降极其缓慢。

4.3 判断训练是否正常的标准:不只盯 loss,要看分位数区间形状

很多人在训练时只看 validation loss 有没有降,这远远不够。Kronos 模型输出了 0.1/0.5/0.9 三分位数,你要去验证这三条曲线是不是嵌套合理的:0.1 分位应该在 0.5 分位下方,0.9 在上方,而且训练充分后这个区间带应该呈现“震荡时收窄、趋势初期变宽”的形状。如果你的模型的 0.1 和 0.9 长期纠缠在一起,说明模型没有学到不确定性结构,这时候大概率不是调参问题,而是特征或标签设计的问题。

我一般会在训练日志里定期打印一个简单的区间统计指标:预测区间覆盖率(PICP),也就是真实值落在 0.1~0.9 区间内的比例,金融数据 5 日预测里这个值在 70%~90% 算是正常,低于 60% 就得回头查标签有没有泄漏,或者序列是否过度平滑。

5. 避坑专题:FaceCat-Kronos 从训练到回测的 5 条踩坑记录

5.1 回测曲线漂亮得离谱?先查前视偏差

现象:模型在测试集上的资金曲线几乎是一条稳定向上的直线,最大回撤不足 2%。原因:某个特征或标签无意中引入了未来信息,最常见的是用了“未来 N 日的最高价”构造动量指标,或者在全样本上做了标准化。解决:检查所有特征的滚动计算是否都只依赖过去数据,标准化时用rolling_mean和rolling_std而非全样本的mean和std。我专门写了一个脚本,把每个特征列向右 shift 一位再重算与标签的相关性,如果在 shift 后相关性仍然显著,这个特征多半带前视,直接删。

5.2 验证集上 PICP 正常,实盘却一直亏

现象:离线测试指标都不错,放上实盘模拟盘连续亏损。原因:测试集和实盘之间的市场状态偏移(regime shift),比如测试集是震荡市,实盘进入趋势市,模型的高分位区间失去了参考意义。解决:这种情况在 FaceCat-Kronos 上可以做一个很实用的操作——用 0.5 分位的预测方向只做信号,同时要求 0.1 和 0.9 区间的宽度必须超过过去 20 日的滚动中位数才开仓,区间过窄说明模型当前没有把握,强制空仓。这会让回测少赚很多,但能保命。

5.3 训练 loss 震荡不收敛,先降学习率还是查数据?

现象:loss 曲线上下抖动,像锯齿一样。原因:学习率偏高是常见因素,但如果已经降到 0.0005 还不行,多半是数据里有异常值,比如某天成交量因为数据源问题多打了一个零。解决:先做特征值分布检查,把所有数值特征的 99.9% 分位数打印出来,和业务常识对比。我遇到过某数据源的成交量偶尔会膨胀 100 倍,一个异常值就能毁掉整个注意力模块的稳定性。剔除异常值用clip而不是删除整行,因为删除会造成时间索引断裂。

5.4 分位数输出区间宽度随训练不变化

现象:训练了几个 epoch,0.1 和 0.9 分位输出几乎重合。原因:可能是 dropout 设得太大(大于 0.5)导致模型学成了一个接近确定性的映射,也可能是分位数权重在损失函数里没有生效。解决:检查损失函数是否真的用了 pinball loss 还是默认改成了 MSE。Kronos 系的实现里分位数损失是核心,如果误改成 MSE,分位数区间就会变成一条线。dropout 我一般不超过 0.3,金融数据本身噪声大,过强的正则会把不确定性也“正则”掉。

5.5 回测引擎里的滑点成本被严重低估

现象:策略在日频上连续两年盈利,按每分钟撮合跑一遍却变成亏损。原因:回测使用的是收盘价成交且不计算滑点,而实际信号触发后你不可能用收盘价买到,尤其信号出现往往伴随放量。解决:至少按双边万五手续费加一跳滑点起步,如果做的是开盘价成交,要在开盘价基础上额外加一个冲击成本估算,一般是近 10 日平均盘口价差的二分之一。这一条永远放最后一步做,因为前面所有模型优化都建立在干净数据上,而滑点只会放大错误决策的影响,不会修正它。

6. 验证与迭代:用统计检验和指数对比判断模型是否真的有价值

训练完一个 FaceCat-Kronos 模型之后,先不要急着跑回测曲线,做三件事:第一,把模型输出的 0.5 分位预测方向和一个“随机方向”基线做对比。具体做法是维持持仓周期和仓位完全不变,但每次开仓方向随机,重复 200 次,画出收益分布。如果真实模型的收益在这 200 条随机曲线里排不到前 10%,你的模型大概率只是靠运气或者扛了更高的风险暴露。第二,计算 IC(信息系数),也就是预测值与实际值的秩相关系数,日频 5 日预测的 IC 稳定在 0.03 以上就已经是可用信号,超过 0.06 在实盘里已经属于稀缺能力。第三,做参数敏感性分析,把context_len从 30 调到 120,每个值重新训一次,如果模型只在某一个窗口下盈利而其他窗口全亏,这个策略的稳健性就值得怀疑;如果各窗口收益差异不大,反而说明模型学到的是更本质的东西。

迭代的顺序我一般是这样:先用毛利判断方向对不对,再逐项加成本、滑点、限制做空等约束,每加一次约束,如果收益从正转负,就回头精修信号;如果收益从负转正,那才是真正找到了被成本结构掩盖的 alpha。在 FaceCat-Kronos 上做迭代,最大优势是分位数输出让每一次迭代的验证逻辑都多了一个维度——我做模型迭代时必看的一个指标是“区间方向正确率”,也就是开仓方向上,未来真实收益和 0.5 分位预测方向一致的比例,这个值稳定在 52% 以上就值得继续投入,低于这个数说明不确定性结构还没被正确学习,需要回炉重造特征。这套“看方向正确率而非绝对收益”的习惯帮我避开了很多次被回测曲线欺骗的乌龙,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询