随机森林在股票收益预测中的实践:从特征工程到回测避坑
2026/9/16 2:56:30 网站建设 项目流程

十多年前我刚接触量化投资的时候,身边不少人都在讨论“用机器学习预测股票收益”这件事。当时大家的共识是:这东西水很深,要么是骗子,要么是天才。后来我自己在实验室里泡了大半年,把随机森林、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段验证,第二折用第1+2段训练、第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(drop=True) # 特征列:根据你的特征工程定义 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_estimators=params.get('n_estimators', 300), max_depth=params.get('max_depth', 6), min_samples_leaf=params.get('min_samples_leaf', 20), max_features=params.get('max_features', 'sqrt'), random_state=42, 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_index=True)

这段代码的精髓在于滚动训练。股票市场规律会漂移,用一个模型吃遍所有年份是不现实的。每隔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与特征筛选、组合优化与风险控制单独展开,把每一步都在真实数据上做完整验证。这一篇先把随机森林这个基础打牢,后面再往上叠加也就不那么虚了。

我在整个实践过程中最深的体会是:量化研究里,模型的复杂度远远不如数据处理的严谨性重要。把数据清洗、特征时点对齐、时序切分、前视偏差检查这几件事做到位,哪怕用最简单的随机森林,也能得到相当可用的结果。反过来,数据一堆问题,就算用上最前沿的模型也只会放大错误。每一个刚开始做这个方向的读者,我都建议你先把这套流程完整跑通,再去追求那些花哨的模型和复杂的因子——你会发现,基本功才是决定上限的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询