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 :doc:`/database/risk`).
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
--------

.. math:: \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:
   :math:`r_t = (P_t - P_{t-1}) / P_{t-1}`.
2. Take the 5th percentile of all returns (NumPy ``percentile``, linear
   interpolation).
3. :math:`\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 :doc:`/database/market` (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
  :doc:`/simulation`.

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 :doc:`/pnl`) 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.
