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_multiplierfrom 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#
It is signed (short positions are negative).
Historical VaR (95%, 1 day)#
Take daily simple returns from the close history: \(r_t = (P_t - P_{t-1}) / P_{t-1}\).
Take the 5th percentile of all returns (NumPy
percentile, linear interpolation).\(\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_eodrows 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.