4. 行业轮动信号中的交易成本建模与净收益修正
把“毛收益”拆成可核算的净收益
行业动量轮动策略的净值曲线通常先由行业指数收益算出,这条曲线默认“零成本、无摩擦”。但真实调仓要付双边佣金、卖出印花税,还要承受大额换手带来的冲击成本。本节在例3的月度调仓动量组合基础上,建立一套可复算的成本模型,把毛收益逐期扣减为净收益,并用换手率量化侵蚀程度。
成本口径先固定下来,避免方向与符号混乱。设单边佣金率 c、卖出印花税率 s、单边冲击成本率 k(以成交金额为基数)。一次调仓中,若某行业权重从 w_old 变为 w_new,则换手额 turn = |w_new − w_old|。买入方向只付佣金与冲击成本,卖出方向额外付印花税,因此单期组合成本率为:
cost_t = Σ_i [ c·|Δw_i| + k·|Δw_i| + s·max(0, w_old,i − w_new,i) ]
= (c + k)·Σ_i|Δw_i| + s·Σ_i max(0, w_old,i − w_new,i)
教学示例参数取:单边佣金 c = 0.0003(万三),卖出印花税 s = 0.0005(千分之五,仅卖出方),单边冲击成本 k = 0.0010(千分之一,假设中小资金、行业ETF流动性中等)。这些是示例假设值,实际应按券商费率、标的流动性与下单方式替换。
用一段可运行代码完成毛/净收益对比
下面代码自包含:构造 6 个行业的月度收益矩阵与动量信号,按例3的“过去N日动量横截面排序、取前2名等权、月度调仓”逻辑生成权重,再逐期扣成本,输出毛净值、净净值、累计换手与侵蚀比例。所有随机数据由固定种子生成,结果可复现,属于教学示例而非实测。
import numpy as np
import pandas as pd
rng = np.random.default_rng(20240604)
T, N = 60, 6 # 60个月,6个行业
industries = [f"IND{i}" for i in range(1, N+1)]
# 示例:各行业月度收益(含少量持续性,便于动量有信号)
base = rng.normal(0.006, 0.05, size=(T, N))
mom_persist = np.zeros((T, N))
for t in range(1, T):
mom_persist[t] = 0.3 * base[t-1] + 0.7 * base[t]
rets = pd.DataFrame(mom_persist, columns=industries)
# 动量信号:过去3个月累计收益,横截面排序取前2
lookback, top_k = 3, 2
weights = pd.DataFrame(0.0, index=rets.index, columns=industries)
for t in range(lookback, T):
signal = (1 + rets.iloc[t-lookback:t]).prod() - 1
picks = signal.nlargest(top_k).index
weights.loc[rets.index[t], picks] = 1.0 / top_k
# 成本参数(示例假设)
c, s, k = 0.0003, 0.0005, 0.0010
gross_nav, net_nav = [1.0], [1.0]
turnover_list, cost_list = [], []
prev_w = pd.Series(0.0, index=industries)
for t in range(T):
w = weights.iloc[t]
dw = (w - prev_w).abs()
sell = (prev_w - w).clip(lower=0)
cost = (c + k) * dw.sum() + s * sell.sum()
port_ret = (w * rets.iloc[t]).sum()
gross_nav.append(gross_nav[-1] * (1 + port_ret))
net_nav.append(net_nav[-1] * (1 + port_ret - cost))
turnover_list.append(dw.sum())
cost_list.append(cost)
prev_w = w
result = pd.DataFrame({
"gross_nav": gross_nav[1:],
"net_nav": net_nav[1:],
"turnover": turnover_list,
"cost": cost_list,
})
print(result.tail())
print("累计换手:", round(sum(turnover_list), 3))
print("毛收益:", round(gross_nav[-1]-1, 4),
"净收益:", round(net_nav[-1]-1, 4),
"侵蚀:", round((gross_nav[-1]-net_nav[-1])/ (gross_nav[-1]-1), 4))
运行后重点看三件事:一是 turnover 序列,月度调仓若每期都大幅换仓,累计换手会远高于1;二是 cost 序列,它随换手线性放大,卖出方向还叠加印花税;三是毛净值与净净值末值的差,即成本侵蚀的绝对幅度。把侵蚀额除以毛收益增量,得到侵蚀比例,可直接用于判断该策略在给定费率下是否还值得执行。
k 不是常数。资金规模越大、标的成交越薄,k 越高。建议对 k 做 0.0005 / 0.0010 / 0.0020 三档敏感性测试,观察净收益曲线何时由正转负,这个临界点就是策略的容量边界。换手率如何侵蚀轮动收益
下图把单期调仓的成本构成与净值分叉关系画在一起:左侧是权重变化如何分别触发佣金、印花税与冲击成本,右侧是毛净值与净净值随累计换手拉开差距的示意。图中数值为示例参数下的结构性关系,用于理解传导路径,不代表某次真实回测结果。
结果判读与可执行检查
把代码输出整理成一张对照表,是本节最直接的交付物。下表字段可直接填入自己的回测结果,用于横向比较不同费率假设下的策略可行性。
| 指标 | 含义 | 示例判读 |
|---|---|---|
| 累计换手 | 各期 Σ|Δw| 之和 | 月度全换仓时约 2×调仓次数,明显偏高 |
| 累计成本 | 各期 cost_t 之和 | 与换手近似线性,卖出方向更重 |
| 毛收益 | 未扣成本净值末值 − 1 | 策略信号本身的收益上限 |
| 净收益 | 扣成本后净值末值 − 1 | 真实可得的近似收益 |
| 侵蚀比例 | (毛收益 − 净收益) / 毛收益 | 超过约 30% 需警惕换手过频 |
| 临界冲击成本 | 使净收益转负的 k 值 | 低于该值策略才具备容量 |
完成本节后,应能回答:给定费率与换手序列,净收益比毛收益少多少、侵蚀比例多大、冲击成本升到多少时策略失效。这三个答案就是后续例5做ETF可交易性验证、例6做宏观条件收益统计时的成本基准。