换手率衰减与 Alpha 半衰期:信号该持有多久
「信号该持有多久」不是拍脑袋的参数,而是由 alpha 半衰期和交易成本共同决定的最优化问题。用 200 只股票、2000 天、真实半衰期 8 天的受控模拟实测:IC 从 1 天滞后的 1.65% 指数衰减,日频调仓年化单边换手 145.7 倍——零成本下 Sharpe 3.05,单边 60bp 直接砍到 -5.52。最优持有期随成本单调右移:10bp 时 3 天、30bp 时 10 天、60bp 时 40 天。更重要的发现有两个:一是降换手的两条路不等价,EMA 平滑信号在每个成本水平都优于机械拉长持有期(30bp 下 1.28 vs 0.94),因为平滑保留了「每天用最新信息、只是慢慢挪仓」的自由度;二是半衰期本身很难估准——单样本点估计 11.9 天 vs 真值 8 天,20 个种子的估计散布 5.4~16.7 天,而只对正 IC 点取对数回归会把估计推到 16.2 天,这是一个不报错的选择偏差。诚实边界:模拟中信号衰减是干净的指数,真实 alpha 衰减常是快慢两段混合;成本模型只有比例费率,没有市场冲击(中阶)。
问题:调仓快了喂佣金,调仓慢了喂衰减#
每个做横截面策略的人迟早撞上同一个问题:信号算出来了,仓该多久调一次?
调得太快,换手率爆炸,佣金、印花税、冲击成本一层层剥皮;调得太慢,信号早就过期了,你持有的是三周前的观点,alpha 已经被别人赚走。这中间显然有一个最优点,但它在哪里?
大部分人的做法是网格搜一遍持有期,挑回测最好的那个。这样做有两个问题:一是搜出来的最优点自带过拟合嫌疑;二是它没有回答「为什么」——换个市场、换个成本环境,这个数字还成立吗?
正确的框架是把问题拆成两个可测量的量:
- Alpha 半衰期:信号的预测能力多少天衰减一半;
- 成本-换手曲线:每次调仓付多少,调仓频率决定一年付多少次。
最优持有期是这两者的交点。本文用一个受控模拟把这条逻辑链完整走一遍——受控的好处是真实半衰期是我们自己设的(8 天),任何估计方法的偏差都无处遁形。
实验设计:把半衰期埋进数据里#
200 只股票、2000 个交易日。信号是横截面标准化的 AR(1) 过程,自相关系数由目标半衰期反推:
收益由昨日信号驱动,即期 IC 设为 0.012(横截面策略的现实量级),日波动 2%:
import numpy as np
rng = np.random.default_rng(42)
N, T = 200, 2000
HL_true = 8.0
rho = 0.5 ** (1.0 / HL_true) # AR(1) 系数,半衰期 8 天
ic0, sigma_r = 0.012, 0.02
# 信号面板:AR(1),方差恒为 1
S = np.zeros((T, N))
S[0] = rng.standard_normal(N)
for t in range(1, T):
S[t] = rho * S[t-1] + np.sqrt(1 - rho**2) * rng.standard_normal(N)
# 收益:t 日信号预测 t+1 日收益(signal-on-i, return-on-i+1)
R = np.zeros((T, N))
R[1:] = ic0 * S[:-1] * sigma_r + \
sigma_r * np.sqrt(1 - ic0**2) * rng.standard_normal((T-1, N))python注意收益方程里的 S[:-1]——信号在 t 日收盘确认,驱动的是 t+1 日的收益。这是回测里最容易犯的 look-ahead 错误的镜像检查点:数据生成过程本身必须先对齐,否则后面测出来的一切都是自欺。
第一步:测 IC 衰减曲线,估半衰期#
用 t 日信号预测 t+h 日收益,h 从 1 扫到 40:
def zscore(x):
return (x - x.mean(axis=1, keepdims=True)) / x.std(axis=1, keepdims=True)
Sz = zscore(S)
lags = np.arange(1, 41)
ics = np.array([
np.corrcoef(Sz[:-h].ravel(), R[h:].ravel())[0, 1]
for h in lags
])python
滞后 1 天的 IC 是 1.65%,之后指数下滑,20 天以外基本淹没在噪声里。对这条曲线拟合 ,半衰期就是 。
这里有一个不报错但会系统性高估的坑。很多人图省事,对 IC 取对数后做线性回归——但远端的 IC 观测值一半正一半负(噪声主导),取对数只能保留正值点。这不是无害的丢数据:被留下的恰好是被噪声往上推的那一半,回归斜率被压平,半衰期被高估。实测对比:
| 估计方法 | 估计半衰期 | 真值 |
|---|---|---|
| 对数回归(只用正 IC 点) | 16.2 天 | 8 天 |
| 非线性最小二乘(用全部点) | 11.9 天 | 8 天 |
对数回归把 8 天估成了 16 天——误差 100%,而且方向恒定。非线性拟合(对 λ 做网格、a 用解析最小二乘)用上了全部 40 个点包括负值,误差小得多:
best_sse, lam_hat, a_hat = np.inf, None, None
for lam_try in np.linspace(0.01, 0.6, 600):
x = np.exp(-lam_try * lags)
a_try = (x @ ics) / (x @ x) # 给定 λ 时 a 的解析解
sse = np.sum((ics - a_try * x) ** 2)
if sse < best_sse:
best_sse, lam_hat, a_hat = sse, lam_try, a_try
hl_hat = np.log(2) / lam_hat # → 11.9 天python但 11.9 vs 8 仍有偏差——这不是方法错了,是采样噪声本身就这么大。跑 20 个独立种子:

20 个估计的均值 9.8 天(轻微高估),标准差 3.1 天,最小 5.4、最大 16.7。同一个数据生成过程,2000 天 × 200 只股票的样本量,半衰期估计仍能差出三倍。 这个数字请在拿真实数据估半衰期时反复回味:你估出的「12 天」和别人估出的「7 天」,可能是同一个真相的两次抽样。
第二步:持有期扫描——最优点随成本右移#
十分位多空组合(做多信号最高 10%、做空最低 10%,各组内等权),每 hold 天再平衡一次,每次换仓按单边成本收费:
def backtest(hold, cost_bps):
w = np.zeros(N); pnl, turns = [], []
for t in range(WARM, T - 1):
if (t - WARM) % hold == 0:
s = Sz[t]
hi, lo = np.quantile(s, 0.9), np.quantile(s, 0.1)
w_new = np.where(s >= hi, 1.0, np.where(s <= lo, -1.0, 0.0))
w_new = np.where(w_new > 0, w_new / (w_new > 0).sum(),
np.where(w_new < 0, w_new / (w_new < 0).sum(), 0.0))
turn = np.abs(w_new - w).sum() / 2
pnl.append(w_new @ R[t+1] - turn * cost_bps * 1e-4)
w = w_new
else:
pnl.append(w @ R[t+1])
pnl = np.array(pnl)
return pnl.mean() / pnl.std() * np.sqrt(252)python先看换手的量级。日频调仓的年化单边换手是 145.7 倍——组合每年翻 145 次。持有 8 天降到 42.8 倍,持有 20 天 21.2 倍。换手大致按 1/hold 衰减:

然后是主结果——四条成本假设下的净 Sharpe 曲线:

| 单边成本 | 日频调仓净 Sharpe | 最优持有期 | 最优净 Sharpe |
|---|---|---|---|
| 0 bp | 3.05 | 1 天 | 3.05 |
| 10 bp | 1.62 | 3 天 | 2.02 |
| 30 bp | -1.25 | 10 天 | 0.94 |
| 60 bp | -5.52 | 40 天 | 0.47 |
三个观察:
第一,成本对高换手策略的杀伤是毁灭性的。 同一个信号,日频调仓在零成本下 Sharpe 3.05,30bp 成本下变成 -1.25——不是变差,是变成一台稳定的亏钱机器。145 倍年换手 × 30bp × 2(双边)≈ 年化 8.7% 的确定性成本,而这个信号的毛年化收益撑不起这个数。
第二,最优持有期随成本单调右移,且大致符合平方根规律。 成本从 10bp 涨到 60bp(6 倍),最优持有期从 3 天移到 40 天(约 13 倍)。理论上在指数衰减 + 比例成本假设下最优持有期近似 ,模拟结果与之量级一致。
第三,最优点右侧的曲线很平,左侧很陡。 30bp 那条线,持有 10 天和 20 天的 Sharpe 差别不大,但从 10 天缩到 3 天直接从 0.94 掉到 0.3 以下。实务含义:拿不准就选长一点,宁可少赚衰减也别多喂成本——不对称性站在慢的一边。
第三步:降换手的两条路不等价#
拉长持有期是最粗暴的降换手方式,但不是唯一的。另一条路是信号平滑:持有期保持 1 天,但用 EMA 把信号钝化,仓位自然变得粘滞:
def backtest_ema(span, cost_bps):
alpha = 2 / (span + 1)
Se = np.zeros_like(Sz); Se[0] = Sz[0]
for t in range(1, T):
Se[t] = (1 - alpha) * Se[t-1] + alpha * Sz[t]
# 之后照常做日频十分位多空,成本照收
...python
| 单边成本 | 拉长持有期(最优) | EMA 平滑(最优) |
|---|---|---|
| 10 bp | 2.02(hold=3) | 2.15(span=3) |
| 30 bp | 0.94(hold=10) | 1.28(span=30) |
| 60 bp | 0.47(hold=40) | 0.35(span=30,扫描上限) |
在 10bp 和 30bp 下,平滑都赢,30bp 时赢了 36%。原因不难理解:拉长持有期意味着「每 10 天才看一次最新信号,中间 9 天装瞎」;而 EMA 平滑是「每天都看最新信号,只是移动得慢」。后者保留了对新信息的持续响应,只牺牲响应速度——信息利用率天然更高。
60bp 那行平滑反而略输,是因为扫描的 span 上限只到 30;把 span 继续放大,平滑路线会追回来。真正的重点不是哪条路永远赢,而是:换手率是预算,怎么花这笔预算是有优劣之分的。同样把年换手从 145 倍压到 20 倍,平滑方案买到的 alpha 比机械降频更多。
实务中还有第三条路本文没有展开:调仓带(no-trade band)——只有当目标仓位偏离现有仓位超过阈值才动手。它和 EMA 类似,都属于「让仓位对信号小抖动脱敏」的家族,一般也优于机械拉长持有期。
快慢信号混合:为什么真实曲线不是干净指数#
本模拟的信号是单一 AR(1),IC 衰减是教科书式的指数。真实信号很少这么干净——常见形态是快慢两段混合:
- 反转类信号:半衰期 2~5 天,两周后彻底没了;
- 质量/价值类信号:半衰期以月计,衰减曲线前段平缓。
一个多因子复合信号的 IC 衰减曲线是这两类的加权叠加,前段陡、后段拖长尾。用单一指数去拟合会得到一个「哪边都不像」的半衰期。正确做法是分解到子信号层面各估各的,再按半衰期给每个子信号配它自己的平滑参数——快信号配短 EMA,慢信号配长 EMA,而不是全家共用一个调仓频率。这也是为什么成熟的组合框架把「信号加权」和「换手控制」做成两层:混在一起时,慢信号会被快信号绑架着高频调仓,白白喂成本。
已知偏差#
这个实验有三处与真实世界的差距,方向都要说清:
- 成本模型只有比例费率,没有市场冲击。 真实冲击成本随交易速度非线性上升,这会进一步惩罚短持有期——最优点比模拟结果更靠右。文中「拿不准选长一点」的结论在真实市场只会更强。
- 信号衰减是平稳的。 真实 alpha 半衰期本身会漂移——拥挤的信号衰减越来越快。三年前估的半衰期今天可能已经砍半,这要求半衰期估计滚动更新,而滚动窗口内的样本量比本文 2000 天更小,估计噪声(±3 天量级)更大。
- 十分位多空 + 等权是最简组合构造。 带风险模型和优化器的组合中,换手还受协方差矩阵变化驱动,成本-换手曲线会更复杂,但「半衰期定调仓节奏」的主逻辑不变。
结论#
三句话带走:
- 先估半衰期,再定调仓频率——顺序不能反。估计时用全部 IC 点做非线性拟合,别对正值点取对数回归(那会把 8 天估成 16 天);并且记住即使方法正确,±40% 的抽样误差是常态。
- 成本翻倍,持有期别只翻一半。最优持有期对成本的弹性接近平方根,且曲线左陡右平——宁慢勿快。
- 降换手优先用信号平滑或调仓带,而不是机械拉长持有期。同样的换手预算,平滑方案保留了对新信息的持续响应,30bp 成本下净 Sharpe 高出三分之一。
换手率不是策略的副产品,它是你为信息新鲜度支付的价格。半衰期告诉你这份新鲜度值多少钱——别为过期的信息付全价,也别为省钱吃过期的饭。