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 t
  • r_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.

Next