因子选股里最容易被忽视、也最容易让人亏钱的误区,不是”因子没 alpha”,而是你以为自己在赚 alpha,其实在赌风格。一个典型的 value 信号(低 PE / 高 book-to-market)在 A 股里天然就和小盘股、金融/能源行业高度绑定——因为历史上这些板块更”便宜”。于是你排个序、做多最便宜的 20%、做空最贵的 20%,账面上确实赚钱,但那笔钱里有多少来自”价值 alpha”、多少来自”我恰好重仓了小盘和金融”?这俩在 2021 年以后的行情里表现天差地别。
结论先放这:用「对行业哑变量 + 对数市值做线性回归、取残差」做截面中性化,可以把风格暴露几乎清零,同时保留大部分 alpha。 在我们的 300 只股票模拟截面里,原始多空组合的行业净权重范数高达 0.55、市值暴露 −1.36(净做多小盘);中性化后这两项分别降到 0.05 和 −0.02,而多空收益只从 1.80 降到 1.25——约 69% 的 alpha 被保住。代价也很明确:你放弃了风格因子本就支付的溢价,得到的是一个更纯净、更抗 regime 切换的信号。

一、信号为什么会被”污染”#
所谓截面因子信号 ,理想情况下应该只承载你想表达的那一点信息(比如”被低估的程度”)。但现实里,几乎所有基本面特征都和行业与市值纠缠在一起:
- 行业绑定:金融、能源的估值中枢天然低于科技、消费。于是”低估值”信号在金融/能源天然偏高、在科技/消费天然偏低。
- 市值绑定:小盘股的 book-to-market 普遍高于大盘股(规模溢价的一部分)。于是”价值”信号天然偏向小盘。
如果你直接对原始信号排序建多空,多头会自动堆在”便宜的行业 + 小盘”,空头会自动堆在”贵的行业 + 大盘”。你的 P&L 就不再只由 alpha 驱动,而被行业因子和市值因子”带着走”。这正是很多因子”样本内漂亮、实盘一换风格就崩”的根因——它根本没在表达你想表达的东西。
二、构造一个被污染的 value 信号(可复现)#
为了把问题讲透,我们模拟一个干净的截面:300 只股票、6 个行业、市值对数正态。关键设定是——让”纯净价值 alpha”与行业/市值完全独立,但让观测到的信号被它们污染,同时让未来收益同时由 alpha 和风格因子驱动(即风格因子本身也”给钱”)。
import numpy as np
rng = np.random.default_rng(20260711)
N = 300
INDUSTRIES = ["金融", "科技", "消费", "医药", "工业", "能源"]
K = 6
ind = rng.integers(0, K, N)
logmcap = rng.normal(0, 1, N) # 对数市值(左=小盘,右=大盘)
# 各行业「价值倾斜」不同:金融/能源偏 value,科技/消费偏 growth
ind_value_tilt = np.array([ 1.1, -0.9, -0.4, 0.0, 0.5, 0.9])
true_quality = rng.normal(0, 1, N) # 纯净价值 alpha(与行业、市值独立)
# 观测信号 = 纯净 alpha + 行业污染 + 市值污染 + 噪声
signal = (1.0 * true_quality
+ 0.9 * ind_value_tilt[ind]
- 0.7 * (logmcap - logmcap.mean()) # 小盘信号更高
+ rng.normal(0, 0.5, N))
# 未来收益:alpha 被定价 + 行业因子 + 市值(小盘)溢价 + 噪声
ind_factor = ind_value_tilt[ind]
size_factor = -(logmcap - logmcap.mean())
fwd = (0.55 * true_quality
+ 0.45 * ind_factor
+ 0.35 * size_factor
+ rng.normal(0, 0.8, N))
print("信号-收益 IC(原始):", round(np.corrcoef(signal, fwd)[0, 1], 3)) # ≈ 0.64python注意 signal 里混进了 ind_value_tilt 和 logmcap,而 fwd 里也混进了它们——这就是真实市场的结构:你想要的 alpha 和你想剔除的风格,同时出现在信号和未来收益里。这决定了中性化不是”免费午餐”,我们下面会看到它的真实代价。
三、中性化怎么做:对风格变量回归取残差#
核心思想一句话:把信号里能被「行业 + 市值」线性解释的部分减掉,剩下的残差就是”剥离风格后的纯信号”。写成回归:
取拟合值 为”风格部分”,残差 即中性化信号。残差在构造上就与行业和市值正交,所以用它排序建的组合,行业/市值暴露必然≈0。
def ls_portfolio(sig, fwd, frac=0.20):
"""做多信号最高 frac,做空最低 frac,返回权重(多 +1/n,空 -1/n,其余 0)"""
order = np.argsort(sig)
n = int(N * frac)
w = np.zeros(N)
w[order[:n]] = -1.0 / n
w[order[-n:]] = +1.0 / n
return w, w @ fwd
# 原始多空
w_raw, ret_raw = ls_portfolio(signal, fwd)
# 中性化:对 [行业哑变量 + log市值] 回归取残差
X = np.column_stack([(ind == k).astype(float) for k in range(K)] + [logmcap])
X = np.column_stack([np.ones(N), X])
beta = np.linalg.lstsq(X, signal, rcond=None)[0]
signal_neu = signal - X @ beta # 正交化后的纯信号
w_neu, ret_neu = ls_portfolio(signal_neu, fwd)
print("信号-收益 IC(中性化后):", round(np.corrcoef(signal_neu, fwd)[0, 1], 3)) # ≈ 0.42pythonIC 从 0.64 掉到 0.42,这不意外也不丢人:原始 IC 里有一部分来自”信号和收益都受风格驱动”的虚假共鸣,剥掉风格后,剩下的才是 alpha 与收益的”真”相关。
四、前后对比:行业与市值暴露#
光看收益会被误导,真正的判决在暴露。我们定义多空组合的行业净权重 = 多头行业占比 − 空头行业占比;市值暴露 = 多头平均 log 市值 − 空头平均 log 市值。
def net_industry(w):
long_mask, short_mask = w > 0, w < 0
out = []
for k in range(K):
lw = np.sum((ind == k) & long_mask) / np.sum(long_mask)
sw = np.sum((ind == k) & short_mask) / np.sum(short_mask)
out.append(lw - sw)
return np.array(out)
net_raw = net_industry(w_raw)
net_neu = net_industry(w_neu)
size_exp_raw = logmcap[w_raw > 0].mean() - logmcap[w_raw < 0].mean()
size_exp_neu = logmcap[w_neu > 0].mean() - logmcap[w_neu < 0].mean()python
原始多空组合的行业净权重严重偏斜——金融被大幅超配、科技被大幅低配,行业净权重范数高达 0.55;市值暴露 −1.36 意味着组合净做多小盘。这根本不是”纯价值”,而是”价值 + 小盘 + 金融”的叠加 bet。

中性化后,六个行业的净权重全部贴近 0,行业净权重范数降到 0.05;市值暴露降到 −0.02。组合终于只表达”价值”这一件事。
五、alpha 还在吗?收益 vs 风格暴露的诚实权衡#
很多人拒绝中性化,是怕”把收益也中性掉了”。我们的实证给出一个平衡的答案:

- 多空收益:原始 1.80 → 中性化 1.25,保留了约 69% 的 alpha。
- 风格暴露综合指数(行业权重范数 + |市值暴露|):原始 0.55 → 中性化 0.05,几乎清零。
这里必须诚实地说清代价:中性化后收益下降,是因为风格因子(行业、市值)本身在样本里是”给钱”的——你剥掉的不只是噪声,还有一部分真金白银的风格溢价。所以中性化不是”让因子更强”,而是把表达从”混杂的风格 bet”换成”纯净的 alpha bet”。它的价值不在更高的收益,而在:
- 可解释性:你的 P&L 终于对应你声称在做的那件事;
- 稳健性:当风格因子进入逆风(比如小盘跑输、金融杀估值),中性化组合不会因为”顺带押了风格”而额外挨打;
- 可叠加性:纯净 alpha 才能和其他因子正交地组合,否则多个因子会重复押注同一批风格。
一句话:中性化是用”风格溢价”换”信号的纯度与稳健”。如果你的信念是”我能持续赚风格溢价的钱”,那另说;但多数时候,我们建因子是为了赚 alpha,而不是为了替自己再开一个风格暴露的仓位。
六、真实陷阱(不踩坑你一定会掉进去)#
1. 中性化会顺手剥掉”真 alpha 里的行业/市值成分”。 如果某个行业的确系统性更便宜、未来确实涨更多(即行业因子和 alpha 同源),回归残差会把这部分也减掉——你”净化”了信号,也”净化”掉了一部分该赚的钱。这不是 bug,是中性化的定义代价,必须心里有数。
2. 哑变量设定错误会制造伪中性。 用申万一级行业还是二级?用总市值还是流通市值?把”电新”和”军工”硬塞进一个哑变量,残差里就会残留这个粗分行业的暴露。中性化的质量上限,取决于你回归里”风格变量”刻画得有多准。
3. 极端市值差导致杠杆失衡。 对数市值跨度大时,回归残差在小盘股上方差更大,排序后会变相超配小盘(哪怕均值已中性)。实务上常改为分行业内做市值中性(先按行业分组,组内再对 log 市值取残差),或加市值分桶,避免这种隐性偏斜。
4. 中性化 ≠ 风险暴露为零。 残差只保证与”你放进回归的变量”正交。如果还有没建模的风格(动量、流动性、换手率),组合照样会暴露。中性化是”列出来的才管”,漏掉的照样咬人。
5. 高频调仓下中性化有滞后与换手成本。 每次换仓都重估回归,权重可能剧烈跳变;残差信号的换手往往比原始信号更高,摩擦成本要在回测里真实计入,别只看毛收益。
小结#
因子中性化不是玄学,就是一句话:把信号对行业和市值回归、取残差,让组合只表达你想表达的那点 alpha。 我们在一个 300 只股票、信号被行业/市值双重污染的模拟截面里验证:中性化把行业净权重范数从 0.55 压到 0.05、市值暴露从 −1.36 压到 −0.02,而多空收益只从 1.80 降到 1.25——约七成 alpha 得以保留。它真正的价值不是”收益更高”,而是让你的 P&L 终于对应你声称在做的策略,并且在风格逆风时不再因为”顺带押了风格”而额外挨打。记住它的边界:中性化会剥掉同源的风格溢价、依赖哑变量设定、只管得住列出来的变量——把它当作”让因子说实话”的工具,而不是”让因子变强”的魔法。