Performance Attribution for Quant Strategies

Performance attribution answers the only question that matters about a track record: how much of the return is replicable risk premia you could buy cheaply, and how much is genuine, market-neutral alpha. For a quant this is not reporting hygiene — it is a statistical estimation problem with all the usual traps: autocorrelated residuals, omitted factors, multiple testing, and in-sample overfitting. This article treats attribution as inference: the regression decomposition and its standard-error pitfalls, the Brinson allocation/selection framework, and how to judge whether an estimated alpha is real or an artifact of the estimation itself.

The decomposition and what can go wrong

In the one-factor (CAPM) world, excess returns decompose as:

r_t - rf = alpha + beta * (r_mkt_t - rf) + e_t

with beta the market exposure, alpha the average return not explained by the market, and e_t idiosyncratic noise. The multi-factor generalization regresses excess returns on a basket of factors — market, size, value, momentum, quality:

r_t - rf = alpha + sum_k beta_k * f_k_t + e_t

Each beta_k times the factor's mean return is that factor's contribution to your P&L; the intercept alpha plus residuals is what the model cannot explain. The estimation hazards are specific and consequential:

PitfallEffect on alpha estimateFix
OLS standard errorsOverstated significanceHAC (Newey-West) errors
Omitted factorInflated alpha (tilt leaks in)Broad, pre-committed factor set
Too many factorsUnstable, meaningless betasParsimony, regularization
In-sample onlyCurve-fit alphaOut-of-sample loadings
Multiple testingSpurious "significant" alphaDeflate the t-stat

Regression attribution done honestly

The mechanics are a few lines, but the standard-error treatment is what separates a defensible attribution from a misleading one.

import numpy as np
import pandas as pd
import statsmodels.api as sm

def factor_attribution(strategy_excess: pd.Series,
                       factors: pd.DataFrame) -> pd.DataFrame:
    """Regress strategy excess returns on factor returns (all excess/long-short)."""
    X = sm.add_constant(factors)
    model = sm.OLS(strategy_excess, X, missing="drop").fit(
        cov_type="HAC", cov_kwds={"maxlags": 5})        # robust to autocorr
    betas = model.params
    contrib = (betas[factors.columns] * factors.mean()).rename("contribution")
    summary = pd.DataFrame({
        "beta": betas[factors.columns],
        "t_stat": model.tvalues[factors.columns],
        "contribution_ann": contrib * 252,
    })
    alpha_ann = betas["const"] * 252
    t_alpha = model.tvalues["const"]
    print(f"Annualized alpha: {alpha_ann:.2%} (t={t_alpha:.2f}), "
          f"R^2={model.rsquared:.2f}, n={int(model.nobs)}")
    return summary

Two details are non-negotiable. First, use HAC (Newey-West) standard errors: strategy returns are autocorrelated and heteroskedastic, and ordinary errors will understate the true uncertainty, manufacturing alpha that is actually noise. Second, judge the alpha by its t-statistic, not its sign — a positive alpha with t = 0.8 is indistinguishable from luck.

The multiple-testing correction

The subtler problem is that you did not test one strategy — you tested many, and you are reporting the survivor. A conventional t > 2 threshold is far too lenient after a search. The deflated Sharpe / multiple-testing adjustment raises the bar with the number of trials:

from scipy.stats import norm

def deflated_t_hurdle(num_trials, alpha=0.05):
    """Bonferroni-style two-sided t-hurdle that rises with trials tested."""
    return norm.ppf(1 - (alpha / num_trials) / 2)

# Tried 50 variants? Demand t > ~3.2 on the survivor's alpha, not 2.0
print(round(deflated_t_hurdle(50), 2))

If the alpha's t-stat does not clear the deflated hurdle, you have not found skill — you have found the best of many noise draws. See deflated Sharpe and multiple testing and strategy significance testing for the full treatment, and pair the regression with Sharpe ratio analysis to size the alpha relative to its own volatility.

The covariance subtlety in contributions

Decomposing P&L into factor contributions is only clean when the factors are mutually orthogonal. Real factors are correlated — value and momentum, quality and low-vol — so the naive betak mean(fk) split double-counts shared variance and the individual contributions become ambiguous. The honest approaches are either to orthogonalize the factors first (sequential or symmetric orthogonalization) and report contributions in that basis, or to report the joint* explained variance and treat the per-factor split as indicative rather than exact. Quietly summing correlated contributions and presenting them as a precise decomposition is one of the most common attribution errors.

Time-varying exposures and rolling betas

Static OLS betas assume the strategy's factor loadings are constant over the whole sample, which is rarely true — a strategy may de-risk in crises, rotate factors, or adapt its signal, so its exposures drift. Estimating a single beta then averages over genuinely different regimes and can both hide real factor timing and misstate alpha. Two standard fixes: rolling-window regression to visualize loading drift, or a Kalman filter to track betas as latent states. The rolling view is usually enough to catch the problem.

import pandas as pd
import statsmodels.api as sm

def rolling_betas(strategy_excess, factors, window=252):
    out = {}
    X = sm.add_constant(factors)
    for end in range(window, len(X)):
        sl = slice(end - window, end)
        m = sm.OLS(strategy_excess.iloc[sl], X.iloc[sl]).fit()
        out[X.index[end]] = m.params
    return pd.DataFrame(out).T

If a beta swings widely through the sample, the full-sample attribution is suspect: the "alpha" may be the residue of timing a factor well (real skill the static model cannot see) or of an unhedged exposure that only shows up in certain regimes. Either way, time-varying loadings are the rule for adaptive and machine-learning strategies, and a static regression quietly misattributes their P&L.

Brinson allocation and selection

For portfolios managed against a benchmark across segments (sectors, regions, asset classes), the Brinson model splits active return into two intuitive effects. For segment i with portfolio weight wi, benchmark weight Wi, portfolio segment return ri, benchmark segment return bi, and total benchmark return b:

allocation_i = (w_i - W_i) * (b_i - b)      # bet on where to be
selection_i  = W_i * (r_i - b_i)            # bet on what to pick
interaction_i = (w_i - W_i) * (r_i - b_i)   # the cross term

Summed across segments, allocation + selection + interaction reconciles exactly to total active return. The diagnostic value is real: a manager whose edge is all selection with no allocation skill should be risk-managed completely differently from a top-down timer. The interaction term is small but should be reported, not silently folded into selection, which misattributes combined effects.

A worked breakdown

Suppose a strategy returned 12% over a period and the regression yields:

SourceExposure (beta)Factor returnContribution
Market0.807%5.6%
Value (HML)0.504%2.0%
Momentum0.306%1.8%
Alpha (intercept)2.6%

Roughly 9.4% came from factor exposures available cheaply in ETFs; only ~2.6% is candidate alpha — and that 2.6% is only credible if its t-stat clears the deflated hurdle and survives out of sample. If adding one more well-known factor collapses the alpha toward zero, you never had alpha; you had an unhedged factor tilt.

A rigorous workflow

  1. Compute excess returns over the risk-free rate.
  2. Regress on a broad, pre-committed factor set so inadvertent exposures are captured

rather than dumped into alpha.

  1. Use HAC standard errors and judge alpha by its robust t-stat.
  2. Deflate the t-stat for the number of specifications you tried.
  3. Estimate loadings in-sample, judge alpha out-of-sample — in-sample alpha is partly

curve fit.

  1. Track stability across windows: real alpha persists; spurious alpha appears and

vanishes as you add factors or roll the sample.

The first thing to inspect is the correlation of your returns with each factor — a high correlation with a cheap factor is the early warning that your "edge" is repackaged beta.

Risk-based vs return-based attribution

The regression above is return-based attribution: it infers exposures from the time series of returns. The complementary approach is holdings-based (or risk-based) attribution, which computes exposures directly from the positions you actually held against a factor-risk model. The two answer different questions and should agree if the model is right. Return-based attribution is cheap and needs only the return stream, but it is noisy and assumes stable loadings; holdings-based attribution is precise and contemporaneous but requires position-level data and a maintained risk model. When they disagree, it is usually a sign of either nonlinear exposure (options, dynamic hedging) or a missing factor — and that disagreement is itself diagnostic information worth chasing down.

A second discipline is to attribute risk, not just return. Decompose realized portfolio variance into factor and specific components using the same factor covariance that drives construction; a strategy whose return is mostly alpha but whose risk is mostly a single factor is a hidden concentration that will surface in the wrong regime. Return attribution tells you where the P&L came from last period; risk attribution tells you where the next drawdown will come from, and the second is often the more actionable.

Honest limits

Attribution is conditional on the factor set: a factor you omit becomes alpha, and a factor you include can absorb genuine skill if it is itself partly your strategy. Linear regression assumes stable loadings, but real strategies change exposure over time (especially anything adaptive or machine-learning based), so static betas can misstate both contributions and alpha. And no amount of robust inference rescues a short sample — with two years of monthly data you simply cannot distinguish a 3% alpha from zero. Treat attribution as the discipline that tells you, before your investors do, whether your returns are skill or rented beta; treat any single number it produces as an estimate with a wide, honestly reported confidence interval.

#performance attribution #factor analysis #alpha #beta #quant finance