简介:这份PDF资料面向金融行业从业者、量化投资初学者及数据科学方向学生,系统梳理机器学习在金融场景中的落地路径,帮助读者建立从数据到模型再到业务应用的完整认知。内容围绕信用风险评估、股票预测、客户行为分析与欺诈检测等典型问题展开,涵盖金融数据挖掘、特征工程、模型评估与部署等关键环节,并涉及逻辑回归、决策树、随机森林、支持向量机、Gradient Boosting等常用算法的实践思路。资源包内共1个PDF文件,压缩后约2.9MB,轻量便携,适合通读与查阅。目前已有394人学习下载,可作为金融机器学习入门与方案参考的实用材料,帮助读者理解数据预处理、模型选型与业务落地的衔接方式,并初步认识数据质量、模型可解释性等现实挑战。
1. 金融机器学习实践:从一份 PDF 到一套能跑通的因子流水线
很多人第一次接触《金融机器学习实践.pdf》,是把它当成一本“翻完就能上手”的操作手册,结果翻到特征工程那一章就卡住了——书里的示例数据是日频的,自己手里的却是分钟级 tick,时间戳对不齐,标签一错位,回测曲线漂亮得离谱,实盘一跑就原形毕露。这份材料真正值钱的地方,不是某个模型公式,而是它把金融场景里“数据—特征—标签—验证—回测”这条链路拆开讲透了。它适合两类人:一类是刚转量化、手里有 Python 基础但没做过完整因子流水线的工程师;另一类是做传统风控或信贷模型、想把这套方法论迁移到二级市场预测上的从业者。核心要解决的问题只有一个:怎么让模型在样本外不翻车,而不是在训练集上刷出 0.99 的 AUC。
2. 金融机器学习的特殊性:为什么不能直接套用 Kaggle 那套
2.1 低信噪比与样本重叠:金融数据的两个硬约束
金融机器学习最反直觉的一点是:你的模型越复杂,过拟合来得越快。Kaggle 比赛里特征和目标之间往往有明确映射,而金融收益率序列的信噪比极低,日频股票收益的可用信号通常不到 5%。更麻烦的是样本重叠——你用未来 5 日收益做标签,相邻两天的样本共享了 4 天信息,独立同分布假设直接失效。常见做法是改用三重障碍法(Triple Barrier)打标签,配合净化交叉验证(Purged K-Fold)和禁运期(Embargo),把重叠样本从训练集里剔掉。我一般会先把标签的持有期和特征窗口对齐,再决定交叉验证的折数和禁运天数,禁运期通常设为标签持有期的 1% 到 2%。
2.2 从 PDF 到代码:最小可复现的目录结构
拿到这份材料后,不要急着逐章抄代码。先搭一个能跑通的最小工程骨架,把数据加载、特征计算、标签生成、模型训练、回测评估五个环节分开。下面是我常用的目录结构,配合一个配置驱动的入口脚本:
finance_ml/ ├── config/ │ └── base.yaml # 数据路径、窗口参数、模型超参 ├── data/ │ ├── raw/ # 原始行情、财务数据 │ └── processed/ # 对齐后的特征与标签 ├── src/ │ ├── data_loader.py # 读取与时间对齐 │ ├── features.py # 因子计算 │ ├── labeling.py # 三重障碍法打标签 │ ├── validation.py # 净化交叉验证 │ └── backtest.py # 回测与绩效归因 ├── notebooks/ │ └── eda.ipynb # 探索性分析 └── run.py # 主入口这个结构的好处是每个环节可以单独测试。比如你怀疑标签错了,直接跑labeling.py的单元测试,不用把整个流水线重跑一遍。配置文件用 YAML 而不是硬编码,是为了后面做参数扫描时不用改代码。
2.3 数据对齐:时间戳、复权与缺失值的三重坑
金融数据对齐是血泪经验最集中的地方。第一,时间戳必须统一到同一时区,A 股用 Asia/Shanghai,美股用 America/New_York,混用会导致日期偏移一天。第二,复权方式要一致,前复权适合回测,后复权适合长期持有分析,但两者不能混着用。第三,缺失值不要直接fillna(0),价格缺失和成交量缺失的处理逻辑完全不同。下面这段代码是我常用的对齐模板:
import pandas as pd import numpy as np def align_data(price_df, factor_df, freq='1D'): """ 将价格与因子对齐到同一时间索引 price_df: 含 open/high/low/close/volume,索引为 DatetimeIndex factor_df: 因子宽表,索引为 DatetimeIndex freq: 重采样频率,日频用 '1D' """ # 统一时区 price_df.index = price_df.index.tz_localize('Asia/Shanghai') factor_df.index = factor_df.index.tz_localize('Asia/Shanghai') # 按频率重采样,价格用 last,成交量用 sum price_resampled = price_df.resample(freq).agg({ 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum' }) factor_resampled = factor_df.resample(freq).last() # 合并后前向填充因子,价格缺失则剔除 merged = price_resampled.join(factor_resampled, how='left') merged = merged.dropna(subset=['close']) merged[factor_resampled.columns] = merged[factor_resampled.columns].ffill() return merged逻辑说明:先统一时区避免日期错位,再按频率重采样。价格用first/max/min/last聚合,成交量用sum,因子用last后前向填充。参数freq根据你的策略周期调整,日频用1D,小时频用1H。注意dropna只针对收盘价,因子缺失可以填充,但价格缺失必须剔除,否则会引入未来信息。
3. 特征工程与标签构造:把 PDF 里的方法落成代码
3.1 分数阶差分:让价格序列既平稳又保留记忆
金融时间序列做平稳性检验时,整数阶差分(一阶差分)会把长期记忆一起差掉,导致模型学不到趋势信息。分数阶差分(Fractional Differentiation)是这份材料里最值得动手实现的方法之一。它通过一个权重序列对原始价格做加权求和,在平稳性和记忆保留之间找平衡。下面是我常用的实现:
import numpy as np import pandas as pd def frac_diff(series, d, threshold=1e-4): """ 分数阶差分 series: 价格序列 d: 差分阶数,通常在 0.2~0.6 之间 threshold: 权重截断阈值,越小保留越多历史 """ # 计算权重 weights = [1.0] for k in range(1, len(series)): w = -weights[-1] * (d - k + 1) / k if abs(w) < threshold: break weights.append(w) weights = np.array(weights[::-1]) # 加权求和 width = len(weights) result = series.rolling(width).apply( lambda x: np.dot(x, weights), raw=True ) return result.dropna() # 使用示例 # price = df['close'] # for d in [0.2, 0.3, 0.4, 0.5]: # diff_series = frac_diff(price, d) # # 做 ADF 检验,选最小的 d 使序列平稳逻辑说明:权重递推公式来自二项式展开,threshold控制截断精度。d的选择需要配合 ADF 检验,通常从 0.2 开始试,找到使序列通过 95% 置信水平的最小d。参数threshold设 1e-4 在日频数据上一般保留 50 到 100 个历史点,太大则记忆丢失,太小则计算量爆炸。
3.2 三重障碍法打标签:比固定阈值更贴近实盘
固定阈值打标签(比如未来 5 日收益大于 2% 记 1)的问题是忽略了波动率变化。三重障碍法同时设置上障碍、下障碍和时间障碍,更接近真实交易中的止盈止损逻辑。实现时要注意障碍宽度用滚动波动率动态调整:
def triple_barrier_label(price, vol, upper=2.0, lower=2.0, max_hold=5): """ 三重障碍法打标签 price: 收盘价序列 vol: 滚动波动率,与 price 同索引 upper/lower: 上下障碍的波动率倍数 max_hold: 最大持有期 返回: 标签序列,1 止盈,-1 止损,0 时间到期 """ labels = pd.Series(index=price.index, dtype=float) for i in range(len(price) - max_hold): entry = price.iloc[i] upper_barrier = entry * (1 + upper * vol.iloc[i]) lower_barrier = entry * (1 - lower * vol.iloc[i]) for j in range(1, max_hold + 1): if price.iloc[i + j] >= upper_barrier: labels.iloc[i] = 1 break elif price.iloc[i + j] <= lower_barrier: labels.iloc[i] = -1 break else: labels.iloc[i] = 0 return labels.dropna()逻辑说明:vol用 20 日滚动标准差计算,upper和lower一般设 1.5 到 2.5 之间。max_hold根据策略周期定,日频常用 5 到 10 天。注意这个循环在长序列上较慢,生产环境可以用 Numba 加速,或者用向量化方式重写。
3.3 特征筛选:用 MDI 和 MDA 剔除冗余因子
特征不是越多越好。我一般先用基于不纯度的特征重要性(MDI)做初筛,再用基于排列的特征重要性(MDA)做二次确认。MDI 对高基数特征有偏,MDA 计算量大但更可靠。两者结合能过滤掉大部分噪声因子。具体做法是:先跑一个随机森林,取 MDI 前 50% 的特征,再对这些特征做 MDA,保留 MDA 为正且标准差较小的因子。这一步能砍掉 60% 以上的无效特征,训练速度提升明显。
4. 模型验证与回测:避开过拟合的五个关键检查点
4.1 净化交叉验证:把重叠样本从训练集里拿掉
普通 K-Fold 在金融数据上会严重高估模型性能,因为训练集和验证集之间存在样本重叠。净化交叉验证的做法是:在每折的训练集里,剔除与验证集标签持有期重叠的样本,并在验证集前后加禁运期。下面是一个简化实现:
from sklearn.model_selection import KFold import numpy as np def purged_kfold(n_samples, n_splits=5, embargo_pct=0.01, label_hold=5): """ 净化交叉验证 n_samples: 样本总数 n_splits: 折数 embargo_pct: 禁运期比例 label_hold: 标签持有期 """ kf = KFold(n_splits=n_splits, shuffle=False) embargo = int(n_samples * embargo_pct) splits = [] for train_idx, val_idx in kf.split(np.arange(n_samples)): # 剔除与验证集重叠的训练样本 val_start, val_end = val_idx[0], val_idx[-1] purged_train = [ i for i in train_idx if i < val_start - label_hold - embargo or i > val_end + label_hold + embargo ] splits.append((np.array(purged_train), val_idx)) return splits逻辑说明:label_hold要和打标签时的max_hold一致,embargo_pct一般设 0.01 到 0.02。这个实现假设数据按时间排序,不能 shuffle。如果要做多折时间序列交叉验证,把KFold换成TimeSeriesSplit再套同样的净化逻辑。
4.2 回测中的三个致命偏差
第一个是前视偏差:用了未来才知道的信息,比如用当日收盘价计算因子却在当日开盘交易。第二个是幸存者偏差:只用了当前还在上市的股票,退市的没算进去。第三个是交易成本低估:回测里手续费设万分之一,实盘可能滑点就吃掉一半利润。我一般会在回测里把手续费设成双边千分之三,滑点按成交量的平方根模型估算,这样得到的曲线虽然难看,但更接近实盘。
4.3 绩效归因:不要只看夏普比率
夏普比率高不代表策略好。我习惯同时看四个指标:年化收益、最大回撤、换手率、以及收益的月度胜率。换手率超过 50 倍每年的策略,基本可以判定为过度交易。月度胜率低于 55% 的策略,实盘心理压力会很大。下面是一个简单的绩效汇总表模板:
| 指标 | 计算方式 | 参考阈值 |
|---|---|---|
| 年化收益 | 日收益均值 × 252 | > 15% |
| 最大回撤 | 累计收益峰值到谷底 | < 20% |
| 夏普比率 | 年化收益 / 年化波动 | > 1.5 |
| 换手率 | 年化单边交易额 / 平均持仓 | < 30 倍 |
| 月度胜率 | 正收益月份占比 | > 55% |
这些阈值不是绝对的,但能帮你快速判断一个策略值不值得继续投入。
5. 避坑与排查:五个让回测曲线“好看得可疑”的常见问题
5.1 现象:回测夏普超过 3,实盘一跑就亏
原因:大概率是前视偏差。检查因子计算里有没有用到shift(-1)或未来数据,标签生成时有没有把当日收盘价算进去。 解决:把所有因子计算统一加shift(1),确保 T 日只能看到 T-1 日及之前的数据。用pandas的shift后做一次时间戳审计。
5.2 现象:训练集 AUC 0.95,验证集 AUC 0.52
原因:样本重叠导致信息泄露。相邻样本共享标签持有期内的价格信息,模型记住了噪声。 解决:改用净化交叉验证,禁运期设为标签持有期的 1% 到 2%。同时检查特征里有没有包含未来信息的滚动统计量。
5.3 现象:因子 IC 很高但回测不赚钱
原因:IC 衡量的是因子与未来收益的秩相关,但没考虑交易成本和换手率。高 IC 因子可能换手极高,利润被成本吃掉。 解决:在因子筛选阶段加入换手率约束,或者用 IC 乘以换手率的倒数作为综合评分。回测时把手续费和滑点调高做压力测试。
5.4 现象:不同股票池回测结果差异巨大
原因:幸存者偏差或股票池选择偏差。用了当前成分股回测历史,退市股票没纳入。 解决:使用历史成分股数据,或者至少把退市股票保留到退市日。股票池选择要固定规则,不能手动挑。
5.5 现象:模型在牛市表现好,熊市回撤巨大
原因:特征或标签对市场状态敏感,模型没有状态切换机制。 解决:加入市场状态特征(如波动率指数、均线斜率),或者训练多个模型按状态切换。也可以直接用集成方法,让模型自己学状态划分。
6. 进阶技巧:用组合净化交叉验证做模型选择
当你手上有多个模型(比如 XGBoost、LightGBM、LSTM)时,单次净化交叉验证的结果波动很大,容易选错。组合净化交叉验证(Combinatorial Purged Cross Validation,CPCV)是这份材料里最值得深入的方法。它把时间序列分成 N 组,每次取其中 k 组做验证,其余做训练,生成多条回测路径。这样你得到的不是一个夏普比率,而是一个夏普比率的分布,能更稳健地比较模型。
具体操作:把数据按时间分成 6 组,每次取 2 组做验证,共生成 15 种组合。对每个模型跑完 15 条路径,计算夏普比率的均值和标准差。选择均值高且标准差小的模型。下面是一个简化的路径生成代码:
from itertools import combinations import numpy as np def cpcv_splits(n_groups=6, n_test_groups=2): """ 组合净化交叉验证路径生成 n_groups: 时间分组数 n_test_groups: 每次验证组数 """ groups = np.arange(n_groups) splits = [] for test_groups in combinations(groups, n_test_groups): train_groups = [g for g in groups if g not in test_groups] splits.append((train_groups, list(test_groups))) return splits # 使用示例 # splits = cpcv_splits(6, 2) # for train_g, test_g in splits: # # 按组索引取数据,训练集还要做净化处理 # pass逻辑说明:n_groups一般设 6 到 10,n_test_groups设 1 到 3。组数越多,路径越多,计算量越大。实际使用时,每组内部还要做净化处理,剔除与验证组重叠的样本。这个方法的代价是计算量翻十几倍,但换来的是对模型稳健性的真实评估。
我自己的习惯是:任何策略在实盘前,必须跑完 CPCV 的 15 条路径,夏普比率的标准差超过 0.5 的直接放弃。这个门槛帮我挡掉了至少一半看起来不错的策略。希望帮到你。
本文还有配套的精品资源,点击获取