PnLReport
PnL as of one instant, under one costing method.
as_of_ms is the instant the caller asked about, carried through
verbatim; it is not read from a clock and need not coincide with any
event. method is the method the state was replayed under. A
report cannot be re-costed, because the lot ledger behind it has
already been consumed one particular way.
realized is the exact sum of the disposals whose realised amount
is KNOWN, and missing_realized_count is how many were left out of
that sum; a caller that ignores the count will silently read an
understated total, which is why the count is not optional.
unrealized is None if ANY held asset is uncertain, so it is
the conservative whole-portfolio figure rather than a partial sum:
the per-asset detail in per_asset is where the known parts stay
visible.
open_lots is sorted by (asset_id, opened_at_ms, lot_id): a
total order with no ties, so the wire output is byte-stable across
runs even when two lots were opened in the same millisecond.
UNDER method="acb", unrealized AND TaxLot.cost_basis DO NOT
AGREE, and that is correct. ACB costs from a per-asset running POOL, so
a disposal consumes pool_cost × take/pool and leaves the pool
reduced proportionally; the lots behind it are untouched and keep
reporting their own remaining basis, because they stay ground truth for
lot-level reporting (docs/internal/DECISIONS.md, "ACB pooling"). Buy 1 at 10, 1
at 20 and 1 at 15, sell one, and the pool holds 30 while the surviving
lots sum to 35: a permanent, intended gap of 5.
Summing TaxLot.cost_basis and comparing it with what unrealized
implies is therefore the wrong check, and it is an easy one to reach for.
basis_source names which cost unrealized actually subtracted,
and unrealized_basis and open_lots_basis expose both
figures, so the difference is inspectable rather than something a caller
has to reverse-engineer and mistake for a bug.
Fields
as_of_msintmethodstrrealizedMoneymissing_realized_countintunrealizedMoney | Noneper_assetMapping[str, AssetPnL]open_lotstuple[TaxLot, ...]flagstuple[str, ...]default ()basis_sourcestrdefault 'lots'unrealized_basisMoney | Nonedefault Noneopen_lots_basispropertySum of every open lot's `cost_basis`, or `None` if any is.__init__
__init__(self, as_of_ms: int, method: str, realized: Money, missing_realized_count: int, unrealized: Money | None, per_asset: Mapping[str, AssetPnL], open_lots: tuple[TaxLot, ...], flags: tuple[str, ...] = (), basis_source: str = lots, unrealized_basis: Money | None = None) -> None
Parameters
as_of_msrequired, intnone
methodrequired, strnone
realizedrequired, Moneynone
missing_realized_countrequired, intnone
unrealizedrequired, Money | Nonenone
per_assetrequired, Mapping[str, AssetPnL]none
open_lotsrequired, tuple[TaxLot, ...]none
flagsoptional, tuple[str, ...], default ()none
basis_sourceoptional, str, default 'lots'none
unrealized_basisoptional, Money | None, default Nonenone