1. 从一次因子失效说起:量化研究里最容易被低估的环节
做量化研究的人,大概都经历过这样的时刻:回测曲线漂亮得不像话,年化收益高得让人心跳加速,结果一上模拟盘就原形毕露,实盘更是惨不忍睹。我最早接触量化的时候,也踩过这个坑。当时用XGBoost做了一个选股模型,训练集上的AUC高到0.85,回测夏普比率超过2,我一度以为自己找到了圣杯。结果实盘跑了三个月,净值曲线一路向下,回撤超过了20%。后来复盘才发现,问题出在因子有效性检验这一步被我完全跳过了——我直接把一堆财务指标和量价数据扔进模型,让算法自己去学,却没有验证这些因子在不同市场环境下是否真的稳定有效。
这件事让我明白了一个道理:量化交易的核心竞争力不在于模型有多复杂,而在于你对数据的理解有多深、对因子有效性的验证有多严谨。深度学习也好,XGBoost也罢,它们都只是工具。工具本身不会让你赚钱,真正让你赚钱的是你对市场规律的洞察,以及把这些洞察转化为可验证、可复现的因子体系的能力。
这篇内容适合两类人看:一类是刚入门量化、正在纠结该用XGBoost还是深度学习的新手;另一类是有一定基础、但模型表现始终不稳定的进阶研究者。我会从因子有效性检验讲起,然后聊XGBoost和深度学习在量化场景下的实际取舍,接着谈过拟合这个绕不开的话题,最后分享一些我在实盘验证中积累的经验。整个过程不堆砌公式,尽量用大白话把逻辑讲清楚。
2. 因子有效性检验:别急着喂给模型,先问问因子答不答应
2.1 IC、IR和分层回测:三个最实用的检验工具
很多人拿到因子之后,第一反应是直接扔进模型训练。这个做法的问题在于,你根本不知道这个因子是真的有预测能力,还是仅仅在特定时间段内碰巧和收益率相关。因子有效性检验的目的,就是在建模之前先把那些"滥竽充数"的因子筛掉。
最常用的三个工具是IC、IR和分层回测。IC是信息系数,衡量的是因子值和未来收益率之间的相关性。具体来说,就是每个截面期计算因子值与下一期收益率的相关系数,然后看这个相关系数序列的均值和稳定性。一般来说,IC均值在0.03以上就算有一定预测能力,0.05以上算不错,0.1以上就非常强了。但光看均值不够,还要看IC的稳定性,这就引出了IR——信息比率,等于IC均值除以IC标准差。IR越高,说明因子的预测能力越稳定。我的经验是,IR大于0.5的因子才值得进一步研究。
分层回测则更直观:把股票按因子值从高到低分成若干组,然后看各组未来的收益表现。一个有效的因子,应该呈现出单调的分层收益——比如因子值最高的那组收益最高,最低的那组收益最低,中间组依次递减。如果分层收益杂乱无章,说明这个因子可能没有实际的区分能力。
import pandas as pd import numpy as np from scipy.stats import spearmanr def calculate_ic(factor_df, return_df): """ factor_df: 因子值,index为日期,columns为股票代码 return_df: 下一期收益率,格式同上 """ ic_series = [] for date in factor_df.index: if date in return_df.index: factor_values = factor_df.loc[date] return_values = return_df.loc[date] # 去除缺失值 valid_mask = factor_values.notna() & return_values.notna() if valid_mask.sum() > 30: # 至少30只股票才计算 ic, _ = spearmanr(factor_values[valid_mask], return_values[valid_mask]) ic_series.append(ic) ic_series = pd.Series(ic_series, index=factor_df.index[:len(ic_series)]) ic_mean = ic_series.mean() ic_std = ic_series.std() ir = ic_mean / ic_std if ic_std != 0 else 0 return ic_mean, ir, ic_series这段代码是我自己常用的IC计算模板,核心逻辑就是逐期计算Spearman秩相关系数。用Spearman而不是Pearson,是因为因子值和收益率之间的关系往往不是线性的,秩相关更能捕捉单调关系。另外注意那个valid_mask.sum() > 30的判断,截面股票数太少的时候,算出来的IC没有统计意义,直接跳过。
2.2 因子正交化:当你的因子们开始"串通"
做完单因子检验之后,还有一个容易被忽略的步骤:因子正交化。什么意思呢?假设你手上有20个因子,其中5个都是动量类的,那这5个因子之间相关性很高,模型在训练的时候会过度依赖这个方向的信息,导致对其他维度的信息利用不足。更糟糕的是,如果这些高相关因子恰好在这个时间段失效,那你的模型就会集体失灵。
正交化的做法有很多种,最简单的是对称正交,也叫Schmidt正交化。核心思路就是让每个因子对已有因子做回归,取残差作为新的因子值。这样处理之后,因子之间两两不相关,模型就能更均衡地利用各个维度的信息。
注意:正交化不是万能的。如果你把因子正交得太狠,可能会把因子本身的预测能力也一起消掉。我的做法是,先看因子相关性矩阵,只对相关性超过0.7的因子做正交化,而且正交化之后要重新做一遍IC检验,确保预测能力没有大幅下降。
2.3 因子衰减与换手率:被忽视的成本杀手
因子有效性检验还有一个维度经常被忽略:因子的衰减速度。有些因子今天有效,明天就失效了,这种因子的换手率极高,交易成本会把收益全部吃掉。检验方法很简单:计算因子值和未来1期、2期、3期……收益率的IC,看IC随期数增加而衰减的速度。如果IC在第2期就掉到接近0,说明这个因子只适合做高频交易,对交易成本极其敏感。
我在实际研究中会做一个简单的成本测算:假设单边交易成本是千分之三,因子对应的组合月换手率是200%,那每个月的成本就是1.2%,一年下来就是14%左右的成本。如果你的因子年化超额收益只有10%,那这个因子实际上是在亏钱的。所以,在做因子筛选的时候,一定要把换手率作为一个硬性指标考虑进去。
3. XGBoost还是深度学习:量化选型的现实考量
3.1 XGBoost在量化场景下的天然优势
聊完因子,接下来就是模型选型的问题。XGBoost和深度学习是当前量化领域最常被拿来对比的两类模型。我先说结论:对于大多数个人研究者和中小型团队来说,XGBoost是更务实的选择。原因有三。
第一,量化数据通常是表格数据,而XGBoost在表格数据上的表现往往不输甚至优于深度学习。深度学习擅长的是图像、文本、语音这类非结构化数据,在结构化表格数据上,树模型依然是王者。你去看Kaggle上那些结构化数据的比赛,获奖方案里XGBoost和LightGBM的出现频率远高于神经网络。
第二,XGBoost对特征工程的要求相对宽松。它自带特征选择能力,对缺失值也有很好的处理机制。而深度学习需要你做大量的特征预处理——归一化、编码、填充缺失值,任何一个环节出问题都可能导致模型不收敛。
第三,XGBoost的训练速度快,调参相对直观。你可以在几分钟内跑完一轮实验,快速迭代。深度学习动辄需要GPU,训练一次可能要几个小时,迭代效率完全不在一个量级。
import xgboost as xgb from sklearn.model_selection import RandomizedSearchCV from scipy.stats import uniform, randint # 定义参数搜索空间 param_dist = { 'n_estimators': randint(100, 1000), 'max_depth': randint(3, 10), 'learning_rate': uniform(0.01, 0.3), 'subsample': uniform(0.6, 0.4), 'colsample_bytree': uniform(0.6, 0.4), 'min_child_weight': randint(1, 10), 'gamma': uniform(0, 0.5), 'reg_alpha': uniform(0, 1), 'reg_lambda': uniform(0.5, 2) } # 使用RandomizedSearchCV进行超参数搜索 xgb_model = xgb.XGBRegressor(objective='reg:squarederror', random_state=42, n_jobs=-1) search = RandomizedSearchCV(xgb_model, param_dist, n_iter=50, cv=5, scoring='neg_mean_squared_error', random_state=42, n_jobs=-1) search.fit(X_train, y_train)这段代码展示了XGBoost超参数自动搜索的典型流程。RandomizedSearchCV比GridSearchCV更高效,因为它不是穷举所有组合,而是随机采样。n_iter=50表示采样50组参数,cv=5表示5折交叉验证。这里有个细节:量化数据的时间序列特性意味着你不能用普通的K折交叉验证,必须用时间序列交叉验证,否则会引入未来信息。这一点后面会详细说。
3.2 深度学习在量化中的适用边界
那深度学习在量化里就完全没用吗?也不是。深度学习在以下几种场景下确实有优势:一是处理非结构化数据,比如从新闻、财报文本中提取情绪因子;二是处理高频数据,比如用CNN或LSTM从tick级数据中提取微观结构特征;三是处理多模态数据,比如同时利用量价数据、文本数据和另类数据。
但深度学习的门槛也更高。你需要配置GPU环境,需要处理过拟合问题(深度学习的参数量远大于XGBoost,过拟合风险更高),需要更精细的正则化策略。如果你只是做日频或周频的选股模型,数据量有限,特征维度也不高,那深度学习的优势很难发挥出来。
我个人的做法是:先用XGBoost快速验证因子和特征的有效性,如果发现模型表现已经接近上限,再考虑用深度学习做进一步的提升。不要一上来就上深度学习,那是在给自己找麻烦。
3.3 一个折中方案:特征工程+树模型+神经网络集成
如果你既想利用深度学习的表征能力,又不想承担太高的过拟合风险,可以考虑一个折中方案:用XGBoost做特征筛选和初步预测,然后把XGBoost的叶子节点索引作为新特征,输入到一个浅层神经网络中。这种做法在工业界被称为GBDT+LR或GBDT+NN,本质上是让树模型负责特征交叉,神经网络负责非线性变换。
这个方案的优点是:树模型部分可以快速训练和调参,神经网络部分只需要一个浅层结构(比如2-3层),参数量可控,过拟合风险相对较低。缺点是流程比较复杂,需要维护两套模型。但对于追求极致性能的团队来说,这个方案值得一试。
4. 过拟合:量化研究中最危险的敌人
4.1 过拟合在量化中的三种典型表现
过拟合这个词大家都不陌生,但在量化场景下,它有几种特别隐蔽的表现形式,值得单独拿出来说。
第一种是时间维度上的过拟合。你在2015年到2020年的数据上训练模型,回测表现很好,但2021年之后模型就失效了。这往往是因为模型学到了特定市场环境下的规律,而不是真正稳健的因子逻辑。比如2015年牛市期间,小市值因子表现极好,如果你的模型过度依赖这个因子,那在2017年之后的市场里就会惨败。
第二种是截面维度上的过拟合。你的模型在全市场股票上表现不错,但在某个行业或某个市值区间内表现很差。这说明模型可能过度拟合了某些特定股票的特征,而不是学到了通用的选股逻辑。
第三种是参数维度上的过拟合。你调了上百组超参数,终于找到一组在回测集上表现完美的参数,但换一段数据就不行了。这是最典型的过拟合,也是超参数搜索中最容易踩的坑。
4.2 时间序列交叉验证:别用错验证方法
很多人做量化研究的时候,直接用sklearn的cross_val_score做交叉验证,这是个大坑。普通的K折交叉验证会把数据随机分成K份,然后轮流用其中一份做验证。但在时间序列数据上,这样做会导致未来数据泄露到训练集中,模型在验证集上的表现会严重高估。
正确的做法是使用时间序列交叉验证,比如TimeSeriesSplit。它的逻辑是:把数据按时间顺序分成K份,每次用前i份做训练,第i+1份做验证。这样训练集永远在验证集之前,不会引入未来信息。
from sklearn.model_selection import TimeSeriesSplit import numpy as np # 假设X和y已经按时间排序 tscv = TimeSeriesSplit(n_splits=5, test_size=252) # 每次用252个交易日做验证 for train_idx, val_idx in tscv.split(X): X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx] # 训练模型并评估 model.fit(X_train, y_train) score = model.score(X_val, y_val) print(f"Validation score: {score:.4f}")这里test_size=252表示每次验证集包含252个交易日,大约一年的数据。这个设置很重要,因为量化模型的验证需要足够长的时间窗口才能看出稳定性。如果验证集太短,可能只是运气好或运气差,不能反映模型的真实泛化能力。
4.3 正则化实战:L1、L2和早停的配合使用
正则化是对抗过拟合的核心手段。在XGBoost中,常用的正则化参数有reg_alpha(L1正则)和reg_lambda(L2正则)。L1正则会让部分特征权重变为0,起到特征选择的作用;L2正则会压缩所有特征的权重,防止某个特征权重过大。
在深度学习中,L2正则化(也叫权重衰减)是最常用的手段。在PyTorch中,可以通过优化器的weight_decay参数来实现:
import torch import torch.nn as nn model = nn.Sequential( nn.Linear(100, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, 1) ) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)这里的weight_decay=1e-4就是L2正则化的系数。Dropout(0.3)则是另一种正则化手段,训练时随机丢弃30%的神经元,防止模型过度依赖某些特定神经元。
早停(Early Stopping)也是常用的技巧。在训练过程中监控验证集损失,如果连续N个epoch验证集损失没有下降,就停止训练。XGBoost中可以通过early_stopping_rounds参数实现:
xgb_model = xgb.XGBRegressor( n_estimators=1000, early_stopping_rounds=50, eval_metric='rmse' ) xgb_model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)early_stopping_rounds=50表示如果验证集RMSE连续50轮没有改善,就停止训练。这个参数能有效防止模型在训练集上过度拟合。
5. 从回测到实盘:那些只有踩过才知道的坑
5.1 回测中的未来函数:最隐蔽的杀手
未来函数是回测中最常见也最致命的问题。所谓未来函数,就是在回测的某个时间点,使用了当时还无法获得的信息。比如你用当天的收盘价计算因子,然后用当天的收盘价买入,这就是典型的未来函数——因为你在收盘前根本不知道收盘价是多少。
更隐蔽的未来函数包括:使用财报数据时,没有考虑财报的发布时间。比如某公司2023年一季报是4月28日发布的,但你在4月1日就使用了这份财报的数据,这就是未来函数。正确的做法是,财报数据必须在发布日之后才能使用,通常还要加上1-2天的延迟,确保信息已经被市场消化。
还有一个容易被忽略的点是股票池的幸存者偏差。如果你用当前的成分股做回测,那些已经退市的股票就被排除在外了,这会导致回测收益被高估。正确的做法是使用历史成分股,或者至少把退市股票也纳入回测范围。
5.2 交易成本与滑点:别让利润被悄悄吃掉
很多人在回测的时候不考虑交易成本,或者只考虑一个很低的固定费率。实盘中,交易成本包括佣金、印花税、过户费,还有滑点。滑点是指你的成交价和预期价之间的差异,在市场流动性差的时候,滑点可能非常大。
我的做法是,在回测中设置一个相对保守的交易成本假设。比如单边佣金万分之二点五,印花税千分之一(卖出时收取),滑点按千分之一到千分之三估算。如果你的策略在这个成本假设下还能盈利,那实盘大概率也能盈利。如果回测时就不考虑成本,那实盘基本上就是给券商打工。
提示:对于高频策略,滑点的估算尤其重要。你可以用历史tick数据模拟成交,看看在当时的市场深度下,你的订单能成交多少、成交价是多少。这个模拟越精细,回测结果越可靠。
5.3 实盘监控与模型衰减:上线只是开始
模型上线之后,不是就万事大吉了。你需要持续监控模型的表现,包括每日的IC、IR、换手率、持仓集中度等指标。如果发现IC持续下降,或者换手率异常升高,就要警惕模型可能正在失效。
我一般会设置几个预警阈值:如果滚动20日的IC均值低于0,且持续5个交易日以上,就触发预警;如果滚动20日的IR低于-0.5,就考虑降低仓位或暂停策略。这些阈值不是固定的,需要根据策略的历史表现来调整。
另外,模型衰减是必然的。市场环境在变,因子有效性在变,模型也需要定期重新训练。我的做法是每季度重新训练一次模型,同时保留一个"影子模型"——用最新数据训练的模型,但不实际交易,只是观察它的表现。如果影子模型持续优于线上模型,就考虑切换。
6. 一些零散但实用的经验
6.1 数据质量比模型复杂度重要十倍
我见过太多人把精力花在调模型上,却忽略了数据质量。缺失值处理不当、异常值没有清洗、数据对齐错误,这些问题对模型表现的影响远大于你换一个更复杂的模型。我的建议是,在做任何建模之前,先花时间把数据清洗干净。具体来说:检查缺失值的分布,如果某个因子缺失超过30%,考虑直接剔除;检查异常值,用分位数法或MAD法识别并处理;检查数据对齐,确保因子值和收益率的时间戳是正确的。
6.2 简单模型+好因子 > 复杂模型+烂因子
这个道理说起来简单,但很多人在实践中会忘记。一个线性回归模型,如果输入的是经过严格检验的有效因子,其表现往往优于一个深度神经网络,如果后者输入的是未经筛选的原始数据。所以,与其花时间调模型,不如花时间研究因子。多读研报,多思考市场逻辑,多尝试新的数据源,这些才是量化研究的根本。
6.3 保持敬畏,控制仓位
最后说一点心态上的经验。量化交易不是印钞机,任何策略都有失效的时候。我在实盘中会严格控制单策略的仓位上限,一般不超过总资金的20%。同时,我会同时运行多个低相关性的策略,分散风险。这样即使某个策略失效,整体回撤也可控。
还有一点:不要因为回测表现好就盲目加杠杆。回测和实盘之间的差距,往往比你想象的大。我见过太多人因为回测曲线漂亮就重仓杀入,结果实盘一回撤就慌了手脚,最后割在最低点。量化交易是一场马拉松,活得久比跑得快更重要。
6.4 工具链的搭建:适合自己的才是最好的
关于工具链,我的建议是不要追求大而全。Python生态已经足够成熟,pandas做数据处理,numpy做数值计算,xgboost和pytorch做建模,backtrader或自己写回测框架,这些组合已经能覆盖绝大多数需求。关键是你要对每个工具的原理有基本了解,知道什么时候该用什么工具,而不是盲目跟风。
比如,有些人一上来就用深度学习框架做量化,结果连数据对齐都没搞明白。我的建议是,先用pandas和numpy把数据处理流程跑通,再用sklearn或xgboost做基线模型,等这些都熟练了,再考虑上深度学习。这个学习路径看起来慢,但实际上是最快的。
6.5 关于持续学习:别被热词带偏
量化领域的热词很多,深度学习、强化学习、大模型,每年都有新概念。但你要清楚,这些技术从论文到实盘,中间有很长的路要走。不要因为某个技术很火就盲目投入,先问问自己:它解决了我当前面临的什么问题?我的数据量和计算资源能支撑它吗?如果答案是否定的,那就先把手头的简单模型做好。
我自己的学习习惯是,每季度精读2-3篇高质量的量化研报或论文,重点看它的因子构建逻辑和验证方法,而不是模型结构。因为因子逻辑是可以迁移的,而模型结构往往依赖于特定的数据环境。这个习惯让我在过去几年里积累了不少有效的因子,也避免了很多无效的尝试。
6.6 一个具体的因子构建案例
最后分享一个我自己构建因子的完整流程,供参考。假设我想构建一个"分析师预期修正"因子。第一步,收集分析师盈利预测数据,包括预测值和预测日期。第二步,计算每个股票在过去一个月内分析师预测的上调幅度,具体做法是:对每个分析师,计算其最新预测与上一次预测的差值,然后对所有分析师取平均。第三步,做因子有效性检验,计算IC和IR,看这个因子是否有预测能力。第四步,做分层回测,看因子值最高的组是否显著跑赢最低组。第五步,检查因子与现有因子的相关性,如果相关性过高,考虑正交化。第六步,把因子输入XGBoost模型,看它是否能提升模型表现。这个流程看起来简单,但每一步都需要仔细打磨,任何一个环节出问题都可能导致因子无效。
这个因子我最终没有采用,因为IC均值只有0.02,IR也不到0.3,预测能力太弱。但这个过程让我对分析师数据的特性有了更深的理解,也为后续的因子构建积累了经验。量化研究就是这样,大部分尝试都会失败,但每一次失败都会让你离成功更近一步。