RiskModels API Documentation
This repository is the authoritative reference for the ERM3 hierarchical equity risk model API—built for quant developers and AI agents who need residual risk, hedge ratios, and explained risk with production-grade fidelity. POST /decompose returns variance shares and ETF hedge ratios for any US equity, separating factor exposure from the residual bet so you can hedge what you don't want.
Live API snapshots
MAG7 macro correlations (L3 residual) and a cross-sectional rank snapshot — produced by
scripts/generate_readme_assets.py
with your API key, same assets as the GitHub README.
Macro sensitivity (VIX, Gold, BTC)
Rank percentile (get_rankings)dollar_ratio), wire↔zarr alias map—single source of truth.Portal guide: _metadata,
data_source /
range, JSON vs Parquet or CSV, and
X-Risk-* headers.
The API is the engine; riskmodels.org is the published research and methodology behind it — the math, the worked examples, and the empirical proofs that justify hedging the residual.
ERM3 factor logic: the strategic value
Actionable quant framing—what each construct buys you in research and execution, and which semantic field to read in the SDK or API.
| Concept | The quant edge | Key field (SDK) | SDK example |
|---|---|---|---|
| Residual Risk (RR) | Uncover alpha. RR is idiosyncratic variance after L3 hedges. High RR (e.g. above 0.50) flags names where SPY and sector ETFs explain less—room for stock-specific edge. | l3_residual_er | client.get_metrics("NVDA", as_dataframe=True)["l3_residual_er"].iloc[0] |
| Hedge Ratio (HR) | Precision hedging. Not a single beta—ERM3 gives dollars of ETF per 1 stock at each L3 layer (market, sector, subsector). | l3_market_hrl3_sector_hrl3_subsector_hr | client.get_metrics("NVDA", as_dataframe=True)[["l3_market_hr","l3_sector_hr","l3_subsector_hr"]] |
| Hierarchical beta | Regression slopes, not hedges. Dimensionless coefficients from the L1/L2/L3 cascade — different units and estimation from *_hr hedge ratios. See SEMANTIC_ALIASES and Methodology. | l1_market_betal2_sector_betal3_subsector_beta | client.get_metrics("NVDA", as_dataframe=True)[["l1_market_beta","l2_sector_beta","l3_subsector_beta"]] |
| Explained Risk (ER) | Risk attribution. Variance share from market, sector, and subsector factors (orthogonalized). At L3, l3_market_er + l3_sector_er + l3_subsector_er + l3_residual_er ≈ 1.0 within tolerance. | l3_market_erl3_sector_erl3_subsector_erl3_residual_er | client.get_metrics("NVDA", as_dataframe=True)[["l3_market_er","l3_sector_er","l3_subsector_er","l3_residual_er"]] |
| Factor correlation (macro + style) | Cross-asset + style context. Rolling correlation vs 10 macro factors (inflation, term_spread, credit, oil, gold, usd, volatility, bitcoin, vix_spot, short_rates) and 8 style factors (momentum, quality, low_vol, value, growth, size, dividend, moat). Style factors paired with return_type=l3_residual isolate pure style tilt inside your idiosyncratic residual — the orthogonal cascade strips market/sector/subsector for free. | correlations(via factor_correlation) | client.get_factor_correlation_single("NVDA", factors=["momentum","quality","value"], return_type="l3_residual") |
| Sign convention | Model safety.
l3_market_hr is typically ≥ 0.
l3_sector_hr and
l3_subsector_hr may be negative
from orthogonalization or a long subsector ETF hedge — see
Methodology;
validate="warn" may flag edge cases. | l3_market_hrl3_sector_hrl3_subsector_hr | float(client.get_metrics("NVDA", as_dataframe=True)["l3_subsector_hr"].iloc[0]) # may be < 0 |
Overview
The RiskModels API provides institutional-grade equity risk analysis for AI agents and quantitative applications:
- Daily factor decompositions — market, sector, and subsector explained-risk fractions for ~3,000 US equities
- Hedge ratios — dollar-denominated ETF hedge amounts at three precision levels (L1 market-only, L2 market+sector, L3 full three-ETF)
- Historical time series — split- and dividend-adjusted daily returns plus rolling hedge ratios going back to 2006
- AI-agent ready — machine-readable manifest at
/.well-known/agent-manifest, per-request billing via prepaid balance
Data coverage: Universe uni_mc_3000 (~3,000 top US stocks), date range 2006-01-04 to present, updated daily.
Why The Engine Matters
- Built to be time-safe — the engine is designed to avoid common sources of forward contamination such as recycled tickers, snapshot shares, and retroactive universe contraction
- Backed by a real Security Master — point-in-time identity, classification, and shares logic support more stable ticker-level outputs
- Hierarchical by design — market, sector, and subsector structure are modeled explicitly rather than compressed into a flat beta view
- Tradeable in practice — hedge ratios are designed to remain executable with liquid raw ETFs, not just synthetic factors
- Built on adjusted returns — split- and dividend-adjusted return series improve consistency through corporate actions and long horizons
For the mathematical detail behind L1/L2/L3 decomposition and hedge construction, see Methodology. For the design choices behind time safety, identity continuity, and tradeable hedge outputs, see ERM3 Engine.
Choose Your Workflow
Quant Research and Hedging
Brokerage-Linked Portfolio Holdings
Risk Metrics
Beyond static HR/ER snapshots, RiskModels exposes on-demand correlation of equity returns against daily macro factor returns (cross-asset context for regimes and crowding).
Macro factor correlation
Correlate a stock's daily returns—gross or
ERM3 residuals (L1 market-only, L2 market+sector, or L3 full hedge)—against
Bitcoin, Gold,
Oil, DXY,
VIX, and UST 10y–2y.
Use POST /api/correlation for single-ticker or batch;
GET /api/metrics//correlation for query-string convenience.
Core Endpoints
| Endpoint | Method | Description | Cost | SDK example (Python) |
|---|---|---|---|---|
| /api/ticker-returns | GET | Daily returns + rolling V3 hedge ratios and explained-risk fractions, up to 15y | $0.005/call | |
| /api/metrics/ | GET | Latest snapshot: HR/ER fields (semantic columns in SDK), vol, Sharpe, sector, market cap | 0.001/call | |
| /api/decompose | POST | Four-bet semantic wrapper: market / sector / subsector / residual with hedge ETFs + consolidated hedge map | 0.001/call | |
| /api/lstar | GET | Per-ticker daily Lstar level (L1/L2/L3) with dispatched hedge ratios, total ER, and daily residual_return at the chosen level (up to 15y). For panel queries use lstar_rr + lstar_level in MetricsV3 instead. | 0.02/call | |
| /api/l3-decomposition | GET | Monthly historical HR/ER time series | 0.02/call | |
| /api/correlation /api/metrics//correlation | POST GET | Macro factor correlation (VIX, Bitcoin, Gold, Oil, DXY, UST 10y-2y). Use POST for batch, GET for single-ticker. | $0.002–0.005/call | |
| /api/batch/analyze | POST | Multi-ticker batch up to 100 (returns + full metrics per position) | 0.005/position | |
| /api/returns-decomposition | GET | One-call daily L1/L2/L3 factor / combined-factor / residual return series. Replaces 6+ per-key round-trips. Add ?include_lstar=true for Lstar dispatch. | 0.02/call | |
| /api/industry-panel | GET | Vasicek industry peer-β cross-section: beta_mean / beta_variance / n_companies by EODHD industry × level. The macro/sector-rotation surface. | 0.02/call | |
| /api/rankings/screen | POST | Server-side percentile / decile / sector filters over the full ds_rankings cross-section. Returns up to 500 rows sorted by rank_ordinal. Stat-arb cross-section, one call. | 0.05/call | |
| /api/batch/lstar | POST | Per-ticker daily Lstar history for up to 100 tickers in one call. Companion to lstar_rr in MetricsV3 (single-name snapshot); use this when you need history across a panel. | 0.005/ticker | |
| /api/universe//members | GET | Active members of a named universe (uni_mc_50/500/1000/3000 or uni_dv_*) at one teo. Active = monthly mask AND daily validity. Returns members + counts + mask_as_of. Foundational endpoint for any cross-sectional workflow. | 0.005/call | |
| /api/signals/residual-reversion/basket | POST | Aggregate the Phase D L3 residual mean-reversion signal across a user-supplied basket (≤500 tickers). Returns weighted aggregate + decile / quality-quintile histograms + per-member rows. Optional signal_quality_min_quintile gate (Phase B: Sharpe lifts from ~0.79 to ~1.28 at quintile 5). | 0.02/call | |
| /api/tickers | GET | Ticker universe search, MAG7 shortcut | Free | |
| /api/balance | GET | Account balance and rate limits | Free | |
| /api/invoices | GET | Invoice history and spend summary | Free | |
| /api/health | GET | Service health | Free | |
| /.well-known/agent-manifest | GET | AI agent discovery manifest | Free | |
MCP coverage note:
riskmodels_list_endpoints returns capability bundles,
not a 1:1 mirror of every route above. Account routes (/balance,
/invoices) and the agent manifest (
/.well-known/agent-manifest) are documented here but
do not appear as separate MCP tool IDs.
/balance, /invoices, /health) use the SDK transport with the same OAuth or API key as data methods. The well-known manifest is fetched from the site root (no auth).
Pricing model: prepaid balance (Stripe). Cached responses are free. Minimum top-up: 10.
POST /decompose — one call, four bets
POST /decompose is a semantic wrapper over the same L3 fields exposed by GET /metrics/{'{ticker}'}. It returns four additive layers — market, sector, subsector, and residual — each tradable layer carrying an er (explained-risk variance fraction), an hr (dollar-ratio hedge ratio), and a hedge_etf. A top-level hedge map flips the sign (negative of each layer hr) and sums across any duplicate ETFs, so you get ready-to-short notionals per instrument.
Billing is identical to GET /metrics/{'{ticker}'} (0.001/call, billing_code: metrics_v3). For the full L1/L2/L3 snapshot (all six HR names), keep using GET /metrics/{'{ticker}'}. For the plain-language intuition behind the four bets — and a worked NVIDIA example — see the four-bets primer on riskmodels.org.
Request
curl -X POST https://riskmodels.app/api/decompose \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ticker":"NVDA"}'
Response
{
"ticker": "NVDA",
"data_as_of": "2026-04-21",
"exposure": {
"market": { "er": 0.45, "hr": 1.10, "hedge_etf": "SPY" },
"sector": { "er": 0.22, "hr": 0.35, "hedge_etf": "XLK" },
"subsector": { "er": 0.20, "hr": 0.60, "hedge_etf": "SMH" },
"residual": { "er": 0.13, "hr": null, "hedge_etf": null }
},
"hedge": { "SPY": -1.10, "XLK": -0.35, "SMH": -0.60 }
}
Sign convention. hedge[etf] is the negative of the layer hr. Positive stock hr → negative ETF dollar ratio (short the ETF per 1 long stock). Negative stock hr → positive ETF dollar ratio (long the ETF as the hedge leg). Residual has no tradable ETF, so its hr and hedge_etf are always null.
ER invariant. Across the four layers, market_er + sector_er + subsector_er + residual_er ≈ 1 (variance fractions). The handler emits a warning when the sum deviates by more than 0.05.
Compare cascade depth: hedge_levels
POST /decompose is the four additive L3-factor bets view (exposure / hedge). Many routes also return a parallel hedge_levels object with L1, L2, and L3 keys. Each value is a snapshot of that standalone hedge depth: semantic market_hr / sector_hr / subsector_hr, matching market_er / sector_er / subsector_er / residual_er, and hedge_etfs (market / sector / subsector tickers). L2.market_hr is always sourced from l2_mkt_hr on the flat metrics row, L3.market_hr from l3_mkt_hr — cascade levels never share prefixed HR or ER inputs across L1 / L2 / L3. Use hedge_levels when you need to choose whether to trade a one-ETF, two-ETF, or three-ETF hedge; keep using exposure for the telescoping four-layer story. See SEMANTIC_ALIASES.md and the OpenAPI HedgeLevelsBlock component.
GET /lstar — historical level dispatch + residual returns
GET /api/lstar returns daily series for one ticker: the server picks L1, L2, or L3 per date using marginal explained-return vs a threshold (default 1%, override with ?threshold=). Parallel arrays include lstar, dispatched market_hr / sector_hr / subsector_hr (nulls below the chosen level), total_er, and residual_return — the daily residual at the chosen level (l1_rr, l2_rr, or l3_rr). Raw l2_sector_er and l3_subsector_er are included for audit.
Billing: 0.02/call (billing_code: lstar_v1). MCP capability id lstar; Python SDK: client.get_lstar(ticker, years=5).
curl -sS "https://riskmodels.app/api/lstar?ticker=NFLX&years=5" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
| jq '{ticker, threshold_used, last: {lstar: .lstar[-1], residual_return: .residual_return[-1]}}'
Panel path — lstar_rr + lstar_level in MetricsV3
For cross-sectional workflows ("best residual for the universe today", not "one ticker over time"), use the MetricsV3 companion pair lstar_rr + lstar_level instead of the per-ticker /api/lstar endpoint. Both are materialized in the returns zarr at the canonical 1% threshold and surfaced on GET /api/metrics/{ticker}, POST /api/batch/analyze, and the V3 history fetches — no per-ticker round-trips.
lstar_rr— the Lstar-dispatched residual return at the level the model picked (L1 → market, L2 → sector, L3 → subsector residual).lstar_level— which level Lstar dispatched to:1= L1,2= L2,3= L3,null= no recommendation (both L2 and L3 ER missing).
Prefer lstar_rr over l3_rr when you want "the residual" — l3_rr is fixed at L3 regardless of whether subsector hedging is statistically warranted, so it overstates cleanness for names where lstar_level is 1 or 2. For a non-default threshold, fall back to the per-ticker /api/lstar?threshold=… path above.
curl -sS "https://riskmodels.app/api/metrics/AAPL" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
| jq '.metrics | {lstar_rr, lstar_level}'
GET /returns-decomposition — full daily decomposition in one call
GET /api/returns-decomposition returns the daily gross return plus the L1/L2/L3 factor return, combined-factor return, and residual return series for one ticker — everything from the returns-decomposition zarr in a single response. Use this instead of pulling l1_cfr, l1_fr, l1_rr, l2_cfr, … one key at a time. Add ?include_lstar=true (or ?dispatch=lstar) to append the Lstar level per date and the Lstar-dispatched residual return.
Billing: 0.02/call (billing_code: returns_decomposition_v1). Python SDK: client.get_returns_decomposition(ticker, years=5, include_lstar=True).
curl -sS "https://riskmodels.app/api/returns-decomposition?ticker=AAPL&years=5&include_lstar=true" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
| jq '{ticker, last: {gross: .gross[-1], l3_rr: .l3_rr[-1], lstar: .lstar[-1], lstar_rr: .lstar_residual_return[-1]}}'
See returns-decomposition-metrics for the per-key cheatsheet and the "Which residual?" guidance.
GET /industry-panel — Vasicek peer-β cross-section
GET /api/industry-panel returns the industry × level peer-β cross-section from the ds_erm3_industry zarr at one trading day (latest by default). Each row is one EODHD 4-digit industry at one cascade level (market / sector / subsector) with beta_mean, beta_variance, n_companies, and total_log_mcap_weight. This is the macro / sector-rotation surface — what the model thinks an industry's "typical" β is across its cap-weighted peer group.
Filter with ?level= (single level), ?teo=YYYY-MM-DD (single day), or ?min_peers= (drop thin industries). Billing: 0.02/call (billing_code: industry_panel_v1). Python SDK: client.get_industry_panel(level="subsector").
curl -sS "https://riskmodels.app/api/industry-panel?level=subsector&min_peers=20" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
| jq '.industries | sort_by(.beta_variance) | reverse | .[:5]'
The full panel is small (~72 industries × 3 levels × 4 metrics) — a 1 KB response that answers "which industries dispersed the most in β this period?" without any per-ticker round-trips.
POST /rankings/screen — universe-wide rank filter
POST /api/rankings/screen runs a server-side filter over the full ds_rankings cross-section at one teo (latest by default). Pick a metric (mkt_cap / gross_return / sector_residual / subsector_residual / er_l1 / er_l2 / er_l3), a cohort (universe / sector / subsector), and a window (1d / 21d / 63d / 252d); then filter by min_percentile, decile (1 = best), or sector_filter (sector-ETF ticker). Returns up to 500 rows sorted by rank_ordinal (1 = best, rank_percentile 100 = best).
The stat-arb cross-section in one call — replaces N per-ticker /rankings round-trips. Billing: 0.02/call (billing_code: rankings_screen_v1). Python SDK: client.screen_rankings(metric=..., cohort=..., window=..., min_percentile=95).
curl -sS -X POST "https://riskmodels.app/api/rankings/screen" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"metric":"subsector_residual","cohort":"subsector","window":"21d","min_percentile":95,"limit":50}' \
| jq '{teo, count: (.rows | length), top3: .rows[:3]}'
POST /batch/lstar — multi-ticker Lstar history
POST /api/batch/lstar is the panel-friendly Lstar surface: up to 100 tickers in one call, each returning the same daily series as GET /lstar (per-date lstar level, dispatched HRs, total ER, residual return). Pair with lstar_rr / lstar_level in MetricsV3 — that's the single-name latest snapshot; this is per-ticker history across a panel.
JSON returns a results map keyed by ticker; parquet/csv returns one long DataFrame ready for cross-sectional analysis. 0.005/ticker, min 0.01/call (25% cheaper than repeated GET /lstar). Python SDK: client.batch_lstar(tickers, years=5) + batch_lstar_to_dataframes() to split into per-ticker frames.
curl -sS -X POST "https://riskmodels.app/api/batch/lstar" \
-H "Authorization: Bearer $RISKMODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tickers":["AAPL","MSFT","NVDA","META","GOOG","AMZN","TSLA"],"years":5}' \
| jq '.results | to_entries | map({ticker:.key, last_lstar: .value.lstar[-1], last_rr: .value.residual_return[-1]})'
🤖 Agent-Native Helpers (Python SDK)
The riskmodels-py package includes methods and metadata designed for agent workflows and LLM reasoning:
| Tool | Purpose |
|---|---|
client.get_lstar() | Daily Lstar-dispatched hedge ratios, total ER, and residual_return at the chosen level (GET /lstar). Returns a DataFrame with date index. |
client.discover(format="json") | Returns JSON digest with method names, parameters (name/type/required/defaults/enums), return types, and tool_definition_hints for Claude Desktop or MCP-style tool synthesis. Acts as a map for agents. |
to_llm_context(obj) | One call → Markdown tables + lineage + semantic field cheatsheet + ERM3 legend. Works on DataFrame, PortfolioAnalysis, xarray.Dataset, or dict. Includes all metadata agents need to interpret results without guessing. |
client.from_env() | Auto-discover API key or OAuth credentials from environment variables (RISKMODELS_API_KEY or RISKMODELS_CLIENT_ID + RISKMODELS_CLIENT_SECRET). |
client.get_dataset() (aliases get_cube, get_panel) | After pip install riskmodels-py[xarray], turns batch long-table returns + rolling HRs into an xarray.Dataset for vectorized portfolio math and multi-ticker panels (format="parquet" default). |
| Ticker alias warnings | SDK logs and emits ValidationWarning when detecting ticker aliases (e.g. GOOGL→GOOG). Format: Warning: ... Fix: Use the canonical symbol in all future calls. Agents can self-correct. |
validate="warn" / "error" | ER sum and HR sign checks. Errors formatted as Error: {issue} Fix: {action} so agents and humans know how to proceed. |
df.attrs["legend"] | Every tabular result includes a short ERM3 legend (same as SHORT_ERM3_LEGEND). Read this instead of guessing column semantics. |
df.attrs["riskmodels_semantic_cheatsheet"] | JSON + bullet list mapping wire keys → semantic names + units. Ground truth for field interpretation. |
df.attrs["riskmodels_lineage"] | JSON string: model version, as-of date, factor set, universe size. Provenance for all API responses. |
Grounding wire keys in docs:
to_llm_context() pulls legend, lineage, and
riskmodels_semantic_cheatsheet from object attrs — aligned with the same
wire↔semantic definitions as
SEMANTIC_ALIASES.md
. Treat that file as narrative ground truth when explaining API JSON: e.g. wire
l3_res_er → idiosyncratic variance share (
l3_residual_er in SDK output) so agents self-correct instead of guessing meanings.
Install from PyPI (riskmodels-py). With xarray support for multi-dimensional portfolio math:
pip install riskmodels-py[xarray]
Minimal patterns
from riskmodels import RiskModelsClient
# Credentials from env (see Quickstart for variable names)
client = RiskModelsClient.from_env()
df = client.get_metrics("NVDA", as_dataframe=True) # attrs: legend, cheatsheet, lineage
pa = client.analyze({"NVDA": 0.5, "AAPL": 0.5}) # portfolio hedge ratios + per-name tables
# Requires the [xarray] extra — batch long table → labeled dimensions
ds = client.get_dataset(["NVDA", "AAPL", "MSFT"], years=2)
# ds is suitable for to_llm_context(ds) and broadcasted portfolio math (see Methodology)
See the package README for complete method signatures and examples.
Support
- Issues & feature requests: github.com/BlueWaterCorp/RiskModels_API/issues
- Email: service@riskmodels.app