第 30 讲:业绩归因实战:赚的是 Beta 还是纯 Alpha?
本讲核心目标
在机构量化投资中,每一位基金经理都必须回答主权财富基金、社保基金与大型机构 LP 的“灵魂拷问”:“你的策略今年跑赢基准 15 个百分点,究竟是因为选股模型拥有真正的 Alpha 预测力,还是因为偷偷超配了微盘股、赌对了行业赛道,亦或仅仅是承担了未被计量的隐性 Beta 风险?”
如果量化团队无法给出精确到基点(bps)的数学业绩归因,任何阶段性的超额都会被机构投资者视为“不可持续的侥幸赌博”。本讲融合 MSCI Barra US-E3 经典模型手册 与 华泰证券多因子体系:深入推导 Brinson-Fachler 行业配置与选股三因素模型;解构 Barra 事后多因子收益剥离模型;呈现行业经典的 US-E3 四维风险分解魔方(Risk Decomposition Cube);推导 因子边际主动风险贡献(FMCAR);并掌握 Carino 多期复利几何链接算法!
1. 经典文献与工业界理论基石
量化投资业绩归因与风险分解领域的经典文献与学术丰碑:
- Brinson, Gary P., & Fachler, Nimrod (1985). "Measuring Non-US Equity Portfolio Performance", The Journal of Portfolio Management, 11(3): 73-78.
提出了著名的 Brinson-Fachler(BF)模型,将行业配置基准修正为行业相对大盘的超额表现,成为全球买方机构标准规范。
- BARRA Inc. (1998). United States Equity Version 3 (US-E3) Risk Model Handbook, Chapter 4 (Modern Portfolio Management and Risk).
确立了投资组合管理的“四大风险分解维度(Modes of Risk Decomposition)”以及著名的三维风险魔方(Risk Decomposition Cube),彻底理顺了基准、主动、系统与特异风险的关系。
- 华泰证券研究所 (林晓明、陈烨, 2016). 《华泰多因子模型体系初探——华泰多因子系列之一》, 第五部分:多因子模型的绩效分析.
系统推导了主动组合收益率的 Jensen Alpha 回归、基于多因子的收益归因方程与因子对主动风险的边际贡献(FMCAR)风险归因算法。
- Carino, David R. (1999). "Combining Attribution Effects Over Time", The Journal of Performance Measurement, 4(4): 5-14.
提出了基于对数转换因子的多期复合归因链接算法(Carino Linking),完美解决了单期可加归因在多期复利下的几何扭曲难题。
- Grinold, Richard C., & Kahn, Ronald N. (1999). Active Portfolio Management, Chapter 17 (Performance Attribution). McGraw-Hill.
奠定了基于因子暴露的历史主动收益分解方程,打通了事前风险预算与事后业绩归因的同构闭环。
2. 为什么买方机构必须严苛区分 Beta 与 Alpha?
在专业资管机构中,依靠行业暴露或市值下沉带来的超额,被称为廉价的 Beta(Cheap Beta);只有在严格剥离了市场大盘(Market)、行业轮动(Industry)与风格漂移(Style)之后剩下的个股特异性收益(Idiosyncratic Return),才是能够收取高昂业绩提成的纯 Alpha(Pure Alpha)!
3. Brinson-Fachler 归因模型严密数学解构
设组合与基准均覆盖
3.1 符号系统与加总核对
:组合与基准在行业 上的配置权重,严格满足 ; :组合与基准在行业 内部的加权收益率; :组合总毛收益率; :基准总毛收益率; :组合与基准的交易摩擦费用率。
净主动超额收益为:
3.2 Brinson-Fachler 三因素数学定义
Brinson & Fachler (1985) 将毛主动收益分解为三个具有明确管理学意义的正交项:
1. 行业配置效应(Allocation Effect, )
衡量基金经理在各行业上的超配/低配决策带来的收益贡献:
- 经济学逻辑:若行业
跑赢基准大盘( ),且组合超配了该行业( ),则配置效应为正;反之若超配了表现落后大盘的行业,配置效应为负。
2. 个股选择效应(Selection Effect, )
衡量基金经理在行业内部挑选个股优于该行业基准表现带来的贡献:
- 经济学逻辑:在基准权重
约束下,该行业内持股的平均收益率超越行业基准的幅度。
3. 交互效应(Interaction Effect, )
行业权重主动偏离与个股选择优势的交叉乘积项:
4. 完美加总恒等式验证
将全市场所有行业的各项效应累加,数学上严格成立:
现金类别不可或缺
许多简陋的教学代码只对股票资产做归因。假设组合留存
4. 多期几何复合归因链接(Carino Linking Algorithm)
在实际基金季报与年报中,投资期包含多个调仓子期(
单期归因项直接简单累加会产生巨大的几何残差扭曲。
4.1 Carino 对数链接因子数学推导
Carino (1999) 证明,通过定义复合对数转换标度因子,可将多期几何收益精确转化为可加线性形式:
设第
整个多期大区间的全局连续对数转换因子为:
定义第
利用该链接乘数对单期归因项进行时间缩放,多期累计总效应满足严密可加性:
5. BARRA US-E3 经典风险分解四分法与 3D 风险魔方
MSCI Barra US-E3 风险手册第 4 章指出,投资组合的风险分解存在四种经典视角,构成了对投资组合全方位的解剖:
这四种模式在几何上等价于沿着三个正交维度切分整个风险蛋糕:
- 维度一:系统性风险(Systematic) vs 残差风险(Residual);
- 维度二:公共因子风险(Common Factor) vs 特异风险(Specific);
- 维度三:基准风险(Benchmark) vs 主动偏离风险(Active)。
6. Barra 结构化因子收益归因与 FMCAR 风险归因对齐
华泰证券多因子报告提出,买方不仅要出具收益归因表,更要把收益来源与风险消耗(FMCAR)并排对照,检验单位风险收益比!
6.1 收益归因方程
主动超额收益分解为:
6.2 风险归因方程(基于 FMCAR)
组合总主动跟踪误差为
6.3 收益-风险对齐体检表(工业标准看板)
如果某个风格因子(例如 Size 或 Beta)贡献了
7. 工业级归因对账引擎实战
在 tutorial/examples/lecture_30_performance_attribution_brinson_barra.py 中,我们实现了生产级 AsharePerformanceAttributionEngine:
- 支持完整的 Brinson-Fachler 行业配置、个股选择、交互项与现金对账拆解;
- 原生支持 Barra 结构化因子损益剥离,精确核对行业收益、风格收益与特异残差;
- 输出基点级(bps)精确度的加总核对报告,闭合误差严格低于
。
可在终端直接运行验证:
python3 -m examples.lecture_30_performance_attribution_brinson_barra"""单期、期初权重的 Brinson 与因子归因;显式包含现金、费用及闭合检查。"""
from dataclasses import dataclass
import numpy as np
import polars as pl
def inputs(portfolio_weights, benchmark_weights, stock_returns, cash_return, portfolio_cost, benchmark_cost):
p, b, r = [np.asarray(x, dtype=float) for x in (portfolio_weights, benchmark_weights, stock_returns)]
if p.ndim != 1 or not len(p) or p.shape != b.shape or p.shape != r.shape:
raise ValueError('股票权重与收益须为等长非空向量')
if not all(np.isfinite(x).all() for x in (p, b, r)) or not np.isfinite([cash_return, portfolio_cost, benchmark_cost]).all():
raise ValueError('归因输入须全部有限')
if min(p.min(), b.min(), portfolio_cost, benchmark_cost) < 0 or p.sum() > 1 + 1e-10 or b.sum() > 1 + 1e-10:
raise ValueError('本例仅支持非负、无杠杆权重;费用须非负')
if r.min() < -1 or cash_return < -1:
raise ValueError('简单收益不能低于 -100%')
cp, cb = max(0., 1 - p.sum()), max(0., 1 - b.sum())
gross = float((p - b) @ r + (cp - cb) * cash_return)
cost_effect = benchmark_cost - portfolio_cost
return p, b, r, cp, cb, gross, cost_effect
@dataclass
class BrinsonAttributionReport:
total_active_return: float # 扣费后主动收益
gross_active_return: float
allocation_effect: float
selection_effect: float
interaction_effect: float
cost_effect: float
reconciliation_error: float
industry_details: pl.DataFrame
def print_summary(self):
print('Brinson 单期归因(含现金类别,费用单列)')
print(self.industry_details)
print(f'毛主动收益={self.gross_active_return:.4%},费用贡献={self.cost_effect:.4%},'
f'净主动收益={self.total_active_return:.4%},闭合误差={self.reconciliation_error:.3g}')
@dataclass
class BarraAttributionReport:
total_active_return: float
industry_pnl: float
style_pnl_by_factor: dict
total_style_pnl: float
residual_pnl: float
cash_pnl: float
cost_effect: float
reconciliation_error: float
def print_summary(self):
print('因子归因:行业、风格、未解释股票残差、现金与费用')
print(f'主动收益={self.total_active_return:.4%},行业={self.industry_pnl:.4%},'
f'风格={self.total_style_pnl:.4%},股票残差={self.residual_pnl:.4%},'
f'现金={self.cash_pnl:.4%},费用={self.cost_effect:.4%}')
print(self.style_pnl_by_factor)
print(f'闭合误差={self.reconciliation_error:.3g};残差不是已证明可持续的选股能力。')
class AsharePerformanceAttributor:
def compute_brinson_attribution(self, portfolio_weights, benchmark_weights, stock_returns,
industry_names, *, cash_return=0., portfolio_cost=0., benchmark_cost=0.):
"""期初权重、同区间总收益;现金权重由剩余权重补齐,成本为占期初资产的比例。
期内有交易时应按成交事件切分子期,再选择明确的多期链接方法;不可用期末权重倒算。
对基准未覆盖的行业,rb=0 仅为分解约定,不等于真实行业基准收益。
"""
p, b, r, cp, cb, gross, fee = inputs(portfolio_weights, benchmark_weights, stock_returns,
cash_return, portfolio_cost, benchmark_cost)
if len(industry_names) != len(p) or any(not isinstance(s, str) or not s or s == '__CASH__' for s in industry_names):
raise ValueError('行业名称须完整,且不能使用保留名称 __CASH__')
p, b, r = np.append(p, cp), np.append(b, cb), np.append(r, cash_return)
names = np.array([*industry_names, '__CASH__'])
rb_total = float(b @ r)
rows = []
for name in sorted(set(names)):
mask = names == name
wp, wb = p[mask].sum(), b[mask].sum()
rb = float(b[mask] @ r[mask] / wb) if wb > 0 else 0.
rp = float(p[mask] @ r[mask] / wp) if wp > 0 else rb
allocation = (wp - wb) * (rb - rb_total)
selection = wb * (rp - rb)
interaction = (wp - wb) * (rp - rb)
rows.append(dict(industry=name, port_weight=wp, bench_weight=wb,
allocation=allocation, selection=selection, interaction=interaction,
total_contrib=allocation + selection + interaction))
details = pl.DataFrame(rows).sort('total_contrib', descending=True)
a, s, i = [float(details[c].sum()) for c in ('allocation', 'selection', 'interaction')]
error = gross + fee - (a + s + i + fee)
if abs(error) > 1e-10:
raise RuntimeError('Brinson 归因不闭合')
return BrinsonAttributionReport(gross + fee, gross, a, s, i, fee, error, details)
def compute_barra_attribution(self, portfolio_weights, benchmark_weights, stock_returns,
exposure_matrix, factor_returns, factor_names, n_industries=6,
*, cash_return=0., portfolio_cost=0., benchmark_cost=0.):
p, b, r, cp, cb, gross, fee = inputs(portfolio_weights, benchmark_weights, stock_returns,
cash_return, portfolio_cost, benchmark_cost)
X, f = np.asarray(exposure_matrix, float), np.asarray(factor_returns, float)
if X.ndim != 2 or X.shape[0] != len(p) or f.shape != (X.shape[1],) or len(factor_names) != len(f):
raise ValueError('暴露、因子收益、名称必须按证券和因子顺序对齐')
if len(set(factor_names)) != len(f) or not isinstance(n_industries, int) or not 0 <= n_industries <= len(f) or not np.isfinite(X).all() or not np.isfinite(f).all():
raise ValueError('因子输入须有限,名称唯一且行业列数合法')
h = p - b
factor_pnl = (X.T @ h) * f
industry = float(factor_pnl[:n_industries].sum())
styles = dict(zip(factor_names[n_industries:], map(float, factor_pnl[n_industries:])))
style = sum(styles.values())
residual = float(h @ (r - X @ f))
cash = float((cp - cb) * cash_return)
error = gross + fee - (industry + style + residual + cash + fee)
if abs(error) > 1e-10:
raise RuntimeError('因子归因不闭合')
return BarraAttributionReport(gross + fee, industry, styles, style, residual, cash, fee, error)
if __name__ == '__main__':
rng = np.random.default_rng(42)
n = 100
names = ['行业A', '行业B']
industries = [names[i % 2] for i in range(n)]
X = np.column_stack([np.eye(2)[np.arange(n) % 2], rng.normal(size=n)])
f = np.array([.002, -.001, .003])
returns = X @ f + rng.normal(0, .01, n)
benchmark = np.full(n, 1 / n)
portfolio = rng.uniform(.1, 1, n)
portfolio *= .9 / portfolio.sum() # 10% 现金也必须归因
engine = AsharePerformanceAttributor()
engine.compute_brinson_attribution(portfolio, benchmark, returns, industries,
portfolio_cost=.0002).print_summary()
engine.compute_barra_attribution(portfolio, benchmark, returns, X, f,
names + ['Size'], n_industries=2, portfolio_cost=.0002).print_summary()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
8. 本讲小结与核心自测
| 核心考点 | 工业标准回答要点 | 常见工程/学术陷阱 |
|---|---|---|
| Brinson-Fachler 与原始 Brinson 模型的本质区别? | BF 模型将行业收益与基准大盘总收益作差 | 原始模型若超配了绝对收益为正、但跑输大盘的行业,仍会错误计算出正向配置贡献。 |
| 为什么单期归因不能直接相加作为多期复合归因? | 复合复利效应 | 忽视几何链接,导致年报归因各项相加与基金净值年累计超额产生数个百分点的巨大对账偏差。 |
| US-E3 的四种风险分解模式分别适用于什么场景? | 总风险适用于中性对冲基金;系统-残差适用于大盘择时策略;主动风险适用于指增基金;主动系统-残差适用于剥离 Beta 择时后的纯多因子选股。 | 所有策略一律只看总波动率,混淆了系统性大盘风险与主动管理能力。 |
既然我们已经掌握了在事后如何用显微镜看透组合的每一分得失,那么在策略上线实盘之后的每一分每一秒,我们该如何防范黑天鹅穿仓、监控模型退化并自动触发熔断止损?
在下一讲中,我们将进入全书收官之作——第 31 讲:实盘运行监控与风控预警看板,构建包含跟踪误差漂移告警、Barra 风格监控、三级熔断降仓与 PSI 特征漂移检测的工业级防御要塞!