简介《Advances in Financial Machine Learning》一书的配套练习实验包面向正在研读金融机器学习、希望动手复现书中方法的读者。内容聚焦书中选定章节的习题实验尤其覆盖 Labeling 与 MetaLabeling、金融场景下的交叉验证、样本权重、分数阶差分特征以及 Tick/Volume/Dollar Bar 构建等关键主题。配套 Jupyter Notebook 与 Python 脚本方便读者边读边跑、对照理解。压缩包共 43 个文件其中 6 个 ipynb 笔记本承载主要实验流程18 个 py 脚本提供数据读取、特征构造等底层支持另有 docs 文档与 Makefile 等工程化配置整体约 2.8MB。作者还整理了 src 目录中的 bars.py、snippets.py 等模块并列出其他受本书启发的 GitHub 项目便于扩展学习。资源包已有 324 人学习下载。对希望逐行理解 AFML 练习解法、搭建个人量化研究环境的读者而言这是一份结构清晰、可直接运行的实验参考。1. 为什么这本书的练习值得做Adv_Fin_ML_Exercises在补什么接触过量化交易的人应该都听过一句话拿普通机器学习套路去做金融时序十有八九在回测里自嗨。Marcos Lopez De Prado那本金融机器学习教材通篇在讲怎么把数据科学手段改造成金融能用的形态可它最大的问题是没有配一套现成习题。Adv_Fin_ML_Exercises这类实验性解决方案仓库就是把书里那些方法论还原成可运行代码让读者验证“原来事件驱动、元标签、组合交叉验证是真能落地的东西”。这个方向适合三类人一是刚读完书但不知道从哪行代码开始的人二是用sklearn做过预测但总在回测翻车的从业者三是想给团队搭一套金融机器学习基线流程的工程师。它解决的核心痛点不是“调参更准”而是把数据泄漏、样本重叠、时序依赖这些金融特有的坑提前暴露出来。我自己的经验是把这本书的练习按原意重写一遍比刷十个Kaggle金融赛都更能锻炼特征工程和评估体系的判断力。2. 三重屏障标签从零实现把事件定义与侧壁参数讲透2.1 先忘掉收益率按“事件”组织数据金融机器学习和普通监督学习最根本的区别是数据不是独立同分布的。普通分类任务里“今天”和“明天”的样本彼此无关但在金融数据里如果你用未来20日收益率做标签那么第10天和第11天的样本窗口就会重叠信息被反复计数。书中第一个动手练习就是建立“事件驱动”思维而不是单纯按时间步长切样本。事件的定义通常是这样的每当某个行情条件被触发比如价格突破滚动均线、波动率超过阈值、特定技术指标穿越就生成一个事件起点。之后沿着这个起点向前看一个固定时间窗判断价格路径是否触碰上障碍、下障碍或者到期未突破。整个处理过程可以抽象为一张事件表表里每条记录的索引是事件发生时间再带上目标变量和到期时间。import numpy as np import pandas as pd # 模拟一段日频行情只做演示用实际替换成自己的bar数据 np.random.seed(42) dates pd.date_range(2018-01-01, periods800, freqB) close 100 * np.exp(np.cumsum(np.random.normal(0, 0.01, len(dates)))) df pd.DataFrame({close: close}, indexdates) # 事件起点用收盘价穿越20日均线触发属于一种常见的择时事件 sma df[close].rolling(20).mean() events df[close][df[close] sma].index events events[::5] # 抽稀避免事件过于密集先跑通这段再回过头理解为什么不能直接把每天的行情都当成样本。这里的核心逻辑是样本必须由“事件”产生而不是由“bar”产生。事件驱动的好处是能天然避免相邻样本带来的标签重叠也为后续的样本权重和交叉验证铺路。平时做练习时最常见的错误就是跳过这一步直接拿全部日线做训练集后面所有坑都从这里开始。2.2 最小三重屏障实现垂直屏障到期日与障碍触碰判定书中招牌的标签方法是三重屏障法三重分别指上障碍、下障碍、垂直时间屏障。上障碍触发代表上涨目标达成标记为正样本下障碍触发代表下跌目标达成标记为负样本如果到期时间窗口内都没触发就标记为零。做练习时最容易误解的是垂直屏障它不是一个固定的“持有期”而是每个事件在时间轴上的到期点。def triple_barrier(close, events, num_bars, trgt): 三重屏障最简实现 close : 价格序列 events : 事件起始时间索引 num_bars: 垂直屏障的bar数量 trgt : 每个事件的障碍幅度上障trgt下障-trgt out pd.Series(indexevents, dtypeint64) for start in events: loc close.index.get_loc(start) end_loc min(loc num_bars, len(close) - 1) window close.iloc[loc:end_loc 1] # 从事件下一个bar开始判定避免把事件当天的涨跌也算作突破 path np.log(window.iloc[1:] / window.iloc[0]) if len(path) 0: out.loc[start] 0 continue target trgt.loc[start] # 先确认是否先触达上障或下障 first_hit None for ret in path: if ret target: first_hit 1 break if ret -target: first_hit -1 break out.loc[start] first_hit if first_hit is not None else 0 return out # 障碍幅度取最近20日对数收益的滚动标准差逐事件动态设定 trgt np.log(close / close.shift(1)).rolling(20).std() trgt trgt.reindex(events).fillna(trgt.median()) labels triple_barrier(close, events, num_bars20, trgttrgt)这里有三处必须说明白的细节。第一判定突破时从事件后的第一根bar开始因为事件触发当天的收盘价已经确定把它算进路径会产生前视偏差。第二滚动标准差在事件稀疏时可能取到NaN用中位数填充是练习里常见的保守做法。第三垂直屏障的bar数不是总样本长度它决定的是“这笔交易最久持有多少根bar”。20根bar对应月频调仓周期如果做高频可以改成10或5做中长线可以用60。2.3 参数该怎么设一个能跑的实验性默认值练习代码如果直接跑真实行情数据经常会遇到数据缺失、停牌、事件过密等问题所以建议先在一个模拟或者小样本上验证逻辑。我这里给出两组默认参数适合做基线对比实验。参数默认值说明num_bars20垂直屏障周期约一个月交易日trgt来源20日滚动标准差障碍幅度跟随波动率而不是固定百分比事件触发收盘价上穿20日均线简单趋势策略样本量适中标签类别1 / 0 / -1三类标签比二分类更贴近书中原意这三个参数里trgt来源是最值得调的地方。如果用固定百分比比如上下各5%在低波动率时段几乎不会触发在高波动率时段又频繁触发导致样本分布严重倾斜。用滚动标准差的好处是让障碍幅度自适应波动率样本量更均衡也更贴近书中“波动率缩放”的思路。另外注意原书建议对trgt做指数平滑去噪练习初期可以先跳过。3. 元标签与样本权重二级模型为什么能救回一半被否掉的样本3.1 元标签不是第二层模型那么简单元标签是书中被讨论最多的方法之一它的出发点很朴素很多样本虽然预测方向正确但信号太弱直接交易会亏在手续费和滑点上。与其让主模型强行区分“强涨”和“弱涨”不如让主模型负责方向让第二个模型负责“这笔信号值不值得交易”。两级模型各管一件事整体精度往往比单模型压阈值高。训练元标签时有个容易搞反的点二级模型的样本不是全量数据而是主模型“有明确建议”的那部分样本。比如主模型预测为正向或负向时才进入二级模型预测为中性则直接不参与。二级模型的标签是“主模型方向是否与真实方向一致”而不是原来的1/0/-1。3.2 最小元标签实现按主模型的决定切样本from sklearn.ensemble import RandomForestClassifier # 造几个无未来信息的特征过去5日收益、波动率、离均线距离 df_feat pd.DataFrame(indexdf.index) df_feat[ret_5] np.log(close / close.shift(5)) df_feat[vol_20] np.log(close / close.shift(1)).rolling(20).std() df_feat[dist_sma] np.log(close / close.rolling(20).mean()) # 对齐标签和特征剔除事件日尚无特征的样本 X df_feat.loc[labels.index].dropna() y labels.loc[X.index] # 主模型三分类预测方向 rf_main RandomForestClassifier(n_estimators200, min_samples_leaf10, random_state0) rf_main.fit(X, y) # 只保留主模型认为方向明确的样本 pred_main rf_main.predict(X) mask pred_main ! 0 # 元标签主模型方向与真实方向一致则为1否则为0 y_meta ((pred_main[mask] * y[mask]) 0).astype(int) X_meta X[mask] rf_meta RandomForestClassifier(n_estimators100, min_samples_leaf5, random_state0) rf_meta.fit(X_meta, y_meta)这段代码是按实验性方式写的注意我没有把所有样本一股脑丢给二级模型而是先用主模型筛出方向信号这一筛选逻辑就是元标签和普通“两阶段模型”的差别。现实项目里你还需要把X_meta替换成独立验证集上的主模型预测否则元标签模型会对主模型的误差模式过拟合这是练习里最隐蔽的一个坑。二级模型的输出是一个概率值你可以设定阈值来调整交易频率。阈值越高交易越少但每笔交易的平均胜率会上升。初学者可以用0.5当作默认阈值再在0.4到0.6之间扫一遍观察最终策略的收益曲线和交易次数。3.3 样本权重与唯一性把重叠惩罚喂给模型书中的样本唯一性概念是指两个事件的时间窗口重叠时它们的信息高度冗余应该被降权。普通机器学习没有这个概念但在金融事件数据里事件A和事件B可能在同一段行情的两端模型反复看到相同信息会高估某些特征的显著性。def sample_weight_from_overlap(events, t1, num_bars): 简单重叠惩罚统计每个bar被多少事件窗口覆盖 每个样本的权重取覆盖数的倒数。 events : 事件起始索引 t1 : 事件实际到期索引这里用屏障期近似 coverage pd.Series(0, indexdf.index) for start in events: loc df.index.get_loc(start) end_loc min(loc num_bars, len(df) - 1) coverage.iloc[loc:end_loc 1] 1 w 1.0 / coverage.loc[events].astype(float) return w / w.sum() * len(w) # 归一化到平均权重为1 weights sample_weight_from_overlap(events, None, num_bars20) rf_main.fit(X, y, sample_weightweights.loc[X.index])这段实现是书中算法的简化版核心思想是重叠越多、权重越低。真正做练习时还可以更精细按事件的实际到期时间计算重叠而不是统一用垂直屏障期。不过对于入门实验用固定窗口估算已经能让结果明显变化。如果你发现加上样本权重后模型表现反而变差先检查是不是事件过滤做得太少——事件太密会让几乎所有样本权重都低到接近均值惩罚形同虚设。4. 用组合交叉验证替换掉普通K折金融数据上最划算的改动4.1 为什么K折在金融时序上会翻车普通K折交叉验证把样本随机分成K份每份里的样本来自不同时间区间。在金融数据上这会带来两个致命问题一是训练集里有未来信息测试集里有过去信息模型相当于提前看过答案二是事件窗口重叠时同一段行情既出现在训练集又出现在测试集评估结果虚高。很多人在练习里跑出漂亮准确率一上实盘就崩多半是评估环节出了问题。书中提出的组合交叉验证在金融实践中被广泛采用。其核心思想是每次实验都保持训练集在时间上早于测试集并抛弃最接近测试集的一段“冷切期”避免训练集尾部的样本与测试集头部的样本信息重叠。和普通K折相比它能在多次前向实验中给出更真实的性能分布。4.2 CPCV的最小实现按事件顺序切分并保留“前向”def cpcv_split(event_index, k5): 组合交叉验证的最小实现。 按事件时间排序后顺序切分k段每次取前i段训练、第i1段测试。 相比随机K折训练集永远在测试集之前。 idx_sorted event_index.sort_values() # 确保按事件时间排序 folds np.array_split(np.arange(len(idx_sorted)), k) for i in range(k - 1): train_idx np.concatenate(folds[:i 1]) test_idx folds[i 1] yield idx_sorted[train_idx], idx_sorted[test_idx] # 使用示例 for train_events, test_events in cpcv_split(labels.index, k5): X_tr, X_te X.loc[train_events], X.loc[test_events] y_tr, y_te y.loc[train_events], y.loc[test_events] clf RandomForestClassifier(n_estimators200, random_state0) clf.fit(X_tr, y_tr) print(测试集准确率:, clf.score(X_te, y_te))这个实现和书中描述略有差异是一个严格的向前验证近似第0次实验用第0段训练、第1段测试第1次实验用第0和第1段训练、第2段测试。这样做的好处是训练集大小逐次增加符合真实场景中“历史数据越来越多”的直觉。坏处是早期实验训练数据太少评估方差偏大所以做练习时至少要用5段数据量大时可以加大到10段。4.3 CPCV练习的默认参数K取多少、看什么指标组合交叉验证里需要设两个参数一是K折数二是“冷切距离”。冷切距离的意思是训练集最后一个事件和测试集第一个事件之间要隔多少个bar。常见做法是隔一个垂直屏障周期也就是20根bar。如果不设冷切事件重叠的样本跨在训练和测试边界上效果和随机K折差不多白改。# 带冷切的拆分训练集尾部砍掉embargo个事件 def cpcv_split_embargo(event_index, k5, embargo20): idx_sorted event_index.sort_values() folds np.array_split(np.arange(len(idx_sorted)), k) for i in range(k - 1): test_idx folds[i 1] # 切割点向前移embargo个事件 cut len(idx_sorted[:folds[i 1][0]]) - embargo cut max(cut, 0) train_idx np.arange(cut) yield idx_sorted[train_idx], idx_sorted[test_idx]评估指标方面不要只看准确率金融数据里三类标签往往不平衡。我一般会同时观察三组数F1_macro、每类别的召回率、以及按时间累积的预测正确率曲线。如果某一段时间的负样本预测完全失效说明模型在这个市场状态下泛化不足比整体准确率更有参考价值。5. 避坑与常见问题做MLDP练习时最常踩的五个坑5.1 随机切分训练集导致回测明显失真现象是样本外测试准确率比实盘高出十几个点换一段行情就失效。原因是普通train_test_split把时间上相邻的样本打散到两边事件窗口重叠造成信息泄漏。解决方法是改用第4章的向前式划分至少也要按年份切分。练习里可以做个对照组同一份数据分别用随机切分和CPCV跑一遍你大概率会看到随机切分的指标高但方差大CPCV的指标低但稳定。这本身就是一个很有价值的实验结论。5.2 特征标准化时把全样本均值方差泄进训练集做特征缩放时用了全样本的均值和标准差这在视觉和NLP任务里影响不大但在金融时序里就是直接看到未来。正确的做法是对训练集fit、对测试集只transform。练习时建议写成Pipeline防止某一步忘记。如果事件里有样本极值缩放参数会被拉偏可以考虑用中位数和分位数替代均值方差。这一点特别隐蔽因为模型拟合出来的系数往往看不出异常只有逐日看特征分布才会发现。5.3 三重屏障侧壁用未来信息定阈值有人把整段价格的滚动标准差作为trgt导致事件还没发生就已经知道未来波动率等于变相偷看行情。波动率本身是可以用历史估算的但必须只用截止到事件发生时刻的数据。解决方法是把trgt的计算限制在事件时间之前比如用事件过去的20日收益率标准差。练习中建议加一条断言assert target.index.max() events.max()凡是trgt索引比事件还晚的都要重新对齐。5.4 元标签的正负样本在两级模型间串味二级模型训练时过滤了样本但过滤条件本身来自主模型在训练集上的预测于是二阶段模型知道了主模型的错误模式。一换数据表现就掉。标准做法是先用CPCV把样本分成训练和测试全程不用全样本拟合主模型。主模型在测试集上输出预测二级模型只能在测试集的预测结果上训练。这个过程比较绕但理解了它才能真正把元标签用在正道上。5.5 特征重要性在特征高度相关时被高估金融特征之间高度相关比如5日收益率和10日收益率、均线距离和波动率。随机森林的特征重要性会把被替代的特征反复压低导致某些明明有效的因子重要性接近0。解决方法是先做相关性筛选或者改用置换重要性并按时间块打乱。不要拿feature_importances_直接下结论金融场景下它更适合用于粗筛而不是精排。6. 进阶验证用MDI/MDA给结论上保险别再当黑匣子6.1 MDI和MDA的最小实现差异MDI是随机森林自带的平均不纯度减少MDA是置换重要性。书中对MDI的批评集中在两点不纯度减少对特征尺度敏感而且高相关性特征会互相摊薄权重。不过MDI也有价值——计算成本极低适合做初筛。MDA更稳健但要小心实现。from sklearn.metrics import accuracy_score from sklearn.model_selection import train_test_split # 用CPCV得到训练集和测试集 train_idx, test_idx next(cpcv_split_embargo(labels.index, k5)) clf RandomForestClassifier(n_estimators200, random_state0) clf.fit(X.loc[train_idx], y.loc[train_idx]) base_acc accuracy_score(y.loc[test_idx], clf.predict(X.loc[test_idx])) importance {} for col in X.columns: X_shuffled X.loc[test_idx].copy() # 破坏单列的顺序保持行结构不变 X_shuffled[col] np.random.permutation(X_shuffled[col].values) acc_shuffled accuracy_score(y.loc[test_idx], clf.predict(X_shuffled)) importance[col] base_acc - acc_shuffled注意这里的排列是整列随机打乱。金融序列里相邻样本有自相关性完全随机打乱会破坏这种相关性导致重要性虚高。我自己的习惯是把测试集按时间切成块在块内随机打乱块间顺序不动这样更贴近“市场状态不变但特征值失真”的场景。6.2 把“实验性”做实的习惯到这一步整套练习的基础闭环已经跑通了事件驱动生成样本三重屏障定义标签元标签过滤弱信号CPCV做可靠评估最后用MDI/MDA判断特征贡献度。这一步再往前的实战经验是每跑一个实验固定随机种子并把中间输出落盘特征、标签、权重、模型权重全部保存。书里反复强调再现性金融场景里同一个模型在不同日期的数据上重新训练差别都可能大到让结论失效不复现的实验等于白做。另一个习惯是给每个模型准备一个“基线对比”用最近价格方向作为预测即只预测上涨看自己的模型能不能跑赢这个弱基线。在信噪比极低的金融数据里很多复杂模型连这个基线都跑不过先测基线再谈调参。这套练习真正会教会你的是对评估体系的敬畏。希望这些具体的实现细节能帮你少走一些我走过的弯路也祝你在跑通每个实验时都能拿到比基线更好的结果。本文还有配套的精品资源点击获取