---
title: "Portfolio risk index (L3 variance decomposition)"
method: POST
path: "/portfolio/risk-index"
tags: ["Risk Metrics"]
---

# Portfolio risk index (L3 variance decomposition)

`POST /portfolio/risk-index`

## Request

- Base URL: `https://riskmodels.app/api`
- URL: `https://riskmodels.app/api/portfolio/risk-index`
- Auth: HTTP bearer

Holdings-weighted L3 explained-risk decomposition (market, sector, subsector, residual), approximate portfolio volatility (weighted `vol_23d`), and optional daily time series of portfolio ER over `years`. Partial resolution: unresolved tickers appear in `errors`; computation uses resolved positions only. Metered; includes `_agent` and `_metadata`.
**Billing:** $0.15 per request (empty `positions` / syncing placeholder responses are not charged).

## Request body

- object
  - `positions` object[], required: May be empty when holdings are not yet available (e.g. Plaid initial sync). In that case the API returns HTTP 200 with `status: "syncing"` and a message instead of portfolio aggregates.
    - `ticker` string, required
  - `timeSeries` boolean: When true, include `time_series` of daily portfolio ER.
  - `years` integer: History span when `timeSeries` is true.

## Response `200`

Portfolio aggregates, per-ticker breakdown, optional time series; or status "syncing" when positions is empty (Plaid / holdings not ready).

- object
  - `status` 'syncing': Present only when `positions` was empty (holdings not ready).
  - `message` string: Human-readable hint when status is syncing.
  - `portfolio_risk_index` object
    - `variance_decomposition` object
      - `market` number, nullable
      - `sector` number, nullable
      - `subsector` number, nullable
      - `residual` number, nullable
      - `systematic` number, nullable
    - `portfolio_volatility_23d` number, nullable
    - `position_count` integer
  - `per_ticker` object
  - `summary` object
    - `total_positions` integer
    - `resolved` integer
    - `errors` integer
  - `time_series` object[]
    - `date` string, date
    - `market_er` number
    - `sector_er` number
    - `subsector_er` number
    - `residual_er` number
    - `systematic_er` number
  - `errors` object[]
    - `ticker` string
    - `error` string
  - `_agent` AgentMeta: 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` object

## Other responses

- `400`: Invalid body or no resolvable tickers.
- `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)
