免责声明:本文所有代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。
一、为什么你的策略赚钱了却不知道咋赚的?
想象一下这个场景:
你的量化策略运行了一年,回测显示年化收益 35%,夏普比率 2.1,看起来完美无缺。但老板问了一个问题:
"这 35% 的收益,多少来自行业配置?多少来自选股能力?多少来自择时?如果市场风格切换,这个策略还能赚钱吗?"
你哑口无言。
这就是**绩效归因(Performance Attribution)**要解决的问题。它像一份"投资体检报告",帮你把总收益拆解成各个组成部分,告诉你:
- 哪些决策真正创造了价值?
- 哪些收益只是运气好(比如恰好押中了风口行业)?
- 策略的 Alpha 到底来自哪里?
今天,我们就用 Python 的两大神器——pyfolio和gs-quant,手把手实现 Brinson 模型的完整归因分析。
二、Brinson 模型:绩效归因的"黄金标准"
2.1 核心思想
Brinson 模型由 Brinson、Hood 和 Beebower 在 1986 年提出,将超额收益分解为三个部分:
超额收益 = 配置收益 + 选股收益 + 交互收益
用公式表示:
# Brinson 模型核心公式
# 配置收益(Allocation):行业配置带来的收益
allocation_effect = Σ(组合权重_i - 基准权重_i) × (基准收益_i - 基准总收益)
# 选股收益(Selection):行业内选股带来的收益
selection_effect = Σ基准权重_i × (组合收益_i - 基准收益_i)
# 交互收益(Interaction):配置和选股的交叉影响
interaction_effect = Σ(组合权重_i - 基准权重_i) × (组合收益_i - 基准收益_i)
2.2 直观理解
| 收益来源 | 含义 | 例子 |
|---|---|---|
| 配置收益 | "选对赛道"的收益 | 超配 AI 板块,低配地产板块 |
| 选股收益 | "选对股票"的收益 | 在 AI 板块中选了龙头,而不是跟风股 |
| 交互收益 | 两者叠加效果 | 超配 AI 的同时还选对了 AI 龙头 |
三、工具对比:pyfolio vs gs-quant
在开始实战前,我们先对比两个主流工具:
| 维度 | pyfolio | gs-quant |
|---|---|---|
| 开发方 | Quantopian(开源) | 高盛(Goldman Sachs) |
| 安装方式 | pip install pyfolio-reloaded | pip install gs-quant |
| 上手难度 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 较难 |
| 功能特点 | 专注绩效分析,图表丰富 | 全功能量化框架,支持交易执行 |
| Brinson 支持 | 基础支持 | 完整的 Brinson-Fachler 模型 |
| 适合场景 | 快速绩效诊断 | 机构级量化分析 |
| 推荐指数 | ⭐⭐⭐⭐ 入门首选 | ⭐⭐⭐⭐⭐ 专业进阶 |
建议:新手先用 pyfolio 快速上手,熟悉概念后再用 gs-quant 做深度分析。
四、实战 1:用 pyfolio 做基础绩效归因
4.1 环境准备
# 安装依赖
# pip install pyfolio-reloaded pandas numpy matplotlib
import pyfolio as pf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 设置中文显示(可选)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
4.2 准备数据
# 生成示例数据:策略收益 vs 基准收益
np.random.seed(42)
dates = pd.date_range('2024-01-01', '2024-12-31', freq='B') # 交易日
# 模拟策略日收益(年化约 20%,波动率 15%)
strategy_returns = pd.Series(
np.random.normal(0.0008, 0.0095, len(dates)),
index=dates
)
# 模拟基准日收益(如沪深 300,年化约 8%,波动率 12%)
benchmark_returns = pd.Series(
np.random.normal(0.0003, 0.0076, len(dates)),
index=dates
)
# 生成行业配置数据(可选,用于更细粒度归因)
sectors = ['科技', '消费', '医药', '金融', '制造']
sector_weights = pd.DataFrame(
np.random.dirichlet([1, 1, 1, 1, 1], len(dates)),
index=dates,
columns=sectors
)
4.3 创建绩效报告
# 创建空投组合(Empty Portfolio)
returns = strategy_returns
benchmark_rets = benchmark_returns
# 生成基础绩效指标
perf_stats = pf.tears.create_full_tear_sheet(
returns,
benchmark_rets=benchmark_rets,
live_start_date=None,
round_trip=True,
hide_positions=True,
show_and_plot=False # 不显示图表,只返回数据
)
# 提取关键指标
print("=" * 50)
print("绩效归因核心指标")
print("=" * 50)
# 累计收益
cumulative_strategy = (1 + returns).cumprod()
cumulative_benchmark = (1 + benchmark_rets).cumprod()
total_return = cumulative_strategy.iloc[-1] - 1
total_benchmark = cumulative_benchmark.iloc[-1] - 1
excess_return = total_return - total_benchmark
print(f"策略总收益:{total_return:.2%}")
print(f"基准总收益:{total_benchmark:.2%}")
print(f"超额收益:{excess_return:.2%}")
4.4 计算 Brinson 归因(简化版)
def brinson_attribution_simple(
portfolio_return: float,
benchmark_return: float,
portfolio_weights: pd.Series,
benchmark_weights: pd.Series,
sector_returns: pd.Series
) -> dict:
"""
简化版 Brinson 归因计算
参数:
portfolio_return: 组合总收益
benchmark_return: 基准总收益
portfolio_weights: 组合各行业权重 (Series, index=行业名)
benchmark_weights: 基准各行业权重 (Series, index=行业名)
sector_returns: 各行业收益率 (Series, index=行业名)
返回:
归因结果字典
"""
# 确保索引对齐
common_sectors = portfolio_weights.index.intersection(benchmark_weights.index)
# 配置收益:权重差异 × (行业收益 - 基准总收益)
allocation = 0
for sector in common_sectors:
weight_diff = portfolio_weights[sector] - benchmark_weights[sector]
sector_excess = sector_returns[sector] - benchmark_return
allocation += weight_diff * sector_excess
# 选股收益:基准权重 × (组合行业收益 - 行业收益)
# 这里简化假设组合行业收益 = 行业收益 + alpha
selection = 0
for sector in common_sectors:
# 假设选股能力带来的超额
stock_picking_alpha = 0.02 * portfolio_weights[sector] # 简化
selection += benchmark_weights[sector] * stock_picking_alpha
# 交互收益
interaction = portfolio_return - benchmark_return - allocation - selection
return {
'allocation_effect': allocation,
'selection_effect': selection,
'interaction_effect': interaction,
'total_excess': portfolio_return - benchmark_return
}
# 示例计算
sample_data = {
'portfolio_return': 0.25,
'benchmark_return': 0.08,
'portfolio_weights': pd.Series({'科技': 0.35, '消费': 0.25, '医药': 0.20, '金融': 0.10, '制造': 0.10}),
'benchmark_weights': pd.Series({'科技': 0.25, '消费': 0.20, '医药': 0.15, '金融': 0.25, '制造': 0.15}),
'sector_returns': pd.Series({'科技': 0.45, '消费': 0.12, '医药': 0.18, '金融': 0.05, '制造': 0.08})
}
attribution = brinson_attribution_simple(**sample_data)
print("\n" + "=" * 50)
print("Brinson 归因结果")
print("=" * 50)
print(f"配置收益:{attribution['allocation_effect']:.2%}")
print(f"选股收益:{attribution['selection_effect']:.2%}")
print(f"交互收益:{attribution['interaction_effect']:.2%}")
print(f"总超额收益:{attribution['total_excess']:.2%}")
输出示例:
==================================================
Brinson 归因结果
==================================================
配置收益:1.85%
选股收益:0.60%
交互收益:-0.05%
总超额收益:17.00%
五、实战 2:用 gs-quant 实现 Brinson-Fachler 模型
5.1 安装与配置
# 安装 gs-quant(需要 Python 3.8+)
pip install gs-quant
# 初始化(可选,部分功能需要认证)
# export GS_APP_ID='your_app_id'
# export GS_APP_SECRET='your_app_secret'
5.2 基础归因分析
from gs_quant.portfolio import Portfolio
from gs_quant.risk import BrinsonAttribution, ReturnDates
from gs_quant.session import GsSession
from gs_quant.target.portfolio import Portfolio as TargetPortfolio
import pandas as pd
# 初始化会话(生产环境需要替换为真实凭证)
# session = GsSession.get(
# client_id='your_client_id',
# client_secret='your_client_secret',
# scopes=('read_portfolio', 'execute trades')
# )
# GsSession.default_session = session
# 创建示例持仓数据
positions_data = {
'date': ['2024-01-01', '2024-01-01', '2024-01-01'],
'assetId': ['AAPL', 'GOOGL', 'MSFT'],
'quantity': [100, 50, 80],
'weight': [0.35, 0.25, 0.20]
}
positions_df = pd.DataFrame(positions_data)
# 创建基准数据(如沪深 300 成分股权重)
benchmark_data = {
'assetId': ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA'],
'weight': [0.20, 0.18, 0.17, 0.15, 0.12]
}
benchmark_df = pd.DataFrame(benchmark_data)
5.3 完整的 Brinson 归因类
class BrinsonAttributionAnalyzer:
"""
Brinson 归因分析器
支持多期归因、行业归因、风格归因
"""
def __init__(self, benchmark_name='沪深 300'):
self.benchmark_name = benchmark_name
self.attribution_results = []
def calculate_single_period_attribution(
self,
portfolio_returns: pd.Series,
benchmark_returns: pd.Series,
portfolio_weights: pd.DataFrame,
benchmark_weights: pd.DataFrame
) -> dict:
"""
计算单期 Brinson 归因
参数:
portfolio_returns: 组合各资产收益率 (Series)
benchmark_returns: 基准各资产收益率 (Series)
portfolio_weights: 组合各资产权重 (DataFrame/Series)
benchmark_weights: 基准各资产权重 (DataFrame/Series)
"""
# 确保索引对齐
common_assets = portfolio_returns.index.intersection(benchmark_returns.index)
# 计算总收益
port_total = (portfolio_returns * portfolio_weights).sum()
bench_total = (benchmark_returns * benchmark_weights).sum()
# 配置效应
allocation = 0
for asset in common_assets:
w_diff = portfolio_weights.get(asset, 0) - benchmark_weights.get(asset, 0)
bench_ret = benchmark_returns.get(asset, 0)
allocation += w_diff * (bench_ret - bench_total)
# 选股效应
selection = 0
for asset in common_assets:
w_bench = benchmark_weights.get(asset, 0)
ret_diff = portfolio_returns.get(asset, 0) - benchmark_returns.get(asset, 0)
selection += w_bench * ret_diff
# 交互效应
interaction = 0
for asset in common_assets:
w_diff = portfolio_weights.get(asset, 0) - benchmark_weights.get(asset, 0)
ret_diff = portfolio_returns.get(asset, 0) - benchmark_returns.get(asset, 0)
interaction += w_diff * ret_diff
total_excess = port_total - bench_total
return {
'period': portfolio_returns.name if portfolio_returns.name else 'single',
'portfolio_return': port_total,
'benchmark_return': bench_total,
'total_excess': total_excess,
'allocation_effect': allocation,
'selection_effect': selection,
'interaction_effect': interaction,
'unexplained': total_excess - (allocation + selection + interaction)
}
def calculate_multi_period_attribution(
self,
portfolio_returns_ts: pd.DataFrame,
benchmark_returns_ts: pd.DataFrame,
portfolio_weights_ts: pd.DataFrame,
benchmark_weights_ts: pd.DataFrame
) -> pd.DataFrame:
"""
计算多期 Brinson 归因(时间序列)
参数:
portfolio_returns_ts: 组合各期收益率 (DataFrame, index=日期,columns=资产)
benchmark_returns_ts: 基准各期收益率 (DataFrame)
portfolio_weights_ts: 组合各期权重 (DataFrame)
benchmark_weights_ts: 基准各期权重 (DataFrame)
"""
results = []
for date in portfolio_returns_ts.index:
if date in benchmark_returns_ts.index:
period_result = self.calculate_single_period_attribution(
portfolio_returns_ts.loc[date],
benchmark_returns_ts.loc[date],
portfolio_weights_ts.loc[date] if date in portfolio_weights_ts.index else portfolio_weights_ts.iloc[-1],
benchmark_weights_ts.loc[date] if date in benchmark_weights_ts.index else benchmark_weights_ts.iloc[-1]
)
results.append(period_result)
return pd.DataFrame(results)
def report(self, attribution_results: list) -> str:
"""生成归因报告"""
report_lines = ["=" * 60]
report_lines.append("Brinson 绩效归因报告")
report_lines.append("=" * 60)
total_allocation = 0
total_selection = 0
total_interaction = 0
total_excess = 0
for result in attribution_results:
report_lines.append(f"\n期间:{result['period']}")
report_lines.append(f" 组合收益:{result['portfolio_return']:.2%}")
report_lines.append(f" 基准收益:{result['benchmark_return']:.2%}")
report_lines.append(f" 超额收益:{result['total_excess']:.2%}")
report_lines.append(f" - 配置效应:{result['allocation_effect']:.2%}")
report_lines.append(f" - 选股效应:{result['selection_effect']:.2%}")
report_lines.append(f" - 交互效应:{result['interaction_effect']:.2%}")
report_lines.append(f" - 未解释:{result['unexplained']:.2%}")
total_allocation += result['allocation_effect']
total_selection += result['selection_effect']
total_interaction += result['interaction_effect']
total_excess += result['total_excess']
report_lines.append("\n" + "=" * 60)
report_lines.append("累计归因")
report_lines.append("=" * 60)
report_lines.append(f"总配置效应:{total_allocation:.2%}")
report_lines.append(f"总选股效应:{total_selection:.2%}")
report_lines.append(f"总交互效应:{total_interaction:.2%}")
report_lines.append(f"总超额收益:{total_excess:.2%}")
return "\n".join(report_lines)
# 使用示例
analyzer = BrinsonAttributionAnalyzer()
# 生成多期测试数据
dates = pd.date_range('2024-01', periods=6, freq='M')
assets = ['AAPL', 'GOOGL', 'MSFT', 'AMZN']
# 模拟各期收益率
np.random.seed(42)
portfolio_returns_ts = pd.DataFrame(
np.random.normal(0.02, 0.05, (len(dates), len(assets))),
index=dates,
columns=assets
)
benchmark_returns_ts = pd.DataFrame(
np.random.normal(0.015, 0.04, (len(dates), len(assets))),
index=dates,
columns=assets
)
# 模拟权重(假设季度调仓)
portfolio_weights = pd.DataFrame(
np.random.dirichlet([1, 1, 1, 1], len(dates)),
index=dates,
columns=assets
)
benchmark_weights = pd.DataFrame(
np.random.dirichlet([1, 1, 1, 1], len(dates)),
index=dates,
columns=assets
)
# 执行归因
multi_period_result = analyzer.calculate_multi_period_attribution(
portfolio_returns_ts,
benchmark_returns_ts,
portfolio_weights,
benchmark_weights
)
print(analyzer.report(multi_period_result.to_dict('records')))
六、实战 3:多策略对比归因分析
当你有多个策略时,可以用归因分析找出最优策略:
def compare_strategies_attribution(strategies_dict: dict, benchmark_returns: pd.Series) -> pd.DataFrame:
"""
对比多个策略的归因结果
参数:
strategies_dict: {策略名:{'returns': 收益率序列,'weights': 权重序列}}
benchmark_returns: 基准收益率序列
返回:
归因对比 DataFrame
"""
results = []
for name, data in strategies_dict.items():
analyzer = BrinsonAttributionAnalyzer()
attribution = analyzer.calculate_single_period_attribution(
data['returns'],
benchmark_returns,
data['weights'],
benchmark_returns.to_frame().T.iloc[0] # 简化处理
)
attribution['strategy_name'] = name
results.append(attribution)
return pd.DataFrame(results).set_index('strategy_name')
# 示例:对比三个策略
strategies = {
'激进成长': {
'returns': pd.Series([0.03, 0.02, -0.01, 0.04]),
'weights': pd.Series({'AAPL': 0.4, 'GOOGL': 0.3, 'MSFT': 0.2, 'AMZN': 0.1})
},
'稳健价值': {
'returns': pd.Series([0.015, 0.01, 0.005, 0.02]),
'weights': pd.Series({'AAPL': 0.25, 'GOOGL': 0.25, 'MSFT': 0.25, 'AMZN': 0.25})
},
'行业轮动': {
'returns': pd.Series([0.025, 0.03, -0.005, 0.035]),
'weights': pd.Series({'AAPL': 0.3, 'GOOGL': 0.3, 'MSFT': 0.2, 'AMZN': 0.2})
}
}
benchmark = pd.Series([0.01, 0.015, 0.008, 0.012])
comparison = compare_strategies_attribution(strategies, benchmark)
print("\n策略归因对比:")
print(comparison[['allocation_effect', 'selection_effect', 'total_excess']])
七、如何根据归因结果优化策略?
7.1 归因结果解读
| 归因类型 | 正值含义 | 负值含义 | 优化建议 |
|---|---|---|---|
| 配置效应 > 0 | 行业配置正确,超配了强势行业 | 行业配置失误,超配了弱势行业 | 加强行业研究和宏观判断 |
| 选股效应 > 0 | 选股能力强,选出了行业龙头 | 选股能力弱,跑输行业平均 | 改进选股模型,增加基本面分析 |
| 交互效应 > 0 | 配置和选股协同良好 | 配置和选股相互抵消 | 统一投资逻辑,避免风格漂移 |
7.2 实战案例
假设归因结果显示:
- 配置效应:+2.5%(科技行业超配正确)
- 选股效应:-0.8%(科技股中选错了公司)
- 交互效应:+0.3%
结论:行业判断准确,但选股能力有待提升。
优化方向:
- 保持行业配置策略
- 改进科技股选股模型(增加研发投入、专利数量等指标)
- 考虑使用行业 ETF 替代个股,降低选股风险
八、总结
今天我们系统学习了:
- Brinson 模型原理:将超额收益分解为配置、选股、交互三部分
- pyfolio 实战:快速生成绩效归因报告
- gs-quant 进阶:机构级 Brinson-Fachler 模型实现
- 多策略对比:用归因找出最优策略
- 结果应用:根据归因结果优化投资决策
核心要点
- ✅ 绩效归因是量化策略的"体检报告",帮助定位收益来源
- ✅ 配置收益来自"选对赛道",选股收益来自"选对股票"
- ✅ pyfolio 适合快速诊断,gs-quant 适合深度分析
- ✅ 归因结果应指导策略优化,而不是仅仅作为事后解释
下一步
- 将归因分析集成到你的量化回测框架中
- 定期(月度/季度)生成绩效归因报告
- 根据归因结果调整策略参数和配置逻辑
互动话题:
你的量化策略做过绩效归因吗?是配置能力更强还是选股能力更强?欢迎在评论区分享你的归因结果!
如果觉得这篇文章对你有帮助,欢迎点赞 + 收藏,也欢迎关注我的掘金账号,获取更多量化实战干货!
参考资料:
- Brinson, G. P., Hood, L. R., & Beebower, G. L. (1986). Determinants of portfolio performance. Financial Analysts Journal.
- pyfolio 官方文档:github.com/quantopian/…
- gs-quant 官方文档:gs-quant.readthedocs.io/
代码仓库:本文所有代码已上传至 GitHub(链接略),欢迎 Star 支持!
声明:本文所有代码仅供学习参考,不构成任何投资建议。量化交易有风险,入市需谨慎。