简介这份《金融机器学习实践》PDF资料面向金融从业者、数据科学学习者与量化投资爱好者聚焦机器学习在金融场景中的落地应用帮助读者理解信用风险评估、股票预测、客户行为分析与欺诈检测等典型问题的建模思路。资源为单份PDF文档压缩包约2.9MB内容围绕金融数据挖掘、特征工程、模型训练与评估展开涉及逻辑回归、决策树、随机森林、支持向量机、Gradient Boosting等常用算法并延伸至Model Serving、GraphX等工程化概念适合作为入门到进阶的体系化参考。目前已有394人学习下载读者可从中获取金融机器学习项目的整体框架、算法选型逻辑与数据预处理要点快速建立从业务问题到模型落地的认知路径也可作为课程学习或实际项目的辅助材料。1. 金融机器学习实践从一份 PDF 到一套能跑起来的因子流水线很多人第一次接触《金融机器学习实践》这类资料是被“机器学习”四个字吸引进来的结果翻了几页发现满屏是收益率、夏普、回撤、标签重叠跟平时做的图像分类、推荐排序完全不是一回事。金融机器学习的核心矛盾在于样本量少、信噪比极低、数据非平稳而且你拿到的历史数据里藏着大量未来信息稍不注意就会做出一个回测漂亮、实盘崩溃的模型。这份资料真正要解决的问题不是教你调 XGBoost 参数而是教你如何把金融问题翻译成机器学习问题再翻译回可执行的交易信号。适合已经会 Python、懂基本机器学习、但一碰金融数据就翻车的从业者也适合量化研究员想系统补齐方法论。下面我按自己复现这类方案的顺序把整条链路拆开讲。2. 金融机器学习的理论地基为什么不能直接套用标准流程2.1 信噪比、非平稳与样本重叠三个绕不开的约束标准机器学习假设样本独立同分布金融数据几乎每条都违反。日频收益率序列的自相关很弱但波动率聚集很强导致模型在低波动期学到的关系到高波动期直接失效。更麻烦的是样本重叠如果你用未来 5 日收益率做标签那么相邻两天的标签共享了 4 天信息训练集和测试集之间天然存在泄漏。常见做法是采用“净化交叉验证”Purged K-Fold和“禁运期”Embargo在训练集和验证集之间砍掉一段重叠样本。我一般会先画一张标签自相关图如果 5 日标签的一阶自相关超过 0.7就必须做净化否则验证分数虚高得离谱。另一个约束是信噪比。金融收益率里可解释部分通常不到 5%这意味着模型稍微复杂一点就会把噪声当信号。所以在这类实践里简单线性模型加正则化往往比深度网络更稳不是因为深度网络不行而是因为样本量根本撑不起那么多参数。资料里反复强调的“先做特征筛选再做模型选择”本质就是在对抗信噪比。2.2 从价格序列到监督学习样本标签怎么造才不泄漏把金融数据变成监督学习样本第一步是确定“预测目标”。常见的有三类未来收益率方向分类、未来收益率大小回归、未来波动率回归。方向分类最直观但阈值怎么定很讲究。如果简单用“涨为 1 跌为 0”在震荡市里标签会极度不平衡。我一般用滚动窗口的收益率分位数来定阈值比如过去 60 天收益率的 30% 和 70% 分位低于 30% 分位标 -1高于 70% 分位标 1中间标 0。这样标签在不同波动环境下都有合理分布。第二步是特征对齐。所有特征必须在 t 时刻收盘后可得标签是 t1 到 t5 的收益。很多人翻车是因为用了当日收盘价计算的指标去预测当日收益这就是典型的未来函数。一个检查习惯把特征矩阵的时间索引和标签索引打印出来确认特征最大时间严格小于标签起始时间。2.3 金融特征工程从量价到微观结构量价特征是最基础的过去 N 日收益率、波动率、成交量变化、换手率、高低价差。但这类特征在日频上已经被挖得很透边际收益有限。资料里更看重的是“分数阶差分”和“波动率缩放”。分数阶差分解决的是价格序列非平稳但一阶差分又把记忆性差没了。用 0.3 到 0.5 阶的分数阶差分可以在保留一定记忆性的同时让序列通过平稳性检验。代码上可以用fracdiff库或者自己实现二项式展开的权重。波动率缩放则是把不同股票、不同时期的收益率除以当期波动率让样本更可比。比如用过去 20 天的已实现波动率做分母得到“波动率调整后收益”。这一步在横截面策略里尤其重要否则高波动股票会主导损失函数。import numpy as np import pandas as pd def frac_diff_weights(d, size): 计算分数阶差分的二项式权重 w [1.0] for k in range(1, size): w.append(-w[-1] * (d - k 1) / k) return np.array(w[::-1]).reshape(-1, 1) def frac_diff(series, d0.4, threshold1e-4): 对序列做分数阶差分保留记忆性 w frac_diff_weights(d, len(series)) # 截断权重避免计算全部历史 w w[np.abs(w) threshold] width len(w) result series.copy() * np.nan for i in range(width, len(series)): window series.iloc[i-width:i].values.reshape(-1, 1) result.iloc[i] float(np.dot(w.T, window)) return result这段代码的关键参数是d一般取 0.3 到 0.5。threshold控制权重截断太小会保留过多历史导致计算慢太大会丢失记忆性。我一般先用 ADF 检验确定最小 d让差分后序列在 5% 显著性下平稳然后在这个 d 附近微调。注意分数阶差分后的序列前 width 个值是 NaN做训练时要对齐标签后丢弃。3. 用 Python 搭一条可复现的因子研究流水线3.1 数据获取与清洗从原始行情到对齐面板数据源常见的有本地 CSV、数据库或行情 API。不管来源如何第一步都是统一成“长表”格式日期、股票代码、开盘、最高、最低、收盘、成交量、复权因子。复权处理是第一个坑前复权价格会随着新除权事件变化导致历史特征不稳定。我一般用后复权价格计算收益率用不复权价格计算成交量相关特征最后在信号生成时再转回实际价格。清洗环节要处理停牌、涨跌停、新股。停牌日直接剔除涨跌停日的成交量特征会失真我一般把涨跌停日的成交量标记为 NaN后续用滚动中位数填充。新股上市前 60 天波动极大通常直接排除。下面是一个清洗函数示例def clean_panel(df): df 为长表包含 date, code, open, high, low, close, volume, adj_factor df df.sort_values([code, date]) # 后复权价格 df[close_adj] df[close] * df[adj_factor] # 日收益率 df[ret] df.groupby(code)[close_adj].pct_change() # 标记涨跌停以是否触及涨跌停价近似 df[limit_up] df[close] df[high] * 0.999 df[limit_down] df[close] df[low] * 1.001 # 涨跌停日成交量置 NaN df.loc[df[limit_up] | df[limit_down], volume] np.nan # 剔除停牌成交量为 0 或缺失 df df[df[volume].notna() (df[volume] 0)] # 剔除上市初期 df[listing_days] df.groupby(code).cumcount() df df[df[listing_days] 60] return df逻辑说明先复权再算收益率避免除权跳空被当成真实收益。涨跌停判断用收盘价与当日高低价的关系近似不同市场规则不同需要按实际规则调整。listing_days用 cumcount 计算简单但有效。清洗后要检查每个交易日剩余股票数量如果某天少于 100 只说明数据有缺失需要回查数据源。3.2 特征计算与标签生成滚动窗口的写法特征计算的核心是“只用历史数据”。所有滚动统计必须用rolling并配合shift确保 t 时刻的特征只用到 t 及之前的数据。下面是一个批量计算量价特征的函数def add_features(df, windows[5, 10, 20, 60]): df df.sort_values([code, date]) g df.groupby(code) for w in windows: df[fret_{w}d] g[ret].transform(lambda x: x.rolling(w).sum()) df[fvol_{w}d] g[ret].transform(lambda x: x.rolling(w).std()) df[fvolume_ratio_{w}d] g[volume].transform( lambda x: x / x.rolling(w).mean() ) # 波动率调整收益 df[ret_vol_adj] df[ret] / df[vol_20d] # 标签未来 5 日收益按滚动分位分三档 df[fwd_ret_5d] g[ret].transform( lambda x: x.rolling(5).sum().shift(-5) ) df[label] np.nan for code, idx in df.groupby(code).groups.items(): sub df.loc[idx, fwd_ret_5d] q30 sub.rolling(252, min_periods60).quantile(0.3) q70 sub.rolling(252, min_periods60).quantile(0.7) label pd.Series(0, indexsub.index) label[sub q70] 1 label[sub q30] -1 df.loc[idx, label] label return df参数说明windows控制特征回看长度日频常用 5/10/20/60。标签用未来 5 日收益分位数用滚动 252 天、最少 60 天保证早期也有标签。注意shift(-5)会让最后 5 天标签为 NaN训练时自动丢弃。分位数计算在 groupby 内部做避免跨股票泄漏。这一步做完建议打印标签分布如果某一类占比低于 10%需要调整分位阈值。3.3 净化交叉验证与模型训练把泄漏堵死净化交叉验证的实现要点按时间排序分成 K 份每次取一份做验证训练集取验证集之前的部分并在训练集末尾砍掉与验证集重叠的样本。如果标签是未来 5 日收益那么验证集开始前 5 天的训练样本必须剔除。禁运期则是在验证集之后再多砍几天防止验证集信息通过特征滚动窗口泄漏到后续训练。下面是一个简化实现from sklearn.model_selection import BaseCrossValidator class PurgedKFold(BaseCrossValidator): def __init__(self, n_splits5, embargo_days5, label_horizon5): self.n_splits n_splits self.embargo_days embargo_days self.label_horizon label_horizon def split(self, X, yNone, groupsNone): indices np.arange(len(X)) fold_size len(X) // self.n_splits for i in range(self.n_splits): test_start i * fold_size test_end (i 1) * fold_size if i self.n_splits - 1 else len(X) test_idx indices[test_start:test_end] train_end test_start - self.label_horizon if train_end 0: continue train_idx indices[:train_end] # 禁运期训练集末尾再砍 embargo_days if self.embargo_days 0: train_idx train_idx[:-self.embargo_days] yield train_idx, test_idx def get_n_splits(self, XNone, yNone, groupsNone): return self.n_splits这个实现假设数据已按时间排序。label_horizon等于标签窗口长度embargo_days一般取特征最大回看窗口的十分之一。训练时用 LightGBM 或逻辑回归都可以我一般先跑逻辑回归看线性信号再用 LightGBM 看非线性增量。如果 LightGBM 比逻辑回归提升不到 10%说明非线性部分基本是噪声不如用线性模型。4. 回测与评估别让漂亮曲线骗了你4.1 回测中的四个隐形泄漏点第一个泄漏点是幸存者偏差只用当前还在上市的股票回测退市股票被剔除收益虚高。解决方法是使用包含退市股票的历史成分股列表。第二个是前视偏差财务数据有披露延迟如果用财报期末日期对齐就用了未来信息。常见做法是财报数据至少滞后 45 天。第三个是交易成本忽略日频换仓的策略双边成本千分之二起步高频调仓的策略成本能吃掉全部收益。第四个是参数过拟合在全部历史上调参然后报告同一段历史的表现。我一般留最近 2 年数据完全不碰作为最终验证。4.2 评估指标夏普之外还要看什么夏普比率是最常用的但它对收益分布的正态假设很敏感。金融收益有厚尾和偏度所以还要看最大回撤、Calmar 比率、胜率、盈亏比。更重要的是看“换手率调整后收益”如果策略年化 20% 但年换手 50 倍扣掉成本可能只剩 5%。我习惯画三张图累计收益曲线、滚动 60 日夏普、月度收益热力图。滚动夏普能看出策略是否在某些市场环境下失效月度热力图能发现季节性异常。def evaluate_strategy(returns, benchmarkNone, freq252): returns: 策略日收益率序列 cum (1 returns).cumprod() ann_ret cum.iloc[-1] ** (freq / len(returns)) - 1 ann_vol returns.std() * np.sqrt(freq) sharpe ann_ret / ann_vol if ann_vol 0 else 0 drawdown cum / cum.cummax() - 1 max_dd drawdown.min() calmar ann_ret / abs(max_dd) if max_dd ! 0 else 0 win_rate (returns 0).mean() return { ann_ret: ann_ret, ann_vol: ann_vol, sharpe: sharpe, max_dd: max_dd, calmar: calmar, win_rate: win_rate }参数说明freq252是日频年化因子周频用 52月频用 12。cum.iloc[-1]是最终净值。这个函数只做基础评估实盘还要考虑滑点和冲击成本。如果回测夏普超过 2 且最大回撤小于 10%大概率有泄漏先回去查数据对齐。5. 避坑与排查金融机器学习里最容易翻车的五件事5.1 现象验证集 AUC 0.75实盘胜率不到 50%原因标签重叠导致验证集泄漏。未来 5 日收益标签在相邻样本间高度相关随机划分验证集时训练集和验证集共享了大量重叠区间。解决改用净化交叉验证并在训练集和验证集之间加禁运期。检查方法是打印训练集和验证集的时间索引确认没有交集且间隔大于标签窗口。5.2 现象模型在 2015 年表现极好2018 年完全失效原因市场结构变化导致特征分布漂移。2015 年高波动、高换手2018 年低波动、低换手模型学到的关系不适用。解决做滚动训练每 252 天重新训练一次加入波动率状态特征让模型自己适应或者直接按波动率分层建模。我一般会监控特征分布的 PSI群体稳定性指标超过 0.2 就触发重新训练。5.3 现象特征重要性排名第一的是“当日收益率”原因未来函数。当日收益率和未来收益在日频上相关性很弱但如果特征计算时用了未来数据比如用当日收盘价算的指标去预测当日开盘到收盘的收益就会泄漏。解决所有特征加shift(1)确保 t 时刻特征只用到 t-1 及之前的数据。检查方法是把特征和标签的相关系数按时间画出来如果 t 时刻特征与 t 时刻标签相关性异常高就是泄漏。5.4 现象回测换手率 3000%扣费后收益为负原因信号抖动。模型每天输出的信号在 0 附近波动导致频繁调仓。解决对信号做平滑比如用 5 日移动平均或者设置调仓阈值信号变化超过一定幅度才交易。我一般把换手率控制在年化 500% 以内超过这个水平交易成本会吃掉大部分收益。5.5 现象训练集损失正常下降验证集损失从一开始就上升原因样本量太少或特征维度太高。金融数据日频、股票池 300 只、5 年历史总共不到 40 万样本如果特征有 200 个模型很容易过拟合。解决先做特征筛选用单变量 IC 筛选出前 30 个特征再用 L1 正则化做嵌入法筛选最后模型用浅层 LightGBM树深度不超过 4。如果验证损失仍然上升说明信噪比太低考虑降低模型复杂度或增加样本。6. 进阶技巧用组合构建把弱信号变成可交易策略单个因子 IC 0.03 在日频上已经不错但直接按因子值排序买前 10% 股票收益波动会很大。进阶做法是“因子合成 风险模型 组合优化”。因子合成可以用等权、IC 加权或最大化 ICIR。IC 加权是滚动计算每个因子的 IC 均值除以 IC 标准差得到权重再归一化。风险模型可以用协方差矩阵收缩比如 Ledoit-Wolf 收缩避免协方差矩阵不可逆。组合优化在约束换手率和行业暴露的前提下最大化预期收益减风险惩罚。from sklearn.covariance import LedoitWolf def combine_factors(factor_df, forward_ret, window252): factor_df: 多因子面板forward_ret: 未来收益 ic_series {} for col in factor_df.columns: ic factor_df[col].groupby(leveldate).apply( lambda x: x.corr(forward_ret.loc[x.index]) ) ic_series[col] ic.rolling(window).mean() / ic.rolling(window).std() icir pd.DataFrame(ic_series) weights icir.div(icir.abs().sum(axis1), axis0) combined (factor_df * weights).sum(axis1) return combined def optimize_weights(expected_ret, cov_matrix, risk_aversion5, max_weight0.05): 简单均值-方差优化带权重上限 n len(expected_ret) from scipy.optimize import minimize def neg_utility(w): return -(w expected_ret - risk_aversion * w cov_matrix w) constraints [{type: eq, fun: lambda w: w.sum() - 1}] bounds [(0, max_weight)] * n res minimize(neg_utility, np.ones(n)/n, boundsbounds, constraintsconstraints) return res.x这段代码里combine_factors用滚动 ICIR 加权避免用全样本 IC 导致前视。optimize_weights是简化版均值方差risk_aversion控制风险惩罚max_weight防止单票过度集中。实盘还要加行业中性、市值中性约束这里不展开。我自己的习惯是任何组合优化结果先看权重分布如果前十大权重合计超过 50%说明约束太松需要收紧。最后说一个我踩过的坑曾经用全样本训练的模型回测夏普 3.5实盘第一个月就回撤 8%。后来发现是标签分位数用了全样本计算导致早期标签包含了未来信息。改成滚动分位数后回测夏普降到 1.2但实盘表现和回测基本一致。这个教训让我养成了一个习惯任何用到“全样本”统计量的地方都要问一句“这个统计量在 t 时刻真的可得吗”。希望帮到你。本文还有配套的精品资源点击获取