机器学习多因子选股实战:因子筛选、模型回测与避坑指南
发布时间:2026/10/11 22:02:19 锦皓数字建站

简介基于机器学习方法构建多因子选股模型的完整实战项目面向量化金融、金融工程及人工智能相关专业的学生与研究者可作为毕业设计或课程设计的参考实现。项目围绕单因子测试与机器学习回测两条主线涵盖因子筛选、共线性分析、特征与标签构建并实现等权重线性模型及SVR、LSTM、XGBoost、随机森林、Adaboost等多种模型的源码最优随机森林模型在择时策略控制下实现约60%累计收益、最大回撤约9%、夏普比率约0.9。压缩包共38个文件含15个Python脚本、10个PDF研究报告、7个图表及Jupyter Notebook等整体约14.71MB文档与代码分层清晰便于快速定位与复现。目前已有467人学习下载适合希望深入理解多因子选股流程并获取可运行源码的读者。1. 机器学习多因子选股回测能到 60% 收益的模型链路长什么样如果你手工给二十来个因子配过权重大概率经历过这种尴尬训练集上收益曲线漂亮一换样本就现原形。最近拆了一套基于机器学习方法构建多因子选股模型的完整工程它把问题拆成两层——先用单因子测试把七大类因子情绪、质量、基础、价值、分析师预期、每股指标、特色技术指标筛一遍再做共线性分析拿到最终因子组合然后交给机器学习模型SVR、LSTM、XGBoost、随机森林、Adaboost 六套模型轮番回测最终随机森林跑出累计收益 60% 左右叠加择时控制后最大回撤压到 9%夏普率 0.9。这套工程是过了答辩的毕设级作品适合正在做量化方向课题、想从打分选股切到机器学习选股、或者单纯想对照一份独立完整代码做复盘的人。2. 单因子测试流程从定义因子列表到共线性筛选的完整闭环2.1 待测因子列表与 runtest 的执行方式单因子测试是整个工程的地基。它的目标是回答一个问题:在同一个回测框架下每个因子单独跑一遍选股到底有没有稳定的预测能力。这个工程把因子分成七类情绪类、质量类、基础类、价值类、行业分析师类、每股指标类、特色技术指标类。分类的动作不是随便分着好看而是为了在后续共线性分析时有一个天然的分组参照——同一类里的因子相关性天然高跨类的因子才更有可能同时进入最终模型。工程里有两条脚本线需要先分清楚find_factor.py 负责从原始行情和财务数据里批量加工因子输出因子值面板single_factor_test.py 负责把因子面板喂进回测引擎一个因子一个任务地跑。两条线分开的好处是数据加工只做一次后面做实验可以反复换参数而不重新算因子。常见的做法是在 single_factor_test.py 里维护一个待测因子列表循环对每个因子执行一次 runtest。核心结构是这样的# single_factor_test.py 核心循环示意 import pandas as pd factor_list [ PE, EP, ROE, SALES_GROWTH, MOM_20, MACD_DIFF, REV_ANALYST, EMOTION_INDEX ] report_dict {} for factor_name in factor_list: report run_test( factor_namefactor_name, start_date2016-01-01, end_date2021-12-31, rebalance_days5 ) report_dict[factor_name] report这里要重点留意两个参数rebalance_days 控制调仓频率基本面因子用 5 日或 10 日调仓都可以量价类因子建议 5 日。回测区间尽量覆盖牛熊两段如果只有单边上涨行情单因子测试的结论会非常乐观到了下跌段就废了。关于单因子分析官方解题指引里有一句提醒很实在并不一定要实现一个完整的回测框架才能解决单因子分析的问题。实际上手动计算因子 IC 和分组收益就能完成 80% 的单因子筛选工作完整回测框架是锦上添花。这套工程是两条路线都给了——纯因子层面有 data_exploration.ipynb 做探索性分析完整回测层面有 run_test 可以跑入门的人可以先从 IPython Notebook 上手。2.2 回测报告字段提取与 CSV 落地单因子回测跑完接下来的动作是保留回测报告、提取字段、存成 CSV。这一步由 get_factor_report.py 完成。# get_factor_report.py 摘要示意 import csv summary_rows [] for factor_name, report in report_dict.items(): row { factor: factor_name, total_return: report.total_return, annual_return: report.annual_return, max_drawdown: report.max_drawdown, sharpe: report.sharpe_ratio, ic_mean: report.ic_mean, icir: report.ic_ir, turnover: report.annual_turnover, } summary_rows.append(row) with open(factor_report.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(summary_rows[0].keys())) writer.writeheader() writer.writerows(summary_rows)这里有一个细节是编码必须写成 utf-8-sig否则 CSV 用 Excel 打开时中文列名会乱码这是很多第一次跑的人会踩的坑。字段里我建议至少保留 ic_mean 和 icir 两个它们比收益更能说明因子稳定性。IC 是因子值与未来收益的截面相关系数ICIR 是 IC 均值除以标准差用于衡量因子预测力的稳定性选因子时优先看这两个指标而不是累计收益。2.3 筛选最优因子与共线性分析拿到 CSV 之后筛选逻辑分两步。第一步是单因子指标筛选看 IC 均值和 ICIR。我的经验标准是 IC 绝对值稳定在 0.03 以上、ICIR 超过 0.5 才算合格因子低于这个水平的基本上就是噪声。结果可视化在 data_exploration.ipynb 里有现成代码IC 时序折线图和分组收益柱状图是两份必看输出——折线图看因子在不同市场环境下的稳定性柱状图看因子在多头组和空头组上是否有单调性。第二步是共线性分析。这一步是很多人容易翻车的地方——同一类因子之间高度相关比如 PE 和 EP 本质上是同一个信息的正反两面如果两个都进模型会让权重在两者之间随机漂移。常用做法是先算相关系数矩阵再看方差膨胀因子VIF。# factor_analysis.py 共线性筛选示意 import pandas as pd from statsmodels.stats.outliers_influence import variance_inflation_factor factor_matrix selected_factors[factor_list].values vif_data pd.DataFrame({ factor: factor_list, vif: [variance_inflation_factor(factor_matrix, i) for i in range(factor_matrix.shape[1])] }) print(vif_data[vif_data.vif 5])VIF 超过 5 说明该因子可以被其他因子线性组合出来保留它只会增加冗余。处理方式是删一个再重算一轮循环迭代到全部低于 5 为止。注意不要一次删太多每次只删 VIF 最高的那个否则可能把一对互相解释的因子全删光反而带进更多噪声。这部分在 4.3 里我会单独展开说。把单因子测试这条线走完得到一份「哪些因子能入选」的清单才轮到机器学习模型上场。3. 机器学习模型回测从等权重 baseline 到随机森林冠军模型3.1 特征与标签构建截面回归的基本形态机器学习模型和传统打分的本质区别在于——打分权重是人拍的模型权重是数据学出来的。但模型的信号源头还是上一步筛出来的因子所以特征与标签的构建决定了模型的上限。特征就是因子面板但直接喂给模型会出问题原因有三个因子分布有厚尾个别极端值会主导树的切分点因子量纲不同对依赖距离的模型SVR不友好缺失值会让部分模型直接罢工。因此一般要做三步处理缺失值填充、极值去尾、标准化。如果还要更严谨可以按行业和市值做中性化把行业偏好和市值偏好先从因子值里剥离。标签的定义方式直接决定模型学什么。常见做法是用未来 N 日收益做标签N 和调仓周期对齐5 日调仓就用未来 5 日收益。# 特征与标签构建示意 factor_panel[label] ( factor_panel.groupby(code)[close].shift(-5) / factor_panel[close] - 1 ) for col in factor_cols: lo, hi factor_panel[col].quantile([0.01, 0.99]) factor_panel[col] factor_panel[col].clip(lo, hi) factor_panel[col] ( factor_panel[col] - factor_panel[col].mean() ) / factor_panel[col].std()标签这块的 shift(-5) 是整条链路里最容易出错的地方。如果不做 shift模型训练的样本里同时含有了 T 日和 T5 日的价格信息相当于把未来答案抄在了考卷上回测时模型预测得神准实盘立刻现原形。标准化同样有讲究——均值和标准差必须在训练集上算出来再应用到验证集不能在全量数据上一把缩尾否则验证集的分布信息混进了训练过程。3.2 等权重线性模型baseline 的真正用法工程里 same_weight_model.py 是等权重线性模型multi_factor_lr.py 是线性回归。这两个是 baseline不是最终方案。baseline 的价值不是拿来拿收益而是给所有复杂模型定一把尺子。如果某个复杂模型在验证集上连线性回归都跑不过说明问题的信号强度根本撑不起非线性假设——这时候与其换更复杂的模型不如回去补因子。等权重模型的逻辑很朴素因子标准化之后直接等权相加得到一个综合分按综合分排名选股# same_weight_model.py 核心逻辑示意 factor_panel[score] ( factor_panel[factor_cols].sum(axis1) .groupby(factor_panel[date]).rank(pctTrue) ) top_stocks factor_panel[factor_panel[score] 0.8]等权重模型的效果在多数因子上不会差因为它天然分散风险、不会过度依赖某一个因子。但它的天花板也很明显——如果某些因子的有效性强于其他因子等权分配会让强因子被弱因子稀释。线性回归能解决权重分配问题但回归出来的负权重因子很容易被解释成反向因子反而给后续逻辑添乱。3.3 从 SVR、LSTM 到随机森林六套模型对比与冠军参数工程内提供了六套可跑的模型脚本svm.py、MLP.py、lstm.py、xgb_model.py、random_forest_reg.py、adaboost_model.py加上 baseline 一共八条线。从最终回测结果看冠军模型是随机森林。这个结果符合我对这类中低信噪比选股数据的判断——随机森林对异常值和缺失值容忍度高不需要像 SVR 那样精细调核函数参数也不像 LSTM 那样依赖大样本量。六套模型在同一份因子面板上的表现差异主要来自对噪声和特征交互的建模方式模型对噪声的容忍度非线性建模能力典型问题SVR低需精细预处理依赖核函数参数敏感调参成本高LSTM低强但需要足够样本样本量不够时容易过拟合XGBoost中强学习率与树深度需要仔细配合随机森林高中树过深时过拟合Adaboost中中对异常样本权重放大明显MLP低强对数据标准化要求最严格随机森林最终占优的原因一是因子面板里缺失值较多的财务类因子也能被树模型用好二是 max_features 的随机性天然抑制了因子间相关性带来的权重漂移。工程里 random_forest_reg.py 的关键参数集中在几个点# random_forest_reg.py 参数示意 from sklearn.ensemble import RandomForestRegressor model RandomForestRegressor( n_estimators300, max_depth6, min_samples_leaf50, max_features0.3, random_state42, )参数各有讲究n_estimators 从 100 加到 300 收益会小幅提升再加就不涨了纯耗时间max_depth 限制在 6 是最关键的防过拟合手段树一旦深过这个值模型就开始记录训练集噪声min_samples_leaf50 保证每片叶子覆盖足够多样本这个参数往往比限制深度更稳max_features0.3 让每棵树只看到 30% 的特征增加树间差异性对相关因子共存的情况很有效。3.4 交易逻辑与回测结果记录的工业化模型训练完后回测阶段有两件事最容易做漂移换仓日和选股逻辑。交易逻辑要在代码里固定下来——每 5 个交易日调仓取模型预测收益最高的前 20% 的股票等权买入持有到下个换仓日。这套逻辑要和单因子测试保持一致不能一个用 5 日、一个用 10 日否则后面做模型对比时对不齐。择时策略在工程里不是预测涨跌而是根据市场状态调整仓位上限。常见实现是计算最近 20 日收益波动率波动率高于阈值时把目标仓位从 100% 降到 50%这种控制方式对 9% 回撤目标的达成起了主要作用。回测结果记录我一般会输出四个指标累计收益、年化收益、最大回撤、夏普率工程里 run_test.bat 一键把整套流程串起来输出按模型名分文件保存方便横向对比。注意最大回撤 9% 是经择时策略风险控制后的数字不是裸模型跑出来的看结果时很容易误读。4. 回测避坑指南前视偏差、过拟合与数据泄漏的排查记录4.1 回测收益 80%实盘一上就亏前视偏差现象回测曲线完美得不像话累计收益比最优模型还高但把同样的信号拿到后续时间段验证收益明显缩水甚至转为亏损。原因最常见的是标签和特征之间混进了未来数据。典型例子是用当日收盘价作为信号生成价但信号其实在收盘后才生成现实中只能以次日开盘价成交再或者对全量数据做标准化历史样本偷看了未来分布。解决把成交价统一改成 T1 开盘价模拟特征与标签在时间轴上严格错位。构造标签时的 shift(-N) 要和特征截断时间核对一遍所有特征都必须是 T 日及之前能拿到的信息。我在回测引擎里通常会加一个 mask把未来数据挡在训练集之外。4.2 训练集收益 50%验证集只剩 15%过拟合现象同一组参数下训练集累计收益接近 50%验证集只有 15%两组数据之间的差距非常稳定。原因树模型对训练集的拟合能力远强于线性模型。max_depth 未限制、min_samples_leaf 过小或者标签里存在异常收益样本都会让模型把个案模式当普遍规律学进去。解决把 max_depth 压到 6 左右把 min_samples_leaf 提高到 30 以上同时用时间序列切分而不是随机切分做交叉验证。调参的目标不是追求训练集最高收益而是追求验证集最高且训练集与验证集差距最小的那组参数。4.3 删完共线性因子收益反降共线性处理过头现象按 VIF 把高相关因子删干净后模型在验证集上的收益不升反降。原因VIF 高不代表因子无效。PE 和 EP 高度相关但 EP 的单因子预测能力可能远强于 PE——把它删了等于把一个强信号换成了弱信号。共线性处理的目的是去冗余不是把所有相关因子都干掉。解决先按单因子 IC 把每个相关性簇里最强的因子找出来保留最强的那一个再做 VIF 检查。VIF 和 IC 要结合着看单看任何一边都会犯错。4.4 年换手 30 倍还能赚钱交易成本被低估现象回测报告显示年换手率极高收益依然是正的但用双边千分之二的手续费加滑点重算之后收益直接腰斩。原因回测代码里手续费参数设得太低或者干脆设成了 0。模型选出的股票组合频繁换仓每次换仓都在创造交易成本只是回测里看不到。解决按双边 0.1% 手续费加 0.1% 滑点估算成本把 cost 参数集中放在配置里而不是散落在回测代码各处。每次调仓时按换手率乘成本扣减净值再评估模型真正赚钱的能力。4.5 数据一更新回测结果对不上因子面板缓存过期现象用最新行情重新跑同一个脚本回测结果和上次记录相比差异巨大连因子排名都变了。原因因子面板是提前生成后落盘的数据更新后没有重新执行因子生成脚本老面板和新行情混用模型输入的因子值时间戳是旧的。解决数据更新后强制先跑一遍 find_factor.py 重新生成因子面板确认面板文件时间戳是新的再依次跑单因子测试和模型回测。我把这个顺序固化成了脚本每次更新数据都走一遍不再手动记。提示这类「数据更新 → 因子生成 → 单因子测试 → 模型回测」的链路一旦手动跑就必然会漏节点最好全部串进 run_test.bat 里。5. 进阶验证滚动时间窗口回测与择时风险控制的落地技巧工程里 time_roll_model.py 是整套资源里最值得研究的一个脚本。它把「一次性回测」升级成「滚动验证」——样本按时间切成多个窗口每个窗口内部训练、紧接着的下一个窗口内验证滚动推进模拟的是模型被持续使用的真实过程。# time_roll_model.py 滚动窗口示意 from datetime import timedelta train_windows [ (2016-01-01, 2017-12-31), (2017-01-01, 2018-12-31), (2018-01-01, 2019-12-31), ] for train_start, train_end in train_windows: val_start train_end timedelta(days1) val_end train_end timedelta(days250) # 每个窗口独立训练 独立验证参数不跨窗口复用 model RandomForestRegressor( n_estimators300, max_depth6, min_samples_leaf50 ) model.fit(train_panel[factor_cols], train_panel[label]) pred model.predict(val_panel[factor_cols])评估滚动回测时先看每个窗口的验证收益是否都为正再看收益波动。如果中间某几个窗口变负通常不是模型坏了而是市场风格切换了——这时候需要工程里的择时逻辑出场用市场上涨家数占比或收益波动率控制仓位上限把最大回撤从裸模型的两位数压到 9% 左右。择时的核心不是预测市场涨跌而是识别高风险的仓位区间并主动降仓。我的个人习惯是把调参顺序固定成先定因子再跑 baseline然后调随机森林的树参数最后加择时参数只允许在验证集上调确定一次就冻结不做二次微调。从那以后我每次拿到新的因子面板都强制先跑一遍 time_roll_model 验证再谈累计收益。整套源代码和文档说明在工程包里是配套的按 README 的目录顺序复现即可希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。