Multi-asset design
==================

The schema is designed for FX, crypto, bonds and equities. v1 implements and
seeds only one **simulated** FX pair (``EURUSD``).

**Principle: a new asset class is added by adding rows and, at most, one subtype
table. It never requires changing ``trades``, ``prices_eod``, ``pnl_eod`` or the risk
tables.** Those tables are deliberately asset-class-neutral.

How it works
------------

1. **``instruments`` is a neutral core.** Every instrument, whatever its class, has
   a unique ``symbol``, an ``asset_class``, a ``quote_ccy`` (the currency its price,
   trades and P&L are denominated in) and a ``price_multiplier``. See
   :doc:`/database/reference`.
2. **Class-specific attributes live in a one-to-one subtype table** keyed by
   ``instrument_id`` (foreign key to ``instruments``, same database). FX's
   ``base_ccy``, ``pip_size`` and ``lot_size`` are in ``fx_instruments``. This avoids a
   wide table full of nullable class-specific columns and lets each subtype carry
   its own ``NOT NULL`` and ``CHECK`` rules.
3. **``price_multiplier`` absorbs differences in quoting convention.** P&L per unit
   of quantity is ``price difference * price_multiplier``, and notional is
   ``quantity * price * price_multiplier``. It is ``1.0`` for FX, crypto and
   equities, and ``0.01`` for bonds quoted in percent of par with quantity as face
   amount. It is positive by ``CHECK``.
4. **``symbol`` is the system's unique internal instrument code**, not a vendor
   ticker. It is the cross-database key (trades, prices, P&L and risk all carry
   it), so it must be unique across asset classes and immutable. Vendor tickers,
   ISIN and FIGI belong in a separate identifiers table (below), because tickers
   collide across venues.
5. **P&L carries its currency (``pnl_eod.ccy``)**, so positions in different
   currencies are never silently summed.

Mapping of the four asset classes
---------------------------------

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

   * - 
     - FX
     - Crypto
     - Bond
     - Equity
   * - ``asset_class``
     - ``FX``
     - ``CRYPTO``
     - ``BOND``
     - ``EQUITY``
   * - Quantity means
     - base-currency units
     - base-asset units (fractional)
     - face amount
     - shares
   * - Price means
     - quote currency per base unit
     - quote asset per base unit
     - clean price, percent of par
     - currency per share
   * - ``price_multiplier``
     - 1.0
     - 1.0
     - 0.01
     - 1.0
   * - ``quote_ccy``
     - quote currency
     - quote asset (may be a stablecoin)
     - bond currency
     - listing currency
   * - Subtype table
     - ``fx_instruments`` **(built)**: ``base_ccy``, ``pip_size``, ``lot_size``
     - ``crypto_instruments`` *(designed)*: ``base_asset``, ``quantity_decimals``, ``price_decimals``, ``venue``
     - ``bond_instruments`` *(designed)*: ``issuer``, ``coupon_rate``, ``coupon_frequency``, ``day_count``, ``issue_date``, ``maturity_date``, ``face_value``
     - ``equity_instruments`` *(designed)*: ``exchange``, ``country``, ``lot_size``
   * - Needs beyond the core
     - forwards and value-date points (later)
     - trading fees; 24/7 market, so an explicit EOD cut-off (e.g. 00:00 UTC)
     - coupon accrual and payments; clean vs dirty price (accrued interest)
     - dividends and splits (corporate actions); adjusted price history

*(built)* means implemented and tested. *(designed)* means specified here and
added by migration when the first instrument of that class is introduced, so no
empty tables exist before they are needed.

Recipe: adding an asset class
-----------------------------

1. Add the value to ``AssetClass`` in ``domain/enums.py``. The ``CHECK`` on
   ``instruments.asset_class`` is generated from the enum; ship a migration to
   update it.
2. Create the subtype table (foreign key to ``instruments.instrument_id``).
3. Insert the instrument rows with the right ``quote_ccy`` and ``price_multiplier``.
4. Load prices with an explicit ``data_origin`` (see :doc:`/database/market`).
5. Book trades as usual and run EOD. P&L and risk need no changes unless the
   class needs behaviour from the "Needs beyond the core" row.
6. If it does, add that behaviour in ``domain/`` (pure and unit-tested) and any new
   table in the database that owns the data (next section).

This is exercised by tests (``tests/test_multi_asset.py``): a bond-style
instrument and a EUR-quoted equity are created with core rows only and run
through booking, P&L and risk.

What is not generic yet
-----------------------

These are real gaps, listed so they are not mistaken for supported features.

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

   * - Gap
     - Owning database
     - Planned table or change
   * - Fees and commissions on trades
     - trade
     - ``trade_fees(trade_id, ccy, amount, fee_type)``
   * - Coupons, dividends, other cash flows
     - trade
     - ``cashflows(id, symbol, book, ccy, amount, kind, pay_date)``
   * - Splits, mergers, symbol changes
     - market
     - ``corporate_actions`` and adjusted-price handling
   * - Vendor identifiers (ISIN, FIGI, ticker, venue)
     - reference
     - ``instrument_identifiers(instrument_id, scheme, value)``, unique ``(scheme, value)``
   * - Currency conversion for P&L aggregation and USD notional and VaR
     - market
     - FX rates (e.g. via FX instruments' own prices) and a reporting-currency setting
   * - Trading calendars and EOD cut-offs per instrument
     - reference
     - ``trading_calendars``
   * - Accrued interest (dirty price) for bonds
     - domain
     - day-count-aware accrual in ``domain/``

Until currency conversion exists, risk (USD notional, VaR, limits) **refuses**
non-USD instruments with an explicit error rather than producing a wrong number.
P&L works for any currency but is reported per currency.
