Guides · risk

Portfolio VaR from positions and a returns matrix

Send a T×N matrix of asset returns plus N market values, and POST /v1/risk/var returns portfolio VaR and Expected Shortfall directly in currency. Correlations are captured automatically: each row aggregates into one portfolio P&L observation, so realized co-movement is baked in — no correlation matrix input.

01How matrix mode works

The endpoint has two mutually exclusive input modes: a single returns series, or returns_matrix + positions. In matrix mode, each period’s portfolio P&L is the row-wise sum of position × return — already in currency, since positions are market values. VaR and ES are then computed on that P&L series, historical and parametric, exactly as in the single-series case.

RuleDetail
Shapereturns_matrix is T×N: rows = periods, columns = assets. All rows must have the same length.
Positionspositions = N market values in the same currency; len(positions) must equal the number of columns.
Minimum dataAt least 30 rows, else HTTP 422.
Mode exclusivityProvide exactly one of returns OR returns_matrix + positions.
portfolio_valueIgnored in matrix mode — results are already in currency, so the response returns amount only, no fraction_of_portfolio.

02A reproducible example

Three assets, one trading year (252 days), €2M total: €1M in asset A, €600k in B, €400k in C. The minimal call, matrix truncated for display:

curl
curl -s https://quantbox.dev/v1/risk/var \
  -H "X-API-Key: $QB_KEY" -H "Content-Type: application/json" \
  -d '{"returns_matrix": [[0.000312, 0.024504, 0.005944],
                       [0.003287, 0.001348, -0.002612],
                       [-0.002441, -0.020606, -0.005891], …],
       "positions": [1000000, 600000, 400000],
       "confidence": 0.95, "horizon_days": 1}'

Runs on the free tier — 500 calls/month, no credit card. Get a key.

The complete script generates the matrix with a seeded numpy generator. The order of the three draws is what makes it reproduciblea, then b, then c from the same generator; swap them and every number changes. Your first row should be exactly [0.000312, 0.024504, 0.005944]:

python — complete and reproducible
import requests
import numpy as np

KEY = "YOUR_API_KEY"  # free key at https://quantbox.dev

# Draw order matters for reproducibility: a, then b, then c.
rng = np.random.default_rng(7)
a = np.round(rng.normal(0.0003, 0.010, 252), 6)  # asset A — ~16% annualized vol
b = np.round(rng.normal(0.0005, 0.018, 252), 6)  # asset B — ~29% annualized vol
c = np.round(rng.normal(0.0002, 0.007, 252), 6)  # asset C — ~11% annualized vol

matrix = np.column_stack([a, b, c]).tolist()     # shape (252, 3): rows = days

r = requests.post(
    "https://quantbox.dev/v1/risk/var",
    headers={"X-API-Key": KEY},
    json={"returns_matrix": matrix,
          "positions": [1_000_000, 600_000, 400_000],
          "confidence": 0.95, "horizon_days": 1},
)
print(r.json())
response
{
  "historical": {
    "var":                {"amount": 26141.08},
    "expected_shortfall": {"amount": 31902.23}
  },
  "parametric_normal": {
    "var":                {"amount": 25992.26},
    "expected_shortfall": {"amount": 32057.76}
  },
  "n_observations": 252,
  "confidence": 0.95,
  "horizon_days": 1,
  "assumptions": {
    "input": "returns_matrix (T x N asset returns) x positions (N market values); results in currency, portfolio_value ignored",
    "horizon_scaling": "sqrt(horizon_days), assumes iid returns",
    "parametric": "normal distribution fitted on sample mean/std (ddof=1)",
    "historical": "linear-interpolated percentile of empirical losses",
    "sign": "positive VaR = potential loss; can be negative if all observations are gains"
  }
}

Reading it: on the €2M portfolio, the 95% 1-day historical VaR is €26,141.08 (about 1.3% of the portfolio) and the historical Expected Shortfall €31,902.23 (about 1.6%); the parametric figures are €25,992.26 and €32,057.76. Amounts rounded to cents for display — the API returns full precision. Note the response carries no fraction_of_portfolio: results are in currency, and portfolio_value would have been ignored if sent.

03Where the correlations went

Nowhere — they are in the data. Because each day’s P&L sums all three assets on the same date, days when assets fall together produce large P&L losses, and that co-movement flows straight into the historical percentile. This is the quiet advantage of historical aggregation over a variance formula you assemble yourself: nothing to estimate, nothing to get wrong about which correlation regime applies.

The parametric figure is fitted on the same aggregated P&L series (sample mean and standard deviation, ddof=1) — which is mathematically equivalent to the classic pᵀΣp portfolio-variance formula with the sample covariance matrix. You get the covariance treatment without ever building Σ.

In this example the two methods land within 0.6% of each other on VaR (€26,141 historical vs €25,992 parametric) — expected, since the three series are drawn from independent normals, so the aggregated P&L really is close to normal. On real data, a wide gap between the columns is your cue that the joint tails are not normal — historical vs parametric VaR covers how to read that gap, and the Expected Shortfall guide covers why the ES column deserves more attention than VaR.

04Fine print

Horizon scaling. This example uses horizon_days: 1, so no scaling is applied. Longer horizons are scaled by sqrt(horizon_days), which assumes iid returns — the assumptions block labels it on every response.

Alignment is on you. The API treats row t as one date across all columns. If your asset series are misaligned (holidays, missing days), the co-movement — and therefore the VaR — is wrong in a way no validator can catch. Align and forward-fill deliberately before calling.

Sign convention. Positive VaR = potential loss; the API returns negative values rather than clamping if the sample only went up.

Your data stays yours. The matrix is anonymous numbers — no tickers, no identifiers — and nothing is stored beyond the request.

Run it on your own book. Key in seconds, then portfolio VaR is one POST from your positions and price history.

Get a free API key

500 calls/month free · no credit card · full schemas in /docs

Related: Value-at-Risk and Expected Shortfall in one call · Expected Shortfall (CVaR) via API · Historical vs parametric VaR · All guides