---
title: "Selection vs drift decomposition"
method: POST
path: "/cohorts/pnl-decomposition"
tags: ["Risk Metrics"]
---

# Selection vs drift decomposition

`POST /cohorts/pnl-decomposition`

## Request

- Base URL: `https://riskmodels.app/api`
- URL: `https://riskmodels.app/api/cohorts/pnl-decomposition`
- Auth: HTTP bearer

Splits a book's realized residual return into two parts:

- **Selection** — what the book earned by holding names that beat their cohort's
  average residual.
- **Drift** — what it earned purely from net exposure to that average, which
  accrues on net weight regardless of any selection skill.

The two sum to the total exactly. This is an identity, not a fitted attribution:

    R_t = Σ_i w_i·(ε_i,t − μ_c(i),t) + Σ_c W_c·μ_c,t

where `w_i` is a position weight, `ε_i,t` its residual, `μ_c,t` its cohort's mean
residual, and `W_c` the net weight in cohort `c`.

This answers *"was I paid for stock-picking, or for being net long the average
stock?"* — a question that is only answerable because ERM3 fits residuals without
an intercept, leaving a non-zero cross-sectional mean that the cohort store
exposes.

Levels must match: `level=sector` (default) demeans each name's sector-level
residual against its sector cohort; `level=market` uses market-level residuals
against the market cohort.

Weights are treated as **constant** over the window and are **not normalized** —
rescaling them would change the drift term, which is proportional to net weight.
Positions that cannot be resolved or mapped to an addressable cohort are dropped
and named in `coverage.dropped` rather than silently omitted.

Realized historical attribution only — not a forecast, not a backtest of a
strategy, and not a recommendation regarding any security.

## Request body

- object
  - `positions` object[], required
    - `ticker` string, required
    - `weight` number, required: May be negative for a short.
  - `level` 'market' | 'sector'
  - `start_date` string, date
  - `end_date` string, date
  - `min_names` integer: Ignore cohort means on days the cohort had fewer than this many members.
  - `include_series` boolean: Include the daily selection/drift series.

## Response `200`

Selection vs drift decomposition

- CohortPnlDecompositionResponse: Realized residual return split into within-cohort selection and net-exposure drift. `totals.selection + totals.drift == totals.residual` by construction.
  - `level` 'market' | 'sector'
  - `basis` string: Which stock residual was used and which cohort mean it was demeaned against.
  - `range` string[], nullable
  - `n_days` integer
  - `coverage` object: What was actually included. Positions that could not be resolved or mapped to an addressable cohort are named here, never silently omitted.
    - `requested` integer
    - `included` integer
    - `dropped` object[]
      - `ticker` string
      - `reason` string
  - `totals` object
    - `residual` number: Cumulative residual return of the book over the window.
    - `selection` number: Part earned by holding names that beat their cohort average.
    - `drift` number: Part earned from net exposure to the cohort average, independent of selection skill.
    - `selection_share` number, nullable: |selection| / (|selection| + |drift|). Null when both are ~0.
  - `by_cohort` object[]
    - `cohort` string
    - `net_weight` number
    - `n_positions` integer
    - `drift` number
    - `selection` number
  - `series` object[]: Daily contributions. Present only when include_series=true.
    - `date` string, date
    - `selection` number
    - `drift` number
    - `residual` number
  - `net_weight` number
  - `gross_weight` number
  - `store_build` string, nullable
  - `disclosures` object
    - `interpretation` string
    - `no_intercept_contract` string, nullable
    - `constant_weights` string
    - `not_advice` string

## Other responses

- `400`: Invalid request
- `404`: Cohort data unavailable
- `429`: Rate limit exceeded.
- `500`: Internal error

---

[API](https://skmtc.dev/riskmodels/apis/riskmodels-api.md) · [All operations](https://skmtc.dev/riskmodels/apis/riskmodels-api/llms.txt) · [OpenAPI document](https://skmtc.dev/riskmodels/apis/riskmodels-api/revisions/1a034cad408a?raw)
