Skip to content

18.4 策略绩效归因

策略产生了收益(或亏损),下一步是回答最关键的问题:这些收益到底来自哪里?绩效归因(Performance Attribution)就是将组合收益分解到不同的来源,帮助管理者理解策略的赚钱逻辑是否与预期一致,识别意外的风险暴露,并驱动策略的持续优化。

Brinson归因模型

Brinson 模型是投资组合绩效归因的经典框架,最初用于传统基金相对于基准的归因分析,但其思想同样适用于量化策略。

归因的三个效应

$$R_{\text{组合}} - R_{\text{基准}} = \underbrace{\sum_i (w_i^P - w_i^B) R_i^B}_{\text{配置效应(Allocation)}} + \underbrace{\sum_i w_i^B (R_i^P - R_i^B)}_{\text{选择效应(Selection)}} + \underbrace{\sum_i (w_i^P - w_i^B)(R_i^P - R_i^B)}_{\text{交互效应(Interaction)}}$$

其中 wiPwiB 分别是组合和基准在第 i 个板块上的权重,RiPRiB 是相应的收益。

配置效应衡量的是超配或低配某个板块所带来的收益贡献。如果策略超配了一个表现好的板块,配置效应为正。

选择效应衡量的是在每个板块内部,选股能力带来的超额收益。选择效应为正意味着策略在板块内选到了跑赢该板块平均水平的股票。

交互效应是前两者的交叉项,通常被合并到选择效应中或单独报告。

量化策略场景中的适配

传统的 Brinson 模型按行业板块分类,但在量化策略中,可以替换为任何有意义的分类维度:

  • 因子维度:规模因子、价值因子、动量因子、质量因子等
  • 策略维度:趋势跟踪子策略、均值回归子策略、事件驱动子策略等
  • 资产类别维度:股票、期货、债券、期权等
  • 行业维度:传统行业分类(申万一级行业、GICS等)
PYTHON122 行 · 4.7 KB
📄此处有展示代码122 行 · 4.7 KB展开 ▼
python
import numpy as np
import pandas as pd
from typing import List, Tuple


class BrinsonAttribution:
    """Brinson 绩效归因分析"""
    
    def __init__(self, portfolio_weights: pd.DataFrame,
                 benchmark_weights: pd.DataFrame,
                 portfolio_returns: pd.DataFrame,
                 benchmark_returns: pd.DataFrame):
        """
        Parameters
        ----------
        portfolio_weights : pd.DataFrame
            组合在各板块的权重,index=日期,columns=板块
        benchmark_weights : pd.DataFrame
            基准在各板块的权重
        portfolio_returns : pd.DataFrame
            组合中各板块的收益
        benchmark_returns : pd.DataFrame
            基准中各板块的收益
        """
        self.pw = portfolio_weights
        self.bw = benchmark_weights
        self.pr = portfolio_returns
        self.br = benchmark_returns
        
    def decompose(self) -> dict:
        """执行 Brinson 分解"""
        # 确保对齐
        common_dates = self.pw.index.intersection(self.bw.index)\
                                     .intersection(self.pr.index)\
                                     .intersection(self.br.index)
        
        pw = self.pw.loc[common_dates]
        bw = self.bw.loc[common_dates]
        pr = self.pr.loc[common_dates]
        br = self.br.loc[common_dates]
        
        # 总超额收益
        portfolio_total = (pw * pr).sum(axis=1)
        benchmark_total = (bw * br).sum(axis=1)
        total_excess = portfolio_total - benchmark_total
        
        # 配置效应:权重差异 * 基准收益
        allocation = ((pw - bw) * br).sum(axis=1)
        
        # 选择效应:基准权重 * 收益差异
        selection = (bw * (pr - br)).sum(axis=1)
        
        # 交互效应:权重差异 * 收益差异
        interaction = ((pw - bw) * (pr - br)).sum(axis=1)
        
        # 按板块分解
        sector_allocation = (pw - bw) * br
        sector_selection = bw * (pr - br)
        sector_interaction = (pw - bw) * (pr - br)
        
        return {
            'total_excess_return': total_excess,
            'allocation_effect': allocation,
            'selection_effect': selection,
            'interaction_effect': interaction,
            'sector_allocation': sector_allocation,
            'sector_selection': sector_selection,
            'sector_interaction': sector_interaction
        }
    
    def summary_report(self) -> str:
        """生成归因摘要报告"""
        result = self.decompose()
        
        # 年化累计
        total_cum = result['total_excess_return'].cumsum()
        alloc_cum = result['allocation_effect'].cumsum()
        select_cum = result['selection_effect'].cumsum()
        interact_cum = result['interaction_effect'].cumsum()
        
        report = []
        report.append("=" * 60)
        report.append("           Brinson 绩效归因报告")
        report.append("=" * 60)
        report.append("")
        report.append(f"【归因周期】{total_cum.index[0].strftime('%Y-%m-%d')}{total_cum.index[-1].strftime('%Y-%m-%d')}")
        report.append(f"【交易日数】{len(total_cum)}天")
        report.append("")
        report.append("【累计效应分解】")
        report.append(f"  配置效应: {alloc_cum.iloc[-1]:.4%}")
        report.append(f"  选择效应: {select_cum.iloc[-1]:.4%}")
        report.append(f"  交互效应: {interact_cum.iloc[-1]:.4%}")
        report.append(f"  总超额收益: {total_cum.iloc[-1]:.4%}")
        report.append("")
        
        # 各效应的波动率
        report.append("【效应稳定性】")
        report.append(f"  配置效应日波动: {alloc_cum.diff().std():.4%}")
        report.append(f"  选择效应日波动: {select_cum.diff().std():.4%}")
        report.append("")
        
        # 信息比率
        tracking_error = result['total_excess_return'].std()
        info_ratio = result['total_excess_return'].mean() / tracking_error if tracking_error > 0 else 0
        report.append(f"【信息比率】{info_ratio:.2f}")
        
        return '\n'.join(report)
    
    def sector_attribution_heatmap(self) -> pd.DataFrame:
        """生成板块归因热力图数据"""
        result = self.decompose()
        
        total_by_sector = result['sector_allocation'].sum() + \
                          result['sector_selection'].sum() + \
                          result['sector_interaction'].sum()
        
        return pd.DataFrame({
            'allocation': result['sector_allocation'].sum(),
            'selection': result['sector_selection'].sum(),
            'interaction': result['sector_interaction'].sum(),
            'total': total_by_sector
        }).sort_values('total', ascending=False)
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `BrinsonAttribution`(Brinson 绩效归因分析)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

因子归因

Brinson 模型使用板块分类,而因子归因将收益分解到系统性因子层面。这是量化策略更自然的归因视角。

时间序列回归归因

将策略收益作为因变量,一系列因子收益作为自变量,进行时间序列回归:

$$R_t^{\text{策略}} = \alpha + \sum_{k=1}^K \beta_k \cdot F_{k,t} + \varepsilon_t$$

其中 Fk,t 是第 k 个因子的收益率,βk 是策略对该因子的暴露(loading),α 是剥离了所有因子暴露之后的纯超额收益(也被称为"选股 Alpha")。εt 是特质收益(idiosyncratic return)。

关键解读

  • α(截距项)是否显著为正?这是策略的核心竞争力——无法被常见因子解释的超额收益。
  • βk 的符号和大小是否符合预期?如果策略号称是动量策略但对动量因子的暴露不显著,就需要警惕。
  • 模型的 R2 是多少?R2 越高,说明策略收益可以用常见因子解释的比例越高,独立 Alpha 的空间越小。
  • 残差 εt 是否存在自相关?如果存在,可能意味着有未被当前因子集捕获的系统性暴露。

截面回归归因(Fama-MacBeth)

与时间序列回归不同,Fama-MacBeth 方法在每个截面(每个时间点)上做一次回归:

第一步:在每个时间点 t,对所有股票做截面回归,得到因子收益的估计:

$$r_{i,t} = \gamma_{0,t} + \sum_{k=1}^K \gamma_{k,t} \cdot X_{i,k,t-1} + \epsilon_{i,t}$$

第二步:对第一步得到的因子收益序列 γ^k,t 求均值并做 t 检验:

$$\hat{\gamma}_k = \frac{1}{T} \sum_{t=1}^T \hat{\gamma}_{k,t}, \quad t(\hat{\gamma}_k) = \frac{\hat{\gamma}_k}{\text{std}(\hat{\gamma}_{k,t}) / \sqrt{T}}$$

截面归因回答的问题是:在控制了其他因子之后,某个因子是否仍然具有预测收益的能力?

PYTHON75 行 · 2.6 KB
📄此处有展示代码75 行 · 2.6 KB展开 ▼
python
class FactorAttribution:
    """因子归因分析"""
    
    def __init__(self, strategy_returns: pd.Series, 
                 factor_returns: pd.DataFrame):
        """
        Parameters
        ----------
        strategy_returns : pd.Series
            策略日度收益
        factor_returns : pd.DataFrame
            因子日度收益,columns为因子名称
        """
        self.sr = strategy_returns
        self.fr = factor_returns
        
    def time_series_regression(self) -> dict:
        """时间序列回归归因"""
        import statsmodels.api as sm
        
        # 对齐数据
        common = self.sr.index.intersection(self.fr.index)
        X = self.fr.loc[common]
        y = self.sr.loc[common]
        
        X = sm.add_constant(X)
        model = sm.OLS(y, X).fit()
        
        return {
            'summary': model.summary().as_text(),
            'alpha': model.params['const'],
            'alpha_pvalue': model.pvalues['const'],
            'betas': model.params.drop('const').to_dict(),
            'r_squared': model.rsquared,
            'residuals': model.resid
        }
    
    def factor_exposure_decomposition(self) -> pd.DataFrame:
        """分解收益到各因子"""
        result = self.time_series_regression()
        
        decompositions = []
        for factor, beta in result['betas'].items():
            factor_contrib = beta * self.fr[factor].mean() * 252  # 年化
            decompositions.append({
                'factor': factor,
                'beta': beta,
                'annual_contribution': factor_contrib
            })
        
        df = pd.DataFrame(decompositions)
        df['contribution_pct'] = df['annual_contribution'] / \
                                  df['annual_contribution'].abs().sum() * 100
        return df.sort_values('contribution_pct', ascending=False)
    
    def rolling_factor_attribution(self, window: int = 60) -> pd.DataFrame:
        """滚动因子暴露分析:检测因子暴露的变化"""
        import statsmodels.api as sm
        
        common = self.sr.index.intersection(self.fr.index)
        sr = self.sr.loc[common]
        fr = self.fr.loc[common]
        
        rolling_betas = []
        for i in range(window, len(common)):
            X = fr.iloc[i-window:i]
            y = sr.iloc[i-window:i]
            X = sm.add_constant(X)
            model = sm.OLS(y, X).fit()
            
            row = {'date': common[i]}
            row.update(model.params.to_dict())
            rolling_betas.append(row)
        
        return pd.DataFrame(rolling_betas).set_index('date')
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `FactorAttribution`(因子归因分析)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

因子暴露的诊断价值

定期运行因子归因不仅是报告需要,更是一种策略诊断工具

  1. 暴露漂移检测:如果滚动回归显示策略对动量因子的暴露从 +0.3 逐渐变成了 -0.1,这意味着策略的行为已经发生了实质性变化(可能是数据漂移或代码变更导致),需要排查。

  2. 隐藏风险识别:策略可能在某个因子上有巨大的非预期暴露。例如一个"市场中性"策略如果对市场因子仍有显著的 β 暴露(例如 β=0.2),说明中性化做得不够彻底。

  3. Alpha 纯度评估:Pure Alpha(即截距项 α)的比例随时间下降,可能意味着策略被市场逐渐"因子化"——即曾经独特的 Alpha 信号现在变成了一个广为人知的因子。

归因报告自动化

对于管理多个策略的团队,手动编写归因报告效率低下且容易出错。自动化归因系统应该具备以下功能:

报告结构

1. 总览(Executive Summary)
   - 策略总收益 vs 基准收益
   - 超额收益来源饼图(配置 vs 选择 vs 交互)
   - 关键因子暴露概览

2. 因子归因详情
   - 各因子暴露及显著性
   - 因子贡献分解
   - 暴露漂移趋势图(滚动窗口)

3. 板块归因详情
   - Brinson分解表格
   - 板块归因热力图
   - 超配/低配板块排名

4. 异常检测
   - 暴露变化超过2个标准差的因子
   - 残差异常的交易日
   - 与历史归因模式的偏离

5. 策略优化建议
   - 基于归因结果的具体优化建议

自动化实现框架

PYTHON61 行 · 2.2 KB
📄此处有展示代码61 行 · 2.2 KB展开 ▼
python
class AttributionReportAutomation:
    """归因报告自动生成器"""
    
    def __init__(self, strategy_manager):
        self.strategy_manager = strategy_manager
        self.report_history = []
        
    def generate_daily_report(self, date: str):
        """生成每日归因简报"""
        # 获取当日策略收益和因子数据
        pass
    
    def generate_weekly_report(self) -> str:
        """生成周度归因深度报告"""
        report = {
            'period': 'Weekly',
            'strategies': {},
            'summary': {},
            'anomalies': [],
            'recommendations': []
        }
        
        for strategy_id, strategy in self.strategy_manager.get_active_strategies().items():
            # 运行 Brinson 分解
            brinson = BrinsonAttribution(
                strategy.weights, strategy.benchmark_weights,
                strategy.returns, strategy.benchmark_returns
            )
            
            # 运行因子归因
            factor_att = FactorAttribution(
                strategy.returns, self.strategy_manager.factor_returns
            )
            
            report['strategies'][strategy_id] = {
                'brinson': brinson.decompose(),
                'factor_exposures': factor_att.factor_exposure_decomposition(),
                'rolling_betas': factor_att.rolling_factor_attribution()
            }
        
        # 异常检测
        for sid, sreport in report['strategies'].items():
            rolling_betas = sreport['rolling_betas']
            # 计算最近的 z-score
            recent = rolling_betas.iloc[-1]
            hist_mean = rolling_betas.mean()
            hist_std = rolling_betas.std()
            z_scores = (recent - hist_mean) / hist_std
            
            for factor, z in z_scores.items():
                if abs(z) > 2.0:
                    report['anomalies'].append({
                        'strategy': sid,
                        'factor': factor,
                        'z_score': z,
                        'current_exposure': recent[factor],
                        'avg_exposure': hist_mean[factor]
                    })
        
        self.report_history.append(report)
        return report
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `AttributionReportAutomation`(归因报告自动生成器)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

归因结果驱动策略优化

归因分析不应止步于报告,它的真正价值在于驱动策略的持续优化。以下是归因结果最常见的应用场景:

场景一:暴露管理

归因发现策略在"小市值因子"上有显著的负暴露(即集中持有大市值股票),而团队预期是市值中性。优化措施:在信号生成阶段加入市值中性化处理。

PYTHON13 行 · 517 B
📄此处有展示代码13 行 · 517 B展开 ▼
python
def neutralize_exposure(signal: pd.Series, exposure: pd.Series):
    """因子暴露中性化"""
    from scipy import stats
    
    # 在截面上对因子暴露做回归,取残差作为中性化后的信号
    mask = signal.notna() & exposure.notna()
    if mask.sum() < 30:
        return signal
    
    result = stats.linregress(exposure[mask], signal[mask])
    neutralized = signal[mask] - (result.slope * exposure[mask] + result.intercept)
    
    return pd.Series(neutralized, index=signal[mask].index)
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `neutralize_exposure`(因子暴露中性化)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

场景二:子策略权重重分配

如果归因显示某个子策略连续 3 个月贡献负的配置效应,应该降低该子策略的资金分配权重,将资金转移到贡献稳定为正的子策略上。

场景三:因子模型升级

当现有因子模型对策略收益的 R2 持续偏低(如 < 0.3),说明策略收益中有很大一部分无法被当前因子集解释。这可能意味着:

  • 策略确实有独特的 Alpha 来源(好事)
  • 因子模型遗漏了重要的因子(需要补充因子库)

场景四:交易成本归因

在归因中引入交易成本的分解,识别哪些策略、哪些板块的交易成本远高于预期,指导执行算法的优化。

绩效归因不仅仅是"解释过去",更是"改进未来"的工具。一个优秀的量化团队会建立从归因到优化的闭环,让每一份归因报告都能转化为实质性的策略提升。