What (One-liner)
One-stop historical risk metrics for a single symbol vs a benchmark: annual return / vol / Sharpe, max drawdown, beta, VaR 95%/99% + CVaR 95%/99%, audit-friendly windows tags. The actual backend of the Canvas risk_management_v1 graphlet.
How to use
POST /portfolio/risk-metrics body:
{
"symbol": "AAPL",
"benchmark": "SPY",
"period": "1y",
"confidence_levels": [0.95, 0.99]
}Canvas wires this automatically — users never fetch by hand. The Batch Signals panel can also trigger it from the "Risk metrics" button.
Core formulas
See the frontmatter for the full derivation. In brief:
- VaR = absolute value of the left-tail quantile
- CVaR = conditional mean of the worst-case tail (ES = expected shortfall)
- Max Drawdown = minimum of the running drawdown
- Beta = Cov(symbol, benchmark) / Var(benchmark)
- Sharpe = (annual_return - 0.045) / annual_vol
Assumptions & applicability
Assumptions: historical simulation is acceptable + daily 1y + 60s cache + rf = 4.5% + yfinance adjusted close.
Fits: US equity / developed-market ETF risk snapshots, Canvas risk-node execution, single-name risk overlays.
Does not fit: parametric VaR, portfolio aggregation, non-US / non-equity, EVT extreme tails, custom rf.
Input / Output contract
{symbol, benchmark?, period?, confidence_levels?} → {symbol, benchmark, period, n_observations, annual_return, annual_volatility, sharpe_ratio, max_drawdown, beta, var_95pct, cvar_95pct, var_99pct, cvar_99pct, windows}.
Beta may be null (benchmark fetch failure or <20 obs). status=partial signals a yfinance failure.
Known limitations
- Historical-simulation assumption (fails in crises)
risk_free_rate = 0.045hardcoded- Single name only (no portfolio aggregation)
- 60s cache (freshness lag)
- beta=null is a silent fallback
- No skewness / kurtosis output (the legacy fn had them; this route dropped them)
References
Jorion (2006) Value at Risk + Artzner et al. (1999) Coherent Measures of Risk (CVaR) + Magdon-Ismail & Atiya (2004) MaxDD + BCBS d457 (2019) MAR50. Full citations in the frontmatter.
Golden Test
tests/golden/fixtures/tier2/portfolio_risk_metrics/ — a yfinance-mock regression (2 symbols with embedded β=1.2), 1e-10 tolerance, 6 tests:
matches_snapshot— full output byte-equaldeterminism— 5 reruns identicalvar_ordering— 99% VaR > 95% VaR (magnitude), CVaR ≥ VaR at equal confidence (coherent-risk inequality)beta_recovery— embedded β=1.2 recovered within ±0.1rejects_missing_symbol— ZZZZ → status=partial, _error_kind=networkresult_structure— 15 fixed top-level keys
Note on testing history
The 2026-04-20 post-audit found that the early golden (closed PR #38) targeted services/analysis/risk.py::analyze_risk_management — a legacy dead function Canvas never used. The trigger was a user's observation that "Canvas looks fine". The golden was rebuilt against the real route and the legacy function deleted.
Changelog
- 1.0.0 (2026-04-20) — First Active (real Canvas route after legacy cleanup)

