halo 的技术博客

返回

问题:调仓快了喂佣金,调仓慢了喂衰减#

每个做横截面策略的人迟早撞上同一个问题:信号算出来了,仓该多久调一次?

调得太快,换手率爆炸,佣金、印花税、冲击成本一层层剥皮;调得太慢,信号早就过期了,你持有的是三周前的观点,alpha 已经被别人赚走。这中间显然有一个最优点,但它在哪里?

大部分人的做法是网格搜一遍持有期,挑回测最好的那个。这样做有两个问题:一是搜出来的最优点自带过拟合嫌疑;二是它没有回答「为什么」——换个市场、换个成本环境,这个数字还成立吗?

正确的框架是把问题拆成两个可测量的量:

  1. Alpha 半衰期:信号的预测能力多少天衰减一半;
  2. 成本-换手曲线:每次调仓付多少,调仓频率决定一年付多少次。

最优持有期是这两者的交点。本文用一个受控模拟把这条逻辑链完整走一遍——受控的好处是真实半衰期是我们自己设的(8 天),任何估计方法的偏差都无处遁形。

实验设计:把半衰期埋进数据里#

200 只股票、2000 个交易日。信号是横截面标准化的 AR(1) 过程,自相关系数由目标半衰期反推:

ρ=0.51/H,H=8 天ρ0.917\rho = 0.5^{1/H}, \quad H = 8 \text{ 天} \Rightarrow \rho \approx 0.917

收益由昨日信号驱动,即期 IC 设为 0.012(横截面策略的现实量级),日波动 2%:

注意收益方程里的 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

信号 IC 随预测滞后的衰减

滞后 1 天的 IC 是 1.65%,之后指数下滑,20 天以外基本淹没在噪声里。对这条曲线拟合 IC(h)=aeλh\text{IC}(h) = a \cdot e^{-\lambda h},半衰期就是 ln2/λ\ln 2 / \lambda

这里有一个不报错但会系统性高估的坑。很多人图省事,对 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 天再平衡一次,每次换仓按单边成本收费:

先看换手的量级。日频调仓的年化单边换手是 145.7 倍——组合每年翻 145 次。持有 8 天降到 42.8 倍,持有 20 天 21.2 倍。换手大致按 1/hold 衰减:

换手率随持有期的衰减

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

净 Sharpe vs 持有期

单边成本日频调仓净 Sharpe最优持有期最优净 Sharpe
0 bp3.051 天3.05
10 bp1.623 天2.02
30 bp-1.2510 天0.94
60 bp-5.5240 天0.47

三个观察:

第一,成本对高换手策略的杀伤是毁灭性的。 同一个信号,日频调仓在零成本下 Sharpe 3.05,30bp 成本下变成 -1.25——不是变差,是变成一台稳定的亏钱机器。145 倍年换手 × 30bp × 2(双边)≈ 年化 8.7% 的确定性成本,而这个信号的毛年化收益撑不起这个数。

第二,最优持有期随成本单调右移,且大致符合平方根规律。 成本从 10bp 涨到 60bp(6 倍),最优持有期从 3 天移到 40 天(约 13 倍)。理论上在指数衰减 + 比例成本假设下最优持有期近似 hc/λh^* \propto \sqrt{c/\lambda},模拟结果与之量级一致。

第三,最优点右侧的曲线很平,左侧很陡。 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 bp2.02(hold=3)2.15(span=3)
30 bp0.94(hold=10)1.28(span=30)
60 bp0.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,而不是全家共用一个调仓频率。这也是为什么成熟的组合框架把「信号加权」和「换手控制」做成两层:混在一起时,慢信号会被快信号绑架着高频调仓,白白喂成本。

已知偏差#

这个实验有三处与真实世界的差距,方向都要说清:

  1. 成本模型只有比例费率,没有市场冲击。 真实冲击成本随交易速度非线性上升,这会进一步惩罚短持有期——最优点比模拟结果更靠右。文中「拿不准选长一点」的结论在真实市场只会更强。
  2. 信号衰减是平稳的。 真实 alpha 半衰期本身会漂移——拥挤的信号衰减越来越快。三年前估的半衰期今天可能已经砍半,这要求半衰期估计滚动更新,而滚动窗口内的样本量比本文 2000 天更小,估计噪声(±3 天量级)更大。
  3. 十分位多空 + 等权是最简组合构造。 带风险模型和优化器的组合中,换手还受协方差矩阵变化驱动,成本-换手曲线会更复杂,但「半衰期定调仓节奏」的主逻辑不变。

结论#

三句话带走:

  1. 先估半衰期,再定调仓频率——顺序不能反。估计时用全部 IC 点做非线性拟合,别对正值点取对数回归(那会把 8 天估成 16 天);并且记住即使方法正确,±40% 的抽样误差是常态。
  2. 成本翻倍,持有期别只翻一半。最优持有期对成本的弹性接近平方根,且曲线左陡右平——宁慢勿快。
  3. 降换手优先用信号平滑或调仓带,而不是机械拉长持有期。同样的换手预算,平滑方案保留了对新信息的持续响应,30bp 成本下净 Sharpe 高出三分之一。

换手率不是策略的副产品,它是你为信息新鲜度支付的价格。半衰期告诉你这份新鲜度值多少钱——别为过期的信息付全价,也别为省钱吃过期的饭

换手率衰减与 Alpha 半衰期:信号该持有多久
https://blog.halo26812.eu.org/blog/turnover-decay-alpha-halflife
Author halo
Published at 2026年7月31日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨