简介:本资源为基于机器学习方法构建多因子选股模型的完整项目源码与文档,面向计算机、人工智能及金融工程方向的学生与量化爱好者,可用于课程设计、毕业设计或量化策略入门实践。包内共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_groups=5): """ 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, labels=False, duplicates='drop') 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, threshold=0.7): """ factor_dict: {因子名: 因子值DataFrame} icir_dict: {因子名: ICIR值} threshold: 相关系数阈值 """ factors = sorted(icir_dict.items(), key=lambda x: abs(x[1]), reverse=True) selected = [] for name, _ in factors: keep = True for sel_name in selected: corr = factor_dict[name].corrwith( factor_dict[sel_name], axis=1 ).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(axis=1), axis=0).div( factor_df.std(axis=1), axis=0 ) # 行业中性化:减去行业均值 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_estimators=200, max_depth=8, min_samples_leaf=50): """ X: 特征矩阵 (n_samples, n_features) y: 标签向量 """ model = RandomForestRegressor( n_estimators=n_estimators, max_depth=max_depth, min_samples_leaf=min_samples_leaf, n_jobs=-1, random_state=42 ) tscv = TimeSeriesSplit(n_splits=5) 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(axis=1), axis=0)这种截面操作放在循环里,确保每个日期只用当天及之前的数据。资源里factor_analysis.py如果做了全样本标准化,需要改掉。
4.4 LSTM 训练不收敛或过拟合严重
现象:lstm.py跑出来 loss 不下降,或者训练集 loss 很低但验证集 loss 很高。
原因:股票数据信噪比极低,LSTM 参数量大,很容易记住噪音。另外序列长度、学习率、batch size 设置不当也会导致不收敛。
解决:大幅增加 dropout(0.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_n=20): importance = pd.Series( model.feature_importances_, index=feature_names ).sort_values(ascending=False).head(top_n) importance.plot(kind='barh', figsize=(10, 8)) plt.gca().invert_yaxis() plt.title('Feature Importance Top {}'.format(top_n)) plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)预测值分层回测更直接:把模型预测的收益率按大小分 5 组,看第 5 组和第 1 组的实际收益差。如果单调性良好,说明模型有区分能力;如果各组收益杂乱无章,模型就是无效的。
| 验证项 | 合格标准 | 不合格时的排查方向 |
|---|---|---|
| IC 均值 | 绝对值 > 0.03 | 检查因子方向、标签对齐 |
| ICIR | > 0.5 | 检查因子稳定性、样本期 |
| 分层单调性 | 第1组到第5组递增或递减 | 检查因子中性化、异常值处理 |
| 特征重要性 | 与单因子测试结果一致 | 检查数据泄露、特征构建 |
| 换手率 | 与调仓频率匹配 | 检查交易逻辑、信号平滑 |
还有一个容易被忽略的点:训练集和测试集的 IC 衰减。如果训练集 IC 是 0.08,测试集掉到 0.01,说明过拟合严重,需要简化模型或增加正则化。如果训练集和测试集 IC 都在 0.03 左右,说明模型泛化能力还行,可以考虑上线模拟盘。
从那以后我每次跑完模型,都强制走一遍特征重要性和分层回测,不看这两个结果,回测净值再好看我也不敢信。希望帮到你。
本文还有配套的精品资源,点击获取