---
title: "Long/short portfolio exposure, ETF hedges and risk (signed dollar positions)"
method: POST
path: "/portfolio/exposure"
tags: ["Risk Metrics"]
---

# Long/short portfolio exposure, ETF hedges and risk (signed dollar positions)

`POST /portfolio/exposure`

## Request

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

Signed book of up to 1000 positions as dollar market values (short < 0). Nothing is normalised, so a book that nets to roughly zero is a valid input. Use this instead of `/portfolio/risk-index`, `/portfolio/risk-snapshot` or `/snapshot` for any book with shorts; those endpoints require positive weights. A book of ETFs only (for example an overlay) is valid. Repeated tickers, and tickers that map to one model security (some share classes), are netted into one position before any squaring; a `share_class_netted` warning names them.

**Returns** - `beta`: L1 beta-dollars (stock sleeve, ETFs held in the book, total, by sector ETF). Stock
  betas are ERM3 L1 market betas whatever `hedge_level` is (`method: l1_market_beta`); ETFs held
  are measured against SPY over the covariance window. `beta.risk_basis` is the market beta on
  the risk basis: xᵀΣe_SPY / σ²_SPY, where x is the same exposure vector the risk split uses
  (at `hedge_level`) and Σ the ETF covariance. It gives `beta_usd`, `beta_pct_of_gross`,
  `by_layer_usd` (market, sector, subsector, direct_etf) and `gross_covered`. The two betas
  differ when L2/L3 sector and subsector legs carry market beta of their own; under
  `hedge_level: l1` they agree. Null when no covariance with SPY could be built.
- `scenario_inputs`: `etfs`, one row per ETF in the covariance, held or used as a hedge leg:
  `beta_to_spy` (cov(ETF, SPY) / var(SPY) over the covariance window, SPY = 1), `annual_vol`,
  `held_usd`, `hedge_leg`, `in_covariance`. With `include_positions: true`, also `positions`:
  one row per modelled stock with the `level` used, `l1_beta`, `hedge_ratios` (ETF → hr at
  that level), `residual_share`, `residual_vol_annual` (√(252·stock_var·max(res, 0))) and
  `residual_vol_annual_usd`. No built-in scenarios: apply your own moves to these inputs.
- `hedges.lstar|l1|l2|l3`: per ETF, `stock_hedge_trade_usd` = Σ value × hr (hr = ETF dollars per
  $1 long stock, negative = short), `direct_etf_exposure_usd` (the book's own ETF holdings),
  and `total_neutralizing_trade_usd` = stock hedge − direct holdings.
- `risk` (at `hedge_level`): systematic variance xᵀΣx, where x is the book's raw-ETF exposure (−stock hedge
  + direct holdings) and Σ is the sample covariance of daily ETF returns over
  `lookback_days` ending on `as_of.snapshot_teo`. Layer contributions xₗᵀΣx sum to the
  systematic variance and can be negative. Residual variance is Σ value²·stock_var·max(lK_res_er, 0),
  where K is each name's level under `hedge_level`: a diagonal approximation that ignores
  residual covariance across names (including common size/value exposure, which stays in the
  residual); it is an approximation, not a bound. Volatility is annualised with √252.
  `risk.residual.top_contributors` lists the names with the largest residual variance;
  its length is the `top_contributors` request field (default 15, up to 1000 or `"all"`).
- `risk.reconciliation`: per name at its level K, the relative gap between the
  covariance-implied systematic variance and the model's own split, stock_var·(1 − lK_res_er).
  The two use different estimators (end-of-window hedge ratios against a sample covariance,
  versus the model's time-varying, decay-weighted betas), so a gap is expected; large values
  say the two views disagree for that name.
- `coverage`: share of submitted gross used by each calculation, dropped positions
  (`symbol_not_found`, `etf_outside_priced_universe`, `insufficient_history`, `no_risk_metrics`,
  `not_at_snapshot_teo`, `no_data_at_as_of`), names excluded from L*, and ETFs without covariance.
  A known ETF (in the ETF catalog or the ERM3 register) that cannot be priced is never
  `symbol_not_found`: it is `etf_outside_priced_universe` (no returns in the store) or, in
  `etfs_excluded_from_covariance`, `etf_no_return_history_in_window` (priced, but its returns
  do not cover the window), each with `see: /api/data/etf/{ticker}`. `symbol_not_found` is kept
  for tickers on no list.
- `warnings`: machine-readable `{code, message, tickers}` for anything that changes how the
  result should be read: `share_class_netted`, `not_at_snapshot_teo`, `hedge_added_variance`,
  `residual_share_extreme`, `residual_share_floored`, `lstar_unavailable_l3_fallback`,
  `ticker_retry_limit`. Report them with the result.


**Residual shares.** ERM3's explained-risk shares at a level (market, sector, subsector, residual) always sum to 1. Each is the variance the held hedge removed over the estimation window, so a share can be negative when a hedge leg added variance, and the residual share can then exceed 1. That is used as is: the name's residual variance is larger than its unhedged variance. A residual share above 1.02 (a 0.02 tolerance, so shares a hair over 1 do not warn) is listed in `risk.residual.hedge_added_variance` with a `hedge_added_variance` warning. A residual share above 2 is still used and also raises `residual_share_extreme`. A residual share below 0 is taken as 0 in the variance term (`residual_share_floored`). The shares themselves are never altered.

**History needed.** ERM3 estimates a name only after at least 126 trading days of returns (within a 252-day window). Newer listings return `insufficient_history` and are excluded from every calculation; their gross still counts in the coverage denominator.

**Hedge level.** Default `hedge_level: "lstar"` hedges and measures each name at its own L* level (`lstar_level`, 1–3). L* stops above a layer that adds no explanatory value, which is where negative incremental explained-risk shares come from. L* levels are published from 2013-01-01: a name with no L* at the model date (every name for an `as_of` before 2013) is hedged and measured at L3, counted under `l3` in `names_by_level`, and listed in an `lstar_unavailable_l3_fallback` warning. A name with neither an L* nor complete L3 data (too little history), or whose L* level lacks data, is excluded from hedges and risk and listed in `coverage.excluded_from_lstar`. `hedge_level: "l1" | "l2" | "l3"` forces one level for every name. `hedges` always carries all four so the levels can be compared.

**Model date.** Every name is read at one common `snapshot_teo`: the date most names in the book share (for a book of ETFs only, the latest SPY return date). Names whose row is on another date are dropped as `not_at_snapshot_teo`, with their own date in `teo`, and a `not_at_snapshot_teo` warning lists them. Missing L2/L3 sector and subsector hedge legs are filled from the point-in-time model history at that same date. ETF-to-sector mapping is the current registry, not point-in-time.

**History.** `as_of` (YYYY-MM-DD, a real calendar date) computes the same output at a past model date from the point-in-time model history. For each name the newest row in the ten calendar days up to `as_of` is found; `snapshot_teo` is the date most names share, and the covariance window ends on it. L1 beta at past dates is −`l1_mkt_hr`, which equals the L1 beta exactly (L1 has one factor, SPY). Tickers resolve against today's registry, so names delisted since `as_of` are dropped as `symbol_not_found`, and names with no model data in that window (e.g. not yet listed) as `no_data_at_as_of`. Same price as a latest call. The earliest supported `as_of` is 2011-12-07: model history is served from the first date whose full estimation window is covered (hedge ratios from 2010-12-08, residual shares and stock_var from 2011-12-07, L* levels from 2013-01-01). An earlier `as_of` returns 422 with `history_available_from` and is not charged.

**Ticker notation.** Tickers that do not match directly are retried under alternative notations (BRK.B / BRK-B). Only the first 100 unmatched tickers are retried; any beyond that are reported as `symbol_not_found` with a `ticker_retry_limit` warning.

**Billing:** per successful call, by book size: **$0.25** up to 25 names, **$1.00** above 25. The tier counts names actually modelled (stocks plus ETFs held), so dropped tickers never raise the charge. The balance check before the call uses the tier for the number of distinct tickers submitted, so a book of more than 25 tickers needs $1.00 of balance even if fewer names end up modelled. Latest and `as_of` cost the same. A request in which no stock has risk metrics and no ETF held has return history returns 422 and is not charged. `X-API-Cost-USD` gives the charge in US dollars; `X-Balance-Remaining` gives the prepaid balance left in billing units of 50,000 per US dollar (1 unit = $0.00002), or `unlimited` on a licensed key.

## Request body

- object
  - `positions` object[], required
    - `ticker` string, required
    - `value` number, required: Signed dollar market value; negative for a short. Must be non-zero.
  - `lookback_days` integer: Trading days in the ETF covariance window. 252 matches the ERM3 estimation window.
  - `hedge_level` 'lstar' | 'l1' | 'l2' | 'l3': Level each name is hedged and measured at. `lstar` uses each name's own L* level.
  - `as_of` string, date: Past model date (YYYY-MM-DD, 2011-12-07 or later; earlier dates return 422). Omit for the latest. Same price. Before 2013-01-01, hedge_level lstar uses L3 for every name.
  - `top_contributors` union: Length of `risk.residual.top_contributors`, 1 to 1000, or `"all"` for every name.
    - integer
    - 'all'
  - `include_positions` boolean: Add per-name rows to `scenario_inputs.positions` (level, L1 beta, hedge ratios, residual volatility). Off by default because books go to 1000 names.

## Response `200`

Exposure, hedge trades, risk split, coverage and warnings.

- object
  - `as_of` object
    - `requested_as_of` string, nullable: The as_of sent
    - `metrics_source` string
    - `snapshot_teo` string, date: The one model date every name is read at.
    - `covariance_start` string, date, nullable
    - `covariance_end` string, date, nullable
    - `covariance_requested_n` integer
    - `covariance_effective_n` integer
    - `vintage_aligned` boolean: True when the covariance window ends on snapshot_teo.
    - `etf_mapping` string
  - `book` object
    - `positions_submitted` integer
    - `gross_usd` number
    - `net_usd` number
    - `long_usd` number
    - `short_usd` number
    - `modelled_stocks` integer
    - `direct_etfs` integer
  - `beta` object
    - `stock_beta_usd` number
    - `direct_etf_beta_usd` number
    - `total_beta_usd` number
    - `total_beta_pct_of_gross` number, nullable
    - `by_sector_usd` object
    - `direct_etf_betas` object: Beta to SPY of each ETF held, over the covariance window.
    - `method` 'l1_market_beta': The stock beta fields above use each stock's ERM3 L1 market beta.
    - `note` string
    - `risk_basis` object, nullable: Market beta on the risk basis, xᵀΣe_SPY / σ²_SPY of the exposure vector at hedge_level. Null without a covariance including SPY.
      - `basis` 'lstar' | 'l1' | 'l2' | 'l3'
      - `beta_usd` number: SPY-equivalent dollars.
      - `beta_pct_of_gross` number, nullable: beta_usd / submitted gross.
      - `by_layer_usd` object: market, sector, subsector, direct_etf; sums to beta_usd.
      - `gross_covered` number, nullable: Share of submitted gross in the exposure vector.
      - `method` 'exposure_covariance_beta'
      - `note` string
  - `hedges` object: Keys lstar, l1, l2, l3; each holds stock_hedge_trade_usd, direct_etf_exposure_usd, total_neutralizing_trade_usd (ETF → USD) and stock_gross_covered_usd. `lstar` also carries names_by_level.
  - `risk` object, nullable: Null when no ETF covariance could be built.
    - `basis` 'lstar' | 'l1' | 'l2' | 'l3'
    - `names_by_level` object
      - `l1` integer
      - `l2` integer
      - `l3` integer
    - `systematic` object: daily_variance_usd2, daily_vol_usd, annual_vol_usd, exposure_usd (ETF → USD), layer_contributions (market, sector, subsector, direct_etf → daily_variance_usd2, share_of_systematic).
    - `residual` object: daily_variance_usd2, daily_vol_usd, annual_vol_usd, method, level_basis, top_contributors (ticker, value_usd, share_of_residual; length set by the top_contributors request field), and hedge_added_variance (ticker, level, residual_share) for names whose residual share exceeds 1.02.
    - `total` object: daily_variance_usd2, daily_vol_usd, annual_vol_usd, systematic_share, residual_share.
    - `reconciliation` object: names, median_relative_error, p90_relative_error.
  - `scenario_inputs` object
    - `covariance_start` string, date, nullable
    - `covariance_end` string, date, nullable
    - `covariance_effective_n` integer
    - `etfs` object[]
      - `ticker` string
      - `beta_to_spy` number, nullable: cov(ETF, SPY) / var(SPY) over the covariance window; null when the ETF is not in the covariance.
      - `annual_vol` number, nullable: √(252 × daily variance) over the covariance window, as a fraction.
      - `held_usd` number: The book's own position in this ETF (0 if none).
      - `hedge_leg` boolean: Used by at least one name's hedge at its level.
      - `in_covariance` boolean
    - `positions_included` boolean
    - `positions` object[]: Only when include_positions is true. One row per modelled stock.
      - `ticker` string
      - `tickers` string[]: Tickers netted into this security.
      - `value_usd` number
      - `sector_etf` string, nullable
      - `subsector_etf` string, nullable
      - `level` 'l1' | 'l2' | 'l3', nullable: Level used under hedge_level; null when excluded.
      - `excluded_reason` 'no_lstar' | 'lstar_level_incomplete', nullable
      - `l1_beta` number, nullable
      - `hedge_ratios` object, nullable: ETF → hr at the level used (ETF dollars per $1 long stock).
      - `residual_share` number, nullable
      - `residual_vol_annual` number, nullable: √(252 × stock_var × max(residual_share, 0)), as a fraction.
      - `residual_vol_annual_usd` number, nullable
    - `note` string
  - `coverage` object
    - `input_gross_usd` number
    - `modelled_stock_gross_usd` number
    - `direct_etf_gross_usd` number
    - `by_calculation` object: Share of submitted gross used by beta, hedge_l1, hedge_l2, hedge_l3, hedge_lstar, residual, systematic, total_risk.
    - `dropped` object[]
      - `ticker` string
      - `value_usd` number
      - `reason` 'symbol_not_found' | 'etf_outside_priced_universe' | 'no_risk_metrics' | 'not_at_snapshot_teo' | 'no_data_at_as_of' | 'insufficient_history'
      - `teo` string, nullable: For not_at_snapshot_teo
      - `detail` string: One plain-English sentence explaining the reason.
      - `see` string: For etf_outside_priced_universe, /api/data/etf/{ticker}.
    - `excluded_from_lstar` object[]
      - `ticker` string
      - `value_usd` number
      - `reason` 'no_lstar' | 'lstar_level_incomplete'
      - `detail` string: One plain-English sentence explaining the reason.
    - `residual_flagged` object[]: Names left out of the residual because their stock_var is negative.
      - `ticker` string
      - `reason` string
    - `etfs_without_covariance` string[]
    - `etfs_excluded_from_covariance` object[]: ETFs (held or hedge legs) left out of the covariance, each with a reason.
      - `ticker` string
      - `reason` 'etf_outside_priced_universe' | 'etf_no_return_history_in_window'
      - `detail` string
      - `see` string: /api/data/etf/{ticker}
  - `warnings` object[]
    - `code` 'share_class_netted' | 'not_at_snapshot_teo' | 'hedge_added_variance' | 'residual_share_extreme' | 'residual_share_floored' | 'lstar_unavailable_l3_fallback' | 'ticker_retry_limit'
    - `message` string
    - `tickers` string[]

## Other responses

- `400`: Invalid request (empty, more than 1000 positions, zero or non-finite value, malformed or impossible as_of).
- `401`: Missing or invalid API key.
- `402`: Insufficient balance or daily spend cap reached.
- `422`: No stock has risk metrics at the model date and no ETF held has return history. Body: `error`, a plain-English `message`, and `dropped` with each ticker's `reason` code and a one-sentence `detail`. Also returned for an `as_of` before 2011-12-07, the earliest supported date; body: `error`, `message`, `detail` (why), `as_of` and `history_available_from`. Not charged.
- `429`: Rate limit exceeded.
- `500`: Exposure computation failed. Not charged.

---

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