RiskModels API Documentation
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/etf/factor-returns | GET | Snapshot of close + 1d / 21d / 63d / 252d total returns for SPY + the 11 GICS sector SPDR ETFs. Public-scope only — tickers outside that set return 400. Pairs with /industry-panel for the daily market + sector index read. | 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