☰
多因子选股入门:从数据清洗到回测的完整知识储备
2026/10/15 18:59:32 网站建设 项目流程

1. 从"拍脑袋"到"讲逻辑":多因子选股到底在解决什么问题

很多人第一次接触量化选股,脑子里想的都是"给我一个公式,告诉我明天买哪只"。这种想法本身没错,但如果你真拿这个思路去做基本面选股,大概率会在前三个月就把信心亏光。原因很简单:单因子选股本质上是在赌一个指标的有效性,而市场环境一变,这个指标就可能彻底失效。

我刚开始做量化的时候,也走过这条路。当时用了一个很朴素的策略:市盈率低于20倍就买入,高于40倍就卖出。回测数据看起来很漂亮,但实盘跑了两个月,收益曲线跟回测完全对不上。后来复盘才发现,那段时间市场风格从价值切换到了成长,低市盈率的票集体趴窝,而高市盈率的成长股反而一路向上。这不是策略写错了,而是单一因子无法覆盖市场的多面性。

多因子选股的核心思路,就是承认"没有哪个指标能永远有效",然后通过同时使用多个维度、相关性较低的因子,让组合在不同市场环境下都有一定的适应能力。打个比方:单因子选股像是只带了一把锤子,看什么都像钉子;多因子选股则是带了一个工具箱,遇到不同的问题能用不同的工具。

具体来说,多因子模型要解决三个层面的问题:

第一层是"选什么"。基本面数据那么多,市盈率、市净率、ROE、营收增速、毛利率、资产负债率、股息率……到底哪些值得纳入?这不是拍脑袋决定的,而是需要从经济学逻辑出发,判断这个指标是否真的能解释股票的未来收益。

第二层是"怎么组合"。选出了五个因子,是等权重打分,还是根据历史表现动态调整权重?因子之间如果高度相关,比如ROE和ROA,同时放进去会不会导致某个维度被过度放大?

第三层是"怎么用"。因子得分算出来之后,是选前10只等权买入,还是选前50只做行业中性化处理?调仓频率是月度、季度还是半年度?这些执行层面的决策,往往比因子本身更能影响最终收益。

这篇文章不会给你一个"稳赚不赔"的因子组合,那种东西不存在。我要做的是把多因子选股前期需要储备的知识点,按照我自己的学习路径和实操经验,系统地梳理一遍。适合已经会写Python基础代码、对pandas有一定了解、但还没真正做过完整多因子策略的读者。如果你连DataFrame的groupby都用不熟练,建议先去补一下pandas的基础操作,否则后面的内容会让你很痛苦。

2. 基本面数据的获取与清洗:脏数据比没数据更可怕

做多因子选股,第一步永远是搞数据。很多人觉得数据获取是最简单的环节,随便找个接口拉下来就行了。但实际情况是,数据清洗占整个策略开发时间的60%以上,而且这一步做不好,后面所有分析都是空中楼阁。

2.1 基本面数据的几个核心来源与各自坑点

目前市面上能获取A股基本面数据的渠道大致分三类:

数据来源类型典型获取方式优点主要坑点
开源财经数据接口各类公开财经API免费、调用方便字段不全、历史数据缺失、频率限制
本地金融数据库购买或学校提供的数据库字段完整、历史长需要SQL查询能力、更新滞后
交易所与官方披露爬取公告或下载文件最权威格式混乱、非结构化、工作量大

我个人的建议是:前期学习阶段用开源接口就够了,但一定要做数据校验。不要拿到数据就直接算因子,先做三件事:

  1. 检查缺失值比例。如果某个字段在30%以上的股票上都是空值,这个因子直接放弃,不要试图用均值填充,那会引入巨大偏差。
  2. 检查时间对齐。财报数据有"报告期"和"公告期"两个时间概念。比如一季报的报告期是3月31日,但公告期可能是4月25日。如果你用报告期来对齐交易数据,就会引入"未来函数"——你在3月31日就知道了4月25日才公布的数据,这在实盘中是不可能实现的。
  3. 检查单位一致性。有些接口的营收单位是"万元",有些是"元",如果不统一,算出来的增速会差四个数量级。

注意:未来函数是多因子回测中最隐蔽的杀手。除了财报公告期的问题,还有停牌股票的复牌处理、成分股调整的时间点等,都需要逐一确认。

2.2 用pandas做数据清洗的实操框架

假设你已经通过某个接口获取了一张包含股票代码、交易日期、市盈率、市净率、ROE等字段的DataFrame,下面是我常用的清洗流程:

import pandas as pd import numpy as np # 假设 df 是原始数据,包含 code, date, pe, pb, roe, revenue_yoy 等字段 # 第一步:统一日期格式并排序 df['date'] = pd.to_datetime(df['date']) df = df.sort_values(['code', 'date']).reset_index(drop=True) # 第二步:处理缺失值——先统计,再决定策略 missing_ratio = df[['pe', 'pb', 'roe', 'revenue_yoy']].isnull().mean() print(missing_ratio) # 如果某个字段缺失超过30%,考虑放弃该因子 # 第三步:异常值处理——用分位数截断而非直接删除 def winsorize(series, lower=0.01, upper=0.99): low = series.quantile(lower) high = series.quantile(upper) return series.clip(low, high) for col in ['pe', 'pb', 'roe', 'revenue_yoy']: df[col] = df.groupby('date')[col].transform(winsorize) # 第四步:检查财报公告期对齐 # 假设 df 中有 report_date 和 announce_date 两个字段 # 必须用 announce_date 来对齐交易日期,而不是 report_date

这里重点说一下为什么用分位数截断而不是直接删除异常值。直接删除会损失样本量,而且A股市场上很多"异常值"其实是真实存在的极端情况,比如某年某公司因为资产重组导致ROE暴涨到200%。如果你直接删掉,就丢失了这个信息。用1%和99%分位数截断,既保留了样本,又限制了极端值对后续标准化和打分的影响。

2.3 数据频率的选择:季度、月度还是日度

基本面数据天然是低频的,财报一个季度才更新一次。但交易数据是日度的,这就产生了一个频率匹配的问题。

我的做法是:因子计算用季度频率,但持仓调整可以月度或季度执行。具体来说,在每个财报公告日之后,更新一次因子值,然后在下一次调仓日使用最新的因子值。中间如果沒有新的财报,因子值保持不变。

这样做的好处是避免了用日度价格数据去"插值"基本面因子,那种做法会引入大量噪音。但缺点是调仓频率不能太高,否则因子值没有变化,调仓就变成了纯粹的交易成本消耗。

3. 因子体系搭建:从经济学逻辑到可计算指标

数据准备好了,接下来要决定用哪些因子。这一步最忌讳的是"看到什么用什么",必须有清晰的经济学逻辑支撑。

3.1 价值因子:便宜是不是硬道理

价值因子是最经典的一类,核心逻辑是"买入相对便宜的东西,长期来看会获得超额收益"。常用的指标包括:

  • 市盈率(PE):价格除以每股收益。越低越"便宜"。
  • 市净率(PB):价格除以每股净资产。适合重资产行业。
  • 市销率(PS):价格除以每股营收。适合尚未盈利的成长型企业。
  • 股息率:每股分红除以股价。越高说明现金回报越实在。

但这里有一个常见的误区:不是所有低PE的股票都值得买。有些公司PE低是因为市场预期它未来盈利会大幅下滑,这种叫"价值陷阱"。我在实操中会做一个简单的过滤:同时要求PE处于行业后30%分位,且最近两个季度营收增速不为负。这样能过滤掉一部分因为基本面恶化而导致的低估值。

3.2 质量因子:好公司是不是好股票

质量因子衡量的是公司的盈利能力和财务健康度,常见指标有:

  • ROE(净资产收益率):巴菲特最看重的指标之一。持续高ROE说明公司有护城河。
  • 毛利率:反映产品的定价能力。
  • 资产负债率:衡量财务杠杆,太高有风险,太低可能说明公司不善用杠杆。
  • 经营性现金流/净利润:判断利润是不是"真金白银"。

质量因子的一个关键问题是行业差异极大。比如白酒行业的毛利率普遍在70%以上,而零售行业可能只有20%。如果你直接拿全市场的毛利率排名,那选出来的全是白酒和医药。所以质量因子必须做行业中性化处理,后面会详细讲。

3.3 成长因子:增速的可持续性比增速本身更重要

成长因子看的是公司的扩张速度,常用指标包括:

  • 营收同比增速
  • 净利润同比增速
  • ROE同比变化
  • 分析师一致预期调整

成长因子最大的坑是基数效应。一家公司去年净利润100万,今年200万,增速100%,看起来很猛,但可能只是因为去年太差了。我在使用成长因子时,会额外加一个条件:最近四个季度的净利润增速标准差不能太大,以此过滤掉那些增速忽上忽下的公司。

3.4 因子相关性检验:别把鸡蛋放在同一个篮子里

选完因子之后,一定要做相关性检验。如果两个因子的相关系数超过0.7,说明它们本质上在衡量同一个东西,同时使用会导致某个维度被过度加权。

# 计算因子之间的Spearman相关系数 factor_cols = ['pe', 'pb', 'roe', 'gross_margin', 'revenue_yoy', 'profit_yoy'] corr_matrix = df[factor_cols].corr(method='spearman') print(corr_matrix) # 如果发现 roe 和 gross_margin 相关性超过0.7 # 考虑只保留其中一个,或者用PCA降维

我一般会保留相关性低于0.5的因子组合。如果两个因子相关性很高,优先保留经济学逻辑更直接的那个。比如ROE和ROA相关性很高,我会保留ROE,因为它是股东更关心的指标。

4. 因子预处理:标准化、中性化与去极值

原始因子值不能直接用来打分,因为不同因子的量纲完全不同。PE可能是5到100,ROE可能是-20%到30%,如果不做处理,直接相加或加权,PE会完全主导结果。

4.1 标准化:让不同因子站在同一起跑线

最常用的标准化方法是Z-Score标准化:

def zscore(series): return (series - series.mean()) / series.std() # 按日期分组做标准化,确保每个截面独立 for col in factor_cols: df[col + '_z'] = df.groupby('date')[col].transform(zscore)

Z-Score之后,每个因子的均值是0,标准差是1。但Z-Score有一个问题:对极端值敏感。如果某个因子有一个超级极端的值,标准化之后其他值都会被压缩到很小的范围内。所以标准化之前一定要先做去极值处理,也就是前面提到的分位数截断。

另一种方法是排名百分位法,直接把因子值转换成0到1之间的排名:

df[col + '_rank'] = df.groupby('date')[col].rank(pct=True)

排名法的好处是完全不受极端值影响,缺点是丢失了因子值之间的相对距离信息。我一般在因子分布比较均匀的时候用Z-Score,分布明显偏斜的时候用排名法。

4.2 中性化:剔除行业和市值的影响

这是多因子选股中最容易被忽视、但影响最大的一步。如果不做中性化,你的因子可能只是在选行业和选市值。

举个例子:银行股的PE普遍在5到8倍,而科技股的PE可能在50倍以上。如果你直接用PE排名选前10%,选出来的全是银行和地产。这不是因为你的因子有效,而是因为行业分布导致的偏差。

中性化的做法是对因子值做回归,取残差:

import statsmodels.api as sm def neutralize(factor_series, industry_dummies, market_cap): # factor_series: 因子值 # industry_dummies: 行业哑变量矩阵 # market_cap: 对数市值 X = pd.concat([industry_dummies, np.log(market_cap)], axis=1) X = sm.add_constant(X) model = sm.OLS(factor_series, X, missing='drop').fit() return model.resid # 对每个因子在每个截面上做中性化 for col in factor_cols: df[col + '_neutral'] = df.groupby('date').apply( lambda x: neutralize(x[col], industry_dummies, x['market_cap']) ).reset_index(level=0, drop=True)

中性化之后,因子值代表的是"在相同行业和相同市值水平下,这只股票的该因子相对高低"。这样选出来的股票,才是真正因为因子本身而入选,而不是因为行业或市值暴露。

注意:中性化用的行业分类标准要统一。我一般用申万一级行业,因为它的分类相对稳定,不会频繁调整。如果用一些细分行业分类,可能会因为分类变动导致中性化结果不稳定。

4.3 因子方向统一:让所有因子"越大越好"

有些因子是越大越好(如ROE、营收增速),有些是越小越好(如PE、PB)。在合成综合得分之前,必须把方向统一。

最简单的方法是对"越小越好"的因子取负值:

# PE和PB是越小越好,取负号 df['pe_neutral'] = -df['pe_neutral'] df['pb_neutral'] = -df['pb_neutral']

这样处理后,所有因子都是"值越大代表越好",后续合成时直接加权求和即可。

5. 因子合成与打分:等权、IC加权还是机器学习

单个因子处理完之后,下一步是把它们合成一个综合得分。这一步的方法选择,直接决定了策略的风格。

5.1 等权合成:简单但不容易出错

等权合成就是每个因子给相同的权重,直接相加或平均:

df['score'] = df[[col + '_neutral' for col in factor_cols]].mean(axis=1)

等权的好处是不需要估计任何参数,避免了过拟合。缺点是假设所有因子同等重要,这显然不符合实际。但在因子数量不多(5个以内)且经济学逻辑都比较扎实的情况下,等权往往能跑赢很多复杂方法。

5.2 IC加权:让数据说话

IC(Information Coefficient)是因子值与未来收益的相关系数。IC加权就是根据每个因子最近一段时间的IC均值来分配权重:

# 假设已经计算了每个因子在过去12个月的月度IC ic_weights = ic_mean / ic_mean.abs().sum() df['score'] = sum(df[col + '_neutral'] * ic_weights[col] for col in factor_cols)

IC加权的问题是IC本身不稳定。一个因子上个月IC很高,下个月可能就变成负的。所以实际使用中,我一般会用过去12个月IC的均值除以标准差(即IC_IR)来加权,这样能一定程度上过滤掉不稳定的因子。

5.3 机器学习合成:谨慎使用

现在很多人喜欢用随机森林、XGBoost之类的模型来合成因子。理论上这能捕捉因子之间的非线性关系,但实操中有几个大坑:

  • 过拟合风险极高。因子数量少、样本噪音大,树模型很容易记住噪音。
  • 可解释性差。你很难说清楚为什么模型给了这个股票高分。
  • 训练集和测试集的划分要非常小心。必须用时间序列切分,不能用随机切分。

我的建议是:如果你刚开始做多因子,先用等权或IC加权跑通整个流程,等有了稳定的回测框架和足够的数据积累,再考虑机器学习方法。不要一上来就上复杂模型,那样你连问题出在哪里都找不到。

6. 回测框架搭建:别让回测骗了你

因子合成完,得到一个综合得分,接下来就是回测。回测是验证策略有效性的关键步骤,但也是最容易自欺欺人的环节。

6.1 回测的基本流程

一个完整的回测至少包含以下步骤:

  1. 确定调仓日期。比如每季度财报公布后的第一个交易日。
  2. 在每个调仓日,根据综合得分排序,选出前N只股票。
  3. 等权或市值加权买入,持有到下一个调仓日。
  4. 计算每个持有期的收益,累乘得到净值曲线。
  5. 与基准(如沪深300)对比,计算超额收益、夏普比率、最大回撤等指标。
# 简化的回测循环框架 portfolio_returns = [] for date in rebalance_dates: # 获取当期因子得分 scores = df[df['date'] == date].sort_values('score', ascending=False) # 选前50只 selected = scores.head(50)['code'].tolist() # 计算持有期收益(假设持有到下一个调仓日) next_date = get_next_rebalance_date(date) period_return = df[(df['date'] == next_date) & (df['code'].isin(selected))]['return'].mean() portfolio_returns.append(period_return) # 计算净值 nav = (1 + pd.Series(portfolio_returns)).cumprod()

6.2 回测中最容易犯的三个错误

第一个错误:幸存者偏差。如果你用的股票池是"当前上市的所有股票",那么已经退市的股票就不在样本里。这些退市股票往往是表现最差的,剔除它们会让回测收益虚高。正确的做法是使用历史成分股列表,每个调仓日只使用当时真实存在的股票。

第二个错误:前视偏差。除了前面提到的财报公告期问题,还有一个常见的是用当天的收盘价买入。如果你在调仓日收盘后计算因子得分,那么最早也只能在下一个交易日开盘买入。用当天收盘价买入,相当于用了未来信息。

第三个错误:交易成本忽略。A股的交易成本包括佣金、印花税和冲击成本。佣金一般万分之二到万分之三,印花税千分之一(卖出时收取),冲击成本取决于资金量和股票流动性。如果策略换手率很高,交易成本能吃掉大部分超额收益。

注意:回测中建议至少设置双边千分之三的交易成本。如果策略月度调仓、每次换手50%,一年下来交易成本就是3.6%,这个数字足以让很多"看起来不错"的策略变成亏损。

6.3 评价指标:不只看收益

评价一个多因子策略,不能只看总收益。我一般会关注以下几个指标:

指标含义参考标准
年化超额收益相对基准的超额5%以上值得关注
信息比率超额收益/跟踪误差0.5以上算不错
最大回撤净值从高点的最大跌幅越小越好,需结合收益看
换手率每期调仓的股票比例影响交易成本
因子IC均值因子与未来收益的相关性0.03以上算有效
IC_IRIC均值/IC标准差0.3以上算稳定

这些指标要综合看。一个策略年化超额20%但最大回撤50%,另一个年化超额10%但最大回撤15%,后者在实际使用中往往更受欢迎,因为回撤小意味着你可以拿得住。

7. 从回测到实盘:那些回测不会告诉你的坑

回测跑通了,指标也好看,是不是就可以直接实盘了?远远不够。回测和实盘之间有一条巨大的鸿沟,我踩过的坑至少包括以下几个。

7.1 流动性问题:小市值股票的买卖冲击

很多多因子策略在小市值股票上表现特别好,因为小市值股票波动大、错误定价多。但实盘中,小市值股票的流动性很差,你买进去可能就把价格推高了,卖出来又把价格砸下去了。回测中假设的"以收盘价成交"在实盘中根本不可能实现。

我的做法是:在选股时加入流动性过滤,剔除日均成交额低于5000万的股票。这个阈值可以根据资金量调整,资金量越大,阈值越高。

7.2 停牌与涨跌停:想买买不到,想卖卖不掉

A股有涨跌停制度,如果调仓日某只股票涨停,你根本买不进去;如果跌停,你也卖不出来。回测中如果不处理这种情况,会高估策略收益。

处理方法很简单:在调仓日,剔除涨停和跌停的股票,不参与交易。虽然这会损失一些机会,但更接近实盘情况。

7.3 因子失效:没有永远有效的因子

这是最根本的问题。任何一个因子都有失效的时候。2021年之前,小市值因子在A股非常有效,但2021年之后,小市值因子经历了长时间的回撤。如果你在2021年初根据历史回测重仓小市值因子,后面会非常难受。

应对因子失效的方法有两个:一是分散,同时使用多个相关性低的因子,避免单一因子失效导致整体崩溃;二是监控,定期计算因子的IC和IC_IR,如果连续多个周期IC为负,就要考虑降低该因子的权重甚至剔除。

7.4 心理因素:回撤时你还能坚持吗

最后说一个不是技术问题但最重要的问题:心理。回测中看到最大回撤20%,你觉得"没问题,我能扛住"。但实盘中,当你真金白银亏了20%,而且连续几个月跑输基准,你还能坚持执行策略吗?

我见过太多人,回测做得漂漂亮亮,实盘一遇到回撤就手动干预,结果把策略搞得面目全非。多因子选股是一个概率游戏,单次调仓的结果是随机的,只有长期执行才能体现因子的有效性。如果你做不到长期坚持,再好的策略也没用。

8. 前期知识储备清单与学习路径建议

最后,把多因子选股前期需要储备的知识点整理成一个清单,方便你对照检查自己的准备情况。

8.1 必备技能清单

技能类别具体内容掌握程度要求
Python基础函数、循环、条件判断、文件读写熟练
pandasDataFrame操作、groupby、merge、apply熟练
numpy数组运算、广播机制了解
统计学均值、标准差、相关系数、回归分析理解概念
金融基础财务报表结构、常用估值指标理解概念
数据库SQL基本查询了解

8.2 推荐的学习顺序

如果你是从零开始,我建议按这个顺序推进:

  1. 先补pandas。多因子选股80%的时间在处理数据,pandas不熟会非常痛苦。
  2. 理解财务报表的基本结构。至少要知道利润表、资产负债表、现金流量表分别反映什么。
  3. 手动计算几个因子。拿一家公司的财报数据,手动算一遍PE、ROE、营收增速,理解每个数字的来源。
  4. 跑通一个单因子回测。用最简单的PE因子,从数据获取到回测评价,完整走一遍流程。
  5. 扩展到多因子。加入ROE和营收增速,做标准化、中性化、合成、回测。
  6. 优化和迭代。尝试不同的因子组合、权重方法、调仓频率,观察结果变化。

这个过程我花了大约三个月,每天投入两到三个小时。如果你有编程基础但金融知识薄弱,可能需要更长时间补金融;反之亦然。

8.3 几个容易走弯路的地方

不要一上来就追求复杂模型。我见过很多人,连单因子回测都没跑通,就开始研究LSTM和Transformer。结果数据清洗没做好,回测框架有前视偏差,模型再复杂也是白搭。

不要迷信回测收益。回测年化50%的策略,实盘能有10%就不错了。回测的作用是验证逻辑,不是预测收益。

不要忽略交易成本。很多策略回测看起来能赚钱,加上交易成本就变成亏损。回测时一定要把成本算进去,而且要用保守的估计。

不要频繁更换因子。今天看这个因子好就用这个,明天看那个因子好就换那个,最后什么也积累不下来。选定几个逻辑扎实的因子,长期跟踪,定期评估,比频繁切换要有效得多。

多因子选股是一个需要长期积累的领域,没有捷径。但只要你把前期这些基础知识打扎实,后面的路会越走越顺。我在实际操作中的体会是:慢就是快,把数据清洗和回测框架做扎实,比急着上策略要重要得多。

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

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

立即咨询