简介:以Jupyter Notebook为载体的金融机器学习练习集,面向希望系统掌握量化建模与金融数据特征的进阶学习者和量化研究者,源自《Advances in Financial Machine Learning》一书的精选实验题,旨在辅助读者将书中的标签策略、交叉验证与特征工程思想落地为可运行代码,解决理论到实践之间的鸿沟。包体共43个文件,以18个Python脚本和6个ipynb笔记本为主体,辅以RST文档、Makefile、图片及配置文件,压缩包仅2.8MB,体量轻巧。已有339人学习浏览,适合用Python复现金融特征工程、交叉验证与标签策略的读者。内容围绕监督式标签与元标签、金融场景下的交叉验证、样本权重、分数化差分特征、Tick/成交量/美元成交额Bar构建等主题展开,并配有bars与snippets等可复用脚本,帮助在本地环境快速运行实验;项目遵循标准数据科学目录结构(数据、模型、报告、文档分离),便于按模块查阅和二次开发。1. 开箱这份金融ML练习笔记:它解决的是教科书到实战之间那条最宽的沟拿到这份名为 Adv_Fin_ML_Exercises 的 JupyterNotebook 资源时,我刚把一本讲金融机器学习进展的经典教材读完第二遍,正卡在“书上的方法都会,但代码里下不去手”的尴尬位置。教材里那些样本标签、元标签、样本权重、特征重要度、防过拟合回测,每一章读起来都很有道理,可一打开自己的回测框架就发现无处安放。这份练习集是某位开发者把自己逐章重做教材习题的完整过程整理成的实验性解决方案,每一个 notebook 对应一组核心概念,代码 + 推导 + 注释混排,直接照跑能把书里散落的逻辑串成一条可落地的流水线。适合两类人:一类是刚读完教材、想在代码里验证一遍的从业者,另一类是自己写策略但反复被带标签错误和回测过拟合折磨、想知道教科书方法到底怎么落地的量化方向开发者。换句话说,这份资源把你从“读懂了”推到“跑通了”这一格。2. 复现环境:从conda依赖到notebook目录的完整搭建2.1 为什么这份资源坚持用notebook而不是纯py脚本第一眼看到全套文件都是 ipynb,我的第一反应和很多人一样:生产环境谁用 notebook。但把书里练习逐个跑下来之后,我理解了这个选择。教材讲的是方法原理,练习里有大量“改一个参数、看一个图、对比一个结论”的动作,notebook 天然适合这种边跑边记录中间产物的流程。更重要的是,金融时序的坑往往藏在不可见的中间变量里:比如标签对齐、数据切分、特征跨越训练集,这些在 py 脚本里要看日志才能发现,在 notebook 里直接看 cell 输出就行。所以把这份资源当成研究验证脚本用,不要当成生产框架用,这是第一课。2.2 环境搭建与目录结构我是在一台没装任何量化依赖的干净机器上复现的,踩了一遍依赖版本之后,整理出的可复现命令如下:conda create -n advfinml python=3.9 -y conda activate advfinml pip install jupyter numpy pandas scipy scikit-learn matplotlib statsmodels pip install mlfinlab==0.13.6 jupyter notebook逻辑说明:前三行建独立环境并装通用科学计算依赖;第四行装的是教材生态里常用的 mlfinlab,这份练习里有一部分方法直接调它,另一部分是作者手写的替代实现;最后一行启动 notebook 服务。参数说明:python 版本选 3.9 而不是 3.11,是因为 scipy 和 mlfinlab 在新版本下有兼容性告警,虽然不影响主要功能,但会刷大量 warning 干扰判断;mlfinlab 版本建议锁 0.13.6,这是个功能较全且 API 稳定的版本。装完后核对目录结构,建议按下面的清单整理:目录/文件职责关键点ch11_feature_importance.ipynb特征重要度与降维含MDI/MDA实现ch10_meta_labeling.ipynb元标签双层模型需先跑完ch11的框架ch3_labeling_triple_barrier.ipynb三重障碍法与本书第3章对应ch9_cross_validation.ipynbpurged K-Fold时序验证核心data_generator.py合成数据生成避免外部数据源差异2.3 依赖版本里藏的版本陷阱这里有一个很多人跑这本书练习都会翻车的点。原书配套练习基于老版 MLFinLab API,比如mlfinlab.sampling、mlfinlab.labeling的导入路径和函数签名在 0.13.6 里还能用,但更老或更新的版本都会报ModuleNotFoundError或参数名不匹配。我试过直接pip install mlfinlab拉最新版,结果TripleBarrier类的构造参数变了,练习代码第一行就红。处理办法是严格按上面的版本表安装,如果资源里某个 notebook 坚持导入老版函数,别改代码,先检查环境。还有个环境细节容易忽略:pandas 版本。书里部分练习用了pd.Series.apply和pd.date_range的旧默认行为,新 pandas 对日期频率字符串的处理有变化,跑 CUSUM 过滤那节经常出现ValueError: cannot convert float NaN to integer,原因是重新采样后索引里混入 NaT。这一步把 pandas 锁在 1.5.x 能省掉一个晚上。3. 特征与标签工程:从固定窗口到三重障碍法的完整实现3.1 三重障碍法:为什么固定未来收益窗口做标签是错的书里最有冲击力的观点之一是:用固定未来 N 根 K 线收益给当前时刻打标签,本质上是对“事后收益”做了无约束建模,一旦未来路径先触达止盈或止损,这个标签就和真实交易行为不匹配。三重障碍法就是把标签生成从“看终点”改成“看路径”:给每个样本设定上障碍、下障碍和水平障碍(时间),价格先碰到哪个就退出并据此打标签。这是练习里第一个动手实现,代码不长,但每一步都关系到后续元标签的输入质量:import numpy as np import pandas as pd def triple_barrier_label(close, events, t_upper=0.02, t_lower=-0.02, t_vertical=20, num_threads=1): """ 三重障碍法打标签, 返回每个事件对应的标签、障碍触碰点、退出时间 close: 价格序列(Series) events: DataFrame, 含 t1(最晚退出时间)和 start_idx(事件起点) """ labels = pd.Series(index=events.index, dtype=float) for i, event in events.iterrows(): start = event['start_idx'] end = event['t1'] window = close.loc[start:end] if window.empty: continue first_upper = window[window > close.loc[start] * (1 + t_upper)] first_lower = window[window < close.loc[start] * (1 + t_lower)] if not first_upper.empty and first_upper.index[0] <= first_lower.index[0]: labels.loc[i] = 1 # 先触上障 elif not first_lower.empty and first_lower.index[0] <= first_upper.index[0]: labels.loc[i] = -1 # 先触下障 else: labels.loc[i] = 0 # 直到时间耗尽 return labels逻辑说明:这个实现把每个事件看成一个独立窗口,在窗口内依次找第一个触到上障和第一个触到下障的位置,先到者决定标签方向。三个障碍的本质是把“到期盈亏”变成“路径盈亏”,这更接近真实交易里止损止盈先触发谁的区别。参数说明:t_upper和t_lower代表障碍距离事件起点价格的比例,0.02 表示 2% 止盈和 2% 止损;t_vertical是最长持有期,我用 20 根 K 线;num_threads在多标的多事件时才有意义,我这里保留参数但串行执行,便于排查。3.2 CUSUM过滤器:用信息强度决定“什么时候采样”另一个常见误区是固定频率采样,比如每小时打一个样本。金融时序是非均匀信息密度的,开盘瞬间的信息量远大于午盘横盘段。书里推荐的 CUSUM 过滤思路是:当价格对当前均值的累计偏离超过某个动态阈值时才采样,这样样本集中在有“新信息”的时间点,而不是均匀散布在无聊时段。练习里的实现用了符号本金累积过程,这是我照书里思路写的一个简化版学习笔记:def cusum_filter(close, threshold=0.01): """ CUSUM过滤器: 累计偏差超过threshold时采一个样本并重置 返回采样点索引列表 """ s_pos = 0.0 s_neg = 0.0 sample_idx = [] avg = close.mean() for idx in close.index: deviation = close.loc[idx] - avg s_pos = max(0.0, s_pos + deviation) s_neg = min(0.0, s_neg + deviation) if s_pos > threshold * avg or s_neg < -threshold * avg: sample_idx.append(idx) s_pos, s_neg = 0.0, 0.0 # 采样后重置 return sample_idx逻辑说明:正负两个累积器分别跟踪向上和向下的偏差,触发阈值后采样并归零。注意这里用的是动态均值做中心化,实际练习里会配合滚动均线,我简化成全局均值是为了先跑通流程。参数说明:threshold控制样本稀疏度,0.01 意味着累计偏离达到当前均价 1% 才产生一个样本,调成 0.001 样本量会明显增多,但噪音也会变大。3.3 标签与过滤器组合时的顺序问题练习里有个细节我一开始没注意,导致后面元标签模型的输入维度对不上。正确流程是先跑 CUSUM 产出样本时间点,再给每个时间点设定事件窗口(比如 t1 = 未来20根K线),最后用三重障碍法打标签。如果反过来,先打标签再过滤,会有大量 CUSUM 之外的样本被喂进标签函数,虽然也能跑,但训练集分布变了。我自己第一次就是顺序反了,导致样本量和书本示例对不上,统计出的标签比例也差很多。顺序错是这种半结构化工件里最隐蔽的坑,建议每步跑完先print(labels.value_counts())核对一次样本分布,再继续。4. 元标签与特征重要度:用两层模型压低噪音干扰4.1 元标签的两层结构:第一层做方向,第二层做大小读完书里元标签那一章,很多人的第一反应是“这不就是二分类叠加二分类吗”。实际跑完练习,我才理解元标签设计的精妙点:第一层模型判断要不要开仓、方向是多是空;第二层模型只在第一层判定“要开仓”的样本上,判断这个仓值不值得下、能不能下进去。这样做的好处是,把“开仓方向”和“开仓大小”两个不同难度的任务拆开,第二层的训练样本天然被第一层筛选,噪音大幅下降。练习里的代码是配合 sk-learn Pipeline 实现的,以下是核心子函数骨架,结构比完整代码更值得抄:def meta_labeling_pipeline(X, first_level_labels, events, clf_first, clf_meta): # 第一层: 全样本训练方向分类器 first_model = clf_first.fit(X, first_level_labels) profit_mask = first_level_labels != 0 # 只取第一层有信号的样本 # 第二层: 基于有利样本, 标签 = 该样本是否盈利 meta_X = X.loc[profit_mask] meta_y = (events.loc[profit_mask, 'returns'] > 0).astype(int) meta_model = clf_meta.fit(meta_X, meta_y) # 推理: 先算方向, 再算置信度 direction_pred = first_model.predict(X) prob_win = meta_model.predict_proba(X)[:, 1] return direction_pred, prob_win这个伪代码级实现把核心拆分逻辑写清楚了。第一层用全样本;第二层只吃被第一层判定为非零标签的样本,标签含义是“这个仓最终有没有盈利”,而不是“涨还是跌”。clf_first我用的是随机森林,clf_meta用的是对数几率回归,因为第二层只有二分类目标且样本量更小,简单线性模型反而更能稳定给出概率。predict_proba输出的概率在第二层里可以直接作为仓位占比,这是元标签最实用的一个副产品。4.2 MDI与MDA:特征重要度的降级用法与边界特征重要度这一章里,书里讲了两种方法:MDI(基于杂质下降的平均值)和 MDA(基于精度下降的平均值)。练习里的源码实现很直观,MDI 就是随机森林内部feature_importances_的直接平均;MDA 则是把某个特征列随机打乱,看模型精度掉多少,掉得越多的特征越重要。def mdi_importance(forest): # 平均每棵树的特征杂质下降, 归一化后排序 imp = np.mean([tree.feature_importances_ for tree in forest.estimators_], axis=0) return imp / imp.sum() def mda_importance(model, X, y, cv, scorer): # 对每个特征列做permutation, 比较精度下降幅度 base_score = cross_val_score(model, X, y, cv=cv, scoring=scorer).mean() drop = {} for col in X.columns: X_shuffled = X.copy() X_shuffled[col] = np.random.permutation(X_shuffled[col]) shuffled_score = cross_val_score(model, X_shuffled, y, cv=cv, scoring=scorer).mean() drop[col] = base_score - shuffled_score return drop逻辑说明:MDI 的计算代价低,但有个已知缺陷:对高基数特征(比如时间戳)会给出虚高重要度,因为树倾向于用这些特征做分裂;MDA 对这种虚高有天然矫正,因为它衡量的是打乱特征后精度的真实损失。参数说明:cv在这里必须用 purged K-Fold(后面会专门讲),否则时序重叠会导致重要度被灌水;scorer我建议用neg_log_loss而不是准确率,因为金融标签类别不平衡,准确率会把多数类放大成不重要。4.3 样本权重:用唯一性分数处理重叠窗口书里反复强调一个被通用 ML 忽略的问题:三重障碍法生成的标签窗口是重叠的,同一条价格 K 线会被多个样本用到,这相当于数据泄漏的另一种形式,会让交叉验证结果虚高。书给的解法是计算每个样本的“唯一性分数”,即这个样本窗口里有多少比例的数据点不被其他样本窗口共享,然后把它作为训练权重。练习里提供了唯一性分数的计算流程,我照着实现后把它接到训练循环里:# 计算每两个事件之间是否共享 def uniqueness_score(events, price_index): """ events: 每个事件有 t1 和 t0 返回每个事件窗口内独立信息占比 """ # 对每个时间点统计同时活跃的事件数 active_counts = np.zeros(len(price_index)) for _, e in events.iterrows(): mask = (price_index >= e['t0']) & (price_index <= e['t1']) active_counts[mask] += 1 # 每个事件的唯一性 = 平均 1 / 活跃事件数 score = {} for i, e in events.iterrows(): mask = (price_index >= e['t0']) & (price_index <= e['t1']) score[i] = np.mean(1.0 / active_counts[mask]) return score这段代码把重叠度量化成 1/N 的平均值:某个时刻同时有 5 个事件窗口覆盖,那么每个事件在该时刻只分到 1/5 的信息权重。把score作为fit()的sample_weight参数传入,就完成了去重叠化。我实测过:不加权重时验证集精度虚高大约 3% 到 5%,加权重后回测曲线明显不再平滑得像伪造的。5. 回测与多重检验:五个最容易翻车的坑5.1 坑一:概率路径依赖让回测结果“一次性”现象:同一个策略代码,第一次跑夏普 1.6,换了一段行情再跑变 0.3,有人就觉得模型失效了。原因:金融时序的样本不是独立同分布的,模型对路径有强记忆,一段行情的涨跌结构和另一段完全不同,训练集本身不具代表性。解决:练习里反复用“概率路径依赖”的概念说明这一点,我后来每次跑实验都强制做滚动窗口验证:前 60% 训练、后 20% 验证、最后 20% 只看不碰,三个区间分别统计 IC 和夏普,任何一段明显差于其他段就说明路径依赖过重,需要先检查标签定义是否渗入未来信息。5.2 坑二:K-Fold交叉验证在时序数据上直接泄漏现象:把普通 K-Fold 用在带重叠标签的金融数据上,验证集 AUC 0.78,一到实盘就变成随机水平。原因:训练集里的样本窗口覆盖了验证集的时间段,模型等于提前见过测试答案;这是教科书式错误,但代码层面很难一眼发现。解决:练习里有完整实现,要点是训练集和验证集之间留出至少一个标签长度的间隙,我一般设embargo=2%的样本总数,同时配合第 4 章的唯一性分数做样本加权,两者结合才能把验证数字压到诚实水平。5.3 坑三:多次回测必然出现“假优秀”策略现象:连续跑 100 组参数,总会挑出一组夏普 2.0 以上的,但换时间段立刻失效。原因:这是在多重检验里打转,100 组实验里就算全是噪声也会出现 5% 的“显著结果”。练习里有一段用随机标签做基准的对照测试,把真实收益标签打乱再跑同一套回测,如果打乱后还有“好结果”,说明回测框架本身在自欺欺人。解决:我每次实验同时跑一组随机标签对照组,任何模型必须跑赢对照组才有资格进入下一轮。5.4 坑四:重塑标签时索引不齐,静默产生未来函数现象:跑完三重障碍法之后,特征 DataFrame 和标签 Series 长度都对得上,但训练后 AUC 奇高,回测曲线完美向上。原因:标签生成过程中用了close.loc[start:end],一旦价格索引里有重复时间戳或者缺失值,pandas 会静默对齐,使得部分样本的标签实际取自未来价格。解决:在标签函数里加一行显式检查:assert close.index.is_monotonic_increasing;再检查索引是否唯一,两行断言能拦住绝大多数静默错误。5.5 坑五:元标签的概率输出被直接当仓位用现象:元标签模型输出 0.8 的获胜概率,实际下单后发现优秀策略经不起交易成本打磨。原因:预测概率等于仓位占比这种做法,只在该模型校准良好的前提下成立;但练习数据是合成或历史数据,成本、滑点、冲击根本没进模型。解决:我一般把概率做分桶离散化,比如 0.6 以下不交易,0.6 到 0.8 半仓,0.8 以上才满仓,并单独做一组扣成本后的盈亏统计,防止概率数字虚高直接传导到仓位。6. 从练习到研究框架:把notebook改造成自己的实验台这批练习 notebook 给我的最大启发不是某个算法实现,而是验证习惯。书里那些“防泄漏”“防过拟合”的方法,单独看每一章都不难,难的是在真实研究里每次实验都强制走完整流程。我最后的做法是把练习 notebook 的核心函数提炼成一个轻量研究框架,让验证成为流水线的一部分。具体拆成三个模块:数据层统一输出干净的时间序列和事件表;特征层负责生成标签、唯一性分数、特征矩阵;验证层强制按五章里讲的顺序执行——先做路径依赖检查、再做 purged K-Fold、最后跑一组随机标签对照。每次实验都先从标记好的合成数据集上校准,确保框架自身没有 bug 再换真实数据。实践下来,这一套改造帮我少走了很多弯路。以前我每次研究都是从数据清洗直接跳到训练,跑完看指标,指标好就以为策略有效,指标不好就改参数,完全没意识到大多数指标波动只是验证方案的噪声。从那以后,我每次建新策略模型都会强制走一遍这个流程:数据层先检查索引唯一性,特征层打印标签频率分布,验证层跑随机标签对照,三层确认无误后才开始调参。希望这份资源也能帮你在噪声和真实信号之间建立起自己的防火墙。本文还有配套的精品资源,点击获取
简介:以Jupyter Notebook为载体的金融机器学习练习集,面向希望系统掌握量化建模与金融数据特征的进阶学习者和量化研究者,源自《Advances in Financial Machine Learning》一书的精选实验题,旨在辅助读者将书中的标签策略、交叉验证与特征工程思想落地为可运行代码,解决理论到实践之间的鸿沟。包体共43个文件,以18个Python脚本和6个ipynb笔记本为主体,辅以RST文档、Makefile、图片及配置文件,压缩包仅2.8MB,体量轻巧。已有339人学习浏览,适合用Python复现金融特征工程、交叉验证与标签策略的读者。内容围绕监督式标签与元标签、金融场景下的交叉验证、样本权重、分数化差分特征、Tick/成交量/美元成交额Bar构建等主题展开,并配有bars与snippets等可复用脚本,帮助在本地环境快速运行实验;项目遵循标准数据科学目录结构(数据、模型、报告、文档分离),便于按模块查阅和二次开发。1. 开箱这份金融ML练习笔记:它解决的是教科书到实战之间那条最宽的沟拿到这份名为 Adv_Fin_ML_Exercises 的 JupyterNotebook 资源时,我刚把一本讲金融机器学习进展的经典教材读完第二遍,正卡在“书上的方法都会,但代码里下不去手”的尴尬位置。教材里那些样本标签、元标签、样本权重、特征重要度、防过拟合回测,每一章读起来都很有道理,可一打开自己的回测框架就发现无处安放。这份练习集是某位开发者把自己逐章重做教材习题的完整过程整理成的实验性解决方案,每一个 notebook 对应一组核心概念,代码 + 推导 + 注释混排,直接照跑能把书里散落的逻辑串成一条可落地的流水线。适合两类人:一类是刚读完教材、想在代码里验证一遍的从业者,另一类是自己写策略但反复被带标签错误和回测过拟合折磨、想知道教科书方法到底怎么落地的量化方向开发者。换句话说,这份资源把你从“读懂了”推到“跑通了”这一格。2. 复现环境:从conda依赖到notebook目录的完整搭建2.1 为什么这份资源坚持用notebook而不是纯py脚本第一眼看到全套文件都是 ipynb,我的第一反应和很多人一样:生产环境谁用 notebook。但把书里练习逐个跑下来之后,我理解了这个选择。教材讲的是方法原理,练习里有大量“改一个参数、看一个图、对比一个结论”的动作,notebook 天然适合这种边跑边记录中间产物的流程。更重要的是,金融时序的坑往往藏在不可见的中间变量里:比如标签对齐、数据切分、特征跨越训练集,这些在 py 脚本里要看日志才能发现,在 notebook 里直接看 cell 输出就行。所以把这份资源当成研究验证脚本用,不要当成生产框架用,这是第一课。2.2 环境搭建与目录结构我是在一台没装任何量化依赖的干净机器上复现的,踩了一遍依赖版本之后,整理出的可复现命令如下:conda create -n advfinml python=3.9 -y conda activate advfinml pip install jupyter numpy pandas scipy scikit-learn matplotlib statsmodels pip install mlfinlab==0.13.6 jupyter notebook逻辑说明:前三行建独立环境并装通用科学计算依赖;第四行装的是教材生态里常用的 mlfinlab,这份练习里有一部分方法直接调它,另一部分是作者手写的替代实现;最后一行启动 notebook 服务。参数说明:python 版本选 3.9 而不是 3.11,是因为 scipy 和 mlfinlab 在新版本下有兼容性告警,虽然不影响主要功能,但会刷大量 warning 干扰判断;mlfinlab 版本建议锁 0.13.6,这是个功能较全且 API 稳定的版本。装完后核对目录结构,建议按下面的清单整理:目录/文件职责关键点ch11_feature_importance.ipynb特征重要度与降维含MDI/MDA实现ch10_meta_labeling.ipynb元标签双层模型需先跑完ch11的框架ch3_labeling_triple_barrier.ipynb三重障碍法与本书第3章对应ch9_cross_validation.ipynbpurged K-Fold时序验证核心data_generator.py合成数据生成避免外部数据源差异2.3 依赖版本里藏的版本陷阱这里有一个很多人跑这本书练习都会翻车的点。原书配套练习基于老版 MLFinLab API,比如mlfinlab.sampling、mlfinlab.labeling的导入路径和函数签名在 0.13.6 里还能用,但更老或更新的版本都会报ModuleNotFoundError或参数名不匹配。我试过直接pip install mlfinlab拉最新版,结果TripleBarrier类的构造参数变了,练习代码第一行就红。处理办法是严格按上面的版本表安装,如果资源里某个 notebook 坚持导入老版函数,别改代码,先检查环境。还有个环境细节容易忽略:pandas 版本。书里部分练习用了pd.Series.apply和pd.date_range的旧默认行为,新 pandas 对日期频率字符串的处理有变化,跑 CUSUM 过滤那节经常出现ValueError: cannot convert float NaN to integer,原因是重新采样后索引里混入 NaT。这一步把 pandas 锁在 1.5.x 能省掉一个晚上。3. 特征与标签工程:从固定窗口到三重障碍法的完整实现3.1 三重障碍法:为什么固定未来收益窗口做标签是错的书里最有冲击力的观点之一是:用固定未来 N 根 K 线收益给当前时刻打标签,本质上是对“事后收益”做了无约束建模,一旦未来路径先触达止盈或止损,这个标签就和真实交易行为不匹配。三重障碍法就是把标签生成从“看终点”改成“看路径”:给每个样本设定上障碍、下障碍和水平障碍(时间),价格先碰到哪个就退出并据此打标签。这是练习里第一个动手实现,代码不长,但每一步都关系到后续元标签的输入质量:import numpy as np import pandas as pd def triple_barrier_label(close, events, t_upper=0.02, t_lower=-0.02, t_vertical=20, num_threads=1): """ 三重障碍法打标签, 返回每个事件对应的标签、障碍触碰点、退出时间 close: 价格序列(Series) events: DataFrame, 含 t1(最晚退出时间)和 start_idx(事件起点) """ labels = pd.Series(index=events.index, dtype=float) for i, event in events.iterrows(): start = event['start_idx'] end = event['t1'] window = close.loc[start:end] if window.empty: continue first_upper = window[window > close.loc[start] * (1 + t_upper)] first_lower = window[window < close.loc[start] * (1 + t_lower)] if not first_upper.empty and first_upper.index[0] <= first_lower.index[0]: labels.loc[i] = 1 # 先触上障 elif not first_lower.empty and first_lower.index[0] <= first_upper.index[0]: labels.loc[i] = -1 # 先触下障 else: labels.loc[i] = 0 # 直到时间耗尽 return labels逻辑说明:这个实现把每个事件看成一个独立窗口,在窗口内依次找第一个触到上障和第一个触到下障的位置,先到者决定标签方向。三个障碍的本质是把“到期盈亏”变成“路径盈亏”,这更接近真实交易里止损止盈先触发谁的区别。参数说明:t_upper和t_lower代表障碍距离事件起点价格的比例,0.02 表示 2% 止盈和 2% 止损;t_vertical是最长持有期,我用 20 根 K 线;num_threads在多标的多事件时才有意义,我这里保留参数但串行执行,便于排查。3.2 CUSUM过滤器:用信息强度决定“什么时候采样”另一个常见误区是固定频率采样,比如每小时打一个样本。金融时序是非均匀信息密度的,开盘瞬间的信息量远大于午盘横盘段。书里推荐的 CUSUM 过滤思路是:当价格对当前均值的累计偏离超过某个动态阈值时才采样,这样样本集中在有“新信息”的时间点,而不是均匀散布在无聊时段。练习里的实现用了符号本金累积过程,这是我照书里思路写的一个简化版学习笔记:def cusum_filter(close, threshold=0.01): """ CUSUM过滤器: 累计偏差超过threshold时采一个样本并重置 返回采样点索引列表 """ s_pos = 0.0 s_neg = 0.0 sample_idx = [] avg = close.mean() for idx in close.index: deviation = close.loc[idx] - avg s_pos = max(0.0, s_pos + deviation) s_neg = min(0.0, s_neg + deviation) if s_pos > threshold * avg or s_neg < -threshold * avg: sample_idx.append(idx) s_pos, s_neg = 0.0, 0.0 # 采样后重置 return sample_idx逻辑说明:正负两个累积器分别跟踪向上和向下的偏差,触发阈值后采样并归零。注意这里用的是动态均值做中心化,实际练习里会配合滚动均线,我简化成全局均值是为了先跑通流程。参数说明:threshold控制样本稀疏度,0.01 意味着累计偏离达到当前均价 1% 才产生一个样本,调成 0.001 样本量会明显增多,但噪音也会变大。3.3 标签与过滤器组合时的顺序问题练习里有个细节我一开始没注意,导致后面元标签模型的输入维度对不上。正确流程是先跑 CUSUM 产出样本时间点,再给每个时间点设定事件窗口(比如 t1 = 未来20根K线),最后用三重障碍法打标签。如果反过来,先打标签再过滤,会有大量 CUSUM 之外的样本被喂进标签函数,虽然也能跑,但训练集分布变了。我自己第一次就是顺序反了,导致样本量和书本示例对不上,统计出的标签比例也差很多。顺序错是这种半结构化工件里最隐蔽的坑,建议每步跑完先print(labels.value_counts())核对一次样本分布,再继续。4. 元标签与特征重要度:用两层模型压低噪音干扰4.1 元标签的两层结构:第一层做方向,第二层做大小读完书里元标签那一章,很多人的第一反应是“这不就是二分类叠加二分类吗”。实际跑完练习,我才理解元标签设计的精妙点:第一层模型判断要不要开仓、方向是多是空;第二层模型只在第一层判定“要开仓”的样本上,判断这个仓值不值得下、能不能下进去。这样做的好处是,把“开仓方向”和“开仓大小”两个不同难度的任务拆开,第二层的训练样本天然被第一层筛选,噪音大幅下降。练习里的代码是配合 sk-learn Pipeline 实现的,以下是核心子函数骨架,结构比完整代码更值得抄:def meta_labeling_pipeline(X, first_level_labels, events, clf_first, clf_meta): # 第一层: 全样本训练方向分类器 first_model = clf_first.fit(X, first_level_labels) profit_mask = first_level_labels != 0 # 只取第一层有信号的样本 # 第二层: 基于有利样本, 标签 = 该样本是否盈利 meta_X = X.loc[profit_mask] meta_y = (events.loc[profit_mask, 'returns'] > 0).astype(int) meta_model = clf_meta.fit(meta_X, meta_y) # 推理: 先算方向, 再算置信度 direction_pred = first_model.predict(X) prob_win = meta_model.predict_proba(X)[:, 1] return direction_pred, prob_win这个伪代码级实现把核心拆分逻辑写清楚了。第一层用全样本;第二层只吃被第一层判定为非零标签的样本,标签含义是“这个仓最终有没有盈利”,而不是“涨还是跌”。clf_first我用的是随机森林,clf_meta用的是对数几率回归,因为第二层只有二分类目标且样本量更小,简单线性模型反而更能稳定给出概率。predict_proba输出的概率在第二层里可以直接作为仓位占比,这是元标签最实用的一个副产品。4.2 MDI与MDA:特征重要度的降级用法与边界特征重要度这一章里,书里讲了两种方法:MDI(基于杂质下降的平均值)和 MDA(基于精度下降的平均值)。练习里的源码实现很直观,MDI 就是随机森林内部feature_importances_的直接平均;MDA 则是把某个特征列随机打乱,看模型精度掉多少,掉得越多的特征越重要。def mdi_importance(forest): # 平均每棵树的特征杂质下降, 归一化后排序 imp = np.mean([tree.feature_importances_ for tree in forest.estimators_], axis=0) return imp / imp.sum() def mda_importance(model, X, y, cv, scorer): # 对每个特征列做permutation, 比较精度下降幅度 base_score = cross_val_score(model, X, y, cv=cv, scoring=scorer).mean() drop = {} for col in X.columns: X_shuffled = X.copy() X_shuffled[col] = np.random.permutation(X_shuffled[col]) shuffled_score = cross_val_score(model, X_shuffled, y, cv=cv, scoring=scorer).mean() drop[col] = base_score - shuffled_score return drop逻辑说明:MDI 的计算代价低,但有个已知缺陷:对高基数特征(比如时间戳)会给出虚高重要度,因为树倾向于用这些特征做分裂;MDA 对这种虚高有天然矫正,因为它衡量的是打乱特征后精度的真实损失。参数说明:cv在这里必须用 purged K-Fold(后面会专门讲),否则时序重叠会导致重要度被灌水;scorer我建议用neg_log_loss而不是准确率,因为金融标签类别不平衡,准确率会把多数类放大成不重要。4.3 样本权重:用唯一性分数处理重叠窗口书里反复强调一个被通用 ML 忽略的问题:三重障碍法生成的标签窗口是重叠的,同一条价格 K 线会被多个样本用到,这相当于数据泄漏的另一种形式,会让交叉验证结果虚高。书给的解法是计算每个样本的“唯一性分数”,即这个样本窗口里有多少比例的数据点不被其他样本窗口共享,然后把它作为训练权重。练习里提供了唯一性分数的计算流程,我照着实现后把它接到训练循环里:# 计算每两个事件之间是否共享 def uniqueness_score(events, price_index): """ events: 每个事件有 t1 和 t0 返回每个事件窗口内独立信息占比 """ # 对每个时间点统计同时活跃的事件数 active_counts = np.zeros(len(price_index)) for _, e in events.iterrows(): mask = (price_index >= e['t0']) & (price_index <= e['t1']) active_counts[mask] += 1 # 每个事件的唯一性 = 平均 1 / 活跃事件数 score = {} for i, e in events.iterrows(): mask = (price_index >= e['t0']) & (price_index <= e['t1']) score[i] = np.mean(1.0 / active_counts[mask]) return score这段代码把重叠度量化成 1/N 的平均值:某个时刻同时有 5 个事件窗口覆盖,那么每个事件在该时刻只分到 1/5 的信息权重。把score作为fit()的sample_weight参数传入,就完成了去重叠化。我实测过:不加权重时验证集精度虚高大约 3% 到 5%,加权重后回测曲线明显不再平滑得像伪造的。5. 回测与多重检验:五个最容易翻车的坑5.1 坑一:概率路径依赖让回测结果“一次性”现象:同一个策略代码,第一次跑夏普 1.6,换了一段行情再跑变 0.3,有人就觉得模型失效了。原因:金融时序的样本不是独立同分布的,模型对路径有强记忆,一段行情的涨跌结构和另一段完全不同,训练集本身不具代表性。解决:练习里反复用“概率路径依赖”的概念说明这一点,我后来每次跑实验都强制做滚动窗口验证:前 60% 训练、后 20% 验证、最后 20% 只看不碰,三个区间分别统计 IC 和夏普,任何一段明显差于其他段就说明路径依赖过重,需要先检查标签定义是否渗入未来信息。5.2 坑二:K-Fold交叉验证在时序数据上直接泄漏现象:把普通 K-Fold 用在带重叠标签的金融数据上,验证集 AUC 0.78,一到实盘就变成随机水平。原因:训练集里的样本窗口覆盖了验证集的时间段,模型等于提前见过测试答案;这是教科书式错误,但代码层面很难一眼发现。解决:练习里有完整实现,要点是训练集和验证集之间留出至少一个标签长度的间隙,我一般设embargo=2%的样本总数,同时配合第 4 章的唯一性分数做样本加权,两者结合才能把验证数字压到诚实水平。5.3 坑三:多次回测必然出现“假优秀”策略现象:连续跑 100 组参数,总会挑出一组夏普 2.0 以上的,但换时间段立刻失效。原因:这是在多重检验里打转,100 组实验里就算全是噪声也会出现 5% 的“显著结果”。练习里有一段用随机标签做基准的对照测试,把真实收益标签打乱再跑同一套回测,如果打乱后还有“好结果”,说明回测框架本身在自欺欺人。解决:我每次实验同时跑一组随机标签对照组,任何模型必须跑赢对照组才有资格进入下一轮。5.4 坑四:重塑标签时索引不齐,静默产生未来函数现象:跑完三重障碍法之后,特征 DataFrame 和标签 Series 长度都对得上,但训练后 AUC 奇高,回测曲线完美向上。原因:标签生成过程中用了close.loc[start:end],一旦价格索引里有重复时间戳或者缺失值,pandas 会静默对齐,使得部分样本的标签实际取自未来价格。解决:在标签函数里加一行显式检查:assert close.index.is_monotonic_increasing;再检查索引是否唯一,两行断言能拦住绝大多数静默错误。5.5 坑五:元标签的概率输出被直接当仓位用现象:元标签模型输出 0.8 的获胜概率,实际下单后发现优秀策略经不起交易成本打磨。原因:预测概率等于仓位占比这种做法,只在该模型校准良好的前提下成立;但练习数据是合成或历史数据,成本、滑点、冲击根本没进模型。解决:我一般把概率做分桶离散化,比如 0.6 以下不交易,0.6 到 0.8 半仓,0.8 以上才满仓,并单独做一组扣成本后的盈亏统计,防止概率数字虚高直接传导到仓位。6. 从练习到研究框架:把notebook改造成自己的实验台这批练习 notebook 给我的最大启发不是某个算法实现,而是验证习惯。书里那些“防泄漏”“防过拟合”的方法,单独看每一章都不难,难的是在真实研究里每次实验都强制走完整流程。我最后的做法是把练习 notebook 的核心函数提炼成一个轻量研究框架,让验证成为流水线的一部分。具体拆成三个模块:数据层统一输出干净的时间序列和事件表;特征层负责生成标签、唯一性分数、特征矩阵;验证层强制按五章里讲的顺序执行——先做路径依赖检查、再做 purged K-Fold、最后跑一组随机标签对照。每次实验都先从标记好的合成数据集上校准,确保框架自身没有 bug 再换真实数据。实践下来,这一套改造帮我少走了很多弯路。以前我每次研究都是从数据清洗直接跳到训练,跑完看指标,指标好就以为策略有效,指标不好就改参数,完全没意识到大多数指标波动只是验证方案的噪声。从那以后,我每次建新策略模型都会强制走一遍这个流程:数据层先检查索引唯一性,特征层打印标签频率分布,验证层跑随机标签对照,三层确认无误后才开始调参。希望这份资源也能帮你在噪声和真实信号之间建立起自己的防火墙。本文还有配套的精品资源,点击获取