25 — Brinson Attribution, Transaction Costs, and Rebalancing¶
Free learning pack¶
- CFA active portfolio management page
reference/concepts/brinson_attribution.md,transaction_costs_and_rebalancing.md
Do not search for more material until these are insufficient.
PREDICT¶
A PM was overweight a sector that outperformed (good allocation) but picked the worst-performing names within it (bad selection). Could the portfolio still beat the benchmark overall? Could it still lose to the benchmark despite the good sector call?
Formula¶
allocation = (w_p - w_b) * r_b
selection = w_b * (r_p - r_b)
interaction = (w_p - w_b) * (r_p - r_b)
These sum to total active return.
import numpy as np
sectors = ["Tech", "Financials", "Energy"]
w_p = np.array([0.45, 0.25, 0.30])
w_b = np.array([0.30, 0.35, 0.35])
r_p = np.array([0.12, 0.05, -0.02])
r_b = np.array([0.15, 0.06, 0.01])
# MANUAL FIRST:
# compute allocation, selection, and interaction per sector, then confirm
# they sum (across all three effects and all three sectors) to
# w_p @ r_p - w_b @ r_b.
allocation = None
selection = None
interaction = None
print(allocation, selection, interaction)
PREDICT (rebalancing cost)¶
Rebalancing back to target weights always has a transaction cost. Under what circumstance would a PM choose not to fully rebalance despite drift from target?
Formula¶
trade_i = (target_weight_i - current_weight_i) * PortfolioValue
cost_i = |trade_i| * cost_bps / 10,000
current_weights = np.array([0.45, 0.25, 0.30])
target_weights = np.array([0.30, 0.35, 0.35])
portfolio_value = 50_000_000
cost_bps = 8
# MANUAL FIRST:
# compute the dollar trade needed in each sector to rebalance to target,
# then the total transaction cost across all three trades.
trades = None
total_cost = None
print(trades, total_cost)
Not implemented here: liquidity-aware cost¶
This uses a flat cost_bps regardless of trade size or market
conditions. Read reference/concepts/liquidity.md for why realistic
market-impact modeling is out of scope here.
Experiment¶
Recompute the Brinson decomposition with w_p = w_b (no active weights
at all). Confirm allocation and interaction both vanish, leaving only
selection.
Reference¶
reference/concepts/brinson_attribution.md
reference/concepts/fixed_income_attribution.md
reference/concepts/transaction_costs_and_rebalancing.md
Promote¶
Use src/pm/attribution.py (brinson_attribution, transaction_cost,
rebalancing_trades) only after your own implementation.
Test¶
pytest tests/test_attribution.py
ORAL CHECK¶
Explain to a PM why allocation and selection can point in opposite directions and roughly cancel, and why that's a meaningfully different outcome from "nothing happened."
Try /tutor concept brinson attribution for an adaptive walkthrough.