
简介基于Kronos框架与AI的金融量化预测工具FaceCat-Kronos由花卷猫量化团队研发面向对深度学习与量化交易感兴趣的开发者及金融数据分析人员主要用于识别传统技术指标难以捕捉的市场波动特征为不同投资周期的交易决策提供参考。压缩包共50个文件、约19.43MB以23个Python脚本为功能核心覆盖数据预处理、模型训练、预测与回测示例另含10张PNG界面及K线截图、8个zbak备份文件、JSON参数配置、README说明文档以及模型目录与DLL动态库结构清晰便于按模块研读。该资源已有562人学习浏览属于中等热度的实战型资料。通过源码和示例可了解完整建模流程借助截图可直观认识预测界面与回测模式配套数据文件与备份便于复现实验适合有一定Python基础的量化初学者作为实践参考。1. Kronos框架和FaceCat的碰撞基础模型做K线预测到底值不值得用把 Kronos 框架——Amazon 开源的时间序列基础模型——接到 FaceCat 量化终端上做 K 线预测这件事我前后折腾了两周。先说结论它能用但不是你想的那种AI 预测涨停它对突发消息和跳空基本无能为力对趋势形成的识别却明显比传统 ARIMA 和指数平滑干净。这个项目解决的是零标注、免训练、多周期的 K 线预测适合有 Python 基础、想验证大模型在量化行情上到底能不能打的人。学习成本不高坑却不少。下面我会按数据、推理、回测、踩坑的顺序把整套链路拆开每个环节都给了能直接跑的代码。2. 整体设计把 FaceCat 的 K 线变成 Kronos 能读懂的 context三层管线拆给你看FaceCat-Kronos 的工程结构不复杂核心就三层数据层负责把行情落地成干净的DataFrame特征层负责把价格序列变成模型能吃的数值张量推理层负责加载 Kronos 权重并输出预测分布。这样拆的好处是每层都能单独调试比如你想把数据源从 FaceCat 换成别的终端模型代码一个字都不用动。2.1 数据层K线导出与交易日补齐FaceCat 本地端可以把日线、小时线直接导出成 CSV导出时选好合约、起始日期和周期就行。我一般拉过去三年的日线大约七百多根K线太少的话后面构建 256 根的 context 窗口会很局促。拿到文件后第一步不是直接喂模型而是先做排序、去重、建索引字段名统一成 ts, open, high, low, close, volume。import pandas as pd df pd.read_csv(fc_klines.csv, parse_dates[ts]) df df.sort_values(ts).drop_duplicates(subsetts) df df.set_index(ts) # 金融序列天然有缺口周末、节假日、临时停牌 # Kronos 内部按固定步长切patch缺一天它不会自动感知 idx pd.date_range(df.index.min(), df.index.max(), freqD) df df.reindex(idx) df[[open, high, low, close, volume]] ( df[[open, high, low, close, volume]].ffill() ) df df.dropna() print(df.tail())逻辑说明Kronos 的 tokenizer 会把 context 切成等宽的 patcht1 紧挨着 t是它的隐含假设。A 股有长假、有临时停牌缺出来的日子不补模型看到的节奏就会整体错位。我补空缺用的是 ffill也就是拿前一天的收盘价顶上去对流动性好的股票影响不大但如果你做的是期货夜盘交易时段本来就不连续这个补齐逻辑要按主力合约的连续收盘价来做不能照抄。参数说明freqD 表示按自然日生成索引ffill 保证 OHLC 不断裂dropna 清掉文件开头可能存在的脏行。有个取舍要记住——补出来的日子本身是假数据所以后面计算持仓天数时不要把周末算成真实交易日否则止损和持仓周期的统计都会被污染。2.2 特征层用收益率而不是原始价格这一层是很多人直接翻车的地方。Kronos 在预训练时见过各种量级的序列但你把 3000 元的茅台和 3 元的 ST 股混在一起喂进去模型数值尺度会失衡预测方差会明显变大。我一般先把收盘价转成对数收益率再做一次标准化让 context 落在零均值、单位方差的范围里。import numpy as np close df[close] ret np.log(close / close.shift(1)).dropna() # 标准化零均值、接近单位方差Kronos 对分布偏移更稳 mu, sigma ret.mean(), ret.std() ctx_series (ret - mu) / sigma # 只取最近 T 根K线作为 context长度建议至少 256 T 256 ctx_tensor torch.from_numpy(ctx_series.values[-T:]).float() print(ctx_tensor.shape)逻辑说明对数收益率比简单 pct_change 好在近似对称、可加性强多日的对数收益直接相加再取指数就能还原成累计收益。标准化用的 mu 和 sigma 只允许在 context 窗口内计算这一点到第 5 章讲归一化泄漏时还会再提这是整个项目里最阴的一个坑。参数说明T256 是我的默认值。Kronos 对 context 长度没有硬限制预训练时见过更长的序列但金融数据喂太长会把很久以前的水平带进来扰动近期判断。标准化用的 mu/sigma 必须保存下来预测完还原收益率的公式是 close[t1] close[t] * exp(mu sigma * y[t1])丢了这些参数就还原不出目标价。2.3 推理层最小可运行的预测闭环第三层就是真正的模型调用。FaceCat-Kronos 把 Kronos 封装在 predictor 模块里对外只暴露三个参数context、frequency、horizon。去掉外壳后是下面这段最小可运行代码。import torch from kronos import Kronos device torch.device(cuda if torch.cuda.is_available() else cpu) model Kronos.from_pretrained(amazon/Kronos-Tiny, device_mapdevice) model.eval() with torch.no_grad(): samples model.generate( contextctx_tensor.unsqueeze(0), # (batch, T) frequency1D, horizon5, num_samples100, ) # samples 形状: (num_samples, batch, horizon)逻辑说明Kronos 是 encoder-only 的概率预测模型generate 不像 ChatGPT 那样逐字吐字符而是从预测分布里采样多条未来路径每条路径就是一个可能的五日收益率序列。我们取 100 条样本本质上是在模拟如果历史重演未来可能走哪几条路。Tiny 参数最小CPU 上跑 256 点 context 也就一两秒。参数说明frequency 是模型判断节奏的提示词1D 代表日线1h 小时线1min 分钟线写错不报错但结果会很飘。horizon5 表示往后预测 5 个交易日。num_samples 控制采样条数回测时 50 条就够实盘前拉到 200。batch 维度必须存在Kronos 不接受一维裸 tensor忘了 unsqueeze(0) 是最常见的低级报错。三层管线通了之后接下来要决定用哪个规格的模型。Kronos 官方给了 Tiny、Small、Base 三档权重Tiny 几十 MBCPU 可跑Base 在金融数据上更稳但推理耗时翻倍。我的习惯是先用 Tiny 把整个链路调通确认信号有区分度再切到 Base 做正式回测。3. 接入 Kronos 模型加载权重、zero-shot 推理与时序对齐的三个关键点3.1 环境安装与模型加载Kronos 依赖 torch 和 transformers安装本身不复杂但环境里如果有旧版 torch加载权重时很容易报 shape mismatch。我会新建一个干净的 conda 环境专门跑这套避免跟其他项目的依赖打架。conda create -n fck python3.10 -y conda activate fck pip install torch2.1 pandas numpy pip install kronos逻辑说明如果 pip 没收录就去官方 GitHub 仓库 clone 源码本地装效果一样。装上之后先跑一遍官方样例确认能出数再接到 FaceCat-Kronos 的 predictor 上。这一步走通之后基本不会再动环境后续踩的坑都在数据侧。参数说明python3.10 是我习惯的版本3.11/3.12 在部分 torch 组合下也能跑但没必要在环境上浪费排查时间。torch2.1 是为了保证 device_map 参数可用老版本只能先 model model.to(device)而且加载完容易忘记把权重搬到 GPU。加载模型时还有三个细节。第一第一次 from_pretrained 会联网下载权重Tiny 只有几十 MB但网络环境不稳定的话建议手动把权重目录放到本地再用本地路径加载省得每次跑脚本都要等下载。第二加载后确认一下模型参数确实在目标设备上否则会出现加载成功但推理还在 CPU的尴尬。第三推理前务必 model.eval()不然 batch norm 和 dropout 会把预测分布搞乱信号会忽大忽小。3.2 频率字符串与 horizon 的对齐frequency 参数不是给人看的是给模型对齐 patch 用的。模型把 context 按固定步长切成等宽 patchpatch 长度和频率取值绑定daily 的 patch 覆盖一天hourly 的 patch 只覆盖一小时完全不是同一套切法。我的经验是交易数据必须用和真实节奏一致的频率字符串horizon 的单位也要和 frequency 对上。def make_prediction(ctx_tensor, freq, horizon, n_samples100): with torch.no_grad(): samples model.generate( contextctx_tensor.unsqueeze(0), frequencyfreq, horizonhorizon, num_samplesn_samples, ) return samples # 日线预测未来5个交易日 daily_samples make_prediction(ctx_tensor, 1D, 5) # 小时线预测未来12根小时K hourly_samples make_prediction(ctx_tensor_h, 1h, 12)逻辑说明最容易犯的错是日线数据标 1D却要求 horizon120以为能产出长期趋势。实际上模型把长 horizon 拆成了迭代生成的 120 步误差随步数累积。我一般控制 horizon 不超过 context 长度的十分之一日线上最多预测 25 根超过就改用滚动预测——也就是每预测一步把真实结果加进 context 再预测下一步。参数说明freq 必须是字符串字面量Kronos 预训练见过的频率集合覆盖常见业务频率但 W-MON 这种 pandas 风格写法它不一定认识。保守做法是统一转成 1D/1h/1min如果是周线先重采样成日线再用 1D 预测别自己造一个 1W 出来。3.3 从预测分布到交易信号这一步是把模型的混沌输出变成可执行指令的关键。Kronos 的 generate 返回的是采样路径不是点估计。如果直接对样本取平均当作预测价等于把 100 条路径的尾部信息全部抹掉信号区分度会差到没法用。正确做法是取分位数而且按交易方向决定看哪个分位。paths samples[:, 0, :] # 去掉 batch 维度只留单标的 # 未来5日的累计收益每条路径连乘再减1 cum_return paths.prod(dim1) - 1.0 lower np.percentile(cum_return.numpy(), 5) median np.percentile(cum_return.numpy(), 50) upper np.percentile(cum_return.numpy(), 95) print(f5日累计收益 5%分位: {lower:.4f} 中位数: {median:.4f} 95%分位: {upper:.4f})逻辑说明用累计收益的分位数而不是单日分位数是因为交易决策关心的是持仓到期时的总盈亏。中位数代表模型的中心预期5% 分位代表 worst case95% 分位代表 best case。我后面判断信号的阈值全都建立在这三个数上中位数明显为正且 5% 分位不为负才考虑做多做空看对称条件。参数说明5/50/95 这组分位数是我反复试下来最稳的组合。5% 分位在回测里充当止损预演它能很直观地暴露 2015 年股灾那种尾部行情。如果你只做短线可以把 horizon 从 5 缩到 2但分位数组不用换。拿到这三个数之后下一步就是设计信号规则、跑回测然后对接 FaceCat 下单。4. 量化落地从信号生成到回测再到 FaceCat 下单的完整链路4.1 信号规则设计有了分位数下一步把它编译成纪律。我从头到尾只用一条规则越复杂越容易在回测里自我感动中位数预测收益大于 1% 且 5% 分位大于 -0.5% 时开多做空条件对称。持仓窗口直接等于 horizon不做中途止盈止损线由 5% 分位决定。def generate_signal(median, lower, upper, horizon_price): # median: 预测累计收益中位数; lower/upper: 5%/95%分位 long_flag median 0.01 and lower -0.005 short_flag median -0.01 and upper 0.005 if long_flag: return long, horizon_price if short_flag: return short, horizon_price return hold, None逻辑说明把中位数当方向、5% 分位当安全边际本质是过滤掉模型自己都不确定的行情。实测中大量信号的 median 虽然为正但 5% 分位已经打到 -3%这种单子接了就是给尾部风险送钱。horizon_price 是用第 2.2 节的还原公式从预测收益率反推出来的目标价下单时要靠它挂限价单。参数说明0.01 和 -0.005 不是金标准不同波动率的品种要重标定。题材股波动大阈值要放到 0.03/-0.01ETF 和指数期货0.005/-0.003 更合适。我的建议是先拿近 20 天的预测结果看一下中位数分布把阈值设在 70 分位附近比拍脑袋强得多。4.2 回测参数与循环代码回测这一段我几乎踩遍了所有能踩的坑。先看参数表这套是我在 FaceCat-Kronos 里默认使用的金额单位是元比例单位是百分比。参数默认值说明手续费率0.0003单边双边 0.0006接近真实券商成本滑点0.0005按预测价成交的偏移取保守值初始资金100000元单笔仓位0.2每次最多用 20% 资金持仓窗口horizon与预测长度一致交易时段09:30-15:00忽略集合竞价避免假成交def backtest(signals, close, fee0.0003, slip0.0005, capital100000): cash, pos capital, 0.0 for ts, sig in signals.items(): # 滑点按同方向叠加在成交价上 price close[ts] * (1 slip if sig.action long else 1 - slip) if sig.action long and pos 0: buy_vol (capital * 0.2) / price cash - buy_vol * price * (1 fee) pos buy_vol elif sig.action hold and pos 0: cash pos * price * (1 - fee) pos 0.0 return cash pos * close[-1]逻辑说明回测循环的核心不是算钱而是强制平仓机制。很多人回测收益虚高是因为信号为 hold 时仓位一直挂着涨跌全算进去。我这边持仓一旦达到 horizon 或出现反向信号就无条件平掉把持仓周期锁死杜绝一直拿到回测结束的作弊路径。参数说明slip0.0005 意味着买入信号实际成交价比收盘价高 0.05%小市值股票上远不够真实冲击成本到 0.1%-0.3% 很正常。回测结果里如果滑点前后年化收益差超过 20%说明你做的不是 alpha而是流动性套利这种策略上实盘必死。4.3 与 FaceCat 订单对接信号有了、回测过了最后一步送进 FaceCat 下单。FaceCat 暴露的是本地交易网关不需要直连交易所只要把账户、合约、方向、价格、数量拼成订单对象提交。下面是项目里订单适配器的简化版。class FaceCatOrderAdapter: def __init__(self, gateway): self.gateway gateway def submit(self, symbol, action, limit_price, shares): req { account: self.gateway.default_account, contract: symbol, direct: buy if action.startswith(long) else sell, price: round(float(limit_price), 2), volume: shares, offset: open, } resp self.gateway.send_order(req) return resp.order_id if resp and resp.code 0 else None逻辑说明下单适配器刻意做得很薄只做字段映射和错误码翻译。网关返回的错误码各家略有不同但 0 表示受理成功是通用惯例。我在项目里把下单和撤单封装成独立服务预测线程和下单线程完全分开避免模型推理卡顿导致订单延迟。参数说明limit_price 用的是 4.1 节里还原出的目标价不是当前市价。限价单比市价单安全但也容易因为价格不到位而踏空实盘时我会把 limit_price 乘 1.002 作为可容忍误差。shares 按手数规则取整A 股 100 的整数倍这个细节一律放进适配器统一处理。链路到这儿是通的。但这四个地方通常让人摔得爬不起来下一章我把踩过的坑逐个摊开。5. 避坑与常见问题Kronos 在金融时序上的四个翻车现场5.1 归一化泄漏回测漂亮实盘拉胯现象同一套代码walk-forward 回测年化 25%拉进模拟盘连续三周跑输基准。逐笔对比发现模拟盘第一天的预测分布方差比回测小很多信号数量也明显变少。原因回测脚本里用了全样本的 mu 和 sigma 做标准化相当于把未来的价格水平信息偷给了模型这是典型的前视偏差。实盘里你手里的 context 只有过去没有全局平均分布自然对不上。解决所有标准化参数只允许在 context 窗口内计算窗口外的数据一律不可见。我在项目里加了一个校验函数——每次预测前把 context 末尾 5 个点临时置零如果预测结果剧烈变化说明归一化参数里混入了未来信息。后来这个校验被做成了 predictor 的固定前置步骤每次调参都会自动跑一遍。5.2 频率字符串写错模型不报错结果全飘现象日线数据用 frequency1D 一切正常。某次做商品期货把 5 分钟线写成 5min模型照常出预测但预测值的自相关结构看起来像白噪声信号全部集中在 hold完全没有交易机会。原因Kronos 的频率字符串不是单位换算它决定模型把 context 切成多大的 patch。写 5min 时模型按自己记忆里的 5 分钟节奏去切日线数据每个 patch 都是错位的特征被彻底抹平。解决先确认数据真实步长再决定 freq 字面量。Kronos 预训练见过的频率集合里交易日数据用 1D小时用 1h分钟级别统一归一成 1min 再喂不要发明 5min 这种变体宁可降采样也不传新频率。我在 predictor 里加了一个高频数据重采样开关5 分钟线先聚合到 1 小时线再预测。5.3 拿中位数当目标价止损单被精准扫掉现象信号显示 5 日预测中位数上涨 0.8%挂限价单买入价格先回调 1.5% 触发止损然后才走出预测的涨幅。连续三次被打止损心态直接崩掉。原因中位数是分布中心不是路径最优点。Kronos 采样出的路径里有大量先跌后涨的走法中位数掩盖了路径的凹凸性。用单点目标价定止损等于无视了路径上可能的回撤幅度。解决止损线不要看中位数看 5% 分位路径的最小值。项目里我先让 100 条采样路径逐条计算持仓期内的最大回撤再取 5% 分位作为止损距离。这样止损线天然包含模型认为可能发生的坏路径。改完之后止损被扫掉的次数少了一半还多。5.4 股票池幸存者偏差刚跑赢就翻车现象回测用的是当前沪深 300 成分股回测收益跑赢指数 12%实盘却跑输。研究之下发现2018 年池子里某只股票还是 ST中途被剔除回测代码自动把它后面的数据删了等于帮你跳过了持有期最难受的退市风险。原因幸存者偏差的本质是股票池随时间变动。回测时用了今天还活着的名单退市、ST、停牌的烂日子全被抹掉收益自然虚高。解决股票池必须用历史快照。项目里把 FaceCat 的历史成分股快照也接进了数据层预测时用预测当天的成分名单回测时逐日切换绝不拿今天的名单去算昨天的账。如果你手里没有历史快照退而求其次用全市场股票并过滤 ST也别用当前成分股。这四个坑解决掉整个管线才算真正可交付。剩下的问题是提升收益Kronos 的 zero-shot 在金融时序上只是起跑线微调和验证方法才是拉开差距的地方。6. 进阶微调 Kronos 与 walk-forward 验证我自己的落地习惯6.1 微调冻结 encoder只动输出头Kronos 的预训练权重来自海量通用时间序列金融只是其中一小类。zero-shot 能做零样本预测但对单只股票的波动结构不敏感。我的做法是拿目标股票近三年的日线数据做微调冻结整个 encoder只训练预测头轮次控制在 10 轮学习率 1e-4。这样既保住模型对通用时序的敏感度又让输出头适配个股的波动范围。项目里封装的 FinetuneTrainer 内部就是常规 PyTorch 训练循环关键是训练集和验证集严格按时间切不做随机 shuffle。trainer FinetuneTrainer( modelmodel, train_loadertrain_loader, lr1e-4, epochs10, freeze_encoderTrue, )逻辑说明冻结 encoder 是为了防止灾难性遗忘。金融数据样本量有限全量微调没几轮就会把预训练学到的通用时序结构冲刷掉验证集上的表现先降后升。只动输出头的话模型对节假日后跳空趋势惯性延续这类通用规律的把握还在只是收益率的预测分布被拉回这只股票的合理范围。参数说明lr1e-4 是我试过的稳定值调大一点就会在验证集上震荡epochs10 足以让输出头收敛再多的轮次就开始过拟合训练集。训练集占 70%验证集用最近 30%顺序不能反。6.2 walk-forward 验证回测的数字会骗人微调完之后验证阶段我强制自己走 walk-forward训练窗口滑动、预测窗口不动每预测完一段就把这段真实数据并入训练集再滚下一天。这套流程能有效暴露前视偏差也能测出模型在不同市场状态下的退化速度。for day in range(start, end): train, test split_window(df, day, lookback500) model retrain(train) # 或加载微调权重 pred model.predict(test_context) actual test[close].iloc[-1] record(day, pred, actual) df_train df_train.append(test) # 滚动并入逻辑说明walk-forward 的核心哲学是每一次预测都只用当时能拿到的数据。模型在每个时点重新预测一次相当于把回测拆成几百次日交易演练。这个循环跑完回测收益曲线上的每一笔盈亏都对应一个真实存在过的时点没有中间变量能作弊。从那以后我每次调任何参数都强制走一遍 walk-forward 再谈上线翻车概率至少砍半。希望帮到你——这套完整代码和样例数据已经在资源里按本文的结构整理好了直接照着拆解就能复现。本文还有配套的精品资源点击获取