๐ฌ FIFO Lot Analysis
FIFO lot analysis is the per-lot complement to Weighted Average Cost (WAC).
WAC answers: "What is my blended average cost for this position?" FIFO lot analysis answers a different question: "How is each individual purchase batch performing through time?"
Instead of merging all acquisitions into one pool, LibreFolio tracks each lot through its own lifecycle โ open, partially closed, fully closed โ and matches sells in FIFO order (first in, first out).
Complement, not replacement
WAC is aggregate and position-level. FIFO lot analysis is granular and lot-level. Both views are useful: one for blended cost basis, one for economic attribution lot by lot.
๐ก What is FIFO Lot Analysis?
A lot is one acquisition batch: for example, one BUY of 100 shares, or one transfer-in that preserves historical cost basis.
When a SELL occurs, the oldest still-open lots are closed first. This creates a lot-by-lot history:
- how much of each lot is still open
- how much has already been sold
- how much sale proceeds that lot has generated
- how much income was earned while that lot was held
- how much return came from price change versus cash income
This makes FIFO lot analysis especially useful when two positions in the same asset were bought at very different prices or dates.
The Lot Life & Custody timeline above makes the lifecycle visual: each bar is one lot, colored by the broker currently holding it, with thickness proportional to the quantity still held in that segment. A bar that ends mid-chart is a fully closed lot; a bar reaching "today" is still open.
๐งฎ Open Return per Lot
Open Return isolates the price-only move of a lot relative to its opening reference price.
In practice:
- if a market quote exists at the lot's opening date, that opening quote becomes
reference_unit_price - if the lot opened before the first available market quote, the system falls back to the lot's own opening cost, scaled to market quote units
reference_price_sourcerecords whether the reference wasexact,fallback, orunavailable
This metric excludes dividends, interest, and realized sale proceeds. It answers: "How much has market price moved since this lot was opened?"
Reference price fallback
When no opening-day market quote exists, LibreFolio uses the lot's acquisition price as the reference base, scaled to the asset's quote convention. This avoids misleading percentage returns on instruments quoted per 100 nominal units.
The fallback is \(\text{OpeningUnitPrice}\times qbq\). For qbq = 100, a bond bought at 0.992 compares against the market quote axis as 99.20, not 0.992.
The WAC / Market Price chart plots each lot as a bubble against the market price line: bubble color marks the broker where the lot was opened, bubble size scales with the lot's opening value. A lot valued only at cost (no live market price) is drawn with a dashed outline.
๐ฐ Total Return per Lot
Total Return is broader than Open Return. It includes the lot's remaining market value, any sale proceeds already realized from that lot, and any allocated income received while the lot was held.
LibreFolio's lot math uses these exact building blocks:
For the scalar lot summary, the return percentage is:
For return history through time, LibreFolio uses:
This answers: "What is full economic return of this lot, including both price movement and cash yield?"
The Value / Return comparison chart, switched to Return mode, plots exactly this percentage โ one line per lot, each measured from its own opening date, over the currently selected lot set.
โ๏ธ qbq Scaling
Some instruments are quoted per base quantity, not per single unit. LibreFolio calls this base quantity qbq (quote_base_quantity).
- For most stocks,
qbq = 1 - For many bonds,
qbq = 100
The exact valuation rule is:
qbq scaling matters
Suppose a bond has face quantity 1,000 and is quoted at 101.50 per 100 nominal.
qbq = 100- lot quantity =
1,000 - market value =
(1,000 / 100) ร 101.50 = 1,015.00
If you compare 101.50 directly with a per-single-unit cost basis such as 0.992, you get nonsense because the two numbers live on different scales.
The correct comparison rescales the lot cost onto the market quote axis:
So the meaningful price comparison is 101.50 vs 99.20, not 101.50 vs 0.992.
Without this scaling, bond returns and valuations can be off by orders of magnitude.
๐ Estimated-at-Cost
If no live market price is available for an asset, LibreFolio does not fail the analysis. Instead, it temporarily values the still-open portion of the lot at cost:
Practical implication:
- the lot still shows residual value
- already realized proceeds still remain visible
- allocated dividends or interest still remain visible
- unrealized volatility is temporarily understated
value_source = ESTIMATED_AT_COSTmarket_pnl = 0- data-quality issue code:
CURRENT_PRICE_ASSUMED_AT_COST
Interpretation
Estimated-at-cost is conservative operational fallback. It means: "We know what you paid, but we do not currently know what market would pay."
The corresponding data-quality warning is an as-of valuation-date statement. It is not a historical union of every day where an asset was ever valued at cost.
๐ธ Income Allocation Across Lots
Dividends and interest linked to an asset are allocated pro-rata across the LONG lots that are eligible as of the day before the income date (D-1), and only across lots held at the paying broker.
Exact allocation rule:
Where:
- \(I\) = income amount received on date \(D\)
- \(\text{Convert}(I, ccy, D)\) = income converted into target currency on date \(D\)
- \(\text{EligibleQty}_i(D)\) = quantity of lot \(i\) open at the paying broker on \(D-1\) (quantity that left that broker in transit still counts as originating there)
- only LONG lots participate in the denominator
The D-1 rule keeps the day of record clean: a purchase made on the income date does not earn that distribution, and a lot sold the day before does not either. Larger eligible lots receive a larger share; lots held at other brokers, or not yet (or no longer) eligible, receive nothing.
Changed in FIFO v5
Earlier versions used the income date itself with all brokers (\(\text{OpenQty}_i(t)\) over every lot). The current engine uses D-1 eligibility scoped to the paying broker. If no lot is eligible, the income is kept as asset-level orphan income (never dropped, never assigned to the wrong lot).
Conservation rule
The allocated lot amounts add back exactly to the converted income event total. Income is distributed, not created.
The lot detail modal's Asset Income row is exactly \(\text{Income}_i\) from the formula above โ the pro-rata slice this specific lot received. When the lot has no live market price, the same modal also shows the Estimated-at-Cost badge from the previous section.
๐ธ Costs & Net Metrics
Asset-linked FEE and TAX are allocated to lots with a deterministic operation-matching ladder, then
subtracted to produce net figures alongside the gross ones.
๐งญ Deterministic cost allocation
A cost pool (same broker, same day, same type) is matched to the first non-empty target in this order:
| Cost | Matching order |
|---|---|
FEE |
same-day trades โ previous-day trades โ open holdings โ asset orphan |
TAX |
same-day income โ same-day trades โ previous-day income โ previous-day trades โ open holdings โ asset orphan |
Within a matched trade the cost crosses to the very lots that trade touched โ a BUY's cost lands on the lot it opened, a SELL's cost lands on the FIFO-consumed lots โ so cost attribution never contradicts the FIFO matching itself. Amounts are converted to the target currency and stored as positive magnitudes.
Conservation
Per pool, \(\sum_i \text{Cost}_i + \text{Orphan} = \text{Convert}(\text{pool}, ccy, D)\). A cost that finds no eligible lot (e.g. a fee booked after the position is fully closed) becomes asset-level orphan cost rather than being dropped or forced onto an unrelated lot.
โ๏ธ Gross vs net
With costs attributed per lot, LibreFolio reports both gross and net performance:
where \(\text{TotalPnL}_i\) already includes income (market P&L + realized P&L + asset income). The per-lot value-history series instead reports a capital-only net P&L, \(\text{pnl}_i - \text{Fees}_i - \text{Taxes}_i\), which excludes income โ each net line mirrors its own gross counterpart minus costs.
Annualized lot return uses the net return, not the gross one:
with \(d_i\) from opening date to closing date for closed lots, or to the analysis end date for open lots. Windows below 30 days return no annualized value; see Net Annualized Return.
Canonical numbers
BUY 10ร100, SELL 4ร120, current price 110, dividend 50, fees 8, taxes 5: Gross Total P&L \(= 60 + 80 + 50 = 190\); Net Total P&L \(= 190 - 13 = 177\); on opening value 1000 that is a 19% gross vs 17.7% net total return.
Costs with asset_id = null are not part of this lot-level view โ they are portfolio-level and handled
by the Portfolio Engine. See
Fee & Tax for the instrument-level theory.
๐ Worked Example
Example: two lots, one dividend, one market price
Assume same stock, same currency, qbq = 1.
| Date | Event | Lot A Open Qty | Lot B Open Qty | Notes |
|---|---|---|---|---|
| Jan 2 | BUY 100 @ $10 | 100 | 0 | Lot A opens with original cost $1,000 |
| Feb 10 | BUY 50 @ $14 | 100 | 50 | Lot B opens with original cost $700 |
| Mar 15 | DIVIDEND $30 | 100 | 50 | Both lots are still open |
| Apr 1 | Market price = $16 | 100 | 50 | Evaluate both lots |
Step 1 โ Allocate dividend pro-rata
Step 2 โ Open Return for each lot
Step 3 โ Market value and Total Return
Since no shares were sold yet, proceeds and realized P&L are both zero.
Step 4 โ Aggregate return across displayed lots
Even though both lots belong to same asset, their returns differ because they were opened at different prices.
๐ From Lots to Aggregate Metrics
Lot-level returns can be rolled up into an aggregate return series, but percentages must not be added directly.
LibreFolio uses this exact aggregate rule across displayed lots:
This lot-level view helps explain where return came from. Higher-level metrics such as ROI and TWRR answer broader portfolio questions:
- ROI focuses on gain relative to invested capital
- TWRR neutralizes external cash-flow timing
- FIFO lot analysis explains contribution and path inside a position
Price lookup is deliberately outside the FIFO engine itself. The engine produces lots and closures; LotsAnalysisService applies the unified resolver (Price Resolution) and estimated-at-cost fallback when deriving valuation metrics.
The Unified Lots Table lists exactly the per-lot rows \(i\) that the aggregate formulas above sum over โ opening date, total return, current value, custody, and status, all filterable to the same visible lot set used by the charts.
๐ Related
- ๐ Weighted Average Cost (WAC) โ blended cost basis view
- ๐ Buy & Sell โ brief FIFO matching overview
- ๐ธ Dividend & Interest โ source of asset-linked income events
- ๐ฐ Taxation โ capital gains and lot matching context
- โ๏ธ Lots Analysis Service โ developer implementation deep-dive