API v2 (alpha)

Pre-stable. /v2/portfolio-risk-state is at 2.0.0-alpha.5, /v2/stress-scenario is at 2.0.0-alpha.2, /v2/backtest/shadow is at 2.0.0-alpha.2, and /v2/risk-distribution is at 2.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

EndpointUse case
POST /v2/portfolio-risk-stateAggregate 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-scenarioApply 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/shadowMulti-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-distributionProbabilistic 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-distribution reads no scoring state at all — it samples a versioned historical dataset.

Authentication

Identical to v1.

Authorization: Bearer <your_api_key>
TypeFormatRate limit
OwnerRISKSTATE_API_KEY env varUnlimited
Externalrs_live_ + 64 hex60 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
  }'
FieldTypeDefaultDescription
positions[]arrayrequired1–50 entries. Each { asset, size_usd, side, venue_type? }. assetETH. sidespot. size_usd > 0 and ≤ 1e12.
stablecoin_holdings_usdobject | number0Either 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_detailsboolfalseInclude _per_asset_state (full v1 risk-state per asset that fed the aggregation).

Response — top-level fields

FieldTypeNotes
api_versionstringCurrently "2.0.0-alpha.5". Pre-stable. Bumped on additive changes.
scoring_versionstringAlways "score_v3" until 2026-10-22 freeze lift.
ttl_secondsint60. Same as v1.
portfolioobjectAggregated book metrics. See Portfolio fields below.
correlationsobject | null90d Pearson matrix + BTC-beta + source: "live" | "cache". 1h-cached. Returns { unavailable: true, reason } if CoinGecko is down — the rest of the response continues.
positions[]arrayPer-position assessment. See Per-position fields below.
portfolio_risk_flagsobjectstructural_blockers[], context_risks[], concentration_warnings[], stablecoin_warnings[]. Hierarchical flag families. Blockers and DANGER concentration / stablecoin flags drive portfolio.portfolio_allowed: false.
stablecoin_breakdownobject | nullPer-issuer breakdown when stablecoin holdings declared. See Stablecoin breakdown fields below. Null otherwise.
auditabilityobjectcomputed_at, latency_ms, key_type, audit_id, cache_status. audit_id is the key in the audit-decisions blob store.
_per_asset_stateobjectOnly when include_details: true. Full v1 risk-state per asset that fed the aggregation. Useful for full audit / debugging.

Per-position fields

FieldTypeNotes
indexint0-based index in the request positions[] array.
assetstring"BTC" or "ETH".
size_usdnumberEchoed from request.
sidestring"long", "short", "neutral".
policy_levelint1–5. Snapshot of the per-asset policy level (lower = more restrictive).
direction_biasstring"LONG_PREFERRED", "SHORT_PREFERRED", or "NEUTRAL" from the per-asset engine.
direction_layerstringWhich engine layer drove the bias: "tactical", "structural_veto", or "legacy_composite".
max_size_fractionnumberPer-asset cap as a fraction of total_notional_usd. Engine-set, regime-dependent.
max_size_usdnumbermax_size_fraction × total_notional_usd.
actual_pctnumberPosition size as % of total_notional_usd.
oversize_factornumberactual_pct / max_size_fraction. >1 means oversized vs cap.
direction_conflictboolTrue when position side opposes the engine's direction_bias. Mirrors the DIRECTION_BIAS_OPPOSITE advisory. Informational.
allowedboolFalse if the position fails any blocking check (structural blocker on the asset OR oversize vs cap). Advisories alone never set this to false.
reason_codesstring[]Only the codes that drove allowed=false. Possible values: STRUCTURAL_BLOCK_<TAG>, POSITION_OVER_MAX_SIZE. Empty when allowed=true.
advisoriesstring[]Informational flags that DO NOT affect allowed. Possible values: DIRECTION_BIAS_OPPOSITE, NEW_ENTRY_BLOCKED_BY_ENGINE, ALL_IN_BLOCKED_BY_ENGINE.

Portfolio fields

FieldTypeNotes
gross_exposure_usdnumberSum of |size| across all positions (long + short).
net_exposure_usdnumberSum of signed sizes (long positive, short negative).
long_exposure_usdnumberSum of long-side positions.
short_exposure_usdnumberSum of short-side positions.
total_notional_usdnumbergross_exposure + stablecoin_holdings_usd. The "deployable book" — denominator for actual_pct.
stablecoin_holdings_usdnumberTotal USD value of declared stablecoin holdings.
stablecoin_concentration_pctnumberstablecoin_holdings / total_notional × 100.
per_asset_concentrationobjectPer-asset breakdown: long_usd, short_usd, gross_usd, gross_pct, net_pct, position_count.
weighted_compositenumber | nullPer-asset composite weighted by gross exposure.
weighted_risk_permission_scorenumber | nullPer-asset risk_permission_score weighted by gross exposure.
portfolio_allowedboolTrue if no structural blockers, no DANGER concentration, no oversized positions, no DANGER stablecoin flag.
btc_equivalent_net_usdnumber | nullNet exposure expressed in BTC-equivalent (using 90d beta). Null if correlations unavailable.
btc_equivalent_gross_usdnumber | nullGross 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.

FieldTypeNotes
total_usdnumberSum across all issuers.
issuer_countintNumber of distinct issuers in the breakdown.
breakdown[]arrayPer-issuer entry: { issuer, name, amount_usd, concentration_pct, price_usd, deviation_pct, peg_status }.
breakdown[].concentration_pctnumberIssuer's % of total stablecoin holdings.
breakdown[].deviation_pctnumberCurrent spot price deviation from $1.00, signed.
breakdown[].peg_statusstring"OK" (|deviation| < 0.5%), "DEPEG_WARNING" (0.5–2%), or "DEPEG_DANGER" (>2%).
flags[]arrayAggregated stablecoin flags: STABLE_CONCENTRATION_<ISSUER>_OVER_50 (WARNING) / _OVER_80 (DANGER), STABLE_DEPEG_<ISSUER> per offending issuer.
peg_health_sourcestring"live" if peg prices were fetched from CoinGecko this request, "cache" if served from the 5min snapshot.
peg_health_age_msintAge of the peg-health data in ms.
unrecognised_issuersstring[]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: false because BTC's actual_pct (58.14) exceeds the per-asset cap max_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.

FieldContentsEffect on allowed
reason_codesONLY codes that drove allowed=false. Empty when allowed=true.Yes — these are the binding reasons.
advisoriesInformational 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_SIZEactual_pct > max_size_fraction.

advisories values:

  • DIRECTION_BIAS_OPPOSITE — position side fights the engine's direction_bias. Mirrored by the boolean direction_conflict.
  • NEW_ENTRY_BLOCKED_BY_ENGINE — the per-asset blocked_actions includes NEW_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 for ALL_IN.

Pre-2026-05-04 these codes lived in reason_codes even when allowed=true, which caused integration confusion (e.g. BLOCKED_ACTION_PRESENT alongside allowed=true). The split was introduced in alpha.5. Old clients reading reason_codes for 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

CacheTTLWhy
risk-state-cache (per-asset, inherited from v1)60sFast-moving market data.
portfolio-correlations1hCorrelation matrix moves slowly.
stablecoin-pegs5minPeg 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

HTTPMeaningCommon causes
200OKNormal success path. Check portfolio.portfolio_allowed and portfolio_risk_flags for the actual decision.
400Bad RequestInvalid JSON, unsupported asset, invalid side / venue_type, position size out of range, stablecoin holdings malformed, empty positions array. Specific message in error field.
401UnauthorizedMissing or invalid Bearer token.
429Rate limit exceededExternal key over 60 req/min sliding window. Retry-After: 60 header included.
503Service UnavailablePer-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"
  }'
FieldTypeDescription
scenariostringOne of flash_crash, funding_blowout, stablecoin_depeg, liquidation_cascade, btc_eth_decoupling, dxy_spike. Required.
scenario_paramsobjectOptional. Deep-merges into the template's shocks. Use to model variant tails (e.g. USDT-only depeg of −5%).

Built-in templates

KeyHistorical anchorHeadline shock
flash_crashLUNA May 2022, FTX Nov 2022, COVID Mar 2020BTC −30%, ETH −35%, vol×1.5, funding −0.05
funding_blowoutApr/Oct 2021, Mar 2024Prices flat; funding +0.3% / 8h (95th pct)
stablecoin_depegUSDC Mar 2023, UST May 2022USDT −3%, USDC −1.5%, others mild
liquidation_cascadeAug 17 2024 yen carry, May 19 2021BTC −15%, ETH −20%, OI −30%, +$200M liq
btc_eth_decouplingAug 2021 (ETH 2.0), Mar 2024 ETF mismatchBTC −5%, ETH +10%
dxy_spikeSep–Oct 2022, Feb 2025 tariffsBTC −8%, ETH −10%, DXY +3%, 10Y +30bps

Response — top-level fields

FieldTypeNotes
api_versionstringCurrently "2.0.0-alpha.2".
scoring_versionstringAlways "score_v3" until 2026-10-22 freeze lift.
scenarioobject{ 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).
baselineobjectPre-shock view. See Baseline / stressed fields below.
stressedobjectPost-shock view. Same shape as baseline.
positions[]arrayPer-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_breakdownobject | nullPer-issuer baseline → stressed delta. Null if no stablecoin holdings.
portfolio_pnl_usdnumberTotal pnl across positions + stables.
portfolio_pnl_pctnumberportfolio_pnl_usd / baseline.total_notional_usd × 100.
pnl_breakdownobject{ positions_pnl_usd, stablecoin_pnl_usd } for attribution.
triggered_flags[]arrayNEW flags fired by the scenario but not at baseline. Diff by code across concentration AND stablecoin flag families.
baseline_flag_countintConcentration + stablecoin flag count at baseline.
stressed_flag_countintSame at stress.
auditabilityobject{ computed_at, latency_ms, key_type, audit_id }.

Baseline / stressed fields

Both branches share this shape:

FieldTypeNotes
gross_exposure_usdnumberSum of |size| across positions. Stressed branch reflects post-pnl values for shorts (see post-pnl semantics below).
net_exposure_usdnumberSum of signed sizes (long − short, post-pnl on stressed branch).
long_exposure_usdnumberSum of long-side positions.
short_exposure_usdnumberSum of short-side positions.
stablecoin_holdings_usdnumberTotal USD of stablecoin holdings (post-shock on stressed branch).
total_notional_usdnumbergross_exposure + stablecoin_holdings. The denominator for portfolio_pnl_pct.
per_asset_concentrationobjectPer-asset { long_usd, short_usd, gross_usd, gross_pct, net_pct, position_count }.
portfolio_allowedboolTrue if no DANGER concentration flag AND no DANGER stablecoin flag. Both flag families covered alpha.2 onward.
flags[]arrayConcentration + 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

HTTPMeaningCommon causes
200OKNormal success path. Read triggered_flags, portfolio_pnl_pct, stressed.portfolio_allowed.
400Bad RequestInvalid JSON; unknown scenario key (response includes registry); invalid scenario_params shape; position validation failure (asset, side, size_usd); stablecoin holdings malformed.
401UnauthorizedMissing or invalid Bearer token.
429Rate limit exceededExternal 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.computeComposite used 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

FieldTypeDefaultDescription
portfolio_path[]arrayrequired1–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, assetETH, sidespot, size_usd > 0).
horizonsint[][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_pctnumber-5Threshold for the left-tail frequency calc. Days with realised return below this fire the left-tail counter per policy bucket.
include_per_daybooltrueSet 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

FieldTypeNotes
api_versionstringCurrently "2.0.0-alpha.2". Pre-stable.
scoring_versionstringAlways "score_v3" until 2026-10-22 freeze lift.
request_summaryobjectEchoes the request + match results. See below.
eth_caveatobject | nullSurfaces 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_appliedobject | nullPer-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.
aggregateobject{ 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[] | nullarrayPer-day breakdown. Only when include_per_day: true. See Per-day fields below.
auditabilityobjectcomputed_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

FieldTypeNotes
n_days_requestedintLength of portfolio_path.
n_days_matchedintDays where a snapshot was found within ±12h of the requested date.
n_days_unmatchedintn_days_requested - n_days_matched.
n_days_with_forwardintDays where the primary horizon's forward return is filled (btc_24h or eth_24h for primary=24).
primary_horizon_hintFirst entry of horizons.
horizons_hint[]Echoes the request horizons.
drawdown_threshold_pctnumberEchoes the threshold.
match_tolerance_hintAlways 12. The closest snapshot must be within ±12h of the requested date.
unmatched_warningobject | nullWhen ≥25% of requested dates are unmatched, emits { ratio, threshold, n_unmatched, n_total, hint }. Null when the ratio is below threshold.

Per-day fields

FieldTypeNotes
datestring | intEchoed from request.
matchedboolFalse if no snapshot within ±12h. When false, only reason + tolerance_h follow.
snapshot_keystringBlob key of the matched snapshot.
snapshot_tsstringISO timestamp of the matched snapshot.
snapshot_ts_epochintUnix seconds.
match_delta_hnumberSigned hours between snapshot ts and requested date. Negative = snapshot before requested.
snapshot_versionstringE.g. "v6.3-server" (full ETH funding capture) or "v6.2-server" (pre-ETH-funding). Drives eth_funding_capture.
quality_scoreint | nullSnapshot's data-quality score (0–100, % of subsources LIVE). Filter for ≥ 80 for inferentially-clean subset.
eth_funding_capturestring"live" if the snapshot's positioning.fundingPctile.ETH is captured (v6.3+), "unavailable" otherwise.
portfolioobjectgross_exposure_usd, position_count for this day's positions.
per_asset_stateobjectPer-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[]arrayPer-position assessment: max_size_fraction, max_size_usd, actual_pct, oversize_factor, allowed, reason (POSITION_OVER_MAX_SIZE when allowed=false).
forward_returnsobject | 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-returns cadence. 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. If n_days_with_forward is unexpectedly low for recent dates, check quality_score and eth_funding_capture — pre-v6.3 snapshots predate the inversion fix.
  • Audit. Every query writes one record to the audit-decisions blob store. The full record is at dec/{key_prefix}/{audit_id}.json; the daily index entry at idx/{YYYY-MM-DD}.json carries endpoint: "backtest-shadow" so clients can filter the /v2/audit/decisions listing by endpoint.
  • policy_config interaction. When the caller's API key has a policy_config attached (see Policy Config), max_gross and max_per_asset[asset] caps tighten the per-asset max_size_fraction on every day in the path. The engine value is preserved in engine_max_size_fraction, engine_policy_level, engine_policy_label, and the codes that fired are listed under policy_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: false and 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
}
FieldTypeDefaultConstraints
positionsarray1–50 of {asset: BTC|ETH, size_usd > 0, side: long|short}. Shorthand: top-level asset / size_usd / side for a single position.
horizon_daysint71–30
condition.regimeenumALLALL | BULL | BEAR | RANGE (kline-rule labels: price vs SMA200 + 30d slope)
condition.vol_bucketenumALLALL | LOW | MID | HIGH (terciles of 30d realized vol over the dataset)
n_pathsint2000100–5000
reference_timeunix snowPass 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

FieldDescription
per_asset_return_pctDistribution 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_usdSame summary over correlation-consistent portfolio P&L in USD, with prob_loss_gt_pct_of_gross
datasetPool version (RETRO_v1@<date>), pool_days, the applied condition, and the provenance disclaimer
distribution_hashSHA-256 over api_version + dataset version + condition + horizon + n_paths + seed + positions — the audit anchor, separate from policy_hash
caveatsAlways 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_timebyte-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_hash input 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_version minor / 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

EndpointVersionDateNotes
/v2/portfolio-risk-state2.0.0-alpha.52026-05-04reason_codes split into blocking + new advisories field; cache-warming parallelised + 12s timeout.
/v2/portfolio-risk-state2.0.0-alpha.42026-05-05Phase 4: stablecoin per-issuer breakdown + peg health.
/v2/portfolio-risk-state2.0.0-alpha.32026-05-05Phase 3: cache warming + audit trail integration.
/v2/portfolio-risk-state2.0.0-alpha.22026-05-05Phase 2: correlation matrix + BTC-beta + BTC-equivalent exposure.
/v2/portfolio-risk-state2.0.0-alpha.12026-05-05Phase 1: aggregation + per-position assessment + concentration warnings.
/v2/stress-scenario2.0.0-alpha.22026-05-04Smoke-test polish: baseline now also checks stablecoin DANGER; triggered_flags is a baseline-aware diff for both flag families.
/v2/stress-scenario2.0.0-alpha.12026-05-05Phase 1: 6 scenario templates, baseline vs stressed, position pnl, custom override.
/v2/risk-distribution2.0.0-alpha.12026-06-11Initial release: conditional historical bootstrap (regime × vol bucket), per-asset + portfolio USD distributions, ES95/ES99, reproducibility via reference_time + distribution_hash.
/v2/backtest/shadow2.0.0-alpha.22026-05-14Audit log + policy_config tightening + multi-horizon aggregate + quality_score per day + unmatched_warning. Brings B4 to institutional parity with B1 + B3.
/v2/backtest/shadow2.0.0-alpha.12026-05-14Phase 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:

  1. At least one institutional pilot has consumed both endpoints for ≥30 days
  2. Forward-compatibility commitments are explicitly published
  3. 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 →.