Skip to content
SlashClone
Made by@qtrader

Attribution analysis

@qtrader/attribution-analysis

Explain why a portfolio beat or lagged its benchmark. Brinson-Fachler allocation, selection, and interaction effects by sector and period, Carino-linked across periods so they add up exactly, plus a plain-English summary. Works offline from a CSV. Not financial advice.

SkillTradingv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1# attribution-analysis
  2. 2
  3. 3Find out why your portfolio beat or lagged its benchmark. This kit splits the gap into sector bets (allocation), stock picks (selection), and their overlap (interaction), links several periods so the numbers add up exactly, and writes a short plain-English summary.
  4. 4
  5. 5**Who it is for:** anyone who runs a portfolio against a benchmark and has to explain the result at month or quarter end: individual investors, small fund teams, investment clubs, students.
  6. 6
  7. 7Not financial advice. Attribution explains past results only.
  8. 8
  9. 9## Example
  10. 11Input `examples/holdings.csv` (two periods, three sectors, made-up tickers):
  11. 13```csv
  12. 14period,sector,security,port_weight,port_return,bench_weight,bench_return
  13. 15P1,Tech,AAA,0.30,0.12,0,
  14. 16P1,Tech,BBB,0.20,0.07,0,
  15. 17P1,Tech,Tech index,0,,0.40,0.08
  16. 18...
  17. 19```
  18. 21Output (`out/attribution.md`, trimmed):
  19. 23> The portfolio returned +6.22% against +5.53% for the benchmark, so it beat by 70 bps. The biggest driver was the overlap of the two (interaction) at +50 bps, then sector bets (allocation) at +39 bps and stock picking (selection) at -19 bps. Tech helped most (+88 bps), mostly from the holdings picked inside it. Energy hurt most (-46 bps), mostly from the holdings picked inside it.
  20. 25| Sector | Allocation | Selection | Interaction | Total |
  21. 26| --- | ---: | ---: | ---: | ---: |
  22. 27| Tech | +0.3013% | +0.3820% | +0.2009% | +0.8843% |
  23. 28| Health | +0.1581% | +0.0148% | +0.1054% | +0.2783% |
  24. 29| Energy | -0.0725% | -0.5879% | +0.1960% | -0.4644% |
  25. 30| **Total** | +0.3870% | -0.1910% | +0.5022% | +0.6982% |
  26. 32## Hand check (verify it yourself)
  27. 34Formulas (Brinson-Fachler), with `Rb` the total benchmark return:
  28. 36- Allocation = (wp - wb) x (rb - Rb)
  29. 37- Selection = wb x (rp - rb)
  30. 38- Interaction = (wp - wb) x (rp - rb)
  31. 40**Period P1.** Sector returns: Tech portfolio = (0.30 x 0.12 + 0.20 x 0.07) / 0.50 = 0.10.
  32. 42| Sector | wp | rp | wb | rb |
  33. 43| --- | --- | --- | --- | --- |
  34. 44| Tech | 0.50 | 0.10 | 0.40 | 0.08 |
  35. 45| Health | 0.30 | 0.04 | 0.30 | 0.05 |
  36. 46| Energy | 0.20 | -0.02 | 0.30 | 0.01 |
  37. 48Rp = 0.5(0.10) + 0.3(0.04) + 0.2(-0.02) = 0.058. Rb = 0.4(0.08) + 0.3(0.05) + 0.3(0.01) = 0.050. Active = 0.008.
  38. 50| Sector | Allocation | Selection | Interaction |
  39. 51| --- | --- | --- | --- |
  40. 52| Tech | 0.10 x (0.08 - 0.05) = 0.003 | 0.40 x 0.02 = 0.008 | 0.10 x 0.02 = 0.002 |
  41. 53| Health | 0 x ... = 0 | 0.30 x (-0.01) = -0.003 | 0 |
  42. 54| Energy | -0.10 x (0.01 - 0.05) = 0.004 | 0.30 x (-0.03) = -0.009 | -0.10 x (-0.03) = 0.003 |
  43. 55| Total | 0.007 | -0.004 | 0.005 |
  44. 570.007 - 0.004 + 0.005 = 0.008, the active return.
  45. 59**Period P2.** Tech portfolio = (0.20 x -0.04 + 0.20 x -0.06) / 0.40 = -0.05.
  46. 61| Sector | wp | rp | wb | rb |
  47. 62| --- | --- | --- | --- | --- |
  48. 63| Tech | 0.40 | -0.05 | 0.40 | -0.04 |
  49. 64| Health | 0.40 | 0.03 | 0.30 | 0.02 |
  50. 65| Energy | 0.20 | 0.06 | 0.30 | 0.05 |
  51. 67Rp = 0.004, Rb = 0.005, active = -0.001. Allocation: 0, 0.1 x 0.015 = 0.0015, -0.1 x 0.045 = -0.0045, total -0.003. Selection: -0.004, 0.003, 0.003, total 0.002. Interaction: 0, 0.001, -0.001, total 0. Sum = -0.001.
  52. 69**Linking (Carino).** Cumulative Rp = 1.058 x 1.004 - 1 = 0.062232. Cumulative Rb = 1.05 x 1.005 - 1 = 0.05525. Active = 0.006982. Simply adding the periods gives 0.008 - 0.001 = 0.007, which is off by 0.0018%, so we link:
  53. 71- k_t = (ln(1 + Rp_t) - ln(1 + Rb_t)) / (Rp_t - Rb_t): k1 = 0.948771, k2 = 0.995520
  54. 72- K = (ln 1.062232 - ln 1.05525) / 0.006982 = 0.944521
  55. 73- Each period's effects are multiplied by k_t / K: 1.004499 for P1 and 1.053994 for P2.
  56. 75Linked allocation = 0.007 x 1.004499 + (-0.003) x 1.053994 = 0.003870. Linked selection = -0.004 x 1.004499 + 0.002 x 1.053994 = -0.001910. Linked interaction = 0.005 x 1.004499 + 0 = 0.005022. Sum = 0.006982, exactly the cumulative active return.
  57. 77`node scripts/selftest.mjs` checks every number above to 12 decimal places, plus edge cases (a sector the benchmark does not hold, weights that do not sum to 1, percent strings).
  58. 79## What it can access
  59. 81- Reads your CSV from `data/`. Writes `out/attribution.json`, `out/attribution.md`, and the review in `out/`.
  60. 82- Runs only its own two scripts with Node. No network, no accounts, no API keys.
  61. 84## Setup
  62. 86Node 22 or newer. No packages to install. Put your CSV in `data/holdings.csv` and ask Claude to "run attribution against the benchmark".