---
title: "(internal/legacy) Multi-ticker batch analysis"
method: POST
path: "/batch/analyze"
tags: ["Risk Metrics"]
---

# (internal/legacy) Multi-ticker batch analysis

`POST /batch/analyze`

## Request

- Base URL: `https://riskmodels.app/api`
- URL: `https://riskmodels.app/api/batch/analyze`
- Auth: HTTP bearer

**Per Snapshot Architecture v3, prefer `POST /snapshot` for new clients.** This endpoint remains callable as an internal building block (cohort scans, parity checks, zarr alignment).
Fetch data for up to 100 tickers in one JSON response (`results` map keyed by ticker). Supported `metrics` flags: `returns` (daily arrays with `l1`/`l2`/`l3` columns — see operation schema for semantics), `hedge_ratios` (latest six hedge ratios with **short** keys `l1_market`, …), `full_metrics` (flat L1/L2/L3 ER/HR snapshot with **long** keys `l1_market_hr`, `l1_market_er`, …). Request every block you need: `metrics` is a whitelist (no silent backfill). For ERM3 zarr parity (`L*_ER` / `L*_HR`), request `["full_metrics","hedge_ratios"]` and see docs/ERM3_ZARR_API_PARITY.md. The `l3_decomposition` flag is accepted but may be unpopulated depending on deployment. Cost: $0.015 per ticker that returns data, minimum $0.03/call. Tickers that fail (`symbol_not_found`, `no_risk_metrics`) are not billed, and a batch where every ticker fails costs nothing.
**ETFs.** ETFs, including the SPY / sector / subsector hedge instruments, have no ERM3 decomposition. With `full_metrics` or `hedge_ratios` they return `status: "error"` with `error_code: "no_risk_metrics"`. Use `/etf-returns` for ETF return series.
**Explained-risk shares.** Incremental shares (sector, subsector) can be slightly negative: after orthogonalization an added layer can explain less than nothing over the window. The residual is defined as 1 minus the systematic shares, so the four L3 shares still sum to 1. Do not clip a negative share to 0 without re-deriving the residual.
## Status code semantics (clients must read `summary` to detect partial / total failure)
This endpoint **always returns HTTP 200** as long as the request is well-formed and authentication passes. Per-ticker failures are reported in `results[ticker].status = "error"` with a human-readable `error` string and a machine-readable `error_code` (`symbol_not_found`, `no_risk_metrics`, `internal_error`), and counted in `summary.errors`. **Treat this as a 207 Multi-Status conceptually**: you cannot infer success from the HTTP code alone.
Recommended client logic: - `summary.errors == 0` → all ok - `0 < summary.errors < summary.total` → partial; retry only the failed tickers (see `results[ticker].error`) - `summary.errors == summary.total` → total failure; surface or escalate (do **not** loop-retry the whole batch — the failure is likely structural, e.g. all tickers unresolvable or upstream zarr gap)
4xx is reserved for malformed requests (`tickers` empty, > 100, unknown `metrics` flags, bad auth).
**Parquet/CSV:** When `format` is `parquet` or `csv`, the response is a single long-format table of daily rows (`ticker`, `date`, `gross_return`, `l1`, `l2`, `l3`) per `#/components/schemas/BatchAnalyzeExportRow`. Only tickers with successful `returns` payloads are included; `full_metrics`, `hedge_ratios`, and `l3_decomposition` are not exported in tabular form. Ensure `metrics` includes `returns` or the table may be empty.

## Request body

- object
  - `tickers` string[], required: List of ticker symbols. Maximum 100.
  - `metrics` string[], required: Which payloads to include per ticker (`full_metrics` recommended for portfolio screens).
  - `years` integer: Number of years of history (applies to `returns` and `l3_decomposition`).
  - `format` 'json' | 'parquet' | 'csv': `json` (default): `BatchAnalyzeResponse` with `results`, `summary`, `_agent`, `_metadata`. `parquet` / `csv`: long-format daily table only — see operation description and `#/components/schemas/BatchAnalyzeExportRow`. Requires `returns` in `metrics` for non-empty data.

## Response `200`

Batch results as JSON (`results` map) or as a single Parquet/CSV table of merged daily `returns` rows across tickers.

- BatchAnalyzeResponse
  - `results` object, required
  - `summary` object, required
    - `total` integer
    - `success` integer
    - `errors` integer
  - `_agent` AgentMeta, required: Per-request billing and telemetry metadata appended to metered responses.
    - `cost_usd` number, float: Cost deducted from prepaid balance for this request. 0 if served from cache.
    - `latency_ms` integer: Server processing time in milliseconds.
    - `request_id` string: Unique request identifier for support lookups.
    - `confidence` object
      - `overall` number, float
      - `factors` object
    - `data_freshness` string, date-time: ISO timestamp of the data's as-of datetime.
    - `billing_code` string: Internal billing classification for this request type.
    - `cache_status` 'HIT' | 'MISS' | 'BYPASS': Whether this response was served from cache.
    - `llm_cost_usd` number, float: POST /chat only: portion of cost from LLM token billing (estimated preflight charge).
    - `tool_cost_usd` number, float: POST /chat only; sum of internal data tool charges for this turn.
    - `tool_calls` integer: POST /chat only; number of tool invocations in this request.
  - `_metadata` RiskMetadata: Lineage and provenance metadata for all data responses. Included in response body as _metadata and as X-Risk-* headers.
    - `model_version` string: ERM3 model version (e.g. ERM3-L3-v30).
    - `data_as_of` string, date: Latest trading date reflected in the response (model outputs / latest snapshot).
    - `factor_set_id` string: Factor set identifier.
    - `universe_size` integer: Number of stocks in the universe.
    - `wiki_uri` string, uri: Methodology documentation URL.
    - `factors` string[]: L3 factor ETF tickers (market + 11 sectors).
    - `data_source` 'zarr': Present on history-style JSON responses. Always `"zarr"` as of the pure-Zarr SSOT cutover: all historical time series (daily metrics, hedge weights, returns decomposition, rankings) are served from consolidated Zarr stores on GCS. The Supabase `security_history` table has been removed; the legacy `"supabase"` value is retained in clients only for backwards compatibility.
    - `range` string[]: Optional inclusive ISO date bounds of the returned history window when `data_source` is set.

## Other responses

- `400`: Invalid request body or too many tickers (> 100).
- `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)
