Reporting
=========

Reporting is the **read side** of the system: everything the dashboard, blotter
and history views show. It lives in one place, ``services/reporting.py``
(``ReportingService``), separate from the services that write (booking, loading,
P&L, risk).

Principles
----------

- **Read persisted results, never recompute.** P&L and risk screens read the EOD
  tables written by the batch (see :doc:`/pnl` and :doc:`/risk`). A
  report shows what was calculated on that date, independent of later trades or
  price changes.
- **Compose across databases at the application layer.** There are no
  cross-database joins; the service reads each database in its own session and
  combines the results in Python.
- **One entry point.** The web layer calls ``ReportingService`` only for reads, so
  reads can later be pointed at a read replica without touching the write path.
- **Reports are views, not tables.** The blotter in particular has no table of its
  own; it is a query over ``trades``.

Reports
-------

.. list-table::
   :header-rows: 1

   * - Method
     - Content
     - Source
     - Ordering and size
   * - ``dashboard()``
     - Latest P&L (5 rows), latest risk metrics (5), recent breaches (10)
     - ``pnl_eod``, ``risk_metrics_eod``, ``limit_breaches``
     - Newest first
   * - ``blotter(limit)``
     - Trades joined with instrument ``description`` and ``asset_class``
     - ``trades`` (trade DB) and ``instruments`` (reference DB)
     - ``trade_date`` desc, ``trade_id`` desc; 200 by default
   * - ``prices(limit)``
     - Daily bars including ``source`` and ``data_origin``
     - ``prices_eod``
     - ``px_date`` desc across all symbols; 200 by default
   * - ``pnl_history(limit)``
     - EOD P&L rows
     - ``pnl_eod``
     - ``calc_date`` desc, ``id`` desc; 200 by default
   * - ``risk_overview(limit)``
     - All limits, recent metrics, recent breaches
     - ``position_limits``, ``risk_metrics_eod``, ``limit_breaches``
     - Metrics and breaches newest first; 200 by default

The blotter join
~~~~~~~~~~~~~~~~

``blotter`` loads recent trades from the trade database, loads all instruments from
the reference database, and pairs them by ``symbol`` into ``BlotterRow(trade, instrument)``. A trade whose instrument is missing still appears, with empty
instrument fields. This is the application-layer equivalent of the foreign key
that cannot cross databases (see :doc:`/database/index`).

Return types and serialization
------------------------------

The service returns detached ORM objects (sessions use ``expire_on_commit=False``,
so loaded attributes remain readable after the session closes) and small
dataclasses (``Dashboard``, ``RiskOverview``, ``BlotterRow``). Turning them into JSON is
the web layer's job (``web/serializers.py``; see :doc:`/web`), so reporting has
no knowledge of HTTP.

Known limitations
-----------------

- **No pagination or filtering.** Lists are capped (200) and ordered newest first.
  Date, symbol, book and asset-class filters are the natural next addition.
- **Not a single snapshot.** Each report reads its databases in separate
  transactions, so the dashboard can show P&L from one EOD run next to risk from
  another if a batch runs in between.
- **Simulated data is labelled on prices only.** ``prices`` exposes ``data_origin``;
  P&L and risk rows do not yet (see :doc:`/simulation`).
- **Per-currency amounts.** Reports show P&L in each instrument's currency with no
  aggregation across currencies.
- **No charts or HTML.** Output is JSON; see :doc:`/web`.
