Skip to content

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:

json
{
  "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

  1. Historical-simulation assumption (fails in crises)
  2. risk_free_rate = 0.045 hardcoded
  3. Single name only (no portfolio aggregation)
  4. 60s cache (freshness lag)
  5. beta=null is a silent fallback
  6. 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-equal
  • determinism — 5 reruns identical
  • var_ordering — 99% VaR > 95% VaR (magnitude), CVaR ≥ VaR at equal confidence (coherent-risk inequality)
  • beta_recovery — embedded β=1.2 recovered within ±0.1
  • rejects_missing_symbol — ZZZZ → status=partial, _error_kind=network
  • result_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)

Verifiable intelligence for the decisions that demand scrutiny.