Skip to content

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.