bluewatercorp/riskmodels
Equity and portfolio risk decomposition for US stocks: split a ticker or a book into market, sector, subsector and stock-specific risk, get the ETF hedge ratio for each layer, and pull point-in-time fundamentals and cost of capital. Live data from the RiskModels API over a hosted MCP server.
Changelog
2026-10-12 — Exposure history feeds follow the served-history start
POST /portfolio/exposure/historyand/history/dailyserved every column from 2006, contrary to the served-history policy (lib/dal/served-history.ts). They now read from 2010-12-08 and withhold each column before its own start: hedge ratios andl1_mkt_beta2010-12-08, explained-risk / residual shares andstock_var2011-12-07,lstar_level2013-01-01 (maskBeforeServedStart). Defaultstartis 2010-12-08 (was 2006-01-01 on the daily feed).- Responses add
history_available_from(first date per column) andwarnings(before_served_history, one per later start date) when the requestedstartis earlier. The cache key includes the floors, so files written with pre-floor rows are not reused. - Docs, OpenAPI, capabilities, llms.txt, pricing copy and the SDK docstrings state these dates instead of "since 2006".
2026-10-08 — Daily exposure history
- New
POST /portfolio/exposure/history/daily: every trading day for up to 1000 names, read straight from the ERM3 stores (no extra stored data), delivered as one Parquet file per year plus the month-end ETF covariance. Full history for 1000 names: ~18 s to read, ~51 s to write. readDailyExposureHistoryreads each symbol chunk once per variable into typed arrays, 16 at a time (76 s → 7 s for 25 names), instead of one object per cell.- SDK:
client.exposure_history(tickers, frequency="daily")downloads and combines the yearly files; each day uses the latest covariance month-end on or before it. - Priced at $2.50 up to 25 names delivered, $10.00 above (2x the month-end feed).
2026-10-08 — Python SDK 0.5.0
riskmodels-py0.5.0 on PyPI addsclient.exposure_history(tickers)andExposureHistoryPack.exposure(holdings): download the month-end model history for a list of names and join it with dated holdings locally (holdings never leave the client).
2026-10-08 — Exposure history files move to a private bucket
- Fix:
POST /portfolio/exposure/historyreturned 500 on every live request because it uploaded Parquet to the publicreportsbucket, which accepts only PDF/PNG. Feed files now go to a private storage bucket and are served only via 1-hour signed URLs.
2026-10-08 — Large history reads no longer try to cache themselves
setCacheskips values overCACHE_MAX_VALUE_BYTES(default 5 MB) and values that cannot be serialized, instead of handing them to Upstash to reject.- History slices over 50,000 rows skip the cache write and lookup. A 300-name full daily history (14.3M rows) used to crash with "Invalid array length" after failed Redis writes; it now completes in 52 s. 25 names: 20 s → 10 s.
2026-10-08 — Exposure history as a model data feed
- New
POST /portfolio/exposure/history: send tickers only (no values) and receive signed URLs to two Parquet files — each name's month-end ERM3 history (hedge ratios at L1/L2/L3, explained-risk and residual shares,stock_var,lstar_level,l1_mkt_beta) and the ETF covariance at each month-end. Holdings never leave the client. - Python SDK:
client.exposure_history(tickers)downloads the feed;pack.exposure(holdings_by_date)joins it with dated holdings locally and returns the same quantities as/portfolio/exposureat each month-end (L* default). The port is pinned to the TypeScript by a shared parity fixture. - Priced per call by names delivered: $1.25 up to 25, $5.00 above 25.
- Served from a precomputed month-end panel, because the daily stores are chunked by full history. The endpoint returns 503 until the panel is published.
- Derived data only; a test asserts no raw price, market cap or return column can be delivered. Design and licensing:
docs/EXPOSURE_HISTORY_FEED.md.
2026-10-08 — /portfolio/exposure priced by book size
- $0.25 per call up to 25 names, $1.00 above 25. Latest and
as_ofcost the same. Billing codeportfolio_exposure_v2. - The tier counts names actually modelled (stocks plus ETFs held): the up-front balance check uses names submitted, and the charge drops to the lower tier if enough names fail to resolve.
- New pricing option
size_tiersfor per-request capabilities;/api/pricingand/api/estimatereport it, and/api/estimatenow knowsportfolio-exposure.
2026-10-08 — /portfolio/exposure: as_of history, L1 fallback, Zarr fill-in fix
- New
as_of(YYYY-MM-DD, 2006-01-01 or later): the same output at a past model date, read from Zarr. Each name uses its newest row on or beforeas_of; the ETF covariance ends on the resulting snapshot date. Same price as a latest call. Names with no model data on that date (e.g. not yet listed) are dropped asno_data_at_as_of. - L1 beta at past dates is
-l1_mkt_hr. At L1 the only factor is SPY, so the hedge ratio is exactly-beta: verified againstds_erm3_betasat 2012-06-29, 2020-03-31, 2024-12-31 and 2026-09-30 (311 of 311 name-dates exact). - No L* fallback: names without an L*, or whose L* level lacks data, are excluded from hedges and risk and listed in
coverage.excluded_from_lstar. Every level uses the same estimation window, so such names have no L1 either (all 350 on 2026-10-07). A name whose model row is entirely empty is dropped asinsufficient_history: ERM3 needs at least 126 trading days within its 252-day window. The spec documents this. - Fix: the Zarr fill-in for empty L2/L3 sector and subsector legs and missing
lstar_levelnever ran, because it requestedl1_mkt_beta, which Zarr does not serve, so the whole read returned nothing. It now requests only Zarr keys and derives the L1 beta.
2026-10-08 — /portfolio/exposure hedges each name at its L* level by default
- New
hedge_levelparameter, default"lstar": each name is hedged and its residual measured at its own L* level (lstar_level, 1–3). L* stops above a layer that adds no explanatory value, which is where negative incremental explained-risk shares come from."l1","l2"and"l3"force one level for every name. hedgesnow carrieslstaralongsidel1/l2/l3, withnames_by_level. Names with no usable L* fall back to the deepest level with complete data and are listed incoverage.lstar_fallback.- On a 30-name sample of a real long/short book, L* needed 12 ETF legs and $1.99M of hedge trades against 28 legs and $3.18M at L3, for the same total risk.
2026-10-08 — POST /portfolio/exposure: long/short book exposure and L3 risk
- New endpoint for signed books: up to 1000 positions as dollar values (short < 0), netted by security and never normalised. The other portfolio endpoints take positive weights; this one takes signed dollar values so a long/short book can be sent as held.
- Returns L1 beta-dollars by sector, the ETF hedge trades at L1/L2/L3 (stock hedge, the book's own ETF holdings, and the total neutralising trade, reported separately), and an L3 risk split. Systematic risk uses the book's raw-ETF exposure with one aligned 252-day ETF covariance ending on the model date; residual risk is a labelled diagonal approximation. ETFs held in the book count as exposure to themselves.
- Every name is read at one model date (stale names are dropped and listed); missing L2/L3 hedge legs are filled from Zarr at that date. Coverage is reported per calculation against submitted gross.
- Priced per successful call (replaced the same day by size-based pricing, above); a book with no modelled names returns 422 and is not charged.
- Hedges go no deeper than L3. Size and value exposure is not modelled as a separate layer and stays in the residual.
SUPABASE_TABLES.md:stock_varis the total 252-day daily variance, not stock-specific variance.
2026-10-08 — Correlation residual returns had the hedge sign reversed
GET /metrics/{ticker}/correlationandPOST /correlationwithreturn_typel1,l2orl3_residualbuilt the residual asr - hr·r_etf. An ERM3 hedge ratio is the ETF position per $1 long stock (l1_mkt_hr ≈ -beta), so that added the market back instead of removing it; NVDA's "L1 residual" variance came out about 3.7× its gross variance. The residual is nowr + Σ hr·r_etf, matching the raw-ETF-returns pairing in the ETF factor returns docs.grosscorrelations were unaffected.
2026-10-08 — Batch: ETFs report an error, failed lookups are free
POST /batch/analyzereports ETFs (including SPY and the sector / subsector hedge instruments) asstatus: "error"witherror_code: "no_risk_metrics"whenfull_metricsorhedge_ratiosare requested. They used to returnsuccesswith every metric null.- Each failed ticker carries an
error_code:symbol_not_found,no_risk_metricsorinternal_error. - Batch billing counts only tickers that return data. A batch where every ticker fails costs nothing; the $0.03 minimum applies only when something was served. The billing middleware gains
context.setBillableItemCount()for any per-item route. - Docs: the
/decomposehedgemap is the layerhritself (negative = short the ETF), not its negative; the OpenAPI spec and SEMANTIC_ALIASES.md now match the code anddocs/api.mdx. - Docs: incremental explained-risk shares (
l3_sec_er,l3_sub_er) can be slightly negative; the residual is 1 minus the systematic shares, so the four still sum to 1.
2026-10-08 — Email links, top-up URL and batch pricing text
- Email links to docs, quickstart, key and usage pages always use riskmodels.app. They were built from
NEXT_PUBLIC_APP_URL, which is riskmodels.net in production, so/api-docsand/quickstartreturned 404. - Account settings and support links in emails point at riskmodels.net, where those pages live. The low-balance email's default top-up link moves from
/settings/billing, which exists on neither host, to/get-key. - The 402
top_up_url/X-Top-Up-URLand the low-balance top-up link use riskmodels.app. - The key-issued email names the
X-API-Cost-USDheader instead of a_cost_usdbody field that responses never carried, and links/openapi.jsonand/api/healthin place of/schemasand/status, which returned 404. POST /batch/analyzedescription now matches itsx-pricing: $0.015/position, minimum $0.03/call.
2026-10-07 — Explicit PIT-lagged debt-cost proxy
GET /api/fundamentals/{ticker}keeps strict reported-datacost_of_debtandwaccunchanged, and adds separately labeledcost_of_debt_imputed/wacc_imputedfields when trusted SEC debt is positive but reported interest expense cannot form a strict debt cost.- The proxy adds the period-end point-in-time 10-year Treasury rate to a monthly
HQMCB10YR - GS10spread. Treasury HQM covers the A/AA/AAA high-quality market; it is not issuer-specific. Provenance includes both inputs and the lagged reference month; month M becomes eligible on day 10 of M+1. - The latest-period ERP × Treasury-tenor sensitivity grid now carries parallel
wacc_imputedcells while preserving strictwacc. Public docs, MCP discovery, plugin guidance, and SDK handling distinguish the two paths.
2026-10-05 — Linked brokerage book on the RiskModels MCP
riskmodels_get_my_positionsreads the caller's synced book from riskmodels.netGET /api/positions(Alpaca Connect, Connect Trade, or Plaid) with the same API key the MCP session already holds.- The payload includes
for_analysisandfor_hedgeforriskmodels_analyze_portfolioandriskmodels_hedge_portfolio. The tool does not place orders. /docs/alpacanow treats one RiskModels server as the analysis path. Alpaca's own MCP remains optional order entry.
2026-09-14 — RMGraph cumulative stock return paths deployed
- Add
cumulative_return_paths@v1using existing daily returns-decomposition data, with original RMGraph PNG/SVG exports and numerical JSON. - Use a common first-close zero baseline; compound gross, combined-factor and residual daily series separately. Reject missing or misaligned observations. Date cutoffs refer to observed returns, not historical knowledge-time availability.
- Preserve existing artifact identities and reuse dated cached bytes. Production JSON, 300-DPI PNG and vector SVG passed for NVIDIA 3m/1y with exact zero starts and repeatable bytes; the stock capability is verified. ChatGPT display and the additional recording remain host checks.
Pending — Original chart component for MCP hosts
- Link the hosted render tool to a self-contained MCP Apps component that displays original PNG content with its data date and receipt. Native image and text response shapes remain available to clients without UI support.
- The component uses no external network requests, storage or chart library, and clears prior images for errors and cancelled requests. ChatGPT compatibility uses its full tool-result envelope.
- A sandboxed browser check passed with the real production image at 600px chat width. Actual ChatGPT display and video capture remain release checks; the successful native image transport alone does not prove inline host display.
2026-09-13 — Signed RMGraph stock comparison deployed
- Serve
risk_comparison@v1for explicit lists of 1–12 stocks using the existing decomposition loader, common-date alignment and artifact store. - Preserve signed factor contributions and pass date-alignment or exclusion notes to the RMGraph layout. Comparison cache identities distinguish stock order and disclosure context; existing watchlist artifacts retain their identities.
- JSON, 300-DPI PNG and vector SVG passed production qualification for NVDA/MSFT/AAPL at 2026-09-11. All 12 source values, including three negatives, are preserved; repeated PNG bytes and receipt are stable. The stock capability is now verified.
2026-09-13 — Display artifact PNGs directly in MCP hosts
- Hosted
riskmodels_render_artifactnow returns PNG bytes as MCP image content, with the resolved data date, source, parameters and receipt in accompanying text. - PNG bytes are no longer duplicated as base64 inside the text payload. Consumers should read the image content block. JSON, SVG and Plotly figure responses retain their existing shape; REST rendering is unchanged.
- Invalid PNG payloads return an explicit tool error. This delivery change does not migrate legacy chart renderers to RMGraph or establish a host recording.
2026-09-12 — Cumulative charts start at an actual close
- Fund daily/monthly charts emit the first observed close once at zero; later intervals compound over the same window as the layered contributions and waterfall.
- Cumulative artifacts validate unique ascending dates, finite observations and a common zero baseline, including maximum history. Invalid inputs return an explicit error.
- Corrected artifacts use a separate cache/receipt identity. Existing issued bytes are preserved. Filer images are regenerated from full numerical artifacts; image-only history remains unavailable until numerical data is published.
- Coordinated Cloud Run renderer release required; this change does not qualify fund/filer portfolio histories or alter the underlying data stores.
2026-09-09 — Canonical LSTAR and level-specific hedge dispatch
- Preserve omitted thresholds in single and batch LSTAR requests so the engine’s materialized selection is used. Explicit threshold overrides retain legacy marginal-ER behavior.
- Propagate materialized LSTAR to metrics, batch analysis, decomposition, portfolio, commentary, and hedge-basket recommendations. Keep the user-segment economic recommendation separate.
- Build hedge-basket legs from the final recommended level, including its own SPY hedge ratio; an L1/L2 recommendation no longer returns the L3 hedge vector.
- Regression coverage checks L1/L2/L3 SPY and finer legs, canonical/custom-threshold dispatch, missing recommendations, and economic downgrades.
All notable changes to the RiskModels API surface and public assets.
[Unreleased]
Changed
-
OpenBB
rm_model_scaffoldfileSelector —multi_file_viewerwidgets require a params entry withroles: ["fileSelector"]. Tearsheet already had it; Model Scaffold (Excel) did not, so Connect Backend failed validation. Same contract asrm_tearsheet: hiddenfileparam +widgets/model-scaffold-options. -
/get-keyone-time reveal sits next to Generate — the full secret used to render above Add credits, so clicking Generate at the bottom only showed the 16-character prefix list. The green copy box now appears in the Generate card and scrolls into view. The list is labeled as a prefix. -
OpenBB Workspace CORS — Adapter at
/openbbechoeshttps://pro.openbb.co,https://pro.openbb.dev, andhttp://localhost:1420(trailing slashes and nested*.openbb.cohosts included). OPTIONS preflight is answered in middleware so it never waits on a cookie session. Widget routes also accept?api_key=in addition toX-API-KEY/Authorization. -
Price book 2026-08-14 — Catalogue reprice (not a flat multiplier). Lookups $0.005; ticker-returns $0.02 + $0.01/extra year;
batch-lstar$0.015/position (restores the advertised 25% discount vsGET /lstar); decision products (lstar,residual-signal) held at $0.02. Institutional licenses: Desk $50k / Firm $150k / Production $250k per year. Existing keys that recorded a paid call before 14 Aug 2026 keep prior rates through 31 Dec 2026. Source:lib/agent/capabilities.ts; public page/pricing. Brief:Futures/docs/PRICING_REPRICE_BRIEF.md.
Added
-
GET /api/artifacts/as-of?slug=&subject_id=— Lists the pre-rendered vintages for one artifact subject: each date, the formats stored under it, and the object URI per format. render-svc error text had pointed callers at this path while nothing served it. Authenticated, free. 404 says whether the slug is unbuilt or the subject is unknown. Registered as capabilityartifact-as-of, soriskmodels_list_endpointsadvertises it andriskmodels_call_endpointmay dispatch it.GET /api/artifacts/capabilitynow carries adiscoveryblock naming both routes and a per-pairas_of_latest(loader|newest_prerendered|unsupported). -
Cohort artifacts accept
as_of=latest—riskmodels_render_artifacton aBW-COHORT-*subject resolveslatestto the newest pre-rendered vintage (newest date holding the requested format). Previously a 400. Still 400 when the subject has no vintage at all; filers keep the explicit-date contract. -
GET /api/industry-panel?by=fact— Dual-shape reader for ERM3ds_erm3_industry_*rekeyedlevel→fact(ERM3 #158;panel_keying=fact, 52 facts). Defaultby=levelkeeps one row per(industry_code, level)(n-weighted mean, law-of-total-variance,n_facts).by=factemits per-fact cells. Multi-fact cells are historical (last L3 day2021-06-22, industry4850= IAI+IYG); latest teo isn_facts=1everywhere.by=factis 409 only on a leftover level-keyed vintage. -
Licensed (institutional) API keys —
agent_accounts.billing_mode=licensedskips prepaid 402/deduct; usage isbilling_eventstelemetry withmetadata.list_price_usd,tickers, andkey_id. Response headersX-Billing-ModeandX-License-Tier. Admin monitor: BWMACRO/admin/licensed-usage. DDL:BWMACRO/supabase/migrations/20260816010000_agent_accounts_licensed_billing.sql. -
J.21 first-touch attribution at sign-in —
POST /api/agent-accounts/attributionwritesgclid/ UTM /channelontoagent_accounts.signup_attributionwhen the user authenticates, not only at key mint. A $0 account row is created only when a paid marker is present or/get-keyasks; starter credits are not granted on that insert. Stripe checkout start and card-added timestamps stamp the same jsonb. -
MCP
instructions+ loaded first prompts — After OAuth connect, Claude/Smithery were listing catalog tools and never calling priced data routes. Initialize now carries serverinstructions; paste-prompts nameriskmodels_compare/GET /api/metrics/AAPL. SSOT:lib/mcp/activation.ts. -
GET /api/stocks/{ticker}/commentary-bundle— One pull for stock-commentary evidence: latest metrics +hedge_levels, trailing return-record summary (sums of daily gross/factor/residual + drawdown), cohort standing, peer variance shares, and residual leadership for the name. Replaces the consumer's multi-endpoint fan-out under the 60/min ceiling. Thin cohorts / short windows null the affected piece and list a reason inrefusals(200, not 422 for the whole bundle). Billed undercohorts. OpenAPI path documented; contract tests cover the return-record summary rules. -
POST /api/chat—get_stock_commentary_bundletool — Single-name risk notes call the commentary-bundle once (capabilitycohorts) instead of fanningget_risk_metrics+get_ticker_returns+get_rankings. Two or more names stay oncompare_tickers. System-prompt Performance examples no longer tell the model to emit oneget_risk_metricsper compared ticker. -
GET /api/cohorts/residual-leadership— Rank members of a peer cohort by cumulative L3 residual (stock-specific) return over a window (?cohort=SMH&window=252d&level=subsector). OnefetchBatchHistorypanel read replaces the consumer's per-member/returns-decompositionfan-out. Window dates come from the cohort ETF's own calendar (not the intersection of members); short-history names are dropped and counted inn_short_history. Values are sums of dailyl3_rr, not compounded paths. Response always carriesdispersion.{best,worst,median,sd}plusn_ranked/n_members— S10 refuses without them. Thin cohorts and windows under ~200 observations → 422; unknown cohort → 404. Billed under existingcohortscapability. Handoff:docs/HANDOFF_COHORT_RESIDUAL_LEADERSHIP.md. -
POST /api/chat— workspace command-bus producer (G.36) — Optional body flagworkspace_tools: trueoffers two agent tools,set_subjectandset_window, whose schemas are generated fromlib/chat/workspace-action-contract.mirror.json— an explicit, versioned mirror of Risk_Models'saction-bus.tsvocabulary (canonical:riskmodels_net/src/lib/workspace/workspace-action-contract.json; drift failsworkspace-action-contract-drift.yml, the AGENTS_CROSS_REPO §1–3 mechanism reversed). Each successful call is emitted as a new, distinct SSE frameevent: actioncarrying the typedWorkspaceAction; existing frames are unchanged and non-workspace consumers ignore the unknown type. Streaming-only by design (the frame is the sole delivery channel — the blocking JSON path never offers the tools), free of billing (capabilityId: null— no data fetch), and fail-closed downstream: the.networkspace client validates every action (dispatchWorkspaceAction) and surfaces typed refusals (unknown_subject,invalid_window, …) in the thread. Until Risk_Models Phase R (roster) lands, the client's known-subject set is a singleton, soset_subjectcan only confirm the subject already on screen. -
Cohort statistics —
GET /api/cohorts,GET /api/cohorts/series,GET /api/cohorts/roster(ERM3 H.146) — First surface overds_erm3_cohorts, ERM3's first artifact published at cohort level rather than per-stock: cross-sectional residual statistics at (teo × cohort) for the market and the 11 GICS sectors, over a 2000-01-03 → present panel. Variables:residual_mean,residual_mean_cw,residual_sd,residual_skew,residual_p10/p90,mean_pairwise_corr,n_names,n_effective,weight_top1,membership_churn,linked_beta(+_se/_r2/_roll63),cohort_factor_return,cohort_residual_return,cohort_ER,factor_source, and the per-day backing instrument viainclude_proxy_source=true. Cross-section $0.02/call, series $0.03/call; the roster discovery endpoint is free.Why it matters: ERM3 residuals are not zero-mean. The regressions are fitted without an intercept, deliberately, so each stock's residual retains its alpha — which leaves the cross-sectional mean non-zero, and until now no consumer could see it.
residual_meanis that quantity: subtract it at the level your residual is defined against before building any relative-ranking signal. The contract text is read from the store's ownattrs.no_intercept_contractand returned in every response'sdisclosuresblock, so it cannot drift from the data it describes. Drift figures are meaningless without their window — the sign is not stable across the sample — so no headline number is quoted anywhere in the surface.residual_sdexposes cross-sectional dispersion (how much selection opportunity a cohort holds) for the first time. It is documented throughout as a conditioning/allocation input, not an alpha source, and is always paired withmean_pairwise_corr. Thin cohorts are filterable viamin_names, andn_effective(inverse-Herfindahl breadth) is recommended overn_namesfor breadth — the gap is large in practice.Identity: cohorts are addressed by ticker. The store's
cohortcoordinate is abw_sym_idand stays internal — all twelve public cohort ids fall in the licensed-identifier-derived tail of that namespace, whichlib/dal/sym-id-scrub.tsexists to keep out of responses (H.24). IP boundary: public scope is SPY + the 11 GICS sector SPDRs, derived fromETF_FACTOR_REGISTRYso the two surfaces cannot drift. The subsector cohort slate is proprietary; a request for one is rejected identically to a nonexistent ticker, so the roster cannot be enumerated by probing. -
POST /api/cohorts/pnl-decomposition— selection vs drift — Splits a book's realized residual return into selection (what it earned by holding names that beat their cohort's average residual) and drift (what it earned purely from net exposure to that average, which accrues on net weight regardless of any selection skill). $0.05/call.The split is an identity, not a fitted attribution:
R_t = Σ_i w_i·(ε_i,t − μ_c(i),t) + Σ_c W_c·μ_c,t, so the two components sum to the total exactly — verified against an independently computedΣ w·εto 10 decimal places. It answers "was I paid for stock-picking, or for being net long the average stock?", a question that is only answerable because ERM3 fits without an intercept and the cohort store now exposes the non-zero mean.level=sector(default) demeans each name's sector-level residual against its sector cohort;level=marketuses market-level residuals. Weights are treated as constant over the window and are not normalized — rescaling them would change the drift term. Positions that cannot be resolved or mapped to an addressable cohort are dropped and named incoverage.dropped, never silently omitted. -
/cohortson riskmodels.app — Dispersion & Opportunity — Cross-section of residual dispersion by cohort withmean_pairwise_corrandn_effectivealways beside it (dispersion conflates idiosyncratic vol with cross-correlation, and effective breadth diverges sharply from headcount — the market cohort carries 2,776 names but ~81 of effective breadth). Thin cohorts are visually de-emphasised. History panel over 3y / 10y / full panel shades spans where the cohort factor ran on a substitute instrument, and a second panel accumulates each cohort's mean residual — the realized return of net exposure alone, i.e. the drift half of the decomposition above. Realized history only; no forecasts or recommendations anywhere on the surface. -
Python SDK —
cohortsmodule —fetch_cohort_series()/fetch_cohort_cross_section()/fetch_cohort_roster(), plusdemean(), which joins the cohort store to a user frame and subtractsresidual_meanat a chosen level, anddecompose_selection_vs_drift().demean()exists so five consumers do not write five subtly different demeaning implementations; it leaves the subtracted quantity in acohort_residual_meancolumn so the correction stays auditable, and returns null rather than an uncorrected value on days with no usable cohort mean. -
MCP tools
riskmodels_get_cohortsandriskmodels_decompose_selection_vs_drift— cross-section or series (series=true) and the book decomposition, each carrying the demeaning, dispersion, and not-advice guidance in its description. -
OpenAPI: nine previously-undocumented endpoints —
POST /v4/decompose(named-block decomposition),GET /hedge-basket/{ticker},GET /batch/latest-metrics,GET /metrics/{ticker}/snapshot.pngandGET /snapshot/{entity_kind}(both return image/PDF bytes, not JSON),GET /funds/search, and the OAuth trioPOST /oauth/register·POST /oauth/token·POST /oauth/revoke. All existed and were callable; none appeared in the spec. Documented with realx-pricingfromlib/agent/capabilities.tsand correct response content types. -
Schemas —
OAuthClientRegistrationRequest/OAuthClientRegistrationResponse/OAuth2RevokeRequest;OAuth2TokenRequest/OAuth2TokenResponse/OAuth2Errorrewritten for the grants that exist. -
Per-IP rate limits on the unbilled public surface —
GET /api/funds/searchandGET /api/13f/filers/searchnow enforce 60 req/min per IP (FUND_SEARCH_IP_RPM,FILER_SEARCH_IP_RPM) and caplimitat 100 rows (was 500, unthrottled).publicRateLimitResponseoption selects a real429(default) vs the Shields.io 200-with-badge payload. -
GET /api/13f/filers/{bw_filer_id}/holdings— filing identity — The response root now identifies which SEC submission the returned panel came from, alongside the existing bi-temporal stamps:accession_number(EDGAR accession of the surviving submission for the selected quarter),filing_type(the SEC form type —13F-HRoriginal,13F-HR/Aamendment,13F-NTnotice — deliberately not namedreport_type, which reads as the report quarter), andamendment_type(cover-page semantics:ORIGINAL/RESTATEMENT/NEW_HOLDINGS/UNKNOWN). Panel-level, not per holding row — one accession state per selected quarter — and underas_ofthey describe the selected accession, so knowledge-mode selection is unchanged. Reader precedence: schema-v2ds_ph.zarrteo-axis coords first, then a keyed lookup in the siblingds_filing_vintages.zarrfor the two fields the compatibility view does not project ontoteo; the second read fires only when an accession is known and a field is still unresolved. All three are present-and-null on panels published before the accession-vintage stores, andamendment_typeis never inferred from a/Asuffix — null means "not classified upstream", which is distinct from theUNKNOWNvalue. Mirrored on the Python SDKget_filer_holdings()and MCPriskmodels_get_filer_holdings; OpenAPI 200 body is now typed (was baretype: object). Reader:readFilerHoldingsTopNinlib/dal/funds-zarr-reader.ts(snapshot cache keyv: 3). -
MCP Tool
riskmodels_get_openapi_spec— Added to@riskmodels/mcpto expose the OpenAPI 3.x specification, allowing agents to read actual API requests and query parameters directly instead of relying solely oncapabilities.json. -
GET /api/snapshot/{entity_kind}/{id}/panels/{slug}— Stock panel drill-down (O.6):l3_explained_risk_hbar,hedge_notionals_hbar,hedge_depth_retained,watchlist_er_stacked, and_full(composed DD page). Thin alias onto Artifact Registry / render-svc. ADR: BWMACROdocs/architecture/SNAPSHOT_CANONICAL_PROCESS_ADR.md. -
Python SDK —
snapshot_panel()— Fetches a single panel PNG/JSON/SVG via the product panel route. -
render-svc — Live stock
POST /decomposeloader for O.6 panels (BW-STOCK-*/ watchlist). -
render-svc
POST /artifacts/render— render params — Optionalparamsobject for per-slug render parameters:top_n(int 1–50, default 12) ontop_holdings_erm_stacked@v1(threads the holdings adapters + module cap; echoed astop_n_requested) andwindow(3m/6m/1y/2y/max, defaultmax) oncumulative_return_strip@v1(trailing date-cutoff slice — cadence-agnostic across daily fund + monthly filer series — rebased so the window's base point reads 0; echoed aswindow_requested). Unknown or slug-inapplicable params → 422; artifact modules declare honored params viaRENDER_PARAMS(undeclared → 501 deploy-skew guard). Params fold into the render-once GCS key ({as_of}.top_n-5.json); empty params keep the legacy key. Requires the matching BWMACRO artifact modules (the render-svc image symlinksbwmacro-src— coordinated rebuild). TS clientrenderArtifact()acceptsparams.
Fixed
- render-svc
watchlist_er_stacked— one real shared date instead of a synthesized one (BWMACROG.71) —_resolve_stock_watchlistfetched every ticker withoutas_of, so each member served its own latest close; when the members disagreed, the resolvedas_offell back todatetime.now(). That date reached the GCS render-once key and theX-Artifact-Resolved-As-Ofheader while the bars underneath were different vintages — a shared comparison axis labelled with a date no member reported. Reproduced against prod data on 2026-08-02: a three-name set with one member a week behind resolved to2026-08-03with rows at2026-07-31/2026-07-24/2026-07-31. The resolver now picks one date the whole set can honour — the minimum of the members' own latest, with an explicitas_ofacting as a ceiling rather than the label — and re-fetches the newer members at that date through/api/decompose's reality-modeas_of(G.42), so every bar is the same vintage. A member with no row at the resolved date is excluded and disclosed, never drawn stale or dropped in silence; a payload carrying no date at all is a502rather than a fallback. The JSON payload gains anas_of_alignmentblock (resolved date,as_of_basis: "report_date", the rule applied, each member's own latest, anything pulled back or excluded), and figures carry the same disclosure on their face when there is something to disclose. Each aligned payload'sdata_as_ofis set to the resolved date, because the artifact module prints that field as its provenance line and an explicit-as_ofdecompose response leaves it at the panel head. The peer-group mode (G.43) of the same slug carried the identical fallback and is fixed with it; its refusal of historicalas_of(peer membership is not point-in-time) is unchanged, and a peer excluded by alignment leaves thepeer_groupmembership block. A set whose members already agree — the state prod exhibits today across every ticker probed — pays no extra fetch, keys to the same object, and shows no note.
Changed
-
render_artifact(chat) andriskmodels_render_artifact(MCP) — the watchlist date claim, moved (G.71) — TheG.72descriptions hedged at the wording layer, telling the model the watchlist was "a shared composition axis, NOT a date-aligned comparison". With the resolver fixed that hedge is false in the other direction. Both surfaces now state that the set resolves to one shared date, nameresolved_as_ofas where that date comes from, and point atas_of_alignment.excludedso a model does not assert completeness the render does not have.tests/artifacts/render-stock-subject-visibility.test.tsasserted the old disclaimer positively and is inverted in the same change — descriptions and resolver have to move together or the driftG.72closed reopens. Deploy order: render-svc first. The descriptions ship on the Next.js deploy and the alignment on a render-svc image rebuild; landing the descriptions first would advertise alignment against a render-svc that does not do it. -
render_artifact(chat) andriskmodels_render_artifact(MCP) — stock subjects are now described — Both tools acceptedBW-STOCK-{TICKER}andBW-STOCK-WATCHLISTand neither said so: the tool description, thesubject_iddescription, the slug examples, and the payload description named onlyBW-FUND-/BW-FILER-/BW-PORTFOLIO-, whilelib/agent/capabilities.tsand the generatedmcp/data/capabilities.jsonnamed the stock forms correctly. Descriptions now carry the stock prefix, the multi-ticker watchlist form (subject_id=BW-STOCK-WATCHLISTwith{"tickers":[…]}, up to 12), and the four slugs verified for stock subjects (l3_explained_risk_hbar,hedge_notionals_hbar,hedge_depth_retained,watchlist_er_stacked).narrative_profile, whichARTIFACT_RENDER_CAPABILITYrecords as never having rendered, is no longer offered as an example. The watchlist form was initially described as a shared axis that was explicitly not date-aligned, because each ticker then resolved at its own latest close;G.71(above, same release) fixed that in the resolver and moved the wording with it. No behaviour change; the schemas already accepted these values.tests/artifacts/render-stock-subject-visibility.test.tspins both surfaces against the capability table so the drift cannot silently recur. -
GET /api/snapshot/{ticker}— Implemented under[entity_kind]/route.ts(same dynamic segment as panels) so Next.js does not reject sibling[ticker]vs[entity_kind]folders. Reserved kinds (stock,fund, …) return 400 with the panel URL shape. Deprecation header still prefers…/panels/_full. -
OpenAPI security scheme:
OAuth2ClientCredentials→OAuth2AuthorizationCode— The spec described aclient_credentialsgrant the API does not implement.POST /api/auth/token(documentedtokenUrl) returns 404, and/api/oauth/tokensupports onlyauthorization_code(PKCE S256) andrefresh_token. The scheme now mirrors/.well-known/oauth-authorization-serververbatim; the/auth/tokenpath is removed and 14 operation-levelsecurityentries updated toOAuth2AuthorizationCode: [mcp:read]. Scope is informational — the API records it for telemetry and authorises on key validity + balance, not scope. -
402responses now carry a reachable top-up URL —top_up_url,_agent.top_up_urland theX-Top-Up-URLheader returned/api/billing/top-up, which does not exist. They now return${APP_URL}/get-key.ERROR_SCHEMA.mdandAUTHENTICATION_GUIDE.mdcorrected to match; the guide's "Mode 2" section (212 lines documenting the non-existent grant, with copy-pasteable cURL/Python/TypeScript) is replaced with the real authorization-code + PKCE flow. -
500responses no longer include exception text — 18 sites across 16 routes echoed the caught error back to the caller, including Supabase admin-client errors from the provisioning endpoints. Detail is logged server-side; theERROR_SCHEMA.mdenvelope is unchanged. -
Rate limiters degrade instead of failing open — the
/api/data/*gateway limiter and the public per-IP limiter fall back to a per-instance ceiling when Upstash is unconfigured or erroring, loggingFAIL_OPEN. Previously both allowed all traffic through, which silently voided the anti-bulk safeguard documented indocs/legal/eodhd-data-license-safeguards.md; that document is corrected to describe implemented behaviour. -
llms.txt— OpenAPI link pointed at/openapi(404); now/openapi.json.
Removed
GET /api/pdf/{symbol}/latest— Undocumented, no callers, and returned 1-hour signed URLs into thereportsbucket with no key or billing while the documentedGET /api/metrics/{ticker}/snapshot.pdfrequires both. Use the latter.
Breaking (clients)
- Python SDK
riskmodels0.4.0 —OAuthClientCredentialsAuth, theclient_id/client_secretconstructor arguments, andRISKMODELS_CLIENT_ID/RISKMODELS_CLIENT_SECRET/RISKMODELS_OAUTH_SCOPEare removed. They drove theclient_credentialsgrant against the 404 endpoint above, so that path never completed a request. Supplying them now raises with migration instructions. Useapi_key=/RISKMODELS_API_KEY. - npm CLI
riskmodels3.0.0 — Same removal for the same reason (cli/src/lib/oauth.tsPOSTedclient_credentialsto{apiRoot}/auth/token).riskmodels config set clientId|clientSecret|oauthScopeare gone; useapiKey.
[0.6.2] — 2026-06-30
Added
GET /api/etf/factor-returns— One-teo snapshot of close + trailing-window total returns (1d / 21d / 63d / 252d) for the public-scope factor ETF set: SPY + the 11 GICS sector SPDR ETFs (XLE / XLB / XLI / XLY / XLP / XLV / XLF / XLK / XLC / XLU / XLRE). The broader BWMACRO factor roster (subsector slates, style picks, macro buckets, broad-market coverage tier) is intentionally not exposed through this endpoint — that classification is proprietary curation IP. Tickers outside the public scope return 400 (never silently dropped). Pairs with/industry-panelfor the daily "what's happening at the market and sector index level" read. Capabilityetf-factor-returns($0.005/request); OpenAPI + MCP capabilities synced. Reader:readEtfFactorReturnsSnapshotinlib/dal/zarr-reader.ts; classification mirror atlib/risk/etf-factor-classification.ts(single source = ERM3erm3/shared/etf_register.py).- Python SDK —
get_etf_factor_returns()— WrapsGET /etf/factor-returns; returns the snapshot dict directly. Registered inclient.discover()output.
[0.6.1] — 2026-06-30
Added
- MCP
riskmodels_get_lstar+riskmodels_batch_lstar— Dedicated SDK-backed tools forGET /lstarandPOST /batch/lstar(marginal-ER dispatch rule, dispatched HRs + residual return series). Unblocks agent discovery without routing throughreturns-decompositionor generic passthrough. capabilities.jsonlstarentry — Standalone capability mirroringlib/agent/capabilities.ts($0.02/request,lstar_v1). Also fixes MCP passthrough allowlist for/lstar.
Changed
- Python SDK semantic map —
lstar_rrnormalizes tolstar_residual_return;COLUMN_AGENT_HINTSdocumentslstar_residual_returnandlstar_level.
[0.6.0] — 2026-06-09
Added
/sitemap.xml— Generated sitemap covering the public marketing + docs surface (10 static routes + everycontent/docsMDX page, enumerated automatically). Auth/account/oauth, the print render-snapshot routes, and per-ticker dynamic pages are excluded.robots.txtnow advertises it via aSitemap:directive.- Landing "Compare" callout — A buyer-facing band below the research-proof block linking to
/compare/barra-axioma("Evaluating against Barra or Axioma?"), giving the comparison page a higher-visibility entry point than the footer/audience-card links alone.
Fixed
- Canonical URLs resolve to the public
.appdomain — AddedmetadataBase(and a sharedCANONICAL_SITE_URLconstant) so relative canonical/OG URLs resolve tohttps://riskmodels.appinstead of the request host. The app also answers on.net(legacy/terms), and a per-host canonical split SEO signal. The sitemap androbots.txtSitemap:directive use the same canonical, notNEXT_PUBLIC_APP_URL(which is.netin some envs).
[0.5.0] — 2026-06-09
Added
/compare/barra-axiomapage — Positioning page for buyers evaluating RiskModels against enterprise risk platforms. Framed as a three-tier spectrum (DIY/Fama-French ↔ RiskModels ↔ enterprise Barra/Axioma) rather than a head-to-head: claims describe what RiskModels does (API-native, pay-per-call, agent-callable, executable ETF hedge ratios, additive four-layer decomposition, published methodology), names the mid-market buyer (RIAs, family offices, allocators, emerging managers), and is honest about when an enterprise platform is the right call. Targets "Barra/Axioma alternative" search intent via title, H1, meta description, and canonical. Linked from the footer ("vs Barra / Axioma") for crawlability.
Changed
- Landing audience card — The third "Built for three workflows" card is now "For RIAs, family offices & allocators" (was "For allocators & diligence"), leading with the wedge "Holdings-level risk without an enterprise platform" and pointing to the new comparison page.
[0.4.0] — 2026-06-05
Added
GET /api/status— Public, aggregate, privacy-safe service reliability: measured latency percentiles (p50/p95/p99) and a 5xx-only success rate over a window (?window_hours, default 24, max 720), plus a per-capability breakdown. Aggregated frombilling_eventsrequest telemetry (the same source/api/healthuses); metered 4xx (auth/payment/rate-limit) are excluded from both latency and success_rate so a flood of fast 401s can't make the service look faster or less reliable than it is. No revenue or user fields exposed. Capabilitystatus-metrics(free). Distinct from/api/metrics/{ticker}(risk metrics) and/api/health(current up/degraded/down state).POST /api/feedback— Trust-loop endpoint: an agent or human flags a result by its_agent.request_idwithrating(up/down),category, and/or a ≤4000-charcomment. Authenticated, free (no metering). Writes topublic.feedback_events(service-role). Returns503until the BWMACRO migration is applied;201once live. Capabilityfeedback(free)./for-agentspage — Human approval surface for risk/compliance teams evaluating the API for their agent fleets: discovery artifacts, onboarding/auth flow, pricing at a glance, and trust/compliance signals. Linked from the docs sidebar (Agents group).- Response
_agent.latency_ms+_agent.provenance— The billing middleware now injects the measured total latency (previously header-only) and a methodology/provenance URL into every JSON success body (non-JSON, errors, and arrays untouched). Many agent HTTP clients drop headers; the body is the durable carrier. - Enriched
/.well-known/agent-manifest.json— Addedreproducibility(point-in-time from 2006, deterministic, live model_version/data_as_of/universe_size, methodology + validation-manifest URLs),pricingsummary,discoverycross-links,reliability(→/api/status),onboarding, andfeedbackblocks. Version stamped3.0.0-agent.agentic-disclosure.jsongainedreproducibility+auditblocks. - Agent integration docs — "Compose & chain" — Worked decompose → hedge → portfolio-snapshot → execution flow, with the schema/citeability/idempotency properties that make chaining safe.
- OpenAPI —
/statusand/feedbackadded under the Utility tag (x-pricingcost 0);public/openapi.json+mcp/data/openapi.jsonregenerated.
Fixed
- Methodology/provenance URL —
_metadata.wiki_uri(and the new_agent.provenance) pointed atriskmodels.net/docs/methodology(404); corrected toriskmodels.app/docs/methodology(200). Single source of truth:METHODOLOGY_URLinlib/constants.ts. - Landing latency badge — Softened the unbacked "Sub-120 ms" hero claim to "Low-latency" (no asserted SLA without a measured production p95).
Cross-repo
- BWMACRO —
supabase/migrations/20260605120000_feedback_events.sql(mirrored toRisk_Models/riskmodels_com/supabase/migrations/). Must be applied to the Supabase project beforePOST /api/feedbackreturns 201. - RM_ORG (riskmodels.org) —
GET /.well-known/methodology.jsonmachine-readable methodology + validation manifest (verbatim published claims only); the API manifest'sreproducibility.validation_manifest_urlpoints at it.
[0.3.9] — 2026-05-27
Added
POST /api/signals/residual-reversion/basket— User-defined ticker list aggregated to a single Phase D L3 residual-reversion signal. 1–500 tickers; equal-weight default + optionalweights[]aligned to tickers + optionalsignal_quality_min_quintile(1–5) gate (Phase B: gross Sharpe lifts from ~0.79 universe-wide to ~1.28 within quintile 5). Returns weighted aggregate + decile / quality-quintile histograms + per-member rows. Trust contract: tickers absent fromds_erm3_residual_signalare silently dropped (upstream mask is SSOT);coverage.missing_tickerssurfaces the gap. Capabilityresidual-signal-basket($0.02/request); OpenAPI + MCP capabilities synced.GET /api/universe/{name}/members— Active membership of a named universe (uni_mc_50/500/1000/3000oruni_dv_*) at one teo (latest by default). Active = monthlyuniverse_maskAND dailyvaliditygate — same dual-gate the ERM3 pipeline applies to produce its output zarrs. Path label validated against theKNOWN_UNIVERSESregistry (mirror oferm3.partitions.KNOWN_UNIVERSES); unknown labels return 400. Response carriesmembers[{symbol, ticker}]+counts {active, in_universe_mask, inactive_from_validity}+ amask_as_ofmonth-end stamp (so callers can disambiguate mask-driven vs validity-driven membership changes). Capabilityuniverse-members($0.005/request).POST /api/portfolio/risk-snapshot—lstar_variance_decompositionblock — Parallel Lstar-aware attribution alongside the existing fixed-L3variance_decomposition. For each holding, picks the ER at the cascade depthlstar_leveldispatched to (L1 → market+residual; L2 → +sector; L3 → +subsector) and weights across the book. Names withlstar_level=nullare dropped —weight_coveredanddropped_counton the block surface the coverage gap. Per-row payloads now includelstar_rr+lstar_levelcolumns. Same call shape; same billing; existing fixed-L3 block unchanged so existing callers don't break.- Python SDK —
get_residual_signal_basket()— WrapsPOST /signals/residual-reversion/basket; returns the aggregate + coverage + per-member rows.get_universe_members()wrapsGET /universe/{name}/members. SDK_VERSIONconstant fix —sdk/riskmodels/capabilities.pywas stamping0.3.0inclient.discover()output despite the package being at 0.3.7+ on disk. Now bumped to0.3.9to matchpyproject.toml; future bumps should keep both in sync.
[0.3.8] — 2026-05-27
Added
POST /api/rankings/screen— Cross-section filter onds_erm3_rankings_*zarr: metric, cohort, window, optionalas_of, percentile/decile/sector filters, andlimit. Capabilityrankings-screen($0.02/request); OpenAPI + MCP capabilities synced.POST /api/batch/lstar— Batch Lstar time series for many tickers (JSON map or long parquet/CSV). ParallelgetLstar()per ticker; capabilitybatch-lstar($0.005/ticker, min $0.01). Python SDK:batch_lstar(),batch_lstar_to_dataframes().- Python SDK —
screen_rankings()— WrapsPOST /rankings/screen; normalizes ranking rows to semantic columns.
Fixed
GET /api/industry-panel— Response envelope now usesbuildMetadataBody(metadata, …)+_agent(was passing three args tobuildMetadataBody, breakingnext build).returns-decomposition-service— Coerce undefined ER inputs tonullbeforepickLstar()(strict TS on Vercel build).tsconfig.json— Exclude localbwmacro-srcsymlink from typecheck (render-svc Docker context only; not part of the portal build).
[0.3.7] — 2026-05-27
Added
- V3 metric keys
lstar_rr+lstar_level— Direct access to the Lstar-dispatched residual return and the level Lstar picked, per (teo, symbol), at the canonical 1% marginal-ER threshold. Reachable viaMetricsV3(GET /metrics/{ticker},POST /batch/analyze, V3 history fetches).lstar_rrreadsresidual_return.sel(level='lstar')fromds_erm3_returns;lstar_levelreads the new uint8lstar_levelcompanion var with 0→null mapping at the API boundary (1/2/3 = L1/L2/L3, null = no recommendation). NewreturnsFlatzarr-spec role inlib/dal/zarr-metric-registry.ts; dispatch inlib/dal/zarr-reader.ts; unit tests intests/zarr-metric-registry.test.ts. For a custom threshold continue to useGET /lstar. ds_erm3_returnsschema_version 2 (ERM3-side) — Returns zarr now carries a 4th level'lstar'plus a flatlstar_level (teo, symbol) uint8companion var, materialized at write time alongside hedge-weights byerm3.shared.output_manager.materialize_lstar_level_in_returns_zarr. Threshold pinned onattrs["Lstar_threshold"]. Schema upgrade path on first run; subsequent runs use an in-placeregion-write that touches only thelstarslice +lstar_levelarray (no rewrite of the ~1.5GB level-indexed cubes). Documented indocs/MACRO_STAT_ARB_ZARR_GUIDE.md.- Supabase
security_history_latest:lstar_rr+lstar_levelcolumns — MigrationBWMACRO/supabase/migrations/20260527120000_security_history_latest_lstar.sqladds the two columns; sync wired viaERM3_RETURNS_DECOMP_SYNC_SPEC+ newERM3_RETURNS_FLAT_SYNC_SPECwith sentinel-0 → null parity with the API mapping. (BWMACRO is the SSOT for Supabase migrations; the ERM3supabase/migrations/dir holds only stale copies and should not be appended to.) GET /api/returns-decomposition— One-call daily gross + L1/L2/L3 factor, combined-factor, and residual return series fromds_erm3_returns. Queryinclude_lstar=trueordispatch=lstarappendslstar+lstar_residual_return(prefers materialized zarr keys when present; else live marginal-ER dispatch atthreshold, default 1%). Capabilityreturns-decomposition($0.02/request); OpenAPI + MCP capabilities synced.GET /api/industry-panel— Cross-section fromds_erm3_industryzarr:beta_mean,beta_variance,n_companies, andtotal_log_mcap_weightby EODHD industry code and cascade level. Capabilityindustry-panel($0.02/request); OpenAPI + MCP capabilities synced.- Python SDK —
RiskModelsClient.get_returns_decomposition()— WrapsGET /returns-decomposition; semantic return columns + optional Lstar dispatch.get_industry_panel()wrapsGET /industry-panel.
Removed
ds_erm3_hedge_weightsLstarobject var +ERcube — TheLstar (teo, symbol)object var was 100% None in production (object-dtype writer round-trip bug); deleted from the writer. TheER (teo, symbol, hedge_level, factor)cube was a redundant reshape of the named scalarL*_*_ERvars — also deleted. vBase 1d stamper updated to drop both from the keccak'd CSV (named scalars still stamped, no information lost; on-chainobject_cidwill differ for the first post-deploy stamp). Incremental rebuilds drop the orphan vars from prior zarrs on read to keep schema clean.- Hedge-shrinkage sidecar (
compute_shrinkage_dataset/compute_shrinkage/ds_erm3_hedge_shrinkage_*.zarr) — Architecture moved to in-pipeline adjusted-betas in PR #33 (commit 4916bcd); the sidecar shim was never deployed to GCS.erm3/core/shrinkage.pyretainscompute_adjusted_betas_and_industry_dataset(production),_vasicek_shrinkage_factor+ vectorized variants. Sidecar tests + staleoutput_subdir/gcs_staging_prefixconfig keys removed.
[0.3.6] — 2026-05-26
Added
GET /api/lstar—residual_returnseries — Parallel daily array of Lstar-dispatched residual returns (l1_rr/l2_rr/l3_rrby chosen level), aligned with existingdatesandlstar. OpenAPILstarResponseupdated; unit tests intests/lstar-service.test.ts.- Python SDK —
RiskModelsClient.get_lstar()— WrapsGET /lstar; returns a DataFrame withdate, dispatched hedge ratios,total_er, andresidual_return. Registered incapabilities.pydiscover output.
Fixed
GET /api/lstarthreshold default — Omitted?threshold=no longer coerces to0viaz.coerce.number(null); server now applies the documented 1% default (threshold_used: 0.01).
[0.3.5] — 2026-05-26
Added
- Unified
hedge_levels(L1/L2/L3) — Shipped on riskmodels.app (PR #108): canonicalHedgeLevelsBlockonGET /metrics/{ticker},POST /decompose,POST /batch/analyze(per-ticker rows),GET /hedge-basket, ticker returns, portfolio snapshots, and chat hedge-basket tooling. MCP registersriskmodels_get_hedge_levels,riskmodels_analyze_portfolio, andriskmodels_hedge_portfolio.riskmodels-py 0.3.5:get_hedge_levels,extract_hedge_levels, portfolio aggregation inportfolio_math.py(including hedge-ratios-only batch rows withoutfull_metrics).@riskmodels/sdk 0.1.2:getHedgeLevels,analyzePortfolio,hedgePortfolio.@riskmodels/mcp 1.0.4: stdio/hosted tool parity for the three portfolio/level tools. JSON Schemamcp/data/schemas/hedge-levels-v1.json; OpenAPIHedgeLevelsBlock. Portal:content/docs/api.mdx. Prod smoke 2026-05-26: metrics, decompose, batch row, hedge-basket, SDKget_hedge_levelsverified live.
Removed
- Snapshot canonicalization PR 3 — Curated stock-snapshot layouts moved to BWMACRO. Deleted from
sdk/riskmodels/snapshots/:r1_risk_profile.py,p1_stock_performance.py,stock_deep_dive.py,s1_forensic.py,s2_waterfall.py,product_tear_sheet.py,_base_template.py,_compare_waterfall.py,_mag7_dna.py,section2_alternatives.py,sec_profile_blurb.py,refine.py. Fromsdk/riskmodels/visuals/:_mag7.py,gallery.py,mag7_l3_er.py,mag7_l3_sigma_rr.py,smart_subheader.py. Demo / zarr-vs-API diff scripts:scripts/preview_l3_plotly.py,scripts/run_visuals_gallery.py,scripts/generate_sdk_visual_gallery.py,sdk/scripts/p1_zarr_vs_api_diff.py,sdk/scripts/mag7_dd_zarr_vs_api.py. Institutional renderers continue to ship as private IP via BWMACRObwmacro/snapshots/stock/.
Changed
-
Public stock snapshots → canonical pipeline —
bulk_dd_render.py(therm_api_publicGCS bucket writer) now buildsCanonicalStockSnapshotviariskmodels.snapshots.canonical.from_components(p1, peer_comparison=…, peer_rankings=…)and renders throughreference_renderer.render_canonical_to_pdf/png. P1Data extracted to slim public modulesdk/riskmodels/snapshots/_stock_data.py(dataclass + builders only — no rendering). Hard rule: the public bucket pipeline never importsbwmacro.*. -
Subsector swatch: slate (
#2a7fbf) → institutional violet (#6d28d9) —Palette.slatefield renamed toPalette.subsectoracross_theme.py,_charts.py,_plotly_charts.py,reference_renderer.py. Snapshot cache key version bumped (SNAPSHOT_CACHE_VERSION = "v2"inapp/api/snapshot/[ticker]/route.ts) so cached PDF/PNG bytes refresh on first read after deploy. -
from_dd_datanow structurally typed —riskmodels.snapshots.canonical.from_dd_datano longer importsDDData(which moved to BWMACRO); typed asAnyand delegates to the newfrom_components(p1, …)adapter.interpretation.compute_features/derive_default_judgmentsimilarly typedAny— both already operated duck-typedly at runtime.
Added
-
agent_accounts.signup_attribution(jsonb) — Persists validated first-touch UTM payload from/get-keywhenPOST /api/agent-keysincludesutm; only fills when column is currently null (oldest qualifying row wins if duplicates exist). Supabase migration20260524120000_add_agent_accounts_signup_attribution.sqlmirrored in Risk_Models and RiskModels_API. -
AOM portfolio support —
riskmodels-py 0.3.3— The Analysis Object Model executor and compiler now acceptportfoliosubjects in both single-analyze and chain forms. Portfolio composition collapses to oneclient.snapshot()call againstPOST /api/snapshot; the response already carries variance decomposition, per-position hedge ratios, and attribution time series, so chains likerisk_decomposition → hedge_actionresolve in a single request rather than fanning out per-ticker. Stock subjects continue to compose normally. Compiler mapsdate_range_presettolookback_days(mtd→21,ytd/1y→252,3y→756,5y→1260). New tests cover portfolio compile shape, mock execute, and lookback presets (16/16 insdk/tests/test_aom.py). Live smoke:sdk/scripts/smoke_aom_portfolio_chain.py. Verified end-to-end against prod 2026-05-01 (warm latency ~3s). -
POST /api/snapshot— Canonical JSON-only portfolio snapshot (type: "portfolio"): L3 variance decomposition, hedge ratios, frozen-weight daily attribution (returns_gross,l1_fr–l3_frstrips +l3_rr), cumulative return and drawdown overlookback_days, concentration-stylerisk_summary. Weights-only or shares-only positions (converted via latestprice_close). ReusesrunPortfolioRiskComputation+fetchBatchHistory; bills asportfolio-risk-snapshot($0.25). Route:app/api/snapshot/route.ts, builderlib/portfolio/canonical-snapshot.ts, ZodSnapshotRequestSchema. OpenAPICanonicalSnapshotPortfolioRequest/CanonicalSnapshotResponse, MCP schemamcp/data/schemas/canonical-snapshot-v1.json. Coexists with GET/api/snapshot/{ticker}(DD assets). -
/ticker-returnsnow accepts ETFs. Previously the route was stocks-only and/etf-returnswas a planned 404 route. Since the zarr reader already falls through tods_etf.zarrfor daily-role keys, the route now detectsasset_type === "etf"and returns a slim payload (date,returns_gross,price_closeonly — L1/L2/L3 hedge/ER columns are omitted from ETF rows rather than returned as null, since ETFs are not factor-decomposed). Response now includes a top-levelasset_typefield so clients can branch. The deprecated SDK wrappersget_returns()andget_etf_returns()both forward toget_ticker_returns()with aDeprecationWarning(existing notebooks keep working);asset_typealso flows through todf.attrs. CLIreturns stock/returns etfsubcommands are deprecated aliases that print a notice and route to/ticker-returns.OPENAPI_SPEC.yamlupdated (+ regeneratedpublic/openapi.jsonandmcp/data/openapi.json). One endpoint, two asset classes. -
POST /decompose— agent-friendly four-bet wrapper over the metrics DAL. Returns a simplifiedexposureobject with four additive layers (market,sector,subsector,residual) — each tradable layer carrying its owner,hr, andhedge_etf(SPY/ sector ETF / subsector ETF fromticker_metadata) — plus a top-levelhedgemap of ETF → dollar ratio (negative of the layerhr, with duplicate ETFs across layers summed). Same billing profile asGET /metrics/{ticker}($0.001,billing_code: metrics_v3). Added: route atapp/api/decompose/route.ts, capabilitydecompose-positioninlib/agent/capabilities.ts, Zod schemaDecomposeRequestSchemainlib/api/schemas.ts, OpenAPI entry +DecomposeRequest/DecomposeResponse/DecomposeLayercomponents inOPENAPI_SPEC.yaml, MCP schemamcp/data/schemas/decompose-v1.json(+schema-paths.json,capabilities.json, regeneratedopenapi.json), Python SDK methodRiskModelsClient.decompose()insdk/riskmodels/client.py+ discover entry, examplesexamples/python/decompose_nvda.pyanddecompose_nvda_vs_aapl.py, unit teststests/decompose.test.ts(Zod + hedge-map sign invariant) andsdk/tests/test_decompose.py. Cross-repo: schema copied toRisk_Models/riskmodels_com/mcp-server/data/schemas/and registered in itsschema-paths.json. -
API key expiry reminder emails — Daily Vercel Cron (
vercel.json→GET /api/cron/notify-expiring-keyswithCRON_SECRET) sends at most three emails per key (14 / 7 / 1 day beforeexpires_at), deduped viaagent_api_keys.expiry_notified_*_at. New React Email templatekey-expiring; useslib/email-service/ Resend (same pipeline as low-balance). Migration20260417180000_agent_api_keys_expiry_notification_flags.sql. -
CLI
riskmodels mcp— Runs the stdio MCP server (mcp/dist/index.js) with path resolution fromRISKMODELS_MCP_SERVER_PATH,--mcp-server-path, cwdmcp/dist/index.js, or monorepo layout. mcp/README.md documents Claude Desktop pitfalls (npx … mcp/ missing subcommand). -
Hosted MCP —
GET/POST /api/mcp/sse— Streamable HTTP (stateless) over Next.js App Router (Node runtime,maxDuration=60). Shared factory atmcp/src/server.tsreused by the stdio binary and the route atapp/api/mcp/sse/route.tsvia a Next-side copy atlib/mcp/server.ts(zod-v4 compatible; registers the same nine SDK-backed tools fromregisterRiskModelsTools+ 5 resources). Auth inlib/mcp/auth.tsacceptsAuthorization: Bearer <key>or?api_key=<key>query-param fallback (forEventSource). Tool calls bill per-invocation at the underlying REST endpoint (no double-charge at the MCP layer). OpenAPI/mcp/sseun-deprecated and updated to describe Streamable HTTP semantics. -
Pure Zarr history path —
fetchHistory/fetchBatchHistoryread consolidated GCS Zarr (ds_daily,ds_erm3_returns_*,ds_erm3_hedge_weights_*viaZARR_GCS_PREFIX/ZARR_FACTOR_SET_ID) for eligible daily V3 keys; Supabase remains for latest tables, rankings, monthly betas, and unknown keys. New moduleslib/zarr-config.ts,lib/dal/zarr-metric-registry.ts,lib/dal/zarr-reader.ts(zarrita+@google-cloud/storage, Upstash cache)._metadata.data_source+_metadata.rangeon/ticker-returns,/l3-decomposition, and data-plane security-history history responses;OPENAPI_SPEC.yamlRiskMetadataextended;RESPONSE_METADATA.md; approved specdocs/API_HISTORY_SUPABASE_AND_ZARR.md. Node.js runtime on affected routes. -
V3 returns decomposition — New optional metrics
l1_cfr,l1_rr,l2_cfr,l2_rr,l3_cfr,l3_rrinsecurity_history(fromds_erm3_returns) and optionalsecurity_history_latestwide columns via idempotent migration; sync state keysecurity_history_returns_decomponerm3_sync_state_v3. Wired through DAL,GET /metrics/{ticker}, data-planeL123_METRIC_KEYSbackfill, OpenAPIMetricsV3, Python SDKMETRICS_V3_TO_SEMANTIC+ hints, SEMANTIC_ALIASES.md, SUPABASE_TABLES.md, portal returns-decomposition-metrics, and README_API.md (ERM3--returns-decomp-tickers/run_syncpointer). Regeneratedpublic/openapi.jsonandmcp/data/openapi.json.
Changed
-
P1 waterfall — sign-coded bars for negative contributions —
_make_cum_waterfall(shared by R1 snapshot, Part 1 opening chart, and_compare_waterfall.render_waterfall_compare) now flips negative bars topal.redwith a diagonal-hatch pattern (Plotlymarker.pattern.shape="/") and places the per-bar value annotation below the bar's ending edge in the bar's own red, so a descending step is unambiguous. Residual bars specifically use pal.green for positives and pal.red-hatched for negatives (sign-coded α story). Systematic-layer bars (SPY / Sector / Subsector) keep their factor palette when positive and turn red-hatched when negative. Bar-value labels are now emitted as explicit annotations (previouslygo.Bar(text=..., textposition=...)), which is why downstream consumers that deep-copy the Bar trace no longer need to special-casetext/textposition(it's gone); they do need to carrymarker.pattern.shapethrough any trace rewriting. -
P1 daily layer attribution uses CFR instead of variance-share × gross —
P1Data.l3_er_series(daily 4-tuple(mkt, sec, sub, res)consumed by Section I waterfall, Section II cumulative-attribution chart,_make_cum_waterfall,_make_cum_chart, and all article-side waterfall renderers) now sources per-layer daily returns from the CFR columns (l1_combined_factor_return,l2_combined_factor_return,l3_combined_factor_return) rather than froml3_*_er × returns_gross. This fixes a correctness bug where per-layer magnitudes reflected variance-share slicing (ER fractions are variance decompositions, not return attribution weights) and therefore did not reconcile with the CFR line terminals shown in the same chart. Telescoped waterfall bars now matchcum_spy / cum_sector / cum_subsectorby construction and sum to gross. Falls back to the legacy ER-based formula (with aUserWarning) when CFR columns are missing or sparse so old snapshots still render. Shipped R1 snapshots will re-generate with different per-layer numbers; the total gross is unchanged. -
Vendor-neutral health + SDK paths —
GET /api/healthteo_coveragerenames the sparse latest-session boolean tolatest_session_returns_pendingand drops vendor naming from OpenAPI/schema copy. Breaking:zarr_contextno longer infers a zarr root or vendor data paths underERM3_ROOT;ERM3_ZARR_ROOTis required for default zarr resolution, and optionalERM3_SECURITY_MASTER_DB/ERM3_TICKER_LIST_CSVsupply full paths for offline company names (seedocs/ERM3_ZARR_API_PARITY.md— Local zarr paths).default_erm3_zarr_path()and bulk/mag7 scripts use the same rules. -
Documentation precision — SEMANTIC_ALIASES.md adds a short hedge ratios vs classical betas note (L2/L3 hierarchy,
dollar_ratio). OPENAPI_SPEC.yamlinfo.descriptionnow describes Supabase V3 tables (security_history,security_history_latest,symbols, supporting surfaces) and rankings fromsecurity_historymetric keys (not legacyerm3_betas/erm3_rankings). SUPABASE_TABLES.md metric_key row no longer equates HR with betas without qualification. README_API.md endpoint table matches behavior (/api/ticker-returnsis L3 HR/ER in the time series;/api/metrics/{ticker}omits Sharpe), and documents the/api/data/*data plane vs OpenAPI. docs/SNAPSHOT_CONTENT_MAP.md uses HR wording consistently. Regeneratedpublic/openapi.jsonandmcp/data/openapi.jsonfrom the spec. -
Portal & email copy — Landing/pricing alignment: components/UseCases.tsx, components/AgenticSection.tsx, components/TerminalShowcase.tsx, app/pricing/page.tsx, emails/low-balance.tsx, lib/chat/system-prompt.ts. README_API.md quickstart examples use the nested
metricsobject and wire HR/ER keys. Removes incorrect Sharpe claim from the low-balance email; clarifies L3 vs full snapshot on ticker returns. -
OpenAPI MCP tool copy —
analyze_portfoliodescription no longer claims Sharpe (not returned by batch/metrics routes). Regenerated JSON; mirrored to Risk_Modelsriskmodels_com/mcp-server/data/openapi.json.
[0.3.0] — 2026-04-07
Added
-
Python SDK (
riskmodels-py0.3.0) — 3D-style namespaces onRiskModelsClient(.stock,.portfolio,.pri,.insights) with.current/.historicalfacades;PerformanceResultfor tabular +.plot()dispatch;riskmodels.visuals(Plotly) for L3 horizontal decomposition and portfolio risk/attribution cascades; optionalpip install riskmodels-py[viz](plotly, matplotlib, seaborn, kaleido); PDF transport helpersget_metrics_snapshot_pdf/post_portfolio_risk_snapshot_pdf;positions_to_weightsaccepts{"ticker","weight"}lists. Seesdk/README.mdandsdk/pyproject.toml. -
Docs —
docs/CHAT_MANUAL_QA.md§8: manual QA checklist for the Risk_Models portal (/chatproxy, anonymous vs JWT, tier caps, streaming note). -
POST /api/chat agentic tools — OpenAI function calling with eight internal tools (DAL-backed): metrics snapshot, L3 decomposition, ticker returns, rankings, factor correlation, macro factors, free ticker search, portfolio risk index. Per-tool billing via
deductBalance; response includestool_calls_summaryand_agent.llm_cost_usd/tool_cost_usd/tool_calls. New modules underlib/chat/;lib/dal/ticker-search.tsshared withGET /api/tickers?search=. Cost preflight:POST /api/estimatewithendpoint: "chat"returnsavailable_toolsand an LLM-only token estimate. -
Transactional email (developer portal) —
lib/email-service.tssends via Resend + React Email (templates underemails/, aligned with Risk_Models).RESEND_API_KEYrequired;RESEND_FROM_EMAIL/RESEND_BCC_EMAILoptional (audit BCC defaults toresend@riskmodels.appperlib/resend-audit.ts). Low-balance alerts fromlib/agent/billing.tslog toemail_logswhenuserIdis provided. Vercel allowlist updated inscripts/doppler-vercel-allowlist.txt. -
Portfolio risk snapshot (Phase 7) —
POST /api/portfolio/risk-snapshotreturns structured JSON or a one-page PDF (format=pdf); capabilityportfolio-risk-snapshot($0.25,risk_snapshot_pdf_v1).GET /api/metrics/{ticker}/snapshot.pdfreuses the same capability for a single-name PDF. Responses are cached ~24h per user (Redis or in-memory); cache hits bill$0and setX-Cache: HIT. PNG export returns 501 until implemented. Shared computation lives inlib/portfolio/portfolio-risk-core.ts; PDF layout inlib/portfolio/risk-snapshot-pdf.ts(pdf-lib). -
OpenAPI
x-pricing— Metered operations inOPENAPI_SPEC.yamlincludex-pricing(capability_id,tier,model,cost_usd,billing_code, optionalmin_charge/ per-token fields) aligned withlib/agent/capabilities.ts. Documented publicGET /pricingin the spec (matchesapp/api/pricing/route.ts). -
GET /api/macro-factors— Read-only long-format daily macro factor returns frommacro_factors(factor_key,teo,return_gross) with optionalstart/end/factors; capabilitymacro-factor-series, OAuth scopemacro-factor-series, JSON Schemamcp/data/schemas/macro-factors-series-v1.json. Python SDKget_macro_factor_series, CLIriskmodels macro-factors, portal doccontent/docs/macro-factors.mdx. -
CI — Root
npm test(Vitest) coversFactorCorrelationRequestSchemaandparseMacroFactorsSeriesQuery. -
Public Python SDK hints —
GET /api/sdk/pythonreturns JSON (package,min_version,upgrade_message,docs_url) for notebooks and CLIs; no auth. Override copy withRISKMODELS_PY_UPGRADE_MESSAGEand minimum version withRISKMODELS_PY_MIN_VERSION(seeapp/api/sdk/python/route.ts). -
Python SDK —
format_metrics_snapshot(row)for human-readable L3 metrics text from aget_metricsdict row;examples/quickstart.pyCLI demo. See packages/riskmodels/README.md.
Changed
-
Premium endpoint pricing (Phase 2) — Raised
cost_usdand bumpedbilling_codeversions for:risk-decomposition/l3-decomposition($0.02),portfolio-risk-index($0.03),batch-analysis($0.005/position, min $0.01),portfolio-returns($0.004/position, min $0.01),plaid-holdings($0.02). SeePREMIUM_TIER_DESIGN.md.OPENAPI_SPEC.yamlbilling copy andmcp/data/capabilities.jsonaligned withlib/agent/capabilities.ts. -
GET /api/sdk/python— Defaultmin_versionand bundledupgrade_messagenow targetriskmodels-py≥ 0.3.0 and the editable pathRiskModels_API/sdk(aligned withsdk/pyproject.toml). -
MCP data sync — Ran
sync-mcp-from-risk-models.sh;mcp-server/data/capabilities.json,schema-paths.json, andschemas/*.jsonmirrored from Risk_Modelsriskmodels_comgenerator output.
Added
-
Python SDK (
packages/riskmodels) —riskmodels-pyon PyPI layout:RiskModelsClient(Bearer + OAuth2 client credentials, optionalhttpxinjection for tests), batch portfolio weighted hedge ratios, Parquet/CSV tabular paths, optional[xarray]get_dataset, agent helpers (discoverMarkdown/JSON,to_llm_context, attrs + ERM3 legend, ticker alias map e.g. GOOGL→GOOG,validate=warn|error|off). See packages/riskmodels/README.md. -
Agentic API landing page integration — Homepage now features agentic-first messaging with new sections:
AgenticSectioncomponent with "Stop Querying. Start Delegating." value propositionUseCasescomponent highlighting four agentic patterns (Pre-Trade Risk Check, Drift Monitoring, Hedge Recommendations, Rebalance Triggers)ComparisonTablecomponent with competitive pricing vs MSCI Barra and Northfield- Updated
Herowith "First Agentic Risk API" badge and new headline - Quickstart page now includes Agentic API examples (Python/TypeScript)
- Cross-linking between traditional REST API and agentic delegation workflows
-
GET /api/healthT coverage — Response includesteo_coverage(latest_teo,latest_teo_coverage_pct,non_null_returns_symbol_count,universe_stock_count, plus a boolean for sparse latest-session gross returns) derived fromsecurity_historyreturns_grossvssymbols(stocks), using a 10% sparse threshold.health-v1.jsonschema updated; copy synced to Risk_Models.
Changed
- OpenAPI tabular exports — Finalized Parquet/CSV documentation:
application/vnd.apache.parquet(matches runtimeContent-Type), sharedFormatQueryTabularparameter, row schemasGrossReturnDailyRowandBatchAnalyzeExportRow,TickerReturnsDailyRow.price_close, batch export semantics (returns-only long table), CSV examples, andbuild:openapinow mirrorsmcp-server/data/openapi.json.
[0.2.0] — 2026-03-24
Added
- Developer portal — Global search over documentation and primary routes (Fuse.js, ⌘K / Ctrl+K). Persistent Live Demo control in the top bar that opens a panel with the public demo API key (when
NEXT_PUBLIC_DEMO_API_KEYis set), one-click copy, and a link to Quickstart; without the env var, Live Demo links to Quickstart. Navbar uses backdrop blur, subtle shadow, gradient primary CTA, and active-route highlighting for clearer hierarchy.
Changed
riskmodels-py0.2.0 (packagepackages/riskmodels) — Version and__version__bumped from 0.1.0.RiskModelsClient.analyzeis a documented alias foranalyze_portfolio.get_dataset(aliasesget_cube,get_panel) returns anxarray.Datasetfrom batch Parquet/CSV long tables when the[xarray]extra is installed. PyPI trove classifier Development Status :: 4 - Beta (was Alpha).
[2026-03-23] — Phase 2–4 migration: self-contained API
Added
- Full agent middleware stack (
lib/agent/) — billing, billing-middleware, api-keys, capabilities, cost-estimator, errors, free-tier, rate-limiter, response-utils, schemas, telemetry. AllcreateAdminClientcalls use@/lib/supabase/admin; module-scope singleton anti-patterns removed throughout. - DAL layer (
lib/dal/) —risk-engine-v3,risk-metadata,response-headers,secmaster; direct Supabase queries replacing former gateway HTTP calls. - Format response helper (
lib/api/format-response.ts) — JSON/Parquet/CSV output. - L3 service (
lib/risk/l3-decomposition-service.ts) — shared decomposition logic used by route and MCP tools. - Redis cache (
lib/cache/redis.ts) — Upstash Redis client with in-memory fallback. - 13 API routes —
/ticker-returns,/l3-decomposition,/batch/analyze,/metrics/[ticker],/tickers,/estimate,/health,/balance,/telemetry,/cli/query,/auth/provision,/auth/provision-free,/auth/free-tier-status. - OpenAPI spec — server URL updated to
https://riskmodels.app/api; added/auth/provision-free,/auth/free-tier-status,/cli/querypath entries.
Added
- ERM3 zarr ↔ API ER/HR mapping — docs/ERM3_ZARR_API_PARITY.md documents zarr-style
L*_ER/L*_HRnames vsPOST /batch/analyzekeys (full_metrics/hedge_ratios),metricswhitelist behavior, lineage headers, tolerances, and an example JSON. OpenAPIBatchFullMetrics/BatchHedgeRatiosnow describe the full L1/L2/L3 surface and zarr aliases;BatchAnalyzeResponsemay include_metadata. - Developer Portal (riskmodels.app) — Next.js site with auth (GitHub + magic link), Stripe Setup for $20 free credits, API key generation with one-time reveal, methodology docs with KaTeX, API reference, quickstart, examples. Get keys at riskmodels.app/get-key.
- Vercel deployment —
DEPLOYMENT.mdwith env vars, Supabase/Stripe config;vercel.json,.env.example;getAppUrl()fallback toVERCEL_URLfor preview deployments. - Parquet/CSV format support —
?format=parquetor?format=csvon/ticker-returns,/returns,/etf-returns; POST bodyformaton/batch/analyze. Returns binary Parquet or text CSV for bulk export. (Note:/returnsand/etf-returnswere later deprecated —/ticker-returnsnow serves both stocks and ETFs. See the Unreleased section.) - Cost estimation endpoint —
POST /api/estimatereturns predicted cost before a request. Authenticated, free.
Changed
- Lineage metadata — All data responses include
_metadata(model_version, data_as_of, factor_set_id, universe_size, wiki_uri, factors) and headersX-Risk-Model-Version,X-Data-As-Of,X-Factor-Set-Id,X-Universe-Size. - Billing — Four previously unbilled endpoints now use
withBilling():metrics/[ticker]($0.001),l3-decomposition($0.005),portfolio/returns($0.002/position),portfolio/risk-index($0.005). - CORS — ticker-returns and etf-returns now include CORS headers for browser requests.
- ETag / 304 — ticker-returns supports
If-None-Match; returns 304 Not Modified when cached data is fresh. - CLI —
riskmodels estimatesubcommand for pre-flight cost estimates.