
十多年前我刚接触量化投资的时候身边不少人都在讨论“用机器学习预测股票收益”这件事。当时大家的共识是这东西水很深要么是骗子要么是天才。后来我自己在实验室里泡了大半年把随机森林、XGBoost、LSTM挨个试了一圈慢慢发现一个事实——随机森林可能是普通人上手股票收益预测最合适的起点不是因为它最聪明而是因为它最皮实。这篇主要讲清楚两件事为什么偏偏是随机森林以及怎么把一套完整的流程真正跑通。内容里既包括数据准备、特征构造、模型调参和回测也包括我踩过的各种坑。适合已经有Python基础、想用机器学习做量化入门但还没想好从哪里下手的读者。1. 先用一句话说明白为什么偏偏是随机森林1.1 股票收益预测的三个死结做股票收益预测首先要承认一个让人沮丧的数学事实股票价格的波动中绝大部分是噪声只有极少一部分是真正的“信号”。不少研究指出日频收益里能被可解释因子刻画的部分常常只有几个百分点。换句话说哪怕你用上了全世界最复杂的模型信噪比就是那么低模型再精巧也很难凭空造出信号。除了信噪比低金融数据还有一个让很多传统机器学习算法头疼的特征非平稳。你辛辛苦苦用2018年到2020年的数据训练出来的规律到2021年的市场里可能说变就变——因为市场环境、参与者结构、政策规则都在变。这就导致“训练集效果好、实盘效果差”成为常态。第三个死结是非线性关系。很多人刚开始做量化时会用线性回归去预测股价结果发现因子和未来收益之间的关系经常不是一条直线。比如小市值因子在特定阶段有效换一个阶段又失效低波动因子在震荡市有超额收益在大牛市反而跑输。这种“时灵时不灵”的交互关系如果只靠线性模型去刻画基本等于盲人摸象。1.2 随机森林恰好在这些死结上抓得住随机森林是一种Bagging类的集成学习方法。它训练多棵决策树每棵树都在不同的随机子样本和随机特征子集上生长最后把各棵树的结果平均起来。这个机制天然带来了几个对金融数据极其友好的特性。第一它对非线性关系的拟合能力很强。决策树本身相当于把特征空间切成很多矩形区域每个区域内给一个常数输出。因此它不需要你事先假设收益和因子之间到底是直线还是曲线关系模型自己会去拟合那些弯弯绕绕。第二它对特征尺度完全不敏感。做逻辑回归或者SVM之前你往往得做标准化、归一化随机森林不需要。因为决策树的切分只看特征的排序不关心它的绝对值大小。在金融场景里有些因子是价格类几千块有些是比率类0.01级别有些是换手率百分比尺度天然差异很大。不用做标准化就少了一个预处理环节也少了一个数据泄漏的潜在窗口。第三它自带“抗过拟合”属性。单棵决策树很深的时候很容易过拟合训练集拟合得完美测试集一塌糊涂。随机森林通过行采样每棵树只用部分样本和列采样每棵树只看部分特征把多棵相关性较低的树组合在一起显著降低了过拟合的风险。这也是它“皮实”的最核心原因。一句话总结随机森林不是收益预测里最强的模型但它是你在有限数据、有限时间去探索“因子到底有没有用”时最不容易翻车的工具。2. 数据准备90%的人第一步就错了2.1 先做数据清洗别急着建模我自己见过太多人拿到行情数据直接pip install一个随机森林库就开跑。这样做不是一定不行但大概率会在后续的验证阶段被各种诡异的结果折磨到怀疑人生。金融数据的清洗和其他领域不太一样除了要去重、补缺失值还得处理很多金融特有的问题。首先是股票交易状态。A股里有停牌、ST、涨跌停这些状态直接把这些样本丢进模型会严重干扰学习。最常见的处理方式是过滤掉停牌和ST股票涨跌停的股票由于无法以合理价格成交通常也会在训练时剔除。如果你用的是复权价格还需要确认前后复权的口径是否一致否则会出现“价格跳变”这种看似是信号、实际是数据问题的假象。其次是数据源的口径测试。我一开始用某免费数据源拉历史行情回测做完以为自己找到了“圣杯”结果后来换了另一个数据源复核收益直接腰斩。原因就是不同数据源对除权除息的处理方式不一样导致价格序列对不上。现在我用数据前有一个习惯先拉几只看好的股票的和不复权、前复权、后复权三条价格曲线人工检查有没有莫名其妙的跳空。这一步虽然简单但能帮你挡掉一大半后期验证时的坑。还有一个极容易被忽略的问题财务数据的发布日期和实际可用日期不一致。上市公司的财报披露有滞后年报往往到第二年4月才出如果你直接用“2020年全年的财务数据”去预测2020年下半年的收益相当于用了未来数据这就是典型的前视偏差look-ahead bias。专业一点的解决方案是建立“point-in-time”数据库每一条记录都标注清楚“这个数据在那一天是实际可获取的”。个人做研究时如果拿不到这么完整的数据至少要留一个心眼把财务类因子的数据都往后平移一段足够的时间模拟披露延迟。2.2 特征体系预测收益你要喂什么数据特征工程的本质是回答一个问题你觉得未来收益和哪些变量存在关系顺着这个思路可以把因子分成几大类。我给自己用的一套基线特征体系大致是下面这样的刚开始做研究的读者可以直接照搬先把流程跑通再慢慢扩充。特征类别典型特征计算说明动量/反转过去5日、20日、60日收益率分别捕捉短期反转和中长期动量波动率过去20日收益率标准差低波动异象是一个长期有效因子成交量过去5日、20日平均换手率换手率反映市场关注度和分歧度流动性Amihud非流动性指标用绝对收益除以成交额衡量价格冲击基本面市盈率、市净率、ROE注意披露时滞需要做时点对齐技术指标RSI、MACD、布林带位置虽然传统但作为特征并不冲突你在构造特征时要有一个意识随机森林能自动找到特征之间的交互关系但它无法帮你“无中生有”。如果输入的特征本身和未来收益毫无关系模型再强也没用。所以因子的经济含义很重要——你选这个特征是因为它对应某种市场参与者的行为模式而不是因为它碰巧在回测里有效。另外强烈建议对连续型特征做分位数处理或先做一层去极值。金融数据里极端值非常多“涨了100%的妖股”“闪崩跌了20%的个股”都会让按均值方差计算的统计量面目全非。随机森林虽然基于排序切分对极端值不如线性模型那么敏感但如果你用一个像“市盈率”这种本身带有明显厚尾分布的因子极端值仍然可能干扰树的切分位置。我在实践中通常会先用分位数对连续特征做裁剪比如3倍MAD即中位数绝对偏差在这个范围之外的都压回边界再做标准化——标准化不是必须的但裁剪对稳定因子分布很有帮助。2.3 训练集/验证集/测试集的时间切分大部分初学者会用sklearn的train_test_split随机打乱数据后划分训练测试集。这在一般机器学习任务里没问题但在时序数据上是大忌。因为金融数据有强自相关性相邻天的样本本身就高度相似随机打乱会把“未来”的信息泄漏到“过去”的训练集里导致测试集分数虚高。正确的做法是严格按照时间顺序切分。举个例子用前60%的时间段做训练集接下来的20%做验证集调参用最后20%做测试集最终评估用。而且验证集和测试集之间要留一段“隔离带”通常是留出1到3个月的间隔防止训练集的预测结果间接影响验证集的效果。还有一个更严谨的选择用TimeSeriesSplit做交叉验证。它是K折交叉验证的时序版本每一折的训练集始终在验证集之前。比如5折的TimeSeriesSplit会把全时间序列切成5段第一折用第1段训练、第2段验证第二折用第12段训练、第3段验证依此类推。这样做能最大化利用有限的历史数据同时又保证不泄漏。我调参的时候基本都用TimeSeriesSplit——宁可多花一点计算时间也不愿看到后期实盘绩效和回测绩效对不上。3. 随机森林的运作机制与关键工程细节3.1 决策树、Bagging、随机特征选择随机森林的原理可以用三句话讲清但理解这三句话对后续调参极其重要。第一句话决策树在做“分段函数”。它的每一层都在问一个问题比如“过去20日收益率是否大于5%”根据回答把样本分到左子树或右子树。不断重复这个切分过程直到满足停止条件。最终每一个叶子节点里的样本会被取平均值作为预测值。预测未来收益本质上就是找到历史上“和当前状态最相似”的那一组样本把这组样本的未来收益平均起来作为当前时点的预测。第二句话Bagging就是“装袋”。随机森林对训练数据做有放回抽样每次抽取一部分样本训练一棵树。因为有放回抽样不同树看到的训练集不同这保证了树和树之间有差异性。有研究证明树之间的相关性越低集成的效果越好——这就是为什么随机森林里“随机”二字是灵魂。第三句话随机特征选择。树在每次切分时并不是从所有特征里找最优切分而是从随机选出的一个特征子集里找。这个子集的大小在分类任务中通常是特征总数的平方根在回归任务中通常是特征总数的三分之一到二分之一。这个机制进一步减小了树之间的相关性让不同的树从不同角度去观察数据。你把这些机制放在一起就会明白随机森林其实是在做一件事用成千上万棵“各执一辞”的树进行投票把个别树因过拟合而产生的偏差平均掉最后保留的是数据里相对稳健的模式。这也是它能在脏数据较多的股票场景里仍然维持稳定表现的原因。3.2 参数调优主要调哪些、怎么调随机森林主要参数并不多但每个参数的影响逻辑都不一样。我把每次建模时最常调整的几个参数整理成了一张表照着这个逻辑去调基本上能覆盖90%的场景。参数作用常见范围调参策略n_estimators树的数量100~1000越大越稳定但边际收益递减关注交叉验证曲线拐点max_depth树的最大深度3~20控制模型复杂度容易被忽略但很关键min_samples_split内部节点再划分所需最小样本数10~100值越大模型越保守防过拟合min_samples_leaf叶子节点最少样本数5~50金融数据建议设大一点避免学到极端样本max_features每个节点随机选择的特征数auto/sqrt/log2回归默认用特征数的1/3左右比较合适random_state随机种子固定任意整数固定它保证实验结果可复现有一个经验我建议你直接采纳先把max_depth限制在一个较小的范围比如5到10然后观察训练集和验证集的表现差异。如果训练集分数远高于验证集说明过拟合可以加大min_samples_leaf或者降低max_depth。如果两边分数接近但都很低说明模型容量不足再慢慢加大max_depth。调参的工具用GridSearchCV或者Optuna都行。我个人更推荐Optuna因为股票数据特征多、参数组合空间大网格搜索的枚举效率太低。有一点要注意如果用了TimeSeriesSplit做交叉验证那么调参过程中的评分都是基于时序样本外表现得到的这样选出来的参数比普通K折更接近真实的上线水平。3.3 特征重要性的正确解读随机森林能输出feature_importances_也就是各特征的平均贡献度。很多人喜欢拿这张表去判断“哪个因子最有效”然后只保留头部几个因子重新训练结果发现模型效果反而下降。这不是随机森林出了问题而是你对特征重要性的理解有偏差。特征重要性衡量的是“在当前模型结构下该特征对预测的帮助有多大”。它存在两个坑相关性高的特征会分摊重要性比如过去5日收益率和过去20日收益率高度相关重要性会被拆散重要性低的特征不等于没用可能是它的作用要以另一个特征为条件才能发挥出来。只保留头部特征可能恰恰把这种交互关系砍掉了。我的习惯是特征重要性表只用来做“初步筛查”。“一个特征的重要性是零”通常说明它真的没什么信息量“重要性特别高”说明它值得你花时间去深挖它的经济逻辑但并不意味着你可以抛弃其他特征。最终的特征集选择还是要以交叉验证的综合评分为准而不是只看重要性排序。4. 从训练到回测一个可以跑通的完整流程4.1 代码整体框架设计下面给出一套极简但完整的随机森林收益预测流程包含数据处理、特征构造、滚动训练和预测输出这几个核心模块。代码用Python写依赖pandas、numpy和scikit-learn。设计上刻意保持简洁方便你在此基础上做迭代。import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import TimeSeriesSplit # 假设df包含columns: [date, stock_code, ret_future_5d, factor1, factor2, ...] df df.sort_values([stock_code, date]).reset_index(dropTrue) # 特征列根据你的特征工程定义 feature_cols [c for c in df.columns if c.startswith(factor)] def train_rf_on_window(train_df, val_df, params): X_train train_df[feature_cols].values y_train train_df[ret_future_5d].values X_val val_df[feature_cols].values y_val val_df[ret_future_5d].values model RandomForestRegressor( n_estimatorsparams.get(n_estimators, 300), max_depthparams.get(max_depth, 6), min_samples_leafparams.get(min_samples_leaf, 20), max_featuresparams.get(max_features, sqrt), random_state42, n_jobs-1 ) model.fit(X_train, y_train) val_pred model.predict(X_val) return model, val_pred # 滚动训练每6个月重训一次每次用前24个月数据 train_months 24 val_months 6 step_months 6 pred_list [] model_list [] for start in range(0, df[date].nunique() - train_months - val_months, step_months * 20): # 这里用交易日天数近似表示月份实际可按月末日期处理 train_end start train_months * 20 val_end train_end val_months * 20 train_df df[(df[day_index] start) (df[day_index] train_end)] val_df df[(df[day_index] train_end) (df[day_index] val_end)] if val_df.empty: continue model, val_pred train_rf_on_window(train_df, val_df, params{max_depth: 6}) temp val_df[[date, stock_code, ret_future_5d]].copy() temp[pred] val_pred pred_list.append(temp) model_list.append(model) pred_all pd.concat(pred_list, ignore_indexTrue)这段代码的精髓在于滚动训练。股票市场规律会漂移用一个模型吃遍所有年份是不现实的。每隔6个月用最近24个月的数据重新训练一次既保证模型能跟上市场风格的变化又避免训练数据过短导致参数估计不稳定。你实际使用的时候需要把day_index替换成你的交易日序号列或者直接用具体日期区间来做分割这样更容易控制边界。训练结束后把每个验证窗口的预测结果拼起来就得到了覆盖整个历史回测期的预测序列。下一步要做的就是检验这个预测序列到底有没有信息量。4.2 用回测检验模型真正的含金量模型预测做出来之后直接看预测值和真实收益的相关系数“看着挺高”是远远不够的。我在实践中最常用的三个检验手段分别从不同角度验证模型的真实能力。第一个是IC分析。IC是信息系数Information Coefficient也就是每个截面通常每个交易日上预测值与未来真实收益之间的Spearman秩相关系数。把每天的IC求平均得到平均IC。经验上平均IC超过0.03就可以认为模型有微弱的预测能力超过0.05就算非常优秀的日频信号了。IC的计算要按截面来做千万不要把所有日期的样本混在一起算否则会被市场整体涨跌掩盖掉真正的横截面预测力。第二个是分层回测。在每个截面把股票按照预测值从高到低排序等分成5层或者10层然后分别计算每一层在未来5日的平均收益。理想情况下分层收益应该呈现单调性——预测值最高的那一层收益最高预测值最低的那一层收益最低。如果分层的收益完全是乱的那就说明模型预测能力即便在统计上显著也不具备落地的可操作性。第三个是Top组合回测。只取预测值最高的前10%或前20%股票构成组合按等权或市值加权持有5天每天调仓或每5天调仓。看这个组合的净值曲线相对于基准比如沪深300有没有超额收益回撤是否可控。这一步才是最终检验“能不能用”的关键。我自己有个习惯做分层回测时会把收益画成柱状图同时把每一层的样本量也标出来。很多模型出现“最高层收益最高”的现象仔细一看最高层只有三五只股票样本量太小说明这个结果是运气并非规律。分层收益的显著性要靠样本量和稳健性一起判断单独看某一个指标很容易被误导。5. 我踩过的坑幸存者偏差、前视偏差与过拟合5.1 幸存者偏差你的历史收益被高估了多少幸存者偏差是量化回测里最坑人、也最隐蔽的问题之一。它的意思是你的样本池里只剩下活到今天的股票那些曾经上市但后来退市的股票已经不在你的数据里了。用这些存活下来的股票做回测收益天然会被高估因为你无形中排除了那些“长期亏损到退市”的差学生。我第一次完整跑通回测时年化收益做到了惊人的55%当时一度以为自己发现了稳赚的策略。后来用数据供应商的全量历史股票列表重新做了一遍数据清洗把退市股加回来收益直接从55%掉到21%。这50%以上的收益差距全部来自幸存者偏差。要规避这个问题只能从数据源上解决。一些数据供应商默认只提供当前在市公司的历史行情你必须主动去申请“包括已退市股票”的历史数据。虽然处理这些退市股票的数据很麻烦——它们的停牌、退市整理期都有一堆噪音但这是做量化研究绕不开的一步。如果数据源确实没有退市股票那就应该明确知道回测结果会偏乐观在实盘时预留足够的安全边际。5.2 前视偏差与标签泄漏前视偏差是另一个我吃过亏的地方。举例来说假设你想构造“市盈率”这个因子你拉到了2022年6月末的市盈率数据。问题在于这个市盈率是用截至2022年6月底的股价和利润计算出来的但利润数据可能要到2022年8月才公布。如果你在回测中使用6月底的市盈率来预测7月的收益实际上用了8月才公布的数据这就是典型的前视偏差。我自己的教训是有一天在复核一个基本面因子策略时发现它的收益高得离谱。排查半天才发现数据仓库里的“净利润”字段是更新到最新报告期的它自动包含了我当时并不知道的季度报告信息。修复方式是给所有财务因子加了一个固定的滞后时间A股市场我通常滞后4到6个月这比财报披露的截止日期还要保守一些能最大程度排除前视偏差。标签泄漏则是另一种前视偏差特征里直接包含了你预测目标的信息。比如你要预测未来5日收益但特征里不小心加了一个未来3日成交量的变量模型会被喂“标准答案”训练时准确率高得吓人上线后崩塌得也吓人。每次构造完特征我都建议你检查一遍所有特征的计算是否只用了当前时点及以前的数据。如果哪一天你发现模型在训练集上的R²超过了0.5先别高兴八成是泄漏了。5.3 过拟合的识别与遏制随机森林相对单棵树不容易过拟合但并不是不会过拟合。尤其是在股票数据这种高噪声环境里模型仍然可以靠记住训练集中那些“碰巧有效”的模式来取得漂亮的回测结果但这些模式在样本外完全不成立。我的识别办法比较“笨”但很好用把预测值和真实值的相关系数画在训练集和验证集上如果训练集IC稳定在0.15、验证集IC只有0.01那基本可以断定模型在训练集上已经死记硬背了。这时候需要做的不是加更多特征而是让模型变“粗”把max_depth降低把min_samples_leaf调大甚至减少特征数量。有人可能会觉得这样会损失训练集表现没错但训练集表现本来就不重要样本外的验证集表现才是唯一的裁判。我还发现一个很多人不知道的小技巧对同一份数据多跑几个不同的随机种子看预测的稳定性。如果预测结果对随机种子极其敏感——种子A的IC是0.02种子B的IC是0.07——说明模型在很大程度上依赖运气还远没有到可以上线部署的状态。一个成熟的模型应该在不同随机种子下有相对一致的预测结果。6. 随机森林收益预测的局限性以及后续方向6.1 为什么说它不是印钞机随机森林在股票收益预测上的“天花板”其实很低这一点必须说清楚。它擅长从有限的特征中捕捉相对稳健的非线性关系但它也有明显的短板它本质上是“查历史相似样本”对市场环境的突变几乎没有应对能力。2020年疫情初期全球市场剧烈下跌、2022年风格急剧切换这类“史无前例”的行情里一切基于历史统计的模型都会失效随机森林也不例外。另一个短板是它不能自动处理时间依赖结构。股票收益序列存在明显的波动率聚集效应大波动后面往往跟着大波动随机森林作为横截面模型只能利用当前特征不能像LSTM那样对时间序列的先后依赖关系直接建模。这也是随机森林收益预测做到一定程度后精度会进入瓶颈期的原因。所以我的建议是把随机森林当作“基线模型”和“因子筛选器”不要指望它成为一劳永逸的印钞机。它的最大价值在于帮你快速验证“这些特征到底含有多少未来收益信息”如果随机森林测出来的IC和分层收益都没有可观的信号那么换再复杂的模型也不会有本质改观。6.2 系列下一步可以从哪些方向延伸如果你跑通了上面的流程接下来有三个比较自然的方向可以走第一换更强的梯度提升模型。XGBoost、LightGBM、CatBoost在金融数据上通常能比随机森林多捕获一些信息因为它们按残差逐步优化对数据的拟合更精细。但要注意这些模型比随机森林更容易过拟合需要用更强的正则化来约束。第二加入更多维度的数据。除了行情和基本面可以试试北向资金流、融资融券余额、分析师一致预期、新闻舆情数据等等。随机森林对高维稀疏特征也能处理得不错这给了你很大的特征扩展空间。第三从截面预测转向组合优化。你手里有了每个股票的预测值下一步就是决定每种股票买多少比例。可以用等权、市值加权也可以引入均值方差优化或者风险平价进行组合构建把预测信号和组合构建分开比单纯追求预测准确率更接近实战。顺便预告一下这个系列后续我会围绕LightGBM与特征筛选、组合优化与风险控制单独展开把每一步都在真实数据上做完整验证。这一篇先把随机森林这个基础打牢后面再往上叠加也就不那么虚了。我在整个实践过程中最深的体会是量化研究里模型的复杂度远远不如数据处理的严谨性重要。把数据清洗、特征时点对齐、时序切分、前视偏差检查这几件事做到位哪怕用最简单的随机森林也能得到相当可用的结果。反过来数据一堆问题就算用上最前沿的模型也只会放大错误。每一个刚开始做这个方向的读者我都建议你先把这套流程完整跑通再去追求那些花哨的模型和复杂的因子——你会发现基本功才是决定上限的东西。