API v2 (alpha)
Pre-stable.
/v2/portfolio-risk-stateis at2.0.0-alpha.5,/v2/stress-scenariois at2.0.0-alpha.2,/v2/backtest/shadowis at2.0.0-alpha.2, and/v2/risk-distributionis at2.0.0-alpha.1. Breaking changes may occur with 2-week notice until 2.0.0. Use v1 for production-stable workflows that don't need portfolio-level aggregation, stress, backtest, or loss distributions.
The v2 surface adds portfolio-aware endpoints on top of the v1 per-asset engine. v1 answers "what's the policy for BTC right now?" — v2 answers "given my actual book of N positions, am I within risk, what would happen under a flash crash, and how would the policy have governed me over the last 30 days?"
Endpoints
| Endpoint | Use case |
|---|---|
POST /v2/portfolio-risk-state | Aggregate up to 50 positions across BTC + ETH, evaluate per-position vs per-asset caps, compute correlation matrix and BTC-equivalent net, surface concentration warnings + stablecoin issuer/peg health. |
POST /v2/stress-scenario | Apply one of 6 deterministic tail templates (or a custom shock) to the same book and return baseline-vs-stressed side-by-side, position pnl, and a baseline-aware diff of triggered flags. |
POST /v2/backtest/shadow | Multi-asset shadow backtest: replay your daily portfolio path against the snapshot store (≤30 days), get per-day policy assessment + aggregate distribution metrics per horizon (policy distribution, return-by-policy, left-tail frequency, attribution estimate). The evidence engine for pilots. |
POST /v2/risk-distribution | Probabilistic loss distributions for a position or portfolio: return quantiles (P01–P99), expected shortfall (ES95/ES99), and P(loss > X), conditioned on market regime × volatility bucket. Deterministic historical bootstrap — same reference_time reproduces the response byte-identically, anchored by a distribution_hash. |
All endpoints share the same Bearer auth. The portfolio, stress, and backtest endpoints write to the same audit-decisions blob store readable via GET /v2/audit/decisions (with endpoint discriminator on each record); risk-distribution is anchored by its own distribution_hash instead (it never touches policy state).
Score freeze. The portfolio/stress/backtest endpoints inherit
scoring_version: "score_v3.2"from v1. The freeze is in effect until 2026-11-19 (clock reset by the 2026-05-19 bug-fix escape valve) — no weight, normalisation, threshold, cap, or aggregation logic will change before then. v2 alpha changes are additive (new fields, clarified contracts) and respect the freeze.risk-distributionreads no scoring state at all — it samples a versioned historical dataset.
Authentication
Identical to v1.
Authorization: Bearer <your_api_key>
| Type | Format | Rate limit |
|---|---|---|
| Owner | RISKSTATE_API_KEY env var | Unlimited |
| External | rs_live_ + 64 hex | 60 req/min sliding window |
Request a key from the home page — only an email is required. Free during beta. Both v1 and v2 endpoints accept the same key.
POST /v2/portfolio-risk-state
Multi-position aggregation. Up to 50 positions, BTC + ETH only, with optional per-issuer stablecoin holdings.
POST https://api.riskstate.ai/v2/portfolio-risk-state
Request
The full JSON body, with every supported field:
{
"positions": [
{ "asset": "BTC", "size_usd": 250000, "side": "long" },
{ "asset": "ETH", "size_usd": 80000, "side": "long" }
],
"stablecoin_holdings_usd": { "USDT": 50000, "USDC": 50000 },
"include_details": false
}
Copy-pasteable curl (replace $RISKSTATE_API_KEY with your key):
curl -X POST https://api.riskstate.ai/v2/portfolio-risk-state \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positions": [
{ "asset": "BTC", "size_usd": 250000, "side": "long" },
{ "asset": "ETH", "size_usd": 80000, "side": "long" }
],
"stablecoin_holdings_usd": { "USDT": 50000, "USDC": 50000 },
"include_details": false
}'
| Field | Type | Default | Description |
|---|---|---|---|
positions[] | array | required | 1–50 entries. Each { asset, size_usd, side, venue_type? }. asset ∈ ETH. side ∈ spot. size_usd > 0 and ≤ 1e12. |
stablecoin_holdings_usd | object | number | 0 | Either a number (legacy total, breakdown unknown) or an object keyed by issuer (USDT, USDC, DAI, FDUSD, TUSD, FRAX). Unknown issuers are accepted but flagged. |
include_details | bool | false | Include _per_asset_state (full v1 risk-state per asset that fed the aggregation). |
Response — top-level fields
| Field | Type | Notes |
|---|---|---|
api_version | string | Currently "2.0.0-alpha.5". Pre-stable. Bumped on additive changes. |
scoring_version | string | Always "score_v3" until 2026-10-22 freeze lift. |
ttl_seconds | int | 60. Same as v1. |
portfolio | object | Aggregated book metrics. See Portfolio fields below. |
correlations | object | null | 90d Pearson matrix + BTC-beta + source: "live" | "cache". 1h-cached. Returns { unavailable: true, reason } if CoinGecko is down — the rest of the response continues. |
positions[] | array | Per-position assessment. See Per-position fields below. |
portfolio_risk_flags | object | structural_blockers[], context_risks[], concentration_warnings[], stablecoin_warnings[]. Hierarchical flag families. Blockers and DANGER concentration / stablecoin flags drive portfolio.portfolio_allowed: false. |
stablecoin_breakdown | object | null | Per-issuer breakdown when stablecoin holdings declared. See Stablecoin breakdown fields below. Null otherwise. |
auditability | object | computed_at, latency_ms, key_type, audit_id, cache_status. audit_id is the key in the audit-decisions blob store. |
_per_asset_state | object | Only when include_details: true. Full v1 risk-state per asset that fed the aggregation. Useful for full audit / debugging. |
Per-position fields
| Field | Type | Notes |
|---|---|---|
index | int | 0-based index in the request positions[] array. |
asset | string | "BTC" or "ETH". |
size_usd | number | Echoed from request. |
side | string | "long", "short", "neutral". |
policy_level | int | 1–5. Snapshot of the per-asset policy level (lower = more restrictive). |
direction_bias | string | "LONG_PREFERRED", "SHORT_PREFERRED", or "NEUTRAL" from the per-asset engine. |
direction_layer | string | Which engine layer drove the bias: "tactical", "structural_veto", or "legacy_composite". |
max_size_fraction | number | Per-asset cap as a fraction of total_notional_usd. Engine-set, regime-dependent. |
max_size_usd | number | max_size_fraction × total_notional_usd. |
actual_pct | number | Position size as % of total_notional_usd. |
oversize_factor | number | actual_pct / max_size_fraction. >1 means oversized vs cap. |
direction_conflict | bool | True when position side opposes the engine's direction_bias. Mirrors the DIRECTION_BIAS_OPPOSITE advisory. Informational. |
allowed | bool | False if the position fails any blocking check (structural blocker on the asset OR oversize vs cap). Advisories alone never set this to false. |
reason_codes | string[] | Only the codes that drove allowed=false. Possible values: STRUCTURAL_BLOCK_<TAG>, POSITION_OVER_MAX_SIZE. Empty when allowed=true. |
advisories | string[] | Informational flags that DO NOT affect allowed. Possible values: DIRECTION_BIAS_OPPOSITE, NEW_ENTRY_BLOCKED_BY_ENGINE, ALL_IN_BLOCKED_BY_ENGINE. |
Portfolio fields
| Field | Type | Notes |
|---|---|---|
gross_exposure_usd | number | Sum of |size| across all positions (long + short). |
net_exposure_usd | number | Sum of signed sizes (long positive, short negative). |
long_exposure_usd | number | Sum of long-side positions. |
short_exposure_usd | number | Sum of short-side positions. |
total_notional_usd | number | gross_exposure + stablecoin_holdings_usd. The "deployable book" — denominator for actual_pct. |
stablecoin_holdings_usd | number | Total USD value of declared stablecoin holdings. |
stablecoin_concentration_pct | number | stablecoin_holdings / total_notional × 100. |
per_asset_concentration | object | Per-asset breakdown: long_usd, short_usd, gross_usd, gross_pct, net_pct, position_count. |
weighted_composite | number | null | Per-asset composite weighted by gross exposure. |
weighted_risk_permission_score | number | null | Per-asset risk_permission_score weighted by gross exposure. |
portfolio_allowed | bool | True if no structural blockers, no DANGER concentration, no oversized positions, no DANGER stablecoin flag. |
btc_equivalent_net_usd | number | null | Net exposure expressed in BTC-equivalent (using 90d beta). Null if correlations unavailable. |
btc_equivalent_gross_usd | number | null | Gross exposure expressed in BTC-equivalent. |
Stablecoin breakdown fields
Returned only when stablecoin_holdings_usd was declared as an issuer-keyed object (or as a positive number — in which case an UNKNOWN issuer entry is synthesised). Sorted by amount_usd descending.
| Field | Type | Notes |
|---|---|---|
total_usd | number | Sum across all issuers. |
issuer_count | int | Number of distinct issuers in the breakdown. |
breakdown[] | array | Per-issuer entry: { issuer, name, amount_usd, concentration_pct, price_usd, deviation_pct, peg_status }. |
breakdown[].concentration_pct | number | Issuer's % of total stablecoin holdings. |
breakdown[].deviation_pct | number | Current spot price deviation from $1.00, signed. |
breakdown[].peg_status | string | "OK" (|deviation| < 0.5%), "DEPEG_WARNING" (0.5–2%), or "DEPEG_DANGER" (>2%). |
flags[] | array | Aggregated stablecoin flags: STABLE_CONCENTRATION_<ISSUER>_OVER_50 (WARNING) / _OVER_80 (DANGER), STABLE_DEPEG_<ISSUER> per offending issuer. |
peg_health_source | string | "live" if peg prices were fetched from CoinGecko this request, "cache" if served from the 5min snapshot. |
peg_health_age_ms | int | Age of the peg-health data in ms. |
unrecognised_issuers | string[] | Issuers in the request not in the registry (USDT, USDC, DAI, FDUSD, TUSD, FRAX). Treated as UNKNOWN issuer with peg_status: "UNKNOWN". |
Example response (alpha.5)
A 75/25 BTC/ETH long-only book with $100k stables, BTC oversized vs the per-asset cap:
{
"api_version": "2.0.0-alpha.5",
"scoring_version": "score_v3",
"ttl_seconds": 60,
"portfolio": {
"gross_exposure_usd": 330000,
"long_exposure_usd": 330000,
"short_exposure_usd": 0,
"net_exposure_usd": 330000,
"total_notional_usd": 430000,
"stablecoin_holdings_usd": 100000,
"stablecoin_concentration_pct": 23.26,
"weighted_composite": 54,
"weighted_risk_permission_score": 48,
"portfolio_allowed": true,
"btc_equivalent_net_usd": 351840,
"btc_equivalent_gross_usd": 351840
},
"correlations": {
"source": "live",
"matrix": { "BTC": { "BTC": 1.0, "ETH": 0.81 }, "ETH": { "BTC": 0.81, "ETH": 1.0 } },
"beta": { "BTC": 1.0, "ETH": 1.273 }
},
"positions": [
{
"index": 0, "asset": "BTC", "size_usd": 250000, "side": "long",
"policy_level": 4, "direction_bias": "LONG_PREFERRED",
"max_size_fraction": 0.42, "max_size_usd": 180600,
"actual_pct": 58.14, "oversize_factor": 1.384,
"direction_conflict": false,
"allowed": false,
"reason_codes": ["POSITION_OVER_MAX_SIZE"],
"advisories": []
},
{
"index": 1, "asset": "ETH", "size_usd": 80000, "side": "long",
"policy_level": 4, "direction_bias": "LONG_PREFERRED",
"max_size_fraction": 0.48, "max_size_usd": 206400,
"actual_pct": 18.60, "oversize_factor": 0.388,
"direction_conflict": false,
"allowed": true,
"reason_codes": [],
"advisories": []
}
],
"portfolio_risk_flags": {
"structural_blockers": [],
"context_risks": ["BTC:HIGH_COUPLING"],
"concentration_warnings": [],
"stablecoin_warnings": []
},
"stablecoin_breakdown": {
"issuers": [
{ "issuer": "USDT", "name": "Tether", "amount_usd": 50000, "pct_of_stables": 50.0, "pct_of_total": 11.63, "peg_deviation_pct": 0.02, "peg_status": "OK" },
{ "issuer": "USDC", "name": "USD Coin", "amount_usd": 50000, "pct_of_stables": 50.0, "pct_of_total": 11.63, "peg_deviation_pct": -0.01, "peg_status": "OK" }
]
},
"auditability": {
"computed_at": "2026-05-05T11:32:14.123Z",
"latency_ms": 184,
"key_type": "external",
"audit_id": "1714905134123-a3kf7p",
"cache_status": {
"BTC": { "hit": true, "age_ms": 14000 },
"ETH": { "hit": true, "age_ms": 14000 }
}
}
}
How to read this:
portfolio.portfolio_allowed: true— concentration is a non-issue here (BTC 75.76% gross of crypto-only is the older view; gross of TOTAL notional including stables is 58.14%).positions[0].allowed: falsebecause BTC'sactual_pct (58.14)exceeds the per-asset capmax_size_fraction (42)× 100. The fix is either reduce BTC or grow the book.correlations.source: "live"means the matrix was fetched fresh;"cache"means it was served from the 1h-cached snapshot.
reason_codes vs advisories
Each per-position assessment reports two arrays. They are deliberately separated.
| Field | Contents | Effect on allowed |
|---|---|---|
reason_codes | ONLY codes that drove allowed=false. Empty when allowed=true. | Yes — these are the binding reasons. |
advisories | Informational codes the engine wants to surface but that don't themselves block this position. | No — allowed can be true AND advisories can be non-empty. |
reason_codes values:
STRUCTURAL_BLOCK_<TAG>— propagated from per-asset risk flags. Hard structural blocker on the underlying asset.POSITION_OVER_MAX_SIZE—actual_pct > max_size_fraction.
advisories values:
DIRECTION_BIAS_OPPOSITE— position side fights the engine'sdirection_bias. Mirrored by the booleandirection_conflict.NEW_ENTRY_BLOCKED_BY_ENGINE— the per-assetblocked_actionsincludesNEW_ENTRY. Existing positions within cap aren't blocked by this endpoint, but customer's own opening workflow should respect this.ALL_IN_BLOCKED_BY_ENGINE— same pattern forALL_IN.
Pre-2026-05-04 these codes lived in
reason_codeseven whenallowed=true, which caused integration confusion (e.g.BLOCKED_ACTION_PRESENTalongsideallowed=true). The split was introduced in alpha.5. Old clients readingreason_codesfor blocking checks continue to work — they just see fewer codes.
Additional scenarios
Hedged book (long + short, same asset)
A basis-trade desk holds spot BTC long + perp BTC short. Net exposure should be near zero; gross is the funding-carry exposure.
curl -sS -X POST https://api.riskstate.ai/v2/portfolio-risk-state \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positions": [
{"asset": "BTC", "size_usd": 500000, "side": "long", "venue_type": "spot"},
{"asset": "BTC", "size_usd": 500000, "side": "short", "venue_type": "perp"}
]
}'
Response highlights:
{
"portfolio": {
"gross_exposure_usd": 1000000,
"net_exposure_usd": 0,
"long_exposure_usd": 500000,
"short_exposure_usd": 500000,
"btc_equivalent_net_usd": 0,
"btc_equivalent_gross_usd": 1000000,
"per_asset_concentration": {
"BTC": { "long_usd": 500000, "short_usd": 500000, "gross_usd": 1000000, "gross_pct": 100.0, "net_pct": 0, "position_count": 2 }
},
"portfolio_allowed": false
},
"portfolio_risk_flags": {
"concentration_warnings": [
{ "level": "DANGER", "code": "CONCENTRATION_BTC_OVER_90", "msg": "BTC is 100% of gross exposure (threshold 90%)" }
]
}
}
Reading: net_exposure_usd: 0 — perfectly hedged, no directional exposure. gross_exposure_usd: 1000000 — full notional carry; funding rate, basis, and counterparty risk apply to gross. portfolio_allowed: false because BTC is 100% of gross (DANGER threshold). For a basis trade this is structural and expected — if your fund's risk policy classifies basis trades as a separate book, send only basis positions in their own portfolio request and don't aggregate with directional book.
Direction conflict (long when bias is SHORT_PREFERRED)
A desk wants to add a long ETH position during a regime where the engine's direction_bias is SHORT_PREFERRED (e.g. tactical EUPHORIA + bearish structural).
curl -sS -X POST https://api.riskstate.ai/v2/portfolio-risk-state \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"positions":[{"asset":"ETH","size_usd":50000,"side":"long"}]}'
Response (when direction_bias is SHORT_PREFERRED):
{
"positions": [
{
"index": 0, "asset": "ETH", "size_usd": 50000, "side": "long",
"direction_bias": "SHORT_PREFERRED",
"direction_layer": "tactical",
"max_size_fraction": 0.35, "max_size_usd": 17500,
"actual_pct": 100.0, "oversize_factor": 2.857,
"direction_conflict": true,
"allowed": false,
"reason_codes": ["POSITION_OVER_MAX_SIZE"],
"advisories": ["DIRECTION_BIAS_OPPOSITE"]
}
]
}
Reading: reason_codes lists ONLY the codes that drove allowed=false. advisories is informational — the engine flagging a soft concern that doesn't itself block this position. direction_conflict: true mirrors the DIRECTION_BIAS_OPPOSITE advisory. An institutional desk receiving this advisory should treat it as "the engine thinks this trade is fighting the regime — justify your conviction or wait." A position can be allowed=true AND carry advisories at the same time.
Stablecoin-heavy book with issuer concentration
A fund treasury holds 1.5M USDT, 200k USDC, 50k DAI plus modest crypto exposure. Post-USDC-depeg-2023 they want to track per-issuer concentration.
curl -sS -X POST https://api.riskstate.ai/v2/portfolio-risk-state \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positions": [
{"asset": "BTC", "size_usd": 250000, "side": "long"},
{"asset": "ETH", "size_usd": 80000, "side": "long"}
],
"stablecoin_holdings_usd": {
"USDT": 1500000,
"USDC": 200000,
"DAI": 50000
}
}'
Response highlights:
{
"portfolio": {
"gross_exposure_usd": 330000,
"stablecoin_holdings_usd": 1750000,
"stablecoin_concentration_pct": 84.13,
"total_notional_usd": 2080000,
"portfolio_allowed": false
},
"stablecoin_breakdown": {
"total_usd": 1750000,
"issuer_count": 3,
"breakdown": [
{ "issuer": "USDT", "name": "Tether", "amount_usd": 1500000, "concentration_pct": 85.71, "price_usd": 1.0001, "deviation_pct": 0.01, "peg_status": "OK" },
{ "issuer": "USDC", "name": "USD Coin", "amount_usd": 200000, "concentration_pct": 11.43, "price_usd": 0.9998, "deviation_pct": -0.02, "peg_status": "OK" },
{ "issuer": "DAI", "name": "Dai", "amount_usd": 50000, "concentration_pct": 2.86, "price_usd": 1.0000, "deviation_pct": 0.00, "peg_status": "OK" }
],
"flags": [
{ "level": "DANGER", "code": "STABLE_CONCENTRATION_USDT_OVER_80", "msg": "USDT is 85.71% of stablecoin holdings (threshold 80%)" }
],
"peg_health_source": "live",
"peg_health_age_ms": 0,
"unrecognised_issuers": []
},
"portfolio_risk_flags": {
"stablecoin_warnings": [
{ "level": "DANGER", "code": "STABLE_CONCENTRATION_USDT_OVER_80", "msg": "USDT is 85.71% of stablecoin holdings (threshold 80%)" }
]
}
}
Reading: portfolio_allowed: false is driven by USDT issuer concentration — even with peg-OK and modest crypto exposure, having 85% of stablecoin reserves in a single issuer is a counterparty risk an institutional desk should rebalance. If USDT had been at $0.985 (1.5% deviation), peg_status would be "DEPEG_WARNING"; at $0.97 (3% deviation) → "DEPEG_DANGER" plus a separate STABLE_DEPEG_USDT flag.
Cache layers
| Cache | TTL | Why |
|---|---|---|
risk-state-cache (per-asset, inherited from v1) | 60s | Fast-moving market data. |
portfolio-correlations | 1h | Correlation matrix moves slowly. |
stablecoin-pegs | 5min | Peg events develop over hours, not seconds. |
When per-asset cache misses, the endpoint warms in parallel (Promise.allSettled over the missing assets, 12s timeout each). If both miss simultaneously and warming fails, the response is 503 with Retry-After: 30 and a cache_status block per asset for diagnosis.
Error responses
| HTTP | Meaning | Common causes |
|---|---|---|
| 200 | OK | Normal success path. Check portfolio.portfolio_allowed and portfolio_risk_flags for the actual decision. |
| 400 | Bad Request | Invalid JSON, unsupported asset, invalid side / venue_type, position size out of range, stablecoin holdings malformed, empty positions array. Specific message in error field. |
| 401 | Unauthorized | Missing or invalid Bearer token. |
| 429 | Rate limit exceeded | External key over 60 req/min sliding window. Retry-After: 60 header included. |
| 503 | Service Unavailable | Per-asset cache cold AND warming attempt failed (Binance / CoinGlass upstream issue). Retry-After: 30 header included. Body includes cache_status per asset for diagnosis. |
Smoke test script
For CI integration or quick health checks:
#!/usr/bin/env bash
set -euo pipefail
API_URL="${API_URL:-https://api.riskstate.ai/v2/portfolio-risk-state}"
TOKEN="${RISKSTATE_API_KEY:?Need RISKSTATE_API_KEY}"
run() {
local name="$1"
local body="$2"
local expect_status="${3:-200}"
local code
code=$(curl -sS -o /tmp/portfolio-resp.json -w '%{http_code}' \
-X POST "$API_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$body")
if [[ "$code" != "$expect_status" ]]; then
echo "[FAIL] $name: expected $expect_status, got $code"
cat /tmp/portfolio-resp.json
return 1
fi
echo "[OK] $name ($code)"
}
run "simple-long" '{"positions":[{"asset":"BTC","size_usd":10000,"side":"long"}]}'
run "hedged" '{"positions":[{"asset":"BTC","size_usd":50000,"side":"long"},{"asset":"BTC","size_usd":50000,"side":"short"}]}'
run "stables" '{"positions":[{"asset":"BTC","size_usd":10000,"side":"long"}],"stablecoin_holdings_usd":{"USDT":50000,"USDC":50000}}'
run "empty" '{"positions":[]}' 400
run "bad-asset" '{"positions":[{"asset":"DOGE","size_usd":10000,"side":"long"}]}' 400
echo "All scenarios passed."
POST /v2/stress-scenario
Apply a deterministic shock template to the same book and return baseline vs stressed side-by-side. Companion to /v2/portfolio-risk-state.
POST https://api.riskstate.ai/v2/stress-scenario
Request
Same positions[] and stablecoin_holdings_usd as the portfolio endpoint, plus a scenario field:
{
"positions": [
{ "asset": "BTC", "size_usd": 750000, "side": "long" },
{ "asset": "ETH", "size_usd": 250000, "side": "long" }
],
"scenario": "flash_crash"
}
Copy-pasteable curl (replace $RISKSTATE_API_KEY with your key):
curl -X POST https://api.riskstate.ai/v2/stress-scenario \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positions": [
{ "asset": "BTC", "size_usd": 750000, "side": "long" },
{ "asset": "ETH", "size_usd": 250000, "side": "long" }
],
"scenario": "flash_crash"
}'
| Field | Type | Description |
|---|---|---|
scenario | string | One of flash_crash, funding_blowout, stablecoin_depeg, liquidation_cascade, btc_eth_decoupling, dxy_spike. Required. |
scenario_params | object | Optional. Deep-merges into the template's shocks. Use to model variant tails (e.g. USDT-only depeg of −5%). |
Built-in templates
| Key | Historical anchor | Headline shock |
|---|---|---|
flash_crash | LUNA May 2022, FTX Nov 2022, COVID Mar 2020 | BTC −30%, ETH −35%, vol×1.5, funding −0.05 |
funding_blowout | Apr/Oct 2021, Mar 2024 | Prices flat; funding +0.3% / 8h (95th pct) |
stablecoin_depeg | USDC Mar 2023, UST May 2022 | USDT −3%, USDC −1.5%, others mild |
liquidation_cascade | Aug 17 2024 yen carry, May 19 2021 | BTC −15%, ETH −20%, OI −30%, +$200M liq |
btc_eth_decoupling | Aug 2021 (ETH 2.0), Mar 2024 ETF mismatch | BTC −5%, ETH +10% |
dxy_spike | Sep–Oct 2022, Feb 2025 tariffs | BTC −8%, ETH −10%, DXY +3%, 10Y +30bps |
Response — top-level fields
| Field | Type | Notes |
|---|---|---|
api_version | string | Currently "2.0.0-alpha.2". |
scoring_version | string | Always "score_v3" until 2026-10-22 freeze lift. |
scenario | object | { key, name, description, historical_anchor, shocks_applied, custom_override_applied }. shocks_applied is the final merged shock object after scenario_params deep-merge (if any). |
baseline | object | Pre-shock view. See Baseline / stressed fields below. |
stressed | object | Post-shock view. Same shape as baseline. |
positions[] | array | Per-position pnl: { index, asset, side, baseline_size_usd, shock_pct_applied, pnl_pct, pnl_usd, stressed_size_usd }. stressed_size_usd is post-pnl on shorts — see post-pnl semantics. |
stablecoin_breakdown | object | null | Per-issuer baseline → stressed delta. Null if no stablecoin holdings. |
portfolio_pnl_usd | number | Total pnl across positions + stables. |
portfolio_pnl_pct | number | portfolio_pnl_usd / baseline.total_notional_usd × 100. |
pnl_breakdown | object | { positions_pnl_usd, stablecoin_pnl_usd } for attribution. |
triggered_flags[] | array | NEW flags fired by the scenario but not at baseline. Diff by code across concentration AND stablecoin flag families. |
baseline_flag_count | int | Concentration + stablecoin flag count at baseline. |
stressed_flag_count | int | Same at stress. |
auditability | object | { computed_at, latency_ms, key_type, audit_id }. |
Baseline / stressed fields
Both branches share this shape:
| Field | Type | Notes |
|---|---|---|
gross_exposure_usd | number | Sum of |size| across positions. Stressed branch reflects post-pnl values for shorts (see post-pnl semantics below). |
net_exposure_usd | number | Sum of signed sizes (long − short, post-pnl on stressed branch). |
long_exposure_usd | number | Sum of long-side positions. |
short_exposure_usd | number | Sum of short-side positions. |
stablecoin_holdings_usd | number | Total USD of stablecoin holdings (post-shock on stressed branch). |
total_notional_usd | number | gross_exposure + stablecoin_holdings. The denominator for portfolio_pnl_pct. |
per_asset_concentration | object | Per-asset { long_usd, short_usd, gross_usd, gross_pct, net_pct, position_count }. |
portfolio_allowed | bool | True if no DANGER concentration flag AND no DANGER stablecoin flag. Both flag families covered alpha.2 onward. |
flags[] | array | Concentration + stablecoin flags evaluated at this branch. |
Three contract details that have surprised first-time integrators
1. stressed_size_usd on hedged shorts is post-pnl, not underlying notional
Worked example — basis-trade desk holds spot BTC long + perp BTC short, runs flash_crash:
curl -sS -X POST https://api.riskstate.ai/v2/stress-scenario \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"positions": [
{"asset": "BTC", "size_usd": 500000, "side": "long"},
{"asset": "BTC", "size_usd": 500000, "side": "short"}
],
"scenario": "flash_crash"
}'
Response (positions block):
{
"positions": [
{ "index": 0, "asset": "BTC", "side": "long", "baseline_size_usd": 500000, "shock_pct_applied": -30, "pnl_pct": -30, "pnl_usd": -150000, "stressed_size_usd": 350000 },
{ "index": 1, "asset": "BTC", "side": "short", "baseline_size_usd": 500000, "shock_pct_applied": -30, "pnl_pct": +30, "pnl_usd": +150000, "stressed_size_usd": 650000 }
],
"portfolio_pnl_usd": 0,
"portfolio_pnl_pct": 0
}
The short reports stressed_size_usd: 650000, not 500000. That's intentional — the position's value to the holder rose by $150k (the short profited from the −30% drop), so stressed_size_usd = baseline_size + pnl_usd reflects book value after the shock. Aggregating across asymmetric long/short books then yields a clean post-shock book value. Net pnl on the hedged book: 0 (perfect hedge).
If you need the underlying notional instead (e.g. for margin-requirement math):
const stressedUnderlying = Math.abs(p.stressed_size_usd - p.pnl_usd); // = baseline_size_usd
This is a modeling choice, not a bug. A stressed_underlying_notional_usd field is a candidate for a future minor version if multiple integrators want both views.
2. baseline.portfolio_allowed covers BOTH crypto concentration AND stablecoin DANGER
Pre-alpha.2 it only covered concentration, so a portfolio with a healthy crypto book + 100% USDT could pass baseline despite a structural stable issue. Alpha.2 onward, baseline checks both flag families. A book with 100% USDT can return baseline.portfolio_allowed: false before any scenario shock — that's the correct read.
3. triggered_flags is a baseline-aware diff, applied to both flag families
A flag that fires under stress AND would have fired at baseline is NOT considered "triggered by the scenario." Pre-alpha.2 stablecoin flags were always reported as triggered; alpha.2 onward both concentration and stablecoin flags use the same diff logic. So a USDC-heavy book under stablecoin_depeg reports only STABLE_DEPEG_USDC as triggered (the concentration flag was already true at baseline).
Custom override (scenario_params)
{
"positions": [{"asset":"BTC","size_usd":50000,"side":"long"}],
"stablecoin_holdings_usd": { "USDT": 800000 },
"scenario": "stablecoin_depeg",
"scenario_params": {
"stablecoin_peg_shift_pct": { "USDT": -5 },
"vol_multiplier": 1.0
}
}
scenario_params deep-merges into the base template. Only the keys you supply override; everything else stays as the template default. response.scenario.custom_override_applied: true confirms the merge.
Determinism
The shocks are pre-specified constants in the function source. There are no upstream API calls during scenario execution — the only inputs are the request body. Two identical requests always produce identical responses (modulo auditability.computed_at).
Error responses
| HTTP | Meaning | Common causes |
|---|---|---|
| 200 | OK | Normal success path. Read triggered_flags, portfolio_pnl_pct, stressed.portfolio_allowed. |
| 400 | Bad Request | Invalid JSON; unknown scenario key (response includes registry); invalid scenario_params shape; position validation failure (asset, side, size_usd); stablecoin holdings malformed. |
| 401 | Unauthorized | Missing or invalid Bearer token. |
| 429 | Rate limit exceeded | External key over 60 req/min sliding window. Retry-After: 60. |
This endpoint does NOT call upstream price providers — all shocks are deterministic. It does NOT return 503 for cache-warming reasons (unlike /v2/portfolio-risk-state). Latency is consistently <500ms.
Smoke test script
#!/usr/bin/env bash
set -euo pipefail
API_URL="${API_URL:-https://api.riskstate.ai/v2/stress-scenario}"
TOKEN="${RISKSTATE_API_KEY:?Need RISKSTATE_API_KEY}"
run() {
local name="$1" body="$2"
printf '\n=== %s ===\n' "$name"
curl -sS -X POST "$API_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$body" \
| jq '{name: .scenario.name, baseline_allowed: .baseline.portfolio_allowed, stressed_allowed: .stressed.portfolio_allowed, pnl_pct: .portfolio_pnl_pct, triggered: [.triggered_flags[].code]}'
}
run "flash crash long-only" '{"positions":[{"asset":"BTC","size_usd":750000,"side":"long"},{"asset":"ETH","size_usd":250000,"side":"long"}],"scenario":"flash_crash"}'
run "flash crash hedged" '{"positions":[{"asset":"BTC","size_usd":500000,"side":"long"},{"asset":"BTC","size_usd":500000,"side":"short"}],"scenario":"flash_crash"}'
run "stablecoin depeg w/ stables" '{"positions":[{"asset":"BTC","size_usd":1,"side":"long"}],"stablecoin_holdings_usd":{"USDT":500000,"USDC":500000},"scenario":"stablecoin_depeg"}'
run "decoupling" '{"positions":[{"asset":"BTC","size_usd":500000,"side":"long"},{"asset":"ETH","size_usd":500000,"side":"long"}],"scenario":"btc_eth_decoupling"}'
run "dxy spike" '{"positions":[{"asset":"BTC","size_usd":1000000,"side":"long"}],"scenario":"dxy_spike"}'
What this endpoint is NOT
- Not a path-dependent simulator. Single-step deterministic shock. Real liquidation cascades and depeg events have order-book dynamics that are not modeled. Treat the output as a snapshot of the post-shock book, not a backtested PnL trajectory.
- Not a Monte Carlo / VaR engine. Scenarios are named historical analogs, not statistical distributions. For VaR-style risk reporting, use this endpoint as input to your own historical-simulation framework.
- Not a hedging optimiser. It tells you what the book looks like under the shock; it does not propose a hedge.
POST /v2/backtest/shadow
Multi-asset shadow backtest. The caller submits a historical portfolio path (up to 30 daily entries). For each entry the endpoint matches the closest snapshot in the store (±12h) and returns:
- BTC policy from the immutable stored snapshot (
snap.composite+snap.policy) — what the engine actually decided at that point in time. - ETH policy rescored on-demand via the same
scoring-core.computeCompositeused by v1, applied to the snapshot's indicators. Snapshots captured from 2026-05-14 onward (v6.3-server) include per-asset funding percentile so ETH rescoring is full-fidelity. Older snapshots fall back to sigmoid funding on the captured current rate (still a real signal, not neutralised). - Aggregate distribution metrics per horizon: policy distribution (BLOCK / CAUTIOUS / GREEN counts), return-by-policy (mean / median / MAE / MFE per bucket), left-tail frequency (% of days realised return below threshold per bucket), naive attribution estimate (drawdown_avoided vs upside_sacrifice).
This is the evidence engine a pilot uses to validate "would the policy have helped me over the last 30 days?". The audit log records every query; institutional callers can prove which historical paths were queried and what the engine returned.
POST https://api.riskstate.ai/v2/backtest/shadow
Request
{
"portfolio_path": [
{ "date": "2026-04-15", "positions": [
{ "asset": "BTC", "size_usd": 250000, "side": "long" },
{ "asset": "ETH", "size_usd": 80000, "side": "long" }
]},
{ "date": "2026-04-16", "positions": [
{ "asset": "BTC", "size_usd": 250000, "side": "long" },
{ "asset": "ETH", "size_usd": 80000, "side": "long" }
]}
],
"horizons": [24, 72, 168],
"drawdown_threshold_pct": -5,
"include_per_day": true
}
Copy-pasteable curl:
curl -X POST https://api.riskstate.ai/v2/backtest/shadow \
-H "Authorization: Bearer $RISKSTATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"portfolio_path": [
{"date": "2026-04-15", "positions": [{"asset":"BTC","size_usd":250000,"side":"long"}, {"asset":"ETH","size_usd":80000,"side":"long"}]},
{"date": "2026-04-16", "positions": [{"asset":"BTC","size_usd":250000,"side":"long"}, {"asset":"ETH","size_usd":80000,"side":"long"}]}
],
"horizons": [24, 72, 168],
"drawdown_threshold_pct": -5
}'
Request fields
| Field | Type | Default | Description |
|---|---|---|---|
portfolio_path[] | array | required | 1–30 entries. Each { date, positions[] }. date is ISO YYYY-MM-DD, Unix seconds, or Unix ms. positions follows the same shape as /v2/portfolio-risk-state (1+ entries, asset ∈ ETH, side ∈ spot, size_usd > 0). |
horizons | int[] | [24, 72, 168] | Forward-return horizons in hours. Allowed: 24, 72, 168, 720. Aggregate is reported for every horizon. The first entry is the primary horizon (used for n_days_with_forward count and audit summary). |
drawdown_threshold_pct | number | -5 | Threshold for the left-tail frequency calc. Days with realised return below this fire the left-tail counter per policy bucket. |
include_per_day | bool | true | Set false to skip the per_day array in the response and only return aggregate + request_summary. Useful for large queries where the caller only needs the headline. |
Response — top-level fields
| Field | Type | Notes |
|---|---|---|
api_version | string | Currently "2.0.0-alpha.2". Pre-stable. |
scoring_version | string | Always "score_v3" until 2026-10-22 freeze lift. |
request_summary | object | Echoes the request + match results. See below. |
eth_caveat | object | null | Surfaces ETH-specific caveats: how many days used pre-v6.3 snapshots (sigmoid funding fallback), and the BTC-dispersion quality proxy note. Null when no ETH positions were in the path. |
policy_config_applied | object | null | Per-key policy_config snapshot (desk_id, max_gross, max_per_asset, leverage_cap) when an external key has a config attached. Null for owner keys and external keys without a config. When non-null, BTC and ETH max_size_fraction are tightened symmetrically. |
aggregate | object | { per_horizon: { 24: {...}, 72: {...}, 168: {...} } }. Each horizon entry has per_asset.BTC and per_asset.ETH (omitted if zero days for that asset). |
per_day[] | null | array | Per-day breakdown. Only when include_per_day: true. See Per-day fields below. |
auditability | object | computed_at, latency_ms, key_type, snapshot_index_size, audit_id. The audit_id is the key under audit-decisions and can be cross-checked via GET /v2/audit/decisions. |
request_summary fields
| Field | Type | Notes |
|---|---|---|
n_days_requested | int | Length of portfolio_path. |
n_days_matched | int | Days where a snapshot was found within ±12h of the requested date. |
n_days_unmatched | int | n_days_requested - n_days_matched. |
n_days_with_forward | int | Days where the primary horizon's forward return is filled (btc_24h or eth_24h for primary=24). |
primary_horizon_h | int | First entry of horizons. |
horizons_h | int[] | Echoes the request horizons. |
drawdown_threshold_pct | number | Echoes the threshold. |
match_tolerance_h | int | Always 12. The closest snapshot must be within ±12h of the requested date. |
unmatched_warning | object | null | When ≥25% of requested dates are unmatched, emits { ratio, threshold, n_unmatched, n_total, hint }. Null when the ratio is below threshold. |
Per-day fields
| Field | Type | Notes |
|---|---|---|
date | string | int | Echoed from request. |
matched | bool | False if no snapshot within ±12h. When false, only reason + tolerance_h follow. |
snapshot_key | string | Blob key of the matched snapshot. |
snapshot_ts | string | ISO timestamp of the matched snapshot. |
snapshot_ts_epoch | int | Unix seconds. |
match_delta_h | number | Signed hours between snapshot ts and requested date. Negative = snapshot before requested. |
snapshot_version | string | E.g. "v6.3-server" (full ETH funding capture) or "v6.2-server" (pre-ETH-funding). Drives eth_funding_capture. |
quality_score | int | null | Snapshot's data-quality score (0–100, % of subsources LIVE). Filter for ≥ 80 for inferentially-clean subset. |
eth_funding_capture | string | "live" if the snapshot's positioning.fundingPctile.ETH is captured (v6.3+), "unavailable" otherwise. |
portfolio | object | gross_exposure_usd, position_count for this day's positions. |
per_asset_state | object | Per-asset policy state. BTC: source: "stored". ETH: source: "rescored" with eth_funding_source: "percentile" | "sigmoid-current" | "neutral-default". When policy_config tightened a max_size_fraction, the engine value is preserved in engine_max_size_fraction + policy_config_applied lists the codes that fired. |
positions[] | array | Per-position assessment: max_size_fraction, max_size_usd, actual_pct, oversize_factor, allowed, reason (POSITION_OVER_MAX_SIZE when allowed=false). |
forward_returns | object | null | { btc_24h, btc_72h, btc_168h, eth_24h, eth_72h, eth_168h, *_mae, *_mfe } for matched-and-filled horizons. null when no forward returns are filled (e.g. the snapshot is too young, or fill-returns hasn't reached it yet — see operational notes). |
aggregate.per_horizon shape
For each requested horizon, per asset (BTC and/or ETH, omitted if zero days):
{
"horizon_h": 72,
"per_asset": {
"BTC": {
"n_days": 12,
"policy_distribution": { "BLOCK": 3, "CAUTIOUS": 7, "GREEN": 2 },
"return_by_policy": {
"BLOCK": { "n": 3, "mean_return_pct": -2.1, "median_return_pct": -1.8, "mae_pct": -3.4, "mfe_pct": 1.2 },
"CAUTIOUS": { "n": 7, "mean_return_pct": 0.4, "median_return_pct": 0.2, "mae_pct": -2.1, "mfe_pct": 2.8 },
"GREEN": { "n": 2, "mean_return_pct": 3.1, "median_return_pct": 3.1, "mae_pct": -0.8, "mfe_pct": 4.2 }
},
"left_tail_frequency_pct": {
"threshold_pct": -5,
"BLOCK": { "n": 3, "below_threshold": 1, "freq_pct": 33.3 },
"CAUTIOUS": { "n": 7, "below_threshold": 0, "freq_pct": 0.0 },
"GREEN": { "n": 2, "below_threshold": 0, "freq_pct": 0.0 }
},
"attribution_estimate": {
"note": "Naive estimate: assumes positions sized exactly to max_size_fraction. Real P&L attribution depends on caller actual sizing vs cap behaviour.",
"drawdown_avoided_pct": 1.85,
"upside_sacrifice_pct": 0.62,
"net_effect_pct": 1.23,
"method": "block_cautious_only"
}
}
}
}
How to read attribution_estimate: the engine claims to have prevented drawdown_avoided_pct of capital loss by sizing down on negative days when policy was BLOCK or CAUTIOUS, at the cost of upside_sacrifice_pct of foregone gains on positive days in those same buckets. net_effect_pct is drawdown_avoided - upside_sacrifice. The estimate is naive — it assumes positions sized exactly to max_size_fraction and ignores the caller's actual position-vs-cap behaviour. Treat as a directional signal, not a guaranteed P&L.
ETH caveat
When ETH positions are in the path, eth_caveat surfaces two honest limitations:
{
"n_eth_days": 12,
"n_pre_v6_3_snapshots": 9,
"note": "9/12 ETH-position days used pre-v6.3 snapshots (no per-asset funding percentile captured). ETH composite still rescored using current sigmoid funding fallback on the captured current rate — not neutralised. Newer snapshots (v6.3+) use percentile-based funding which is more discriminating.",
"eth_max_size_quality_source": "btc_dispersion_proxy",
"eth_max_size_quality_note": "ETH max_size_fraction uses BTC composite dispersion as quality proxy. ETH-specific dispersion quality is a v4 enhancement (see specs/institutional-roadmap-2026H2.md V4-3)."
}
The capture v6.3 schema ships 2026-05-14. Pre-v6.3 snapshots remain readable — ETH composite is rescored from indicators using sigmoid fallback for the funding subscore. From 2026-05-14 + 30 days onward, every ETH-position day in the queryable window has full percentile-based funding.
Operational notes
fill-returnscadence. Forward returns are filled by a scheduled function every 6h. After 2026-05-14 the iteration order is newest-first, so any date within the last ≤30 days has fresh forward returns within hours of capture. Older snapshots backfill in the background. Ifn_days_with_forwardis unexpectedly low for recent dates, checkquality_scoreandeth_funding_capture— pre-v6.3 snapshots predate the inversion fix.- Audit. Every query writes one record to the
audit-decisionsblob store. The full record is atdec/{key_prefix}/{audit_id}.json; the daily index entry atidx/{YYYY-MM-DD}.jsoncarriesendpoint: "backtest-shadow"so clients can filter the/v2/audit/decisionslisting by endpoint. policy_configinteraction. When the caller's API key has apolicy_configattached (see Policy Config),max_grossandmax_per_asset[asset]caps tighten the per-assetmax_size_fractionon every day in the path. The engine value is preserved inengine_max_size_fraction,engine_policy_level,engine_policy_label, and the codes that fired are listed underpolicy_config_applied.reason_codes. BTC and ETH are tightened symmetrically.- Match window. The endpoint matches each requested date to the closest snapshot in the store within ±12h. Snapshots are captured every ~4h, so a daily requested date will typically match within 0–2h. If a date is unmatched (older than the snapshot window or in a capture gap), it shows up as
matched: falseand does not contribute to the aggregate. - No path-dependent simulation. This endpoint is read-only over stored snapshots. It does not re-simulate the engine pipeline against re-fetched historical data — that would be impossible to reproduce, since some upstream sources (e.g. CoinGlass paid endpoints, blockchain.info MVRV) revise their history. Stored snapshots are the immutable audit trail.
What this endpoint is NOT
- Not a P&L simulator. Attribution is a structural estimate of "policy on vs policy off"; it doesn't model your actual fills, slippage, or position-vs-cap behaviour.
- Not a strategy backtester. It evaluates the engine's policy decisions against actual market outcomes, not the strategy of opening or closing positions. Use the per-day output to feed your own backtester if you need full strategy P&L.
- Not async. Synchronous up to 30 days. Larger windows + path-streaming are planned post-Track-D (Postgres-backed audit).
POST /v2/risk-distribution
Probabilistic loss distributions via deterministic conditional historical bootstrap. Where /v2/stress-scenario answers "what happens under this one shock", this endpoint answers "what does the loss distribution look like" — for a position or a portfolio, conditioned on market regime and/or volatility bucket.
The sampling pool is a versioned joint BTC+ETH daily return history (2018-03 → present, ~3,000 days, reconstructed from public exchange klines — not the live track record; provenance is echoed in every response). BTC and ETH are resampled on the same day indices, so their empirical correlation — including its regime dependence — is preserved without copula assumptions (measured path-level ρ ≈ 0.87).
Request
{
"positions": [
{ "asset": "BTC", "size_usd": 100000, "side": "long" },
{ "asset": "ETH", "size_usd": 50000, "side": "short" }
],
"horizon_days": 7,
"condition": { "regime": "BEAR", "vol_bucket": "ALL" },
"n_paths": 2000,
"reference_time": 1781136000
}
| Field | Type | Default | Constraints |
|---|---|---|---|
positions | array | — | 1–50 of {asset: BTC|ETH, size_usd > 0, side: long|short}. Shorthand: top-level asset / size_usd / side for a single position. |
horizon_days | int | 7 | 1–30 |
condition.regime | enum | ALL | ALL | BULL | BEAR | RANGE (kline-rule labels: price vs SMA200 + 30d slope) |
condition.vol_bucket | enum | ALL | ALL | LOW | MID | HIGH (terciles of 30d realized vol over the dataset) |
n_paths | int | 2000 | 100–5000 |
reference_time | unix s | now | Pass it to make the response fully reproducible |
Returns 422 if the conditioned pool has fewer than 120 days (thin intersections like BULL × HIGH) — relax the condition.
Response — key fields
| Field | Description |
|---|---|
per_asset_return_pct | Distribution summary per asset: p01 … p99 quantiles, mean, es95 / es99 (mean of worst 5% / 1%), prob_loss_gt_pct (P(return < −2/−5/−10/−20%)) |
portfolio_pnl_usd | Same summary over correlation-consistent portfolio P&L in USD, with prob_loss_gt_pct_of_gross |
dataset | Pool version (RETRO_v1@<date>), pool_days, the applied condition, and the provenance disclaimer |
distribution_hash | SHA-256 over api_version + dataset version + condition + horizon + n_paths + seed + positions — the audit anchor, separate from policy_hash |
caveats | Always present. Read them. |
Example (BEAR-conditioned, 7d, the book above): BTC p05 ≈ −18%, es95 ≈ −25%; the hedged portfolio compresses to p05 ≈ −$8.4k on $150k gross.
Determinism
PRNG is seeded from reference_time + the full condition. Same request + same reference_time → byte-identical response and identical distribution_hash. Omit reference_time and the server stamps the current time (response is then unique but still hash-anchored).
What this endpoint is NOT
- Not a prediction. Historical resampling answers "if the future draws from the conditioned past, what range of outcomes follows" — nothing more.
- Not part of scoring. No composite / policy /
policy_hashinput is read or written. The scoring freeze does not apply because there is no scoring here to freeze. - Not the live track record. The pool is a retrospective public-history reconstruction with its own documented caveats (daily granularity, kline-only regime labels, no funding/fees in path returns).
Forward-compatibility commitments
Will NOT break existing integrations:
- Adding new optional fields to request body (ignore if unknown)
- Adding new fields to response body (ignore if not consumed)
- Adding new stablecoin issuers / scenario templates
- Bumping
api_versionminor / patch
Will be announced as breaking with 2-week notice:
- Renaming or removing any documented response field
- Changing the type of any documented field
- Tightening validation that currently accepts certain inputs
- Changing the semantics of
portfolio_allowed(e.g. adding a new mandatory blocker class) - Renaming any of the 6 built-in scenario keys
- Changing the deep-merge semantics of
scenario_params
Internal, not part of the contract:
- Cache TTL adjustments
- Concentration / depeg threshold tuning (will be parametric per-key in future via
policy_config) - Adding cache layers
- Changing internal cache key shapes
Versioning
| Endpoint | Version | Date | Notes |
|---|---|---|---|
/v2/portfolio-risk-state | 2.0.0-alpha.5 | 2026-05-04 | reason_codes split into blocking + new advisories field; cache-warming parallelised + 12s timeout. |
/v2/portfolio-risk-state | 2.0.0-alpha.4 | 2026-05-05 | Phase 4: stablecoin per-issuer breakdown + peg health. |
/v2/portfolio-risk-state | 2.0.0-alpha.3 | 2026-05-05 | Phase 3: cache warming + audit trail integration. |
/v2/portfolio-risk-state | 2.0.0-alpha.2 | 2026-05-05 | Phase 2: correlation matrix + BTC-beta + BTC-equivalent exposure. |
/v2/portfolio-risk-state | 2.0.0-alpha.1 | 2026-05-05 | Phase 1: aggregation + per-position assessment + concentration warnings. |
/v2/stress-scenario | 2.0.0-alpha.2 | 2026-05-04 | Smoke-test polish: baseline now also checks stablecoin DANGER; triggered_flags is a baseline-aware diff for both flag families. |
/v2/stress-scenario | 2.0.0-alpha.1 | 2026-05-05 | Phase 1: 6 scenario templates, baseline vs stressed, position pnl, custom override. |
/v2/risk-distribution | 2.0.0-alpha.1 | 2026-06-11 | Initial release: conditional historical bootstrap (regime × vol bucket), per-asset + portfolio USD distributions, ES95/ES99, reproducibility via reference_time + distribution_hash. |
/v2/backtest/shadow | 2.0.0-alpha.2 | 2026-05-14 | Audit log + policy_config tightening + multi-horizon aggregate + quality_score per day + unmatched_warning. Brings B4 to institutional parity with B1 + B3. |
/v2/backtest/shadow | 2.0.0-alpha.1 | 2026-05-14 | Phase 1: multi-asset shadow backtest (BTC stored / ETH rescored), per-day policy assessment, aggregate metrics (policy distribution, return-by-policy, left-tail, attribution). Snapshot schema bumped to v6.3 with per-asset funding capture. fill-returns inverted to newest-first ordering. |
Pre-stable. Stabilises to 2.0.0 jointly when:
- At least one institutional pilot has consumed both endpoints for ≥30 days
- Forward-compatibility commitments are explicitly published
- Pricing tier integration ships (free vs paid access policy)
Until 2.0.0, breaking changes are announced with 2-week notice via the version table above plus an email to all active API keys.
Looking for v1?
POST /v1/risk-state is the production-stable per-asset endpoint. Use it when you only care about a single asset and want the lowest-latency, most-tested surface. See API v1 Reference →.