# `POST /screener/metrics`

**Purpose:** Return filings that match a DSL filter tree, grouped by company (default) or flattened.

Query the universe of US-listed filers by composing metric conditions ("ROE > 20% for the last 5 years AND PE < 20"). Supports OR/NOT, time-window modifiers (`for_latest` / `for_consecutive` / `for_at_least`), 1–120 period lookback, USD-normalized values for ADR foreign filers, and paging up to 500 cumulative results.

> **Plan-tier gate:** `POST /screener/metrics`, `POST /screener/ownership`, `POST /screener/initial-holdings`, `POST /screener/institutional-holdings` and `POST /screener/events` require the **Pro plan or higher**; Free / Starter callers receive `403 PLAN_TIER_INSUFFICIENT` (see [API key, account, and plan errors](/documents/errors.md#api-key-account-and-plan-errors)). [`GET /screener/metrics/filters`](/documents/get-screenermetricsfilters.md) requires the **Starter plan or higher**.

> **Pricing note:** Filters that touch price-sensitive metrics (`market_cap`, `enterprise_value`, all 13 valuation ratios) use the price observed on each filer's filing date; these dates differ across filers.

**Request body** (unknown fields return 422):

| Field | Type | Default | Notes |
|---|---|---|---|
| `as_of_date` | ISO date | today (UTC) | Must be ≤ today. Filings with `filing_date > as_of_date` are excluded. |
| `lookback` | int 1–120 | 1 | Most recent N filings per company within scope. |
| `include_quarterly` | bool | `false` | `false` → annual only (10-K / 20-F / 40-F). `true` adds 10-Q. |
| `tickers` | string[] | omitted | Whitelist, max 100. Case-insensitive (normalized to upper). Empty or omitted = full universe. |
| `exclude_tickers` | string[] | `[]` | Blacklist, max 100. |
| `usd_only` | bool | `false` | When `true`, exclude foreign-currency reporters. |
| `sectors` | string[] | omitted | Restrict the universe to these canonical sector buckets (the 12 in [Industry classification (Pro+)](/documents/coverage.md#industry-classification-pro)). Omitted = no sector restriction. |
| `where.filter` | DSL tree | (required) | See [Filter DSL (`/screener/metrics`)](/documents/post-screenermetrics.md#filter-dsl-screenermetrics). Leaf count 1–10, nesting depth ≤ 4. |
| `where.sort_by` | string | see [Filter DSL (`/screener/metrics`)](/documents/post-screenermetrics.md#filter-dsl-screenermetrics) | Metric name, `"ticker"`, `"period_end"`, `"filing_date"`, or `"match_count"`. |
| `where.sort_order` | `"asc"` \| `"desc"` | `"asc"` | |
| `include_metrics_using_filing_date_price` | bool | `true` | `false` rejects price-sensitive metrics in `filter` / `sort_by` / `include_metrics`. |
| `exclude_derivations` | string[] | `[]` | Subset of `derived`, `as_reported`, `composite`, `class_sum`, `segment_sum`, `company_override`, `proxy_substituted`, `TTM_rollforward`. |
| `include_metrics` | string[] | `[]` | Extra metric names (≤ 10) to include in the response beyond filter-referenced ones. |
| `group_by_ticker` | bool | `true` | `true` → grouped by company with nested `filings[]`. `false` → flat per-filing rows. |
| `page` | int ≥ 1 | 1 | 1-indexed. |
| `page_size` | int 1–100 | 50 | Counts companies (when grouped) or filings (when flat). |
| `detail` | string | `"full"` | `"light"` leaves out each company's `classification.source.url` and the `lookback_applied` and `include_quarterly_applied` echoes. See [Response detail](/documents/responses.md#response-detail). |

**Pagination cap:** `page × page_size ≤ 500` (422 above it). When the next page would pass the cap, `has_more` is `false`, `next_page` is `null`, and `_warnings` says the limit was reached.

**Response (`group_by_ticker=true`, default):**

```json
{
  "page": 1, "page_size": 50,
  "has_more": true,
  "next_page": 2,
  "as_of_date_applied": "2024-12-31",
  "lookback_applied": 5,
  "include_quarterly_applied": false,
  "results": [
    {
      "ticker": "AAPL",
      "match_count": 5,
      "filings": [
        {
          "metric_id": 46,
          "form_type": "10-K",
          "fiscal_year": 2024, "quarter": "FY",
          "period_end": "2024-09-28",
          "filing_date": "2024-11-01",
          "reporting_currency": "usd",
          "metrics": {
            "return_on_equity": {"value": 1.5774, "derivation": "derived", "unit": "ratio"},
            "pe_ratio": {"value": 33.21, "derivation": "derived", "unit": "ratio", "price_date": "2024-11-01"},
            "market_cap": {"value": 3.37e12, "derivation": "as_reported", "unit": "usd", "price_date": "2024-11-01"}
          }
        }
        /* T-1, T-2, … up to lookback filings within company */
      ]
    }
  ],
  "credits_used": 242
}
```

**Response (`group_by_ticker=false`):** identical envelope, but `results[]` is a flat list of `{ticker, metric_id, form_type, fiscal_year, quarter, period_end, filing_date, reporting_currency, metrics}` — one row per matching filing.

For **Pro+** callers each `results[]` entry (grouped or flat) also carries an industry **`classification`** block when the company has one — see [Industry classification (Pro+)](/documents/coverage.md#industry-classification-pro).

**`match_count`** is the number of filings within `lookback` that satisfy the **whole** filter tree on a per-filing basis (using each filing's own metric values).

**Foreign-currency filers (ADRs)** carry `fx_rate_used` + `currency` on FX-converted metric entries:

```json
"market_cap": {
  "value": 2.42e11,            // USD-converted from JPY
  "derivation": "as_reported",
  "unit": "usd",
  "currency": "jpy",
  "fx_rate_used": 145.2,
  "price_date": "2024-06-30"
}
```

All screener filtering, sorting, and comparison use the USD-converted value. USD reporters omit `fx_rate_used` and `currency`.

**Within-company `filings[]` ordering** is `filing_date DESC, period_end DESC` (newest first). For time-series ordering, use `group_by_ticker=false` with `sort_by="period_end"` or `"filing_date"`.

**Errors:** `422 VALIDATION_ERROR` (schema; a metric name outside the catalog also returns `_supported_metric_names`), `402 CREDITS_INSUFFICIENT`, `403 PLAN_TIER_INSUFFICIENT` below Pro, **`504 QUERY_TIMEOUT`** when the query exceeds the 30-second server timeout (charged).

**Example Request:**

```json
{
  "tickers": ["NVDA", "AMD", "AVGO", "QCOM", "TXN", "INTC", "MU", "ADI"],
  "lookback": 4,
  "include_quarterly": true,
  "where": {
    "filter": {
      "metric": "gross_margin",
      "op": ">",
      "value": 0.5,
      "for_latest": 4
    },
    "sort_by": "gross_margin",
    "sort_order": "desc"
  },
  "page_size": 20
}
```

## Filter DSL (`/screener/metrics`)

### Leaf node

```json
{"metric": "<name>", "op": "<operator>", "value": <number | [lo, hi]>,
 "for_latest": <N> | "for_consecutive": <N> | "for_at_least": <N>}
```

| Operator | Value shape | Notes |
|---|---|---|
| `>`, `>=`, `<`, `<=` | single number | |
| `between` | `[lo, hi]` (`lo ≤ hi`) | Inclusive on both ends. |

`=` and `!=` are not supported.

`value` must be numeric. `null` is rejected. Filings where the metric value is missing are excluded from filter comparisons (no synthetic match).

### Temporal modifiers (mutually exclusive, at most one per leaf)

| Modifier | Meaning |
|---|---|
| *(none)* | Latest filing in `lookback` must satisfy. Equivalent to `for_latest: 1`. |
| `for_latest: N` | Most recent N filings (right-anchored) all satisfy. |
| `for_consecutive: N` | Some N adjacent filings within `lookback` all satisfy (sliding window over `filing_date`). |
| `for_at_least: N` | At least N filings within `lookback` satisfy (any positions). |

`N ≥ 1` and `N ≤ lookback`. Tickers without enough filings in `lookback` to evaluate the modifier are filtered out.

### Composite nodes

```json
{"and": [<node>, <node>, …]}
{"or":  [<node>, <node>, …]}
{"not": <node>}
```

`where.filter` may itself be a bare leaf (no wrapper required for single-condition queries).

### Limits

- Leaf nodes total: **1–10** (0 → rejected as "filter required"; >10 → rejected as "too complex")
- Nesting depth: **≤ 4**
- `include_metrics` length: **≤ 10**
- `tickers`, `exclude_tickers`: each **≤ 100** entries

## Credit formula (`/screener/metrics`)

`POST /screener/metrics` charges per call (paginate ⇒ each page bills independently):

```text title="Formula"
credits = max(65, 425
              − tickerDiscount             // ceil(200 × (100 − n_tickers) / 100); 0 (whole universe, or ≥ 100 tickers) … 198 (1 ticker)
              − lookbackDiscount           // 0 (lookback ≥ 30) … 78 (lookback = 1)
              − quarterlyDiscount          // 60 when include_quarterly=false, else 0
              + lookbackSurcharge          // 0 (lookback ≤ 30) … 240 (lookback = 120)
              + filterSurcharge            // 40 × (leaves − 1), 0 … 360
              + temporalSurcharge          // per-leaf 60 (for_latest / for_at_least) or 100 (for_consecutive), capped 600
              + includeMetricsSurcharge)   // 20 × |include_metrics \ filter_metrics|, 0 … 200
```

Examples:

| Scenario | Credits |
|---|---|
| 1 ticker, lookback=1, annual only, 1 leaf (best case) | **89** |
| Whole universe, lookback=30, quarterly+annual, 1 leaf | **425** |
| 2 tickers, lookback=5, annual, 2 leaves, 1 `for_latest`, 2 extra `include_metrics` | **242** |
| Whole universe, lookback=120 (Business or Enterprise plan), 10 leaves all `for_consecutive`, 10 extra include_metrics | **1825** (worst case) |

`lookback` is priced after it is clamped to your plan's history window.

**Charge applies even when `results` is empty, and on `504 QUERY_TIMEOUT`**. Errors returned before the charge (`422`, `402`, `403`, authentication errors, `429`) are not charged.

`GET /screener/metrics/filters` is free.
