portfolio-attribution
Skill16 starsChanged 8 months agoMITView on GitHub
Decomposing portfolio returns into allocation, selection, and factor contributions. Attribution decomposes the active return (portfolio return minus benchmark return) into components that explain where and how value was added or lost. Two major frameworks: Decomposes active return into three effects for each sector s: Allocation Effect: did the manager overweight sectors that outperformed? Selection Effect: did the manager pick better stocks within each sector? Interaction Effect: combined effect of overweighting sectors where stock picks were also good Total active return:…
In this repository83 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- live-trading-monitoringSkill
- monte-carlo-simulationSkill
- overfitting-preventionSkill
- research-notebooksSkill
- strategy-evaluationSkill
- walk-forward-optimizationSkill
- cross-chain-arbitrageSkill
- crypto-derivativesSkill
- defi-protocolsSkill
- mev-strategiesSkill
- on-chain-analyticsSkill
- stablecoin-analysisSkill
- tokenomicsSkill
- yield-farmingSkill
- alternative-dataSkill
- data-qualitySkill
- data-vendorsSkill
- feature-engineeringSkill
- sentiment-analysisSkill
- web-scraping-financeSkill
- binomial-treesSkill
- black-scholesSkill
- credit-derivativesSkill
- exotic-optionsSkill
- hedging-strategiesSkill
- interest-rate-derivativesSkill
- monte-carlo-pricingSkill
- structured-productsSkill
- vol-surfacesSkill
- vol-tradingSkill
- execution-algosSkill
- latency-optimizationSkill
- market-microstructure-theorySkill
- order-flow-analysisSkill
- performance-metricsSkill
- smart-order-routingSkill
- tick-data-processingSkill
- transaction-cost-analysisSkill
- central-bank-analysisSkill
- commodities-fundamentalsSkill
- economic-indicatorsSkill
- fx-fundamentalsSkill
- geopolitical-riskSkill
- yield-curve-analysisSkill
- alternative-allocationsSkill
- benchmark-trackingSkill
- black-littermanSkill
- factor-investingSkill
- markowitz-optimizationSkill
- rebalancing-strategiesSkill
- risk-paritySkill
- alpha-researchSkill
- bayesian-methodsSkill
- deep-learning-tradingSkill
- factor-modelsSkill
- kalman-filtersSkill
- ml-for-financeSkill
- regime-detectionSkill
- The first 60 shown.
The file
Files
Portfolio Performance Attribution
Decomposing portfolio returns into allocation, selection, and factor contributions.
When to Activate
- User needs to explain why a portfolio outperformed or underperformed its benchmark
- Implementing Brinson-Fachler attribution for equity portfolios
- Building factor-based attribution using regression or risk models
- Performing fixed income attribution (duration, credit, curve)
- Linking single-period attribution across multiple periods
Core Concepts
What Is Attribution?
Attribution decomposes the active return (portfolio return minus benchmark return) into components that explain where and how value was added or lost. Two major frameworks:
- Holdings-based (Brinson): uses portfolio and benchmark weights and returns by sector/region
- Returns-based (factor regression): regresses portfolio returns on factor returns to identify exposures
- Risk-model-based: uses a multi-factor risk model to decompose returns into factor and specific components
Brinson-Fachler Attribution
Decomposes active return into three effects for each sector s:
Allocation Effect: did the manager overweight sectors that outperformed?
AE_s = (w_p,s - w_b,s) * (R_b,s - R_b)Selection Effect: did the manager pick better stocks within each sector?
SE_s = w_b,s * (R_p,s - R_b,s)Interaction Effect: combined effect of overweighting sectors where stock picks were also good
IE_s = (w_p,s - w_b,s) * (R_p,s - R_b,s)Total active return: R_p - R_b = ΣAE_s + ΣSE_s + ΣIE_s
Where:
- w_p,s, w_b,s: portfolio and benchmark weight in sector s
- R_p,s, R_b,s: portfolio and benchmark return in sector s
- R_b: total benchmark return
Factor-Based Attribution
Uses a multi-factor risk model (e.g., Barra, Axioma, Northfield):
R_p - R_f = Σ (β_p,k - β_b,k) * f_k + α_specificWhere β_p,k is the portfolio's exposure to factor k, and f_k is the factor return. Decomposes into:
- Factor timing: return from active factor bets
- Specific return: stock-level alpha (idiosyncratic return)
Methodology
Step 1: Brinson Attribution Implementation
import pandas as pd
import numpy as np
def brinson_fachler(portfolio_weights, benchmark_weights,
portfolio_returns, benchmark_returns):
"""
Single-period Brinson-Fachler attribution by sector.
All inputs are Series indexed by sector.
"""
total_bench_return = (benchmark_weights * benchmark_returns).sum()
allocation = (portfolio_weights - benchmark_weights) * (benchmark_returns - total_bench_return)
selection = benchmark_weights * (portfolio_returns - benchmark_returns)
interaction = (portfolio_weights - benchmark_weights) * (portfolio_returns - benchmark_returns)
result = pd.DataFrame({
'port_weight': portfolio_weights,
'bench_weight': benchmark_weights,
'port_return': portfolio_returns,
'bench_return': benchmark_returns,
'allocation': allocation,
'selection': selection,
'interaction': interaction,
'total_active': allocation + selection + interaction
})
# Verify: sum of total_active should equal portfolio return - benchmark return
port_return = (portfolio_weights * portfolio_returns).sum()
bench_return = (benchmark_weights * benchmark_returns).sum()
assert abs(result['total_active'].sum() - (port_return - bench_return)) < 1e-10
return resultStep 2: Multi-Period Linking
Single-period attribution does not compound correctly across periods. Linking methods:
Carino Method (logarithmic smoothing):
def carino_linking(single_period_attributions, portfolio_returns, benchmark_returns):
"""
Link single-period Brinson attribution across T periods using Carino's method.
"""
T = len(portfolio_returns)
# Cumulative returns
cum_port = np.prod(1 + portfolio_returns) - 1
cum_bench = np.prod(1 + benchmark_returns) - 1
# Carino scaling factors
k_t = []
for t in range(T):
rp = portfolio_returns[t]
rb = benchmark_returns[t]
if abs(rp - rb) < 1e-12:
k_t.append(1.0 / (1 + cum_port))
else:
ln_ratio_t = np.log(1 + rp) - np.log(1 + rb)
ln_ratio_total = np.log(1 + cum_port) - np.log(1 + cum_bench)
k_t.append((ln_ratio_t / (rp - rb)) / (ln_ratio_total / (cum_port - cum_bench)))
# Scale each period's attribution by k_t
linked = {}
for effect in ['allocation', 'selection', 'interaction']:
linked[effect] = sum(
k_t[t] * single_period_attributions[t][effect]
for t in range(T)
)
return linkedGRAP Method (geometric): another popular linking approach used by major custodians.
Step 3: Factor-Based Attribution
def factor_attribution(portfolio_exposures, benchmark_exposures,
factor_returns, specific_returns):
"""
Attribution using a multi-factor risk model.
portfolio_exposures: DataFrame (dates x factors) of portfolio factor exposures
benchmark_exposures: DataFrame (dates x factors) of benchmark factor exposures
factor_returns: DataFrame (dates x factors) of realized factor returns
specific_returns: Series of portfolio-level specific returns
"""
active_exposures = portfolio_exposures - benchmark_exposures
# Factor contribution = active exposure * factor return (each period)
factor_contrib = active_exposures * factor_returns
# Group by factor category
style_factors = ['Value', 'Momentum', 'Quality', 'Size', 'Volatility']
industry_factors = [f for f in factor_returns.columns if f.startswith('IND_')]
attribution = {
'style': factor_contrib[style_factors].sum(axis=1).sum(),
'industry': factor_contrib[industry_factors].sum(axis=1).sum(),
'specific': specific_returns.sum(),
'total_active': factor_contrib.sum(axis=1).sum() + specific_returns.sum()
}
return attributionStep 4: Fixed Income Attribution
Fixed income attribution requires different decomposition:
def fi_attribution(portfolio, benchmark):
"""
Fixed income attribution components:
1. Income return (coupon/yield carry)
2. Treasury curve effect (parallel shift + twist + curvature)
3. Spread change effect (credit spread widening/tightening)
4. Selection effect (individual security alpha)
"""
components = {}
# Income effect: difference in portfolio vs benchmark yield
components['income'] = portfolio['yield'] - benchmark['yield']
# Duration effect: sensitivity to parallel rate move
rate_change = benchmark['rate_change']
components['duration'] = -(portfolio['duration'] - benchmark['duration']) * rate_change
# Curve positioning: key rate duration attribution
for tenor in ['2Y', '5Y', '10Y', '30Y']:
krd_active = portfolio[f'krd_{tenor}'] - benchmark[f'krd_{tenor}']
components[f'curve_{tenor}'] = -krd_active * benchmark[f'rate_change_{tenor}']
# Spread effect
spread_change = benchmark['spread_change']
components['spread_duration'] = -(portfolio['spread_duration'] - benchmark['spread_duration']) * spread_change
# Credit selection (residual)
components['selection'] = portfolio['total_return'] - benchmark['total_return'] - sum(components.values())
return componentsStep 5: Attribution Reporting
def attribution_report(attribution_df, title="Performance Attribution"):
"""
Standard attribution report format.
"""
report = f"""
{title}
{'='*60}
Portfolio Return: {port_ret:>8.2%}
Benchmark Return: {bench_ret:>8.2%}
Active Return: {active_ret:>8.2%}
--- Decomposition by Sector ---
{'Sector':<20} {'Alloc':>8} {'Select':>8} {'Inter':>8} {'Total':>8}
{'-'*60}
"""
for sector, row in attribution_df.iterrows():
report += f" {sector:<20} {row['allocation']:>8.2%} {row['selection']:>8.2%} "
report += f"{row['interaction']:>8.2%} {row['total_active']:>8.2%}\n"
report += f" {'-'*60}\n"
report += f" {'TOTAL':<20} {attribution_df['allocation'].sum():>8.2%} "
report += f"{attribution_df['selection'].sum():>8.2%} "
report += f"{attribution_df['interaction'].sum():>8.2%} "
report += f"{attribution_df['total_active'].sum():>8.2%}\n"
return reportExamples
Equity Attribution by Sector
# Sector weights and returns
sectors = ['Technology', 'Healthcare', 'Financials', 'Energy', 'Consumer']
port_w = pd.Series([0.35, 0.20, 0.15, 0.10, 0.20], index=sectors)
bench_w = pd.Series([0.28, 0.15, 0.18, 0.12, 0.27], index=sectors)
port_r = pd.Series([0.08, 0.05, 0.03, -0.02, 0.04], index=sectors)
bench_r = pd.Series([0.06, 0.04, 0.04, -0.01, 0.03], index=sectors)
result = brinson_fachler(port_w, bench_w, port_r, bench_r)
# Allocation to Tech (overweight in strong sector) = positive
# Selection in Tech (8% vs 6%) = positiveInterpreting Results
Key insights to surface:
- Was alpha from sector allocation (top-down) or stock selection (bottom-up)?
- Which sectors had the largest attribution? Is it consistent over time or a one-off?
- Is the interaction effect large? If so, the manager may be making correlated allocation and selection bets
- Does attribution match the stated investment process? (A bottom-up manager should show selection, not allocation)
Quality Gate
- Attribution sums exactly to total active return (no residual beyond rounding)
- Multi-period attribution uses a proper linking method (Carino, GRAP, or Menchero)
- Sector/region classification is consistent between portfolio and benchmark
- Transaction effects excluded from holdings-based attribution (or accounted for separately)
- Currency effects separated from local returns for international portfolios
- Factor-based attribution uses a risk model consistent with the portfolio's universe
- Interaction effect interpreted correctly — not lumped into allocation or selection arbitrarily
- Reported at multiple levels: total, sector, country, and individual security
- For fixed income: duration, curve, and spread effects isolated before reporting selection
- Attribution is stable and explainable — large unexplained residuals indicate model problems
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

