PerformanceAttribution
Alpha/beta decomposition, factor attribution, per-strategy P&L breakdown
PerformanceAttribution is v1’s module for answering “where did our P&L come from?“. It decomposes returns into alpha (skill) and beta (market exposure), runs factor attribution against common factors (market, size, value, momentum), and breaks P&L down per strategy.
Import
python
from horizon.fund._performance_attribution import (
PerformanceAttribution,
AttributionResult,
FactorAttribution,
)
What it decomposes
Alpha Return **above** what the portfolio's beta exposure would predict. This is skill: the part you can't explain with passive market exposure.
Beta Return explained by market exposure. Not skill. you'd get this by just holding the benchmark in the same amount.
Factor loadings How much of the return is explained by the Fama-French factors (market, size, value, momentum, quality, low-vol) + residual.
Per-strategy P&L When you run multiple strategies, attribute total P&L to each strategy's contribution.
The math
Alpha/beta regression:
r_p(t) = α + β × r_m(t) + ε(t)
Where:
r_p(t)is portfolio return at time tr_m(t)is market (benchmark) returnαis the intercept (alpha)βis the slope (beta)ε(t)is the residual
Significance via t-statistic on α. Alpha is “real” if |t| > 2.
Factor attribution extends to multiple factors:
r_p(t) = α + β_mkt × r_mkt + β_size × r_size + β_val × r_val + β_mom × r_mom + ε(t)
API
python
class PerformanceAttribution:
def __init__(
self,
benchmark: str = "SPY",
risk_free_rate: float = 0.04,
) -> None:
...
def alpha_beta(
self,
portfolio_returns: list[float],
benchmark_returns: list[float],
) -> AttributionResult:
"""Regression-based alpha/beta decomposition."""
def factor_attribution(
self,
portfolio_returns: list[float],
factor_returns: dict[str, list[float]],
) -> FactorAttribution:
"""Multi-factor regression attribution."""
def per_strategy_pnl(
self,
trades: list[Trade],
) -> dict[str, float]:
"""Sum realized P&L per strategy_id."""
def brinson_attribution(
self,
portfolio_weights: dict[str, float],
portfolio_returns: dict[str, float],
benchmark_weights: dict[str, float],
benchmark_returns: dict[str, float],
) -> BrinsonResult:
"""Brinson-Fachler allocation/selection/interaction attribution."""
Result types
python
@dataclass(frozen=True)
class AttributionResult:
alpha: float # daily alpha
alpha_annualized: float
beta: float
t_stat_alpha: float # significance of alpha
t_stat_beta: float
r_squared: float # how much of return is explained
n_observations: int
information_ratio: float # alpha / residual std
@dataclass(frozen=True)
class FactorAttribution:
alpha: float
factor_loadings: dict[str, float] # per-factor beta
factor_contributions: dict[str, float] # per-factor P&L contribution
residual: float # unexplained
r_squared: float
@dataclass(frozen=True)
class BrinsonResult:
allocation_effect: float # from weight choices
selection_effect: float # from security picks
interaction_effect: float # cross term
total_active: float
Usage
python
from horizon.fund._performance_attribution import PerformanceAttribution
attr = PerformanceAttribution(benchmark="SPY", risk_free_rate=0.04)
# Alpha/beta vs benchmark
portfolio_rets = [0.002, -0.001, 0.004, ...] # daily
benchmark_rets = [0.001, -0.002, 0.003, ...] # daily SPY
result = attr.alpha_beta(portfolio_rets, benchmark_rets)
print(f"Alpha: {result.alpha_annualized:.2%} annualized")
print(f" t-stat: {result.t_stat_alpha:.2f} ({'significant' if abs(result.t_stat_alpha) > 2 else 'NOT significant'})")
print(f"Beta: {result.beta:.3f}")
print(f"Information ratio: {result.information_ratio:.2f}")
print(f"R²: {result.r_squared:.2%} of return explained by benchmark")
# Multi-factor attribution
factor_rets = {
"market": benchmark_rets,
"size": small_cap_minus_large_cap_returns,
"value": value_minus_growth_returns,
"momentum": momentum_returns,
}
fa = attr.factor_attribution(portfolio_rets, factor_rets)
print(f"\nFactor attribution:")
print(f" Alpha: {fa.alpha:.3%}")
for factor, loading in fa.factor_loadings.items():
contribution = fa.factor_contributions[factor]
print(f" {factor}: β={loading:+.3f}, contribution={contribution:+.3%}")
print(f" Residual: {fa.residual:+.3%}")
Per-strategy attribution
When running a Horizon multi-strategy backtest, attribute P&L per strategy:
python
import horizon as hz
from horizon.fund._performance_attribution import PerformanceAttribution
from horizon.quant import TSMomentum, BollingerMeanRev
result = hz.run(
mode="backtest",
strategies=[
TSMomentum(lookback=20),
BollingerMeanRev(window=20),
],
...
)
attr = PerformanceAttribution()
pnl_by_strategy = attr.per_strategy_pnl(result.trades)
for strategy_name, pnl in sorted(pnl_by_strategy.items(), key=lambda x: -x[1]):
print(f" {strategy_name}: ${pnl:,.2f}")
Brinson attribution
Brinson-Fachler decomposes active return (portfolio vs benchmark) into:
- Allocation effect: did the portfolio overweight sectors that outperformed?
- Selection effect: within each sector, did we pick the right stocks?
- Interaction effect: cross term between the two
python
portfolio_weights = {"Tech": 0.40, "Finance": 0.20, "Energy": 0.10, ...}
portfolio_returns = {"Tech": 0.08, "Finance": 0.03, "Energy": 0.01, ...}
benchmark_weights = {"Tech": 0.30, "Finance": 0.15, "Energy": 0.10, ...}
benchmark_returns = {"Tech": 0.06, "Finance": 0.02, "Energy": 0.02, ...}
brinson = attr.brinson_attribution(
portfolio_weights, portfolio_returns,
benchmark_weights, benchmark_returns,
)
print(f"Allocation: {brinson.allocation_effect:+.2%}")
print(f"Selection: {brinson.selection_effect:+.2%}")
print(f"Interaction: {brinson.interaction_effect:+.2%}")
print(f"Total active return: {brinson.total_active:+.2%}")
When to use
End-of-period reporting Monthly / quarterly reviews where you need to explain returns to stakeholders.
Strategy selection Deciding which strategies to keep in the book: the ones with significant alpha (t-stat > 2) are keepers.
Risk budget reallocation Move capital from low-alpha strategies to high-alpha ones, conditional on correlation.
Understanding what you own "We made 10% this quarter" is meaningless if 8% came from market beta and 2% from stock selection. Factor attribution makes this explicit.
Pitfalls
Source
python/horizon/fund/_performance_attribution.py. ~350 lines.