第四章 多期Brinson归因模型:从单期到多期的跨越
单期Brinson归因,说白了就是给某一特定时间段(比如一个月)的业绩拍一张快照。但投资是连续的过程,客户更关心的是「这一年你到底怎么赚的」。把单期结果简单累加?嗯,这里有个坑——交互效应会重复计算。
我刚开始做多期归因时,直接拿单期结果相加,结果发现总超额收益和实际对不上。后来才明白,多期归因的核心挑战在于:如何把各期的超额收益「干净地」链接起来。
4.1 多期归因的三大挑战
先说说为什么不能简单累加。假设组合两个月的数据:
| 月份 | 组合收益 | 基准收益 | 超额收益 |
|---|---|---|---|
| 1月 | +10% | +5% | +5% |
| 2月 | -5% | -2% | -3% |
简单相加:5% + (-3%) = 2%。但实际累计超额收益应该是 (1+10%)/(1+5%) × (1-5%)/(1-2%) - 1 ≈ 1.86%。差了0.14个百分点。这就是复利效应带来的偏差。
多期归因面临三个核心挑战:
- 复利扭曲:各期超额收益在复利作用下不能线性相加
- 交互效应漂移:单期的交互效应在多期链接中会「变形」
- 路径依赖:同样的单期结果,不同顺序会产生不同的累计值
核心矛盾:单期归因是加法模型(超额收益 = 配置 + 选股 + 交互),多期归因是乘法模型(累计收益 = ∏(1+r_i))。把加法结果塞进乘法框架,必然产生残差。
4.2 链接算法:算术 vs 几何
解决多期链接问题,业界主要有两条路:算术链接和几何链接。我个人的习惯是:给客户汇报用算术,内部精细分析用几何。为什么?往下看。
4.2.1 算术链接(Arithmetic Linking)
算术链接的思路很直接:把各期超额收益按某种权重加权求和。最常用的是对数差分法:
# 算术链接 - 对数差分法
import numpy as np
def arithmetic_linking(period_returns, benchmark_returns):
"""
period_returns: 各期组合收益列表
benchmark_returns: 各期基准收益列表
"""
excess_log = np.log(1 + np.array(period_returns)) - np.log(1 + np.array(benchmark_returns))
total_excess = np.exp(np.sum(excess_log)) - 1
return total_excess
# 示例
port = [0.10, -0.05, 0.03]
bench = [0.05, -0.02, 0.01]
print(f"算术链接超额收益: {arithmetic_linking(port, bench):.4%}")
算术链接的优点是可加性好——各期贡献加起来等于总超额。但缺点也很明显:权重依赖于基准收益的波动,解释起来有点绕。
避坑指南:我曾经用算术链接做归因,发现某期权重出现负数。查了半天才发现,当基准收益为负且绝对值大于组合收益时,对数差分会给出负权重。遇到这种情况,建议改用几何链接。
4.2.2 几何链接(Geometric Linking)
几何链接更符合直觉:把超额收益看作一个独立的「超额因子」。
# 几何链接
def geometric_linking(period_returns, benchmark_returns):
"""
几何链接:超额因子连乘
"""
excess_factors = [(1+r_p)/(1+r_b) for r_p, r_b in zip(period_returns, benchmark_returns)]
total_excess = np.prod(excess_factors) - 1
return total_excess
port = [0.10, -0.05, 0.03]
bench = [0.05, -0.02, 0.01]
print(f"几何链接超额收益: {geometric_linking(port, bench):.4%}")
几何链接的优点是数学上干净——总超额就是各期超额因子的连乘积减1。但缺点也很要命:各期贡献不能直接相加,给客户解释时很费劲。
| 对比维度 | 算术链接 | 几何链接 |
|---|---|---|
| 数学形式 | 加权求和 | 连乘 |
| 可加性 | ✅ 各期贡献可加 | ❌ 不可直接相加 |
| 解释难度 | 中等(权重需说明) | 较高(需理解因子概念) |
| 适用场景 | 客户汇报、合规披露 | 内部精细分析、研究 |
4.3 Carino算法详解
有没有一种方法,既能保留算术链接的可加性,又能避免对数差分的权重问题?有,这就是Carino算法。我个人非常喜欢这个方法,它在实际项目中帮我解决了不少麻烦。
4.3.1 算法原理
Carino算法的核心思想是:用一个「缩放因子」把各期超额收益调整到同一个量纲下。具体来说:
- 计算整个区间的总超额收益(几何链接)
- 计算各期的「Carino因子」——该期收益对总收益的边际贡献
- 用Carino因子缩放各期超额收益,使其加起来等于总超额
def carino_decomposition(period_returns, benchmark_returns):
"""
Carino算法:将总超额收益分解到各期
返回各期贡献和总超额
"""
port = np.array(period_returns)
bench = np.array(benchmark_returns)
# 1. 计算总超额收益(几何)
total_excess = np.prod((1+port)/(1+bench)) - 1
# 2. 计算Carino因子
# 公式:k_t = [ln(1+r_p,t) - ln(1+r_b,t)] / (r_p,t - r_b,t)
# 当收益差为0时,k_t = 1/(1+r_p,t)
diff = port - bench
log_diff = np.log(1+port) - np.log(1+bench)
# 处理除零情况
k = np.where(np.abs(diff) > 1e-10, log_diff / diff, 1/(1+port))
# 3. 计算总缩放因子
total_k = np.sum(k * diff)
# 4. 计算各期贡献
contributions = (diff * k / total_k) * total_excess
return contributions, total_excess
# 示例
port = [0.10, -0.05, 0.03]
bench = [0.05, -0.02, 0.01]
contrib, total = carino_decomposition(port, bench)
print(f"各期贡献: {contrib}")
print(f"总超额收益: {total:.4%}")
print(f"贡献之和: {np.sum(contrib):.4%}")
4.3.2 Carino算法的优势
- 完美可加:各期贡献之和严格等于总超额收益
- 无权重争议:不需要引入外部权重,完全由数据驱动
- 数学优雅:从对数收益的线性性质出发,逻辑自洽
注意:Carino算法在单期收益差为0时会出现除零。代码中我用了一个小阈值来处理,实际项目中建议用极限值替代。另外,当某期收益波动极大(比如超过50%)时,Carino因子可能变得不稳定,需要结合业务判断是否使用。
4.3.3 实战案例:用Carino做多期Brinson归因
把Carino算法和Brinson模型结合,就能得到完整的多期归因结果。下面是一个完整的例子:
def multi_period_brinson(holdings, returns, benchmark_weights, benchmark_returns):
"""
多期Brinson归因(Carino链接)
参数:
holdings: list of dict, 每期组合持仓权重
returns: list of dict, 每期组合各板块收益
benchmark_weights: dict, 基准权重(假设不变)
benchmark_returns: list of dict, 每期基准各板块收益
"""
n_periods = len(holdings)
allocation = []
selection = []
interaction = []
for t in range(n_periods):
# 单期Brinson分解
alloc_t = sum((holdings[t][s] - benchmark_weights[s]) * benchmark_returns[t][s]
for s in holdings[t])
selec_t = sum(benchmark_weights[s] * (returns[t][s] - benchmark_returns[t][s])
for s in returns[t])
inter_t = sum((holdings[t][s] - benchmark_weights[s]) *
(returns[t][s] - benchmark_returns[t][s])
for s in holdings[t])
allocation.append(alloc_t)
selection.append(selec_t)
interaction.append(inter_t)
# 用Carino链接各期
port_returns = [sum(holdings[t][s] * returns[t][s] for s in holdings[t])
for t in range(n_periods)]
bench_returns = [sum(benchmark_weights[s] * benchmark_returns[t][s]
for s in benchmark_weights)
for t in range(n_periods)]
# 计算Carino因子
diff = np.array(port_returns) - np.array(bench_returns)
log_diff = np.log(1+np.array(port_returns)) - np.log(1+np.array(bench_returns))
k = np.where(np.abs(diff) > 1e-10, log_diff / diff, 1/(1+np.array(port_returns)))
total_k = np.sum(k * diff)
total_excess = np.prod((1+np.array(port_returns))/(1+np.array(bench_returns))) - 1
# 分解总超额
alloc_total = np.sum(k * np.array(allocation)) / total_k * total_excess
selec_total = np.sum(k * np.array(selection)) / total_k * total_excess
inter_total = np.sum(k * np.array(interaction)) / total_k * total_excess
return {
'allocation': alloc_total,
'selection': selec_total,
'interaction': inter_total,
'total_excess': total_excess
}
4.4 多期归因知识体系
下面这张图总结了多期Brinson归因的核心脉络:
4.5 选型建议
说了这么多,到底该用哪种方法?我根据实际项目经验,给出几条建议:
- 给监管/客户看:用算术链接(对数差分法),简单易懂,合规部门也认可
- 内部投研分析:用Carino算法,精度高、可加性好,方便做多期对比
- 学术研究/策略回测:用几何链接,数学上最干净,避免任何人为权重干扰
我的经验:在实际项目中,我通常同时跑三种方法。如果三种结果差异很小(比如小于0.1%),说明数据质量好,用哪种都行。如果差异很大,就要回头检查单期归因是否有问题——这往往是数据或模型出错的信号。
多期归因没有「银弹」。理解每种方法的假设和局限,根据场景灵活选择,才是真正的专业能力。嗯,这一章的内容就到这里,希望对你有所帮助。