# Decile one, not ticker by ticker.

The most common quant question is the same shape every time. *"Which names are in the top decile of [some metric] today?"*

The honest answer for most APIs is also the same. *"Loop over the universe, pull the metric per ticker, sort client-side, take the top 10%."*

That answer scales like the universe. Three thousand names, one HTTP call each. Ten metrics, three windows, three cohorts — and you're paying for ninety thousand calls to ask one cross-sectional question.

**There is a better question shape, and an API surface that matches it.**

---

## One call, one cross-section

`POST /api/rankings/screen` takes a **metric, cohort, window** triple and a **filter** (percentile, decile, or sector), and returns up to **500 rows** from the universe — already ranked, already filtered, ready to act on.

```json
POST /api/rankings/screen
{
  "metric":  "subsector_residual",
  "cohort":  "subsector",
  "window":  "21d",
  "decile":  1,
  "limit":   50
}
```

That's the entire request. The response is a sorted list of the top-decile 21-day subsector residual names — what stat-arb desks call "the long book before the carry filter." Same call shape works for market-cap leadership inside a sector, for short-window outperformers, for any of the seven indexed metrics across any of the three cohorts and four windows.

**$0.02 per call. One round-trip.**

---

## Why this shape exists

The ranking is precomputed. `ds_rankings_{etf}_{universe}.zarr` holds 96 ranking variables per (teo, symbol) — every metric × cohort × window combination already sorted and percentile-tagged by the ERM3 pipeline at 1d cadence. The screen endpoint isn't doing the math; it's doing **server-side selection** on a panel that's already ranked.

The alternative — pulling each ticker's row and ranking client-side — recomputes what the pipeline already computed, costs N times more, and asks for N times more network round-trips. The screen endpoint replaces N with 1.

---

## What it changes

| Before | After |
|---|---|
| Loop 3,000 tickers through `/rankings` to find decile 1 | One call returning the 300 names in decile 1 |
| Fetch metric column, sort in pandas, take top 5% | `min_percentile: 95, limit: 150` server-side |
| Subset by sector after the fact | `sector_filter: "XLK"` in the request body |
| Pay per-ticker for a question that's cross-sectional | Pay per **screen** |

The mechanic isn't surprising once you've seen it. The product change is that the screening is **a first-class request shape on the API**, not a workflow you have to assemble.

---

## Use cases

### Stat-arb residual screen

The classic. Subsector residual return, top decile, 21-day window. Stat-arb long book candidates.

```python
from riskmodels import RiskModelsClient
client = RiskModelsClient.from_env()

longs = client.screen_rankings(
    metric="subsector_residual",
    cohort="subsector",
    window="21d",
    decile=1,           # 1 = best
    limit=100,
    as_dataframe=True,
)
```

### Cap-rank leadership inside a sector

Which financials are the biggest by market cap today, ranked within their sector cohort.

```python
top_xlf = client.screen_rankings(
    metric="mkt_cap",
    cohort="sector",
    window="1d",
    sector_filter="XLF",
    limit=20,
)
```

### Cross-windows momentum check

Same metric, two windows, one decile. Names in top decile of 252d AND top decile of 21d are the persistent winners.

```python
persistent = (
    client.screen_rankings(metric="gross_return", cohort="universe", window="21d", decile=1, limit=300)
    .merge(
        client.screen_rankings(metric="gross_return", cohort="universe", window="252d", decile=1, limit=300),
        on="ticker",
        suffixes=("_21d", "_252d"),
    )
)
```

---

## Where it sits next to the rest of the stack

The rank screen is the **cross-sectional gateway**. Pair it with:

- **`POST /api/batch/lstar`** — feed the screen's ticker list into a batch Lstar history pull for the names that passed. The combination is "what's the residual-clean Lstar history for today's top-decile screen?" — two HTTP calls instead of N + N.
- **`GET /api/industry-panel`** — the macro view (industries × β) sitting alongside the micro view (tickers × rank). Cross-reference: high within-industry `beta_variance` × top-decile residual screen = "industries where stock-picking is paying off and the picks are running."
- **`POST /decompose`** — for each name that passes the screen, the four-bet hedge map is one more call away.

---

## The principle

> Don't loop a cross-sectional question. Ask it cross-sectionally.

The ERM3 pipeline already ranks the universe daily. The screen endpoint just lets you query that rank like a database — one filter, one response, one bill.

[Try the rankings screen on the API →](https://riskmodels.app/api-reference) · [Full endpoint docs →](https://riskmodels.app/docs/rankings-screen)
