Fixed Income Brinson (Bucket Mode)¶
1. Calculation Name¶
Duration Bucket Brinson–Fachler Attribution
2. Description and Mathematical Formula¶
The bucket mode plugin extends classic Brinson–Fachler attribution by operating on key-rate duration buckets (e.g., 0–1Y, 1–3Y). For each bucket \( b \):
\[ \begin{aligned} A_b &= (w^P_b - w^B_b) \times (r^B_b - R^B) \\\\ S_b &= w^B_b \times (r^P_b - r^B_b) \\\\ I_b &= (w^P_b - w^B_b) \times (r^P_b - r^B_b) \end{aligned} \]
where \( R^B = \sum_b w^B_b r^B_b \) is the benchmark return.
3. Input Sample Data¶
| Bucket | Portfolio Weight | Portfolio Return | Benchmark Weight | Benchmark Return |
|---|---|---|---|---|
| 0–1Y | 10% | 1.10% | 15% | 0.90% |
| 1–3Y | 25% | 1.50% | 20% | 1.20% |
| 3–5Y | 20% | 1.80% | 20% | 1.50% |
| 5–10Y | 25% | 2.40% | 30% | 2.00% |
| 10Y+ | 20% | 2.80% | 15% | 2.30% |
Benchmark return \( R^B = 1.62\% \).
4. Mathematical Solution¶
- Allocation effects (bps):
0–1Y = +3.6, 1–3Y = -2.1, 3–5Y ≈ 0.0, 5–10Y = -1.9, 10Y+ = +3.4 → +3.0 - Selection effects (bps):
0–1Y = +3.0, 1–3Y = +6.0, 3–5Y = +6.0, 5–10Y = +12.0, 10Y+ = +7.5 → +34.5 - Interaction effects (bps):
0–1Y = -1.0, 1–3Y = +1.5, 3–5Y = 0.0, 5–10Y = -2.0, 10Y+ = +2.5 → +1.0 - Total active return \( = 3.0 + 34.5 + 1.0 = 38.5 \) bps.
5. Sample Python and R Code¶
import pandas as pd
data = pd.DataFrame(
{
"bucket": ["0-1Y", "1-3Y", "3-5Y", "5-10Y", "10Y+"],
"w_p": [0.10, 0.25, 0.20, 0.25, 0.20],
"r_p": [0.011, 0.015, 0.018, 0.024, 0.028],
"w_b": [0.15, 0.20, 0.20, 0.30, 0.15],
"r_b": [0.009, 0.012, 0.015, 0.020, 0.023],
}
)
R_b = (data["w_b"] * data["r_b"]).sum()
data["allocation"] = (data["w_p"] - data["w_b"]) * (data["r_b"] - R_b)
data["selection"] = data["w_b"] * (data["r_p"] - data["r_b"])
data["interaction"] = (data["w_p"] - data["w_b"]) * (data["r_p"] - data["r_b"])
data[["allocation", "selection", "interaction"]] *= 10000 # bps
totals = data[["allocation", "selection", "interaction"]].sum()
print(data)
print(totals)
library(dplyr)
data <- tibble::tibble(
bucket = c("0-1Y", "1-3Y", "3-5Y", "5-10Y", "10Y+"),
w_p = c(0.10, 0.25, 0.20, 0.25, 0.20),
r_p = c(0.011, 0.015, 0.018, 0.024, 0.028),
w_b = c(0.15, 0.20, 0.20, 0.30, 0.15),
r_b = c(0.009, 0.012, 0.015, 0.020, 0.023)
)
R_b <- sum(data$w_b * data$r_b)
data <- data %>%
mutate(
allocation = (w_p - w_b) * (r_b - R_b) * 10000,
selection = w_b * (r_p - r_b) * 10000,
interaction = (w_p - w_b) * (r_p - r_b) * 10000
)
totals <- summarise(data,
allocation = sum(allocation),
selection = sum(selection),
interaction = sum(interaction)
)
data
totals
6. Output Table¶
| Bucket | Allocation (bps) | Selection (bps) | Interaction (bps) |
|---|---|---|---|
| 0–1Y | +3.6 | +3.0 | -1.0 |
| 1–3Y | -2.1 | +6.0 | +1.5 |
| 3–5Y | +0.0 | +6.0 | +0.0 |
| 5–10Y | -1.9 | +12.0 | -2.0 |
| 10Y+ | +3.4 | +7.5 | +2.5 |
| Total | +3.0 | +34.5 | +1.0 |
7. Conclusion¶
Bucket-mode Brinson pinpoints which maturity segments drove fixed-income outperformance. This template mirrors FinFacts outputs and is useful for analytics notebooks that reconcile desk-level attribution with enterprise reporting.