Risk calculations#

End-of-day risk is computed after P&L, for the same calc_date, and persisted to risk_metrics_eod and limit_breaches (see Risk database (risk.db)). Pure functions are in domain/risk.py; services/risk.py loads inputs and persists results. These are v1 simplifications and should not be presented as a more sophisticated risk model than they are.

Inputs#

For each (symbol, book) position in pnl_eod for calc_date:

  • net quantity, mark price and currency, from the persisted P&L row;

  • price_multiplier from the reference database;

  • the close history for the symbol up to and including ``calc_date`` (never later, so past dates can be recomputed without look-ahead);

  • the active limits for that (symbol, book).

Currency rule#

Risk is reported in USD. If a position’s currency is not USD, the run fails with UnsupportedInstrumentError and writes nothing, because converting would need FX rates that v1 does not have. A wrong USD number is worse than no number.

Notional#

\[\text{notional\_usd} = \text{net\_quantity} \times \text{mark\_price} \times \text{price\_multiplier}\]

It is signed (short positions are negative).

Historical VaR (95%, 1 day)#

  1. Take daily simple returns from the close history: \(r_t = (P_t - P_{t-1}) / P_{t-1}\).

  2. Take the 5th percentile of all returns (NumPy percentile, linear interpolation).

  3. \(\text{VaR} = \left| q_{5\%} \times \text{notional} \right|\), a positive number.

Properties and limits:

  • The window is the entire history up to calc_date; there is no look-back cap.

  • With fewer than two closes there are no returns and VaR is 0.0.

  • The absolute value means a short and a long position of the same size have the same VaR, a known simplification.

  • The price basis of the close history is not recorded yet. VaR returns should be computed on one basis (MID) throughout; see Market database (market.db) (Bid, ask and mid).

  • It uses one asset’s own history. There is no portfolio aggregation, correlation, parametric or Monte Carlo VaR, stress testing or VaR backtesting.

  • On the simulated v1 data VaR only reflects the generator’s sigma; see Simulated data: why and how.

Limit checks#

For each active limit on the position, a breach row is logged when:

  • |net_quantity| > max_net_quantity (NET_QUANTITY), or

  • |notional_usd| > max_notional_usd (NOTIONAL_USD).

Values equal to the limit are not breaches. Observed and limit values are both stored. Checks run after the fact in the EOD batch; there is no pre-trade limit check, so a trade that breaches a limit is accepted and reported at the next run.

Behaviour#

  • Idempotent. A rerun replaces that date’s metrics and breaches in one transaction.

  • Needs P&L first. If there are no pnl_eod rows for the date, the run logs a warning and replaces that date’s risk results with nothing. Run the EOD batch (see P&L and the EOD batch) rather than the risk step alone.

  • Reads persisted P&L. Risk reads the trade database’s stored positions, so it reflects the last P&L run, not trades booked since.