Risk Metrics

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

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.

post/portfolio/exposure

Request

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

Request body

lookback_daysinteger

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_ofstring 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.

include_positionsboolean

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

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

hedgesobject

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.

Changes

No recorded changes to this operation across all 1 revision of this API.