ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器学习重写多因子研究框架:从线性加权到非线性建模的工程实践

机器学习重写多因子研究框架:从线性加权到非线性建模的工程实践 简介这份资源是一套面向金融量化研究者与数据科学从业者的机器学习多因子研究框架配套完整源代码与文档说明适合希望将机器学习落地到投资决策场景的中高级学习者。框架覆盖机器学习收益模型、风险模型与组合优化三大模块收益模型部分采用XGBoost与LightGBM两种梯度增强算法处理高维因子并引入贝叶斯优化完成超参数寻优风险模型涉及波动率、VaR与条件VaR等指标组合优化则包含均值-方差与Black-Litterman等思路。资源包共306个文件以csv数据、ipynb实验笔记、py脚本为主辅以db数据库、java与c源码及makefile等工程文件压缩包约38.98MB目录按模块组织便于检索。目前已有349人学习下载读者可据此复现从因子选择、模型训练、参数调优到组合配置的完整流程并借鉴其中的日志与实验记录排查问题。1. 多因子研究框架为什么值得用机器学习重写一遍做过量化的人大多有过这样的经历手工堆了几十个因子IC 看着还行一回测就崩换个时间段又活过来反复调参数调到怀疑人生。传统多因子框架的问题不在于因子不够多而在于因子之间的关系是线性的、静态的而市场是动态的、非线性的。机器学习恰好擅长处理这类高维、非线性、时变的问题所以把机器学习引入多因子研究框架本质上是用模型自动学习因子之间的交互关系和时变权重替代人工拍脑袋的线性加权。这套框架适合谁一是已经有因子库、但组合方式还停留在等权或简单回归的量化研究员二是想从零搭一套可复现研究流程的数据科学从业者三是需要一套标准化流程来管理因子挖掘、模型训练、回测验证全链路的团队。它解决的核心问题是让因子组合从「人工经验驱动」变成「数据驱动 可验证」同时把整个研究过程沉淀成可复用的代码和文档而不是散落在各个 notebook 里。2. 多因子研究框架的骨架数据、因子、模型、回测四层怎么搭2.1 四层架构的职责划分与数据流一套能跑通的多因子研究框架不管用什么语言实现骨架都逃不开四层数据层、因子层、模型层、回测层。数据层负责行情、财务、另类数据的清洗和对齐因子层负责从原始数据计算出可用于建模的特征模型层负责用机器学习算法把因子映射成预测信号回测层负责把信号转成持仓并计算绩效。数据流是单向的原始数据 → 清洗对齐 → 因子计算 → 特征工程 → 模型训练 → 信号生成 → 组合构建 → 回测评估。每一层的输出是下一层的输入层与层之间通过标准化的数据结构解耦。常见做法是用 DataFrame 作为层间传递的统一格式索引统一为「日期 标的」的多级索引这样任何一层替换实现都不影响其他层。我一般会在数据层和因子层之间加一个「因子存储」环节把计算好的因子落盘成 parquet 或 hdf5避免每次跑模型都重算因子。这个环节看起来不起眼但当你因子数量上百、回测频率到日频甚至分钟频时省下的时间是以小时计的。2.2 用 Python 搭出最小可运行骨架下面这段代码搭出一个四层骨架的最小实现重点看层与层之间的接口定义而不是具体算法。import pandas as pd import numpy as np from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import TimeSeriesSplit class DataLayer: 数据层负责加载和对齐原始数据 def load(self, price_path, factor_path): # 价格数据索引为 (date, asset) price pd.read_parquet(price_path) # 因子数据同样索引列名为因子名 factors pd.read_parquet(factor_path) # 按索引对齐避免未来函数 aligned factors.join(price[close], howinner) return aligned class FactorLayer: 因子层计算或加载因子做标准化和去极值 def process(self, df, factor_cols): X df[factor_cols].copy() # 横截面去极值按日期分组用 3 倍 MAD 截断 def winsorize(group): med group.median() mad (group - med).abs().median() return group.clip(med - 3 * mad, med 3 * mad) X X.groupby(leveldate).transform(winsorize) # 横截面标准化 X X.groupby(leveldate).transform(lambda g: (g - g.mean()) / g.std()) return X class ModelLayer: 模型层训练机器学习模型并输出预测 def __init__(self, modelNone): self.model model or GradientBoostingRegressor( n_estimators100, max_depth3, learning_rate0.05 ) def train_predict(self, X, y, n_splits5): tscv TimeSeriesSplit(n_splitsn_splits) preds pd.Series(indexy.index, dtypefloat) for train_idx, test_idx in tscv.split(X): self.model.fit(X.iloc[train_idx], y.iloc[train_idx]) preds.iloc[test_idx] self.model.predict(X.iloc[test_idx]) return preds class BacktestLayer: 回测层根据预测信号构建组合并计算绩效 def run(self, preds, returns, top_pct0.2): # 按日期分组选预测值最高的 top_pct 作为多头 def select(group): n max(1, int(len(group) * top_pct)) return group.nlargest(n).index positions preds.groupby(leveldate).apply(select) # 简化处理等权持有计算次日收益 port_ret returns.loc[positions].groupby(leveldate).mean() return port_ret这段代码的关键在于接口设计DataLayer.load返回统一索引的 DataFrameFactorLayer.process只做因子预处理不碰价格ModelLayer.train_predict用时间序列交叉验证避免未来函数BacktestLayer.run接收预测值和真实收益。参数方面n_splits5是时间序列 CV 的折数因子少时可以降到 3top_pct0.2是选股比例A 股常用 0.1 到 0.3 之间max_depth3是树模型的深度因子间交互复杂时可以加到 5但要注意过拟合。2.3 因子预处理里最容易被忽略的三个参数因子预处理看起来简单但三个参数设错后面模型再好也白搭。第一个是去极值的倍数。上面代码用的是 3 倍 MAD这是比较保守的做法。如果因子分布厚尾严重可以放宽到 5 倍但不要直接删掉极值因为极值里往往包含真实的信息。第二个是标准化的方式。横截面标准化按日期分组做 z-score比全局标准化更合理因为不同日期的因子分布本身就在漂移。第三个是缺失值处理。因子缺失不能简单填 0因为 0 在标准化后不是中性值。常见做法是先做横截面中位数填充再标准化或者把缺失值单独作为一个指示变量。提示因子预处理的所有操作都必须按日期分组进行任何跨日期的全局操作都会引入未来函数。3. 用机器学习模型替代线性加权的具体做法3.1 为什么树模型和神经网络在多因子上表现不同线性加权本质上假设每个因子对收益的贡献是独立的、可加的。机器学习模型打破了这个假设但不同模型打破的方式不一样。树模型GBDT、XGBoost、LightGBM擅长捕捉因子的非线性关系和阈值效应。比如动量因子在极端值时可能反转树模型可以通过分裂点自动学到这个转折。但树模型对因子间的平滑交互捕捉能力有限而且对噪声因子比较敏感。神经网络MLP、LSTM擅长捕捉因子间的复杂交互和时序依赖但需要更多数据才能训稳而且可解释性差。我一般会先用 LightGBM 跑一版基线因为它的训练速度快、对缺失值友好、特征重要性可以直接看。如果因子数量超过 50 个、样本量足够再尝试加一层 MLP 做 stacking。不要一上来就上深度学习多因子研究的样本量通常撑不住。3.2 训练集、验证集、测试集的时间切分规则多因子模型的切分和普通机器学习不一样绝对不能随机切分。常见做法是滚动窗口或扩展窗口。滚动窗口训练集固定长度比如 3 年验证集 1 年测试集 1 年然后整体向前滚动。扩展窗口训练集从起点开始累积验证集和测试集固定长度。滚动窗口适合市场结构变化快的场景扩展窗口适合数据量少、需要更多训练样本的场景。下面是一个滚动窗口切分的实现def rolling_split(dates, train_years3, val_months6, test_months6): 按时间滚动切分返回 (train_idx, val_idx, test_idx) 列表 dates pd.to_datetime(dates) splits [] start dates.min() end dates.max() train_end start pd.DateOffset(yearstrain_years) while train_end pd.DateOffset(monthsval_months test_months) end: val_end train_end pd.DateOffset(monthsval_months) test_end val_end pd.DateOffset(monthstest_months) train_idx dates[(dates start) (dates train_end)] val_idx dates[(dates train_end) (dates val_end)] test_idx dates[(dates val_end) (dates test_end)] splits.append((train_idx, val_idx, test_idx)) # 向前滚动训练集起点不变终点前移 train_end train_end pd.DateOffset(monthstest_months) return splits参数说明train_years3是训练窗口长度A 股常用 2 到 5 年val_months6是验证窗口用于早停和超参选择test_months6是测试窗口用于评估最终绩效。滚动步长等于测试窗口长度保证每次测试集不重叠。3.3 特征重要性和模型可解释性怎么落地机器学习模型被诟病最多的是黑匣子。但在多因子研究里可解释性不是可选项因为你需要知道模型到底在赚什么钱。常见做法有三层第一层看特征重要性feature importanceLightGBM 可以直接输出 gain 和 split 两种重要性第二层看 SHAP 值它能告诉你每个因子对每个样本预测值的贡献方向和大小第三层做因子归因把模型预测值对原始因子做回归看模型是否过度依赖某几个因子。import shap import lightgbm as lgb # 训练 LightGBM model lgb.LGBMRegressor(n_estimators200, max_depth4, learning_rate0.05) model.fit(X_train, y_train) # 特征重要性 importance pd.Series(model.feature_importances_, indexX_train.columns) print(importance.sort_values(ascendingFalse).head(10)) # SHAP 值 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) # 按因子汇总 SHAP 绝对值的均值 shap_importance pd.Series( np.abs(shap_values).mean(axis0), indexX_test.columns ) print(shap_importance.sort_values(ascendingFalse).head(10))特征重要性告诉你哪些因子被模型频繁使用SHAP 告诉你因子影响的方向和幅度。如果某个因子的 SHAP 方向和市场逻辑相反大概率是过拟合或者数据有问题需要排查。4. 回测与验证怎么判断模型是真的有效还是过拟合4.1 回测里必须看的五个指标回测不是只看年化收益。我一般会同时看五个指标年化收益、夏普比率、最大回撤、换手率、IC 均值。年化收益和夏普比率是基础但要注意夏普高不一定好如果换手率极高扣掉交易成本后可能所剩无几。最大回撤看的是策略在最差情况下的表现多因子模型的最大回撤如果超过 20%需要检查是不是因子暴露过于集中。换手率直接决定交易成本日频调仓的策略换手率通常在 5 到 15 倍之间。IC 均值看的是预测值和真实收益的相关性IC 大于 0.03 就算有效大于 0.05 算不错。指标合理范围异常时的排查方向年化收益8%20%过高检查是否未来函数夏普比率0.82.0过高检查是否过拟合最大回撤 20%过大检查因子暴露换手率515 倍过高检查调仓频率IC 均值 0.03过低检查因子有效性4.2 用样本外测试和滚动回测验证稳健性样本内表现好不代表什么关键是样本外。我一般会做两件事一是留出最近 1 到 2 年的数据完全不参与训练只在最后评估时用二是做滚动回测看策略在不同时间段的表现是否稳定。滚动回测的实现思路是用滚动窗口切分数据每个窗口训练一个模型在对应的测试集上生成信号最后把所有测试集的信号拼起来算整体绩效。这样能看出模型在不同市场环境下的适应能力。如果某个窗口的表现特别差需要单独分析那段时间的市场特征看是模型问题还是市场结构变化。def walk_forward_backtest(X, y, returns, splits): 滚动回测每个窗口训练模型拼接测试集预测 all_preds [] for train_idx, val_idx, test_idx in splits: model lgb.LGBMRegressor(n_estimators200, max_depth4) model.fit(X.loc[train_idx], y.loc[train_idx]) preds pd.Series( model.predict(X.loc[test_idx]), indextest_idx ) all_preds.append(preds) all_preds pd.concat(all_preds) # 用拼接后的预测值做组合回测 port_ret BacktestLayer().run(all_preds, returns) return port_ret参数说明splits来自前面的rolling_split函数每个窗口独立训练。注意验证集在这里没有用到实际使用时可以用验证集做早停。拼接预测值时要注意索引对齐避免不同窗口的预测值混在一起。5. 避坑与排查多因子机器学习框架的五个血泪教训5.1 未来函数回测收益高得离谱的第一嫌疑现象回测年化收益 50% 以上夏普超过 3但实盘完全跑不出来。原因最常见的是因子计算用了未来数据。比如用当日收盘价计算因子但回测时假设当日收盘就能交易或者财务数据用了公告日期之前的报告期数据。解决所有因子计算必须严格对齐「可获取时间」。价格数据用前一日收盘或当日开盘财务数据用公告日期而非报告期。写完因子后用「把数据整体向后移一天」的方式做一次回测如果收益大幅下降说明有未来函数。5.2 因子共线性模型重要性失真现象特征重要性显示某几个因子特别重要但去掉它们后模型表现几乎不变。原因因子之间存在高度共线性模型随机选择了其中一个。树模型对共线性不像线性回归那么敏感但 SHAP 值会被分散到多个共线因子上。解决在因子层做相关性筛选两两相关性超过 0.8 的因子只保留一个。或者用 PCA 做降维但降维后的主成分可解释性差我一般优先做相关性筛选。5.3 样本不平衡极端收益主导训练现象模型预测值集中在中间区域对极端收益的预测能力很差。原因收益分布厚尾极端值在训练时主导了损失函数。MSE 损失对极端值敏感导致模型偏向预测均值。解决对标签做截面排序或分位数变换把回归问题转成排序问题。或者用 Huber 损失替代 MSE降低极端值的影响。常见做法是把收益按日期做截面 rank然后归一化到 0 到 1 之间。5.4 过拟合训练集和测试集表现差距大现象训练集 IC 0.1测试集 IC 0.01。原因模型复杂度太高、因子太多、训练时间太长。树模型的max_depth和n_estimators是主要嫌疑。解决降低模型复杂度max_depth控制在 3 到 5n_estimators控制在 100 到 300。加正则化参数LightGBM 的lambda_l1和lambda_l2设 0.1 到 1。用早停验证集 IC 连续下降就停。5.5 数据对齐错误索引不匹配导致信号错位现象回测信号和持仓对不上或者某些日期的信号为空。原因因子数据和价格数据的索引不一致join 时产生了缺失或错位。解决在数据层统一索引格式所有数据都用(date, asset)多级索引。join 后用isna().sum()检查缺失情况缺失超过 10% 的日期要单独排查。回测前打印前几行信号和持仓肉眼确认对齐。6. 把框架跑得更稳的两个进阶技巧第一个技巧是用因子正交化替代简单的相关性筛选。相关性筛选只能去掉线性相关的因子但因子之间可能存在非线性依赖。正交化是把新因子对已有因子做回归取残差作为新的因子值这样能保证新因子携带的是增量信息。实现上用statsmodels的 OLS 或sklearn的LinearRegression按日期分组做横截面回归。注意正交化后的因子可解释性会下降需要权衡。from sklearn.linear_model import LinearRegression def orthogonalize(new_factor, base_factors): 将 new_factor 对 base_factors 做横截面正交化 residuals pd.Series(indexnew_factor.index, dtypefloat) for date, group in new_factor.groupby(leveldate): X base_factors.loc[date] y group reg LinearRegression().fit(X, y) residuals.loc[date] y - reg.predict(X) return residuals第二个技巧是用模型集成替代单模型。把 LightGBM、XGBoost、MLP 的预测值做加权平均权重可以用验证集 IC 或等权。集成能降低单模型的过拟合风险但会增加计算成本和维护复杂度。我一般只在单模型稳定后再上集成而且集成后的模型数量不超过 3 个。最后说一个我自己的习惯每次改完因子或模型参数先跑一遍「随机标签测试」——把真实收益随机打乱后训练模型如果回测收益仍然显著为正说明框架里有未来函数或数据泄露。这个测试花不了几分钟但能帮你省下大量排查时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表