Specification · 005-tax-lots

FIFO/LIFO tax lot tracking

Every buy opens a lot; every sell relieves lots by policy — FIFO or LIFO — realizing gain to the cent, with an audit record for each closed lot.

Status Accepted Spec ID 005-tax-lots Parent 001-portfolio-analytics Owner Brian Corbin · @briancorbinxyz Reviewers Brian Corbin · @briancorbinxyz Created Principlesv0.1.0 Smallest demoableBuy 100 then 100 at different prices, sell 150 under FIFO; realized gain relieves the oldest lot fully and the next partially, to the cent. Read time

Tax lots decompose a position into the individual purchases that make it up, because realized gain depends on which shares you sold. This slice folds the ledger into an ordered set of open lots per instrument; on a sell it relieves lots by a configured policy — FIFO (oldest first) or LIFO (newest first) — splitting a lot on partial consumption and computing realized gain as proceeds minus relieved basis. Policy is fixed at relief time and recorded on an immutable audit record per closed lot. Realized and unrealized gain must reconcile to a full replay to the cent.

I Independent
Depends on the umbrella model and the ledger (002). Its aggregate basis must agree with the position engine (003) but it maintains its own lot decomposition.
N Negotiable
Outcome (correct FIFO/LIFO relief, to-the-cent realized gain, an audit trail), not a named ordered structure.
V Valuable
Unlocks SC-001 — defensible realized-gain numbers finance can file on.
E Estimable
Lot open, ordered relief, and partial-lot split are well-defined; the policy set is fixed to FIFO/LIFO for this slice.
S Small
One lot book, five functional requirements; wash sales and HIFO/specific-lot are deferred.
T Testable
Relief order, split arithmetic, and reconcile-to-replay are all mechanically checkable against worked examples.

1 · Context

A position's net quantity says nothing about its tax treatment: sell 150 of a 200-share position and the realized gain depends entirely on which lots you relieve. FIFO and LIFO give materially different numbers, and finance must be able to show why a figure is what it is. So this slice keeps the lot-level decomposition the position engine deliberately does not — an ordered book of open purchases — and relieves it deterministically on every sell.

It owns lots, relief policy, realized/unrealized gain, and the audit record. It reads events from 002; portfolio weighting and net position math are siblings.

2 · User scenarios & testing

Each story MUST be independently testable.

US1 · Open lots on buys P1

As a finance analyst, I want each buy to open a distinct lot carrying its quantity, cost basis, and acquisition time, so the position is decomposed into relievable purchases.

Acceptance: after a series of buys, the open-lot book lists one lot per buy in acquisition order, and their quantities sum to the long position.

US2 · Relieve by FIFO or LIFO on a sell P1

As a finance analyst, I want a sell to consume open lots by the configured policy and compute realized gain, so the number reflects the elected method.

Acceptance: under FIFO a sell relieves the oldest lots first; under LIFO the newest first; a partial consumption splits the boundary lot; realized gain equals proceeds minus relieved basis, to the cent.

US3 · Auditable realized gain P2

As a tax preparer, I want an immutable record per relieved lot — policy, proceeds, basis, gain, holding period — so every realized figure is traceable.

Acceptance: each relief emits an audit record naming the lot, the policy applied, and the components of the gain; summing the records reproduces cumulative realized gain.

Edge cases

3 · Requirements

Conformance keywords (MUST, SHOULD, MAY) follow RFC 2119.

Functional

Each buy MUST open a tax lot per (account, instrument) carrying quantity, cost basis, acquisition timestamp, and ledger sequence, held in an acquisition-ordered structure.

On a sell the slice MUST relieve open lots by the configured policy — FIFO (oldest acquired first), LIFO (newest first), or HIFO (highest cost basis first) — MUST split the boundary lot on partial consumption, and MUST compute realized gain as proceeds minus relieved basis.

Relief policy (FIFO, LIFO, or HIFO) MUST be selectable per account (or per instrument), MUST be fixed at relief time, and MUST be recorded on the realized-gain record for auditability.

The slice MUST compute unrealized gain per open lot on price ticks and cumulative realized gain across relieved lots, both reconciling to a full replay.

Each relieved lot SHOULD emit an immutable audit record — lot sequence, policy, proceeds, relieved basis, realized gain, holding period — for downstream tax reporting.

Non-functional

Lot relief on a sell MUST meet SLO-relief for a position of up to 1,000 open lots.

in-process wall-clock to match, split, and record lot relief for one sell (good ÷ valid sells)

Realized and unrealized gain MUST reconcile to a full replay per SLO-pnl-reconcile.

daily incremental-vs-replay P&L reconciliations matching to the cent (matching ÷ total)

The open-lot set MUST stay bounded per SLO-lotset: a fully-closed position holds zero lots, so memory tracks open lots, not lifetime trades.

open-lot count as a fraction of open (unrelieved) buys per position, sampled at peak (open lots ÷ open buys)

Out of scope (deferred)

  • Net quantity and position-level VWAP basis — this slice keeps lot-level detail, not the aggregate.
  • Market-value weighting of holdings — unrelated to lot relief.
  • Wash-sale disallowance and the resulting basis/holding-period adjustments — a jurisdiction-specific overlay.
  • Additional relief policies (HIFO, specific-lot, average-cost) beyond FIFO/LIFO.
  • Basis adjustments from splits, spin-offs, and return-of-capital.

4 · Data model

The slice owns the lot book and the audit records; it folds the umbrella's TradeEvents and PriceTicks.

Lot
{ account, instrument, openSeq, acquiredTs, qtyOpen, basisPerUnit, side (long|short) }, held acquisition-ordered.
RealizedGainRecord
{ relievedLotSeq, sellSeq, policy (FIFO|LIFO), qty, proceeds, relievedBasis, realizedGain, holdingPeriod } — immutable.

Invariant: at any sequence, the sum of open-lot quantities equals the position engine's net long quantity for that instrument (and symmetrically for shorts). The two slices decompose the same truth differently; they must agree in aggregate.

5 · Success criteria

Buy 100 @ 10 then 100 @ 12 and sell 150: under FIFO realized gain relieves the first lot fully and 50 of the second (basis 100·10 + 50·12), leaving a 50-share open lot @ 12; under LIFO it relieves the second fully and 50 of the first. And for a non-monotonic book — buy 100 @ 10, 100 @ 14, 100 @ 12, sell 150 — HIFO relieves the two costliest lots first: 100 @ 14 fully and 50 @ 12 (relieved basis 100·14 + 50·12 = 2000), leaving 50 @ 12 and 100 @ 10 open and realizing the smallest gain of the three policies. Each is computed to the cent and matches a full replay.

Each relief emits an audit record whose fields sum, across all reliefs, to the cumulative realized gain, and whose recorded policy matches the account's elected method at relief time.

6 · Assumptions

Relief policy is elected before the sell and does not change retroactively; a policy change applies only to future reliefs.

Wash-sale and corporate-action adjustments are out of scope here, so basis and holding period derive purely from the trade events as recorded.

7 · Open questions

None outstanding — the FIFO/LIFO relief semantics, partial-lot split, per-account policy election fixed at relief time, and the immutable audit record were all settled in the interview; further policies (HIFO, specific-lot) are deferred, not open.

8 · Conformance index

Auto-built from every <spec-requirement> in this document.

9 · Change log

  1. Initial draft. Child of 001-portfolio-analytics: acquisition-ordered lots (FR-001), FIFO/LIFO relief with partial-lot split and realized gain (FR-002), per-account policy fixed at relief time and recorded (FR-003), realized/unrealized reconciliation (FR-004), immutable audit records (FR-005). Envelope: relief latency (NFR-001), P&L reconcile-to-replay (NFR-002), bounded open-lot set (NFR-003). Wash sales and HIFO/specific-lot deferred.
  2. Applied 2026-07-22-hifo-relief: Applied 2026-07-22-hifo-relief: added HIFO (highest-basis-first) relief policy alongside FIFO/LIFO — FR-002/FR-003 extended, SC-001 gains a distinguishing three-lot example; HIFO promoted from TBD-hifo-specific-lot..