ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

多因子选股模型实战:从单因子测试到机器学习回测的完整指南

2026/10/2 8:33:06 拓冰建站 浏览量
多因子选股模型实战:从单因子测试到机器学习回测的完整指南 简介本资源为基于机器学习方法构建多因子选股模型的完整项目源码与文档面向计算机、人工智能及金融工程方向的学生与量化爱好者可用于课程设计、毕业设计或量化策略入门实践。包内共38个文件以15个Python脚本、10份PDF研报、7张结果图及1份说明文档为主涵盖单因子测试、因子共线性分析、特征与标签构建、等权重线性模型及SVR、LSTM、XGBoost、随机森林、AdaBoost等多模型回测对比压缩包约14.71MB。项目最终随机森林模型累计收益约60%经择时风控后最大回撤控制在9%左右夏普率约0.9并保留回测报告与可视化结果。已有464人学习下载适合希望理解多因子选股全流程、借鉴因子筛选与模型调参思路的读者参考。1. 从一份毕设源码说起多因子选股模型到底怎么落地很多人第一次接触量化选股都是从「多因子模型」这四个字开始的。打开聚宽、米筐或者本地回测框架满屏的因子、IC、IR、分层回测概念看了一堆真到自己动手还是不知道从哪下手。这份 TIDIBEI-master 的资源包恰好是一个已经跑通的完整案例从单因子测试、因子筛选、共线性分析到用随机森林、SVR、LSTM、XGBoost、AdaBoost 等模型做收益预测再到回测和择时风控整条链路都有对应脚本。它适合两类人一类是正在做机器学习方向毕设、需要一份能跑通的参考实现的学生另一类是想把多因子选股从理论落到代码、但缺一个完整骨架的从业者。资源里最优的随机森林模型累计收益在 60% 左右经择时风控后最大回撤压到 9% 附近夏普率约 0.9这个成绩不算惊艳但作为一套可复现的教学级实现参考价值很实在。2. 因子体系与单因子测试先把「哪些因子能用」这件事说清楚2.1 因子分类与资源里的因子图谱这份资源把因子按经济含义分成了几大类从文件名就能看出来情绪类、质量类、基础类、价值类、行业分析师类、每股指标类、特色技术指标类。这个分类方式跟华泰多因子系列报告里的框架基本一致属于业内比较通用的做法。情绪类因子通常包括换手率、成交量变化、融资融券余额变化等反映市场参与者的行为质量类因子看的是 ROE、毛利率、资产负债率这些基本面质量指标价值类因子就是 PE、PB、PS 及其分位数每股指标类包括 EPS、每股净资产、每股现金流行业分析师类则是一致预期、评级调整、目标价变动这些卖方数据。基础类因子一般是市值、行业哑变量这类需要中性化的变量。资源里find_factor.py和get_factor_report.py这两个脚本一个负责从原始数据里提取因子一个负责生成因子报告。实际使用时因子数据来源通常是 Wind、聚源或者 Tushare资源本身没有绑定具体数据源你需要根据自己的数据环境做适配。提示因子分类不是越细越好。分类的目的是方便做中性化和归因如果某个因子你自己都说不清它赚的是什么钱那它在模型里大概率是噪音。2.2 单因子测试的完整流程与代码实现单因子测试是整套流程里最容易被跳过、但最不能跳过的一步。资源里single_factor_test.py和run_test.bat配合使用思路是定义待测因子列表对每个因子跑一次分层回测保留回测报告提取字段存成 CSV再做可视化最后筛选出表现最好的因子。下面是一个单因子测试的核心逻辑示例我按资源里的思路整理成可运行的 Python 代码import pandas as pd import numpy as np from scipy import stats def single_factor_test(factor_df, return_df, n_groups5): factor_df: 因子值index为日期columns为股票代码 return_df: 下期收益率格式同上 n_groups: 分层数量默认5层 ic_series [] group_returns {i: [] for i in range(n_groups)} for date in factor_df.index: factor factor_df.loc[date].dropna() ret return_df.loc[date].reindex(factor.index).dropna() common factor.index.intersection(ret.index) if len(common) 50: continue factor factor[common] ret ret[common] # 计算Rank IC ic, _ stats.spearmanr(factor, ret) ic_series.append(ic) # 分层回测 labels pd.qcut(factor, n_groups, labelsFalse, duplicatesdrop) for g in range(n_groups): group_ret ret[labels g].mean() group_returns[g].append(group_ret) ic_series pd.Series(ic_series) result { IC_mean: ic_series.mean(), IC_std: ic_series.std(), ICIR: ic_series.mean() / ic_series.std(), IC_win_rate: (ic_series 0).mean() } group_df pd.DataFrame(group_returns) result[long_short] (group_df[n_groups-1] - group_df[0]).mean() return result, group_df这段代码的逻辑说明对每个交易日先取当日因子值和下期收益率的交集样本量少于 50 个就跳过避免小样本噪音。然后用 Spearman 秩相关系数算 Rank IC比 Pearson 更稳健因为因子和收益的关系往往不是线性的。分层回测用pd.qcut按因子值分 5 组看多空组合的收益差。最后输出的 ICIR 是 IC 均值除以 IC 标准差衡量因子稳定性的核心指标。参数方面n_groups一般取 5 或 10取 5 时每组样本更多、统计更稳取 10 时单调性看得更清楚。样本量阈值 50 是个经验值A 股全市场股票多的时候可以调到 100 以上。IC 的评估标准IC 均值绝对值大于 0.03 算及格大于 0.05 算不错ICIR 大于 0.5 说明因子比较稳定。2.3 因子共线性分析与最终因子集确定单因子测试筛完之后不能直接把所有通过的因子扔进模型。因子之间往往高度相关比如 PE 和 PB 在很多时候同向变动换手率和成交量变化也高度相关。共线性会导致线性模型系数不稳定树模型虽然对共线性不敏感但冗余因子会稀释重要因子的权重。常见做法是计算因子间的相关系数矩阵设定一个阈值比如 0.7对相关性超过阈值的因子对保留 ICIR 更高的那个。资源里factor_analysis.py应该就是做这件事的。更严谨的做法是用方差膨胀因子VIF或者主成分分析但工程上相关系数矩阵加贪心筛选已经够用。def remove_collinear(factor_dict, icir_dict, threshold0.7): factor_dict: {因子名: 因子值DataFrame} icir_dict: {因子名: ICIR值} threshold: 相关系数阈值 factors sorted(icir_dict.items(), keylambda x: abs(x[1]), reverseTrue) selected [] for name, _ in factors: keep True for sel_name in selected: corr factor_dict[name].corrwith( factor_dict[sel_name], axis1 ).mean() if abs(corr) threshold: keep False break if keep: selected.append(name) return selected逻辑是按 ICIR 绝对值从高到低排序依次判断当前因子与已选因子的平均截面相关系数超过阈值就丢弃。这样保证留下来的因子既有预测力又彼此独立。阈值 0.7 是常用起点因子特别多时可以降到 0.5因子少时可以放宽到 0.8。3. 机器学习模型选型与回测从 baseline 到最优模型3.1 特征标签构建与等权重 baseline资源里same_weight_model.py是等权重线性模型作为 baseline 存在。别小看这个 baseline它的作用是给你一个下限参考如果复杂模型跑不过等权重说明特征或标签构建有问题不是模型不够强。特征构建的核心是把因子值做标准化和中性化。标准化一般用截面 z-score中性化是剔除市值和行业的影响。标签构建通常是下期收益率但要注意几个细节收益率要复权停牌和涨跌停的样本要处理预测周期要和调仓频率匹配。def build_features(factor_df, industry_df, mktcap_df): 截面标准化 行业市值中性化 zscore factor_df.sub(factor_df.mean(axis1), axis0).div( factor_df.std(axis1), axis0 ) # 行业中性化减去行业均值 neutralized zscore.copy() for date in zscore.index: for ind in industry_df.loc[date].unique(): mask industry_df.loc[date] ind neutralized.loc[date, mask] - zscore.loc[date, mask].mean() return neutralized这段代码先做截面 z-score再按行业分组减去行业均值。市值中性化通常用回归取残差的方式这里为了简洁用分组均值替代。实际使用时行业分类建议用中信一级或申万一级市值用流通市值对数。3.2 多模型对比SVR、LSTM、XGBoost、随机森林、AdaBoost资源里提供了svm.py、lstm.py、xgb_model.py、random_forest_reg.py、adaboost_model.py、MLP.py、GBDT.py、multi_factor_lr.py基本覆盖了主流的机器学习模型。每个脚本的结构类似读数据、构建特征标签、切分训练测试集、训练模型、预测、回测。以随机森林为例资源里RF_line3.py应该是表现最好的那个版本。随机森林在这个场景下的优势是对特征尺度不敏感、能处理非线性关系、不容易过拟合相比单棵决策树、特征重要性可以直接输出。XGBoost 和 GBDT 表现通常接近但调参更敏感。LSTM 适合处理时序信息但股票数据的信噪比太低LSTM 容易过拟合需要很强的正则化。SVR 在小样本上表现不错但全市场几千只股票的数据量下训练速度是瓶颈。from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import TimeSeriesSplit def train_rf(X, y, n_estimators200, max_depth8, min_samples_leaf50): X: 特征矩阵 (n_samples, n_features) y: 标签向量 model RandomForestRegressor( n_estimatorsn_estimators, max_depthmax_depth, min_samples_leafmin_samples_leaf, n_jobs-1, random_state42 ) tscv TimeSeriesSplit(n_splits5) scores [] for train_idx, val_idx in tscv.split(X): model.fit(X[train_idx], y[train_idx]) score model.score(X[val_idx], y[val_idx]) scores.append(score) model.fit(X, y) return model, scores参数说明n_estimators取 200 是精度和速度的平衡点再往上收益递减max_depth控制在 8 以内防止过拟合股票数据噪音大树太深必然过拟合min_samples_leaf设 50 以上保证每个叶子节点有足够样本预测更稳定。TimeSeriesSplit是时序交叉验证不能用普通的 KFold否则会用未来数据预测过去这是量化里最常见的翻车点之一。3.3 交易逻辑与回测结果记录模型输出的是预测收益率要转化成交易信号还需要一套逻辑。常见做法是每期选预测收益率最高的 N 只股票等权重买入持有到下一调仓期。资源里time_roll_model.py应该是做滚动训练的run_test.bat负责批量执行。回测结果记录要包含每期持仓、每期收益、累计净值、最大回撤、夏普率、换手率。这些指标里夏普率和最大回撤是最重要的两个。资源里提到经择时策略后最大回撤控制在 9% 左右说明原始模型回撤更大择时模块起了作用。择时逻辑通常是基于市场状态判断比如均线、波动率、成交量等在熊市时降低仓位或空仓。注意回测里最容易自欺欺人的地方是交易成本。印花税、佣金、冲击成本加起来高频调仓的策略收益会被吃掉一大截。资源里没有明确提成本设置你自己复现时一定要加上双边千分之三是个保守起点。4. 避坑与排查这套代码跑不起来时先看这几条4.1 数据对齐报错或大量 NaN现象运行single_factor_test.py或模型脚本时报ValueError: operands could not be broadcast或者结果里大量 NaN。原因因子数据和收益率数据的股票代码、日期索引不一致。常见情况是因子数据用 Wind 代码如 600000.SH收益率数据用 Tushare 代码如 600000或者日期一个是字符串一个是 datetime。解决统一代码格式和日期类型。用pd.to_datetime转日期用.str.zfill(6)补全代码再取交集。资源里没有绑定数据源这一步必须自己适配。4.2 模型训练集测试集切分用了随机切分现象模型在测试集上 IC 很高但实盘或滚动回测表现差很多。原因用了train_test_split默认的随机切分导致未来数据泄露到训练集。股票数据是时序的随机切分等于作弊。解决用TimeSeriesSplit或者手动按时间切分训练集永远在测试集之前。资源里time_roll_model.py就是做滚动训练的参考它的切分逻辑。4.3 因子标准化用了全样本统计量现象回测结果虚高但实盘表现不稳定。原因标准化时用了全样本的均值和标准差包含了未来信息。正确做法是每个截面日单独算均值和标准差或者用扩展窗口。解决把factor_df.sub(factor_df.mean(axis1), axis0)这种截面操作放在循环里确保每个日期只用当天及之前的数据。资源里factor_analysis.py如果做了全样本标准化需要改掉。4.4 LSTM 训练不收敛或过拟合严重现象lstm.py跑出来 loss 不下降或者训练集 loss 很低但验证集 loss 很高。原因股票数据信噪比极低LSTM 参数量大很容易记住噪音。另外序列长度、学习率、batch size 设置不当也会导致不收敛。解决大幅增加 dropout0.3 到 0.5减小隐藏层维度32 或 64用早停early stopping序列长度不要超过 20 个交易日。如果还是不行说明这个场景下 LSTM 本身就不合适换回树模型。4.5 回测净值曲线过于平滑现象累计收益曲线几乎直线向上回撤极小。原因大概率是用了未来函数比如用当日收盘价买入、用当日收盘价计算收益或者因子计算里用了未来数据。解决买入价格用次日开盘价或次日 VWAP因子计算严格只用历史数据。把回测逻辑从头到尾检查一遍重点看shift的方向有没有搞反。5. 进阶技巧用特征重要性和分层回测验证模型是否真的学到了东西模型跑通只是第一步更关键的是判断它到底学到了什么。我一般会做两件事看特征重要性做预测值分层回测。随机森林和 XGBoost 都能直接输出特征重要性。如果排名靠前的因子跟你单因子测试筛出来的因子对不上说明模型可能在拟合噪音。比如单因子测试里 ROE 的 ICIR 最高但模型里换手率的 importance 排第一那就得警惕了。import matplotlib.pyplot as plt def plot_importance(model, feature_names, top_n20): importance pd.Series( model.feature_importances_, indexfeature_names ).sort_values(ascendingFalse).head(top_n) importance.plot(kindbarh, figsize(10, 8)) plt.gca().invert_yaxis() plt.title(Feature Importance Top {}.format(top_n)) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)预测值分层回测更直接把模型预测的收益率按大小分 5 组看第 5 组和第 1 组的实际收益差。如果单调性良好说明模型有区分能力如果各组收益杂乱无章模型就是无效的。验证项合格标准不合格时的排查方向IC 均值绝对值 0.03检查因子方向、标签对齐ICIR 0.5检查因子稳定性、样本期分层单调性第1组到第5组递增或递减检查因子中性化、异常值处理特征重要性与单因子测试结果一致检查数据泄露、特征构建换手率与调仓频率匹配检查交易逻辑、信号平滑还有一个容易被忽略的点训练集和测试集的 IC 衰减。如果训练集 IC 是 0.08测试集掉到 0.01说明过拟合严重需要简化模型或增加正则化。如果训练集和测试集 IC 都在 0.03 左右说明模型泛化能力还行可以考虑上线模拟盘。从那以后我每次跑完模型都强制走一遍特征重要性和分层回测不看这两个结果回测净值再好看我也不敢信。希望帮到你。本文还有配套的精品资源点击获取