What the skill tells Claude

This is the skill's instruction file, SKILL.md, as Claude reads it.

Purpose

Decompose a portfolio's active return versus its benchmark into three additive effects per GICS sector:

Effect Formula What it measures
Allocation (w_p − w_b) × (r_b_seg − R_b_total) Was the sector over/under-weight decision correct?
Selection w_b × (r_p_seg − r_b_seg) Did stock picks within the sector beat the benchmark?
Interaction (w_p − w_b) × (r_p_seg − r_b_seg) Combined weight + return divergence

Multi-period effects are chained with Carino (1999) logarithmic smoothing so the linked total always reconciles to the cumulative active return.


Invocation

/equity-attribution-bf <user request>

Examples: - /equity-attribution-bf Here is my portfolio xlsx, benchmark is S&P 500, Q1 2026 - /equity-attribution-bf Run attribution on the uploaded holdings file - /equity-attribution-bf [paste JSON holdings]


Input Schema

The skill accepts input in three forms (in order of preference):

Form 1 — Spec-compliant xlsx (preferred)

A file produced by build_portfolio_xlsx.py or matching the attribution_data_specifications.md schema. Must have a sheet whose name contains "holding" or "security" (case-insensitive) with these columns:

Field Required Notes
period_id YES e.g. "2026-01"
period_start YES YYYY-MM-DD
period_end YES YYYY-MM-DD
security_id YES ISIN / CUSIP
security_name YES Human-readable name
ticker YES Exchange ticker
segment_l1 YES GICS Sector
segment_l2 NO GICS Industry Group
country YES ISO 3166
currency YES ISO 4217
portfolio_weight YES Decimal, BOD, sums to 1.0
benchmark_weight YES Decimal, BOD, sums to 1.0
portfolio_return YES Decimal total return
benchmark_return YES Decimal; use R_b_total for off-benchmark securities
portfolio_mv_start RECOMMENDED Market value in base currency
portfolio_mv_end RECOMMENDED Market value in base currency

Form 2 — JSON holdings

{
  "portfolio_name": "My Fund",
  "benchmark_name": "S&P 500",
  "base_currency": "USD",
  "linking_method": "carino",
  "periods": [
    {
      "period_id": "2026-01",
      "period_start": "2025-12-31",
      "period_end":   "2026-01-31",
      "holdings": [
        {
          "ticker": "MSFT",
          "security_name": "Microsoft Corp",
          "security_id": "US5949181045",
          "segment_l1": "Information Technology",
          "country": "US",
          "currency": "USD",
          "portfolio_weight": 0.14,
          "benchmark_weight": 0.12,
          "portfolio_return": 0.080,
          "benchmark_return": 0.080
        }
      ]
    }
  ]
}

Form 3 — Natural language + prompted collection

If neither file nor JSON is provided, the skill prompts for each required field in sequence (see Prompt Rules below).


Step-by-Step Workflow

The skill MUST execute these steps in order:

Step 1 — Detect input form

  1. Check whether the user uploaded an xlsx file.
  2. Check whether the message contains JSON with "holdings" key.
  3. Otherwise, enter prompted collection mode.

Step 2 — Validate inputs (scripts/validate_inputs.py)

Run the validator against the detected input. The validator checks:

  • portfolio_weight sums to 1.0 ± 1e-4 per period
  • benchmark_weight sums to 1.0 ± 1e-4 per period
  • No null segment_l1 values
  • portfolio_return and benchmark_return are numeric
  • period_start < period_end
  • At least one period present
  • At least two securities present per period

If validation passes → proceed to Step 3. If validation fails → display a clear error table with column, issue, and fix instructions. Do not run attribution on invalid data.

Step 3 — Normalise inputs

Convert the validated input into the canonical holdings_by_period dict:

{ "2026-01": [ {row_dict}, ... ], "2026-02": [...], ... }

Apply these normalisations automatically: - Strip whitespace from string fields - Convert period dates to YYYY-MM-DD strings - Cast weights and returns to float - Infer benchmark_return for off-benchmark securities (set = period benchmark total return) - Infer segment_l1 = "UNCLASSIFIED" if missing (and warn)

Step 4 — Run attribution (scripts/run_attribution.py)

  1. Instantiate AttributionEngine from AttributionEngine.py
  2. Call run_all_periods_from_holdings() with segment_level="segment_l1"
  3. Call carino_link()
  4. Call save_json() → attribution_results.json

Step 5 — Generate output report

  1. Build attribution_report.xlsx using the same report builder pattern as build_attribution_xlsx.py — Cover, Q1_Summary, one sheet per period, Security_Detail, Methodology.
  2. Save to /workspace/attribution_report.xlsx

Step 6 — Deliver results

Present a structured summary:

Q1 2026 Attribution Summary — [Portfolio Name]
─────────────────────────────────────────────────
Portfolio Return    :  +X.XX%
Benchmark Return    :  +X.XX%
Active Return       :  +X.XX%

Effect              Linked Value
────────────────────────────────
Allocation          +X.XX%
Selection           +X.XX%
Interaction         +X.XX%
─────────────────────────────────
Total Active        +X.XX%   ✓ reconciles

Key insight: [Top positive segment] drove [+X.XX%] from [allocation/selection].
             [Biggest drag] detracted [−X.XX%].

Attach attribution_report.xlsx and attribution_results.json.


Prompt Rules (Prompted Collection Mode)

When inputs are missing or incomplete, prompt for them one group at a time:

Group 1 — Portfolio identity (ask once)

"I need a few details to run the attribution. Please provide: 1. Portfolio name (e.g. 'US Core Equity Fund') 2. Benchmark name (e.g. 'S&P 500') 3. Base currency (e.g. 'USD') 4. Reporting period(s) (e.g. 'Jan, Feb, Mar 2026' or 'Q1 2026')"

Group 2 — Holdings data (ask if no file/JSON)

"Now I need the holdings for each period. For each security please provide: ticker, segment (GICS sector), portfolio weight, benchmark weight, return. You can paste a table, CSV snippet, or upload an xlsx file."

Group 3 — Missing fields (ask per field only if needed)

Prompt individually only for fields that are missing after partial input: - Missing segment_l1: "What GICS sector does [TICKER] belong to?" - Missing benchmark_weight: "Is [TICKER] in the benchmark? If yes, what is its weight?" - Missing benchmark_return: "What was the benchmark return for [TICKER] in [PERIOD]?
(If it's not in the benchmark, I'll use the total benchmark return as a proxy.)" - Missing period dates: "What are the start and end dates for [PERIOD_ID]?"

Never ask for information that can be inferred: - Off-benchmark securities get benchmark_weight = 0.0 automatically - Off-benchmark benchmark_return defaults to that period's total benchmark return - security_id defaults to ticker when ISIN is unavailable (with a warning)


Validation Error Messages

Check Error message template
Portfolio weights ≠ 1.0 Period {id}: portfolio weights sum to {sum:.4f}, expected 1.0. Adjust weights to sum exactly to 1.0.
Benchmark weights ≠ 1.0 Period {id}: benchmark weights sum to {sum:.4f}, expected 1.0. Adjust or confirm benchmark composition.
Missing segment_l1 {ticker}: segment_l1 is null. Provide a GICS sector (e.g. 'Information Technology').
Non-numeric return {ticker} period {id}: portfolio_return '{value}' is not a number. Provide a decimal return (e.g. 0.042 for 4.2%).
period_start ≥ period_end Period {id}: start date {start} is not before end date {end}.
Fewer than 2 securities Period {id}: only {n} security found. Attribution requires at least 2 securities.

Output Format

File Contents
attribution_report.xlsx Cover, Q1_Summary, per-period sheets, Security_Detail, Methodology
attribution_results.json Machine-readable full dataset with segment detail and Carino k-factors

Constraints

  • Weights MUST be beginning-of-period (BOD). Never use end-of-period weights.
  • Returns MUST be in decimal form (0.042, not 4.2).
  • The attribution residual MUST be < 1e-6 per period. If not, re-check weight sums.
  • Only carino linking is supported in this version.
  • Fixed income, FX overlay, and derivatives attribution are out of scope (use Campisi or Karnosky-Singer skills).
  • Maximum 200 securities per period; maximum 12 periods per run.

Files in This Skill Package

equity-attribution-bf/
├── SKILL.md                        ← This file
├── assets/
│   └── icon.png                    ← Skill icon
├── scripts/
│   ├── validate_inputs.py          ← Input validator and normaliser
│   └── run_attribution.py          ← Attribution runner (wraps AttributionEngine)
└── references/
    ├── input_schema.md             ← Full field-level schema reference
    └── bf_formulas.md              ← Brinson-Fachler formula derivations

Shared as is, with no warranty. Not investment, legal or tax advice. See Disclosures.