---
title: "Residual mean-reversion factor — user-defined basket aggregate"
method: POST
path: "/signals/residual-reversion/basket"
tags: ["Risk Metrics"]
---

# Residual mean-reversion factor — user-defined basket aggregate

`POST /signals/residual-reversion/basket`

## Request

- Base URL: `https://riskmodels.app/api`
- URL: `https://riskmodels.app/api/signals/residual-reversion/basket`
- Auth: HTTP bearer

Aggregate the Phase D L3 residual mean-reversion signal across a user-supplied basket of up to **500 tickers**. Returns the weighted aggregate of `residual_z_5d`, `signal_strength`, `industry_percentile`, `residual_autocorr_5d`, and `l3_subsector_er`, plus decile and signal-quality-quintile histograms and per-member rows. Equal-weight default; supply `weights[]` aligned 1:1 with `tickers` for custom weighting. Optional `signal_quality_min_quintile` (1–5) gates members below that quintile out of the aggregate while keeping them visible in the `members[]` rows.

**Trust contract:** tickers not present in `ds_erm3_residual_signal` at the latest teo are silently dropped — the upstream mask is the source of truth for "good" rows. The response's `coverage` block surfaces what was requested, what landed in the basket, and which tickers were missing.

Companion to:
- `GET /residual-signal/{ticker}` — per-name snapshot + history.
- `GET /residual-signal/latest` — full universe cross-section.
- `GET /residual-signal/decile/{n}` — one decile bucket.

Combo-input factor — see the per-ticker route for the framing and capacity disclosure that's repeated on every response.

**Billing:** $0.02 per request.

## Request body

- object
  - `tickers` string[], required: Symbols to aggregate (1–500). Aliases (e.g. GOOGL→GOOG) resolved.
  - `weights` number[]: Optional non-negative weights aligned 1:1 with `tickers`. Equal-weight when omitted. Sum need not be 1 — the aggregator normalizes.
  - `signal_quality_min_quintile` integer: Optional gate on `signal_quality_quintile`. Phase B: gross Sharpe rises from ~0.79 (universe) to ~1.28 at quintile 5. Members below this still appear in `members[]` with `passed_quality_gate=false` but do NOT contribute to the aggregate.

## Response `200`

Basket aggregate, distributions, and per-member rows.

- object
  - `as_of_date` string, date
  - `aggregate` object
    - `residual_z_5d` number, nullable
    - `signal_strength` number, nullable
    - `industry_percentile` number, nullable
    - `residual_autocorr_5d` number, nullable
    - `l3_subsector_er` number, nullable
    - `decile_distribution` object: Count of members per decile bucket (keys "1"–"10" and "null"). Includes every in-zarr member regardless of the quality gate.
    - `quality_quintile_distribution` object: Count of members per signal_quality_quintile bucket (keys "1"–"5" and "null").
  - `coverage` object
    - `requested` integer
    - `in_zarr` integer: Members present in ds_erm3_residual_signal at this teo.
    - `contributed` integer: In-zarr members that also passed the optional quality gate.
    - `weight_covered` number: Sum of input weights for contributing members.
    - `missing_tickers` string[]: Tickers asked for but absent from the zarr.
  - `members` object[]
    - `date` string, date: Trading day (present on history items; omitted on the latest `signal`).
    - `residual_z_5d` number, nullable: 5-day cumulative L3 residual return, z-scored by 60-day residual vol. Negative = oversold.
    - `signal_strength` number, nullable: Absolute value of residual_z_5d.
    - `decile_rank` integer, nullable: 1-10 cross-sectional decile of residual_z_5d (1 = most oversold).
    - `industry_percentile` number, nullable: 0-1 percentile of residual_z_5d within the stock's EODHD industry cohort.
    - `residual_autocorr_5d` number, nullable: Trailing-252d lag-5 autocorrelation of the 5-day cumulative residual. Negative = mean-reverting.
    - `l3_subsector_er` number, nullable: Subsector variance-explained fraction — the signal-quality conditioning variable.
    - `signal_quality_quintile` integer, nullable: 1-5 quintile of l3_subsector_er (5 = tight subsector tracker = cleanest reversion regime).
    - `ticker` string, required: Stock ticker.
    - `weight` number
    - `in_zarr` boolean
    - `passed_quality_gate` boolean
  - `capacity_note` string
  - `methodology_link` string

## Other responses

- `400`: Invalid request body (missing tickers, weights length mismatch, etc).
- `401`: Missing or invalid Bearer token.
- `402`: Insufficient balance.
- `429`: Rate limit exceeded.

---

[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)
