Skip to content

โš™๏ธ Portfolio Engine โ€” Mathematical Model

๐Ÿ’ก Overview

This page formally defines the mathematical model underlying LibreFolio's portfolio calculation engine. All other metric pages (NAV, Book Value, Period P&L, Yield on Cost, WAC, Deposited Capital) reference this page for their precise computation rules.


๐Ÿ“ 1. Notation and Sets

Symbol Meaning
\(V(u)\) All brokers visible to user \(u\)
\(S \subseteq V(u)\) Selected (filtered) broker scope
\(A\) Set of assets with positions
\(C^*\) Target currency
\([t_0, t_1]\) Requested evaluation frame
\(q(a,b,t)\) Quantity of asset \(a\) at broker \(b\) on date \(t\)
\(p(a,t)\) Valuation price of asset \(a\) on date \(t\)
\(\mathrm{fx}(c_1, c_2, t)\) Exchange rate from currency \(c_1\) to \(c_2\) on date \(t\)

๐Ÿ“ 2. Valuation Price

\[ \operatorname{mark}(a,t)= \begin{cases} \text{MARKET}(a,t) & \text{same-day asset-system quote}\\ \operatorname{avg}(\text{TRADE}(a,t)) & \text{same-day BUY/SELL/priced ADJUSTMENT observations}\\ \text{last observation before }t & \text{carried forward (LOCF)}\\ \varnothing & \text{no observation on or before }t \end{cases} \]
  • The unified resolver is the single valuation brain: MARKET โ†’ TRADE_AVG โ†’ CARRIED โ†’ MISSING.
  • CARRIED is last-observation-carried-forward. It carries days_back staleness metadata.
  • estimated=True means TRADE-origin. A stale carried MARKET quote is stale, not estimated.
  • Marks stay in native currency; each consumer converts to \(C^*\) at valuation date \(t\).
  • Cost-basis FX remains pinned to the transaction date.
  • Cash stays in the currency it was recorded in, broker by broker, and is valued at the rate of \(t\) (ยง5); each cash movement is still booked, and each external cash flow enters the capital, at the rate of its own date.
  • WAC is never used as valuation price.

See Price Resolution for the resolver contract and data-quality semantics.


๐Ÿ“ 3. Position State

For each position \((a, b)\) with \(q(a,b,t) > 0\):

\[ \mathrm{MV}(a,b,t) = \frac{q(a,b,t)}{qbq(a)}\cdot \operatorname{mark}(a,t)\cdot \mathrm{fx}\bigl(\mathrm{ccy}_{mark}, C^*, t\bigr) \]
\[ \mathrm{CB}(a,b,t) = q(a,b,t) \cdot w(a,b,t) \]
\[ \mathrm{UGL}(a,b,t) = \mathrm{MV}(a,b,t) - \mathrm{CB}(a,b,t) \]

Where \(w(a,b,t)\) is the Weighted Average Cost for position \((a,b)\) at date \(t\), kept in \(C^*\) at historical rates (ยง4): the cost basis takes no exchange rate at \(t\), so for a foreign asset the exchange-rate effect is part of \(\mathrm{UGL}\).


๐Ÿ“ 4. WAC Iterative Update

Maintained per-position \((a,b)\) with pool state \((\hat{q}, \hat{c})\), the cost \(\hat{c}\) kept in \(C^*\):

Acquisition (qty \(> 0\), amount \(P\) paid in currency \(c\) on date \(d\)):

\[ \hat{q}_{\text{new}} = \hat{q} + q_{\text{tx}}, \quad \hat{c}_{\text{new}} = \hat{c} + P \cdot \mathrm{fx}(c, C^*, d), \quad w = \frac{\hat{c}_{\text{new}}}{\hat{q}_{\text{new}}} \]

\(P\) is the cash paid for a BUY, or the per-unit cost basis override times \(q_{\text{tx}}\) for a TRANSFER or ADJUSTMENT; \(\mathrm{fx}(C^*, C^*, d) = 1\). When no rate exists on or before \(d\), or the acquisition has no cost basis, the quantity enters and the cost does not: the position's cost is flagged incomplete instead of counting zero.

Reduction (qty \(< 0\)):

\[ w_{\text{pre}} = \frac{\hat{c}}{\hat{q}}, \quad \hat{q}_{\text{new}} = \hat{q} - |q_{\text{tx}}|, \quad \hat{c}_{\text{new}} = \hat{q}_{\text{new}} \cdot w_{\text{pre}} \]

Split (linked to a split event): \(\hat{q}\) changes, \(\hat{c}\) does not.

Ordering

Within the same date: additions processed before reductions. Ensures SELL reads the correct WAC including same-day BUYs.


๐Ÿ“ 5. Portfolio Aggregation

\[ \mathrm{MV}(t) = \sum_{(a,b) \in S} \mathrm{MV}(a,b,t) \]
\[ \mathrm{NAV}(t) = \mathrm{MV}(t) + \mathrm{Cash}(t) + \mathrm{InTransit}(t) \]
\[ \mathrm{Book}(t) = \mathrm{OCB}(t) + \mathrm{Cash}(t) + \mathrm{InTransitBook}(t) \]
\[ \mathrm{UGL}(t) = \mathrm{NAV}(t) - \mathrm{Book}(t) \]

\(\mathrm{Cash}(t)\) is each broker's cash, kept in the currencies it was recorded in and valued at the rate of \(t\):

\[ \mathrm{Cash}(t) = \sum_{b \in S} \sum_{c} N_{b,c}(t) \cdot \mathrm{fx}(c, C^*, t) \]

where \(N_{b,c}(t)\) is the balance of broker \(b\) in currency \(c\) after its movements dated on or before \(t\), scaled by the broker's ownership share, and \(\mathrm{fx}(c, C^*, t)\) is the latest rate on or before \(t\) (\(1\) for \(c = C^*\)). The same term is in NAV and in Book, so the exchange-rate effect on cash cancels in UGL: it is booked as an exchange result (ยง6) and belongs to the Total P&L.


๐Ÿ“ 6. Three-Pool Cash Model โ€” Per-Broker \((K_b, R_b, W)\)

Three accumulator pools track cash provenance. \(K\) and \(R\) are maintained per-broker \(b\); \(W\) is global (exits the system entirely).

Pool Scope Meaning
\(K_b\) Per-broker External capital still in broker \(b\) as cash
\(R_b\) Per-broker Generated returns still in broker \(b\) as cash
\(W\) Global Returns that left the system (hidden, restorable on re-deposit)

Key property

A BUY on broker \(b_1\) can only consume \(R_{b_1}\), never \(R_{b_2}\). Cash does not teleport between brokers โ€” only explicit transfers move pool balances.

๐Ÿ” Update rules (per-transaction on broker \(b\), chronological)

Icon & Type Update Formulas Logic & Description

DEPOSIT
\(D > 0\)
\(r = \min(D,\, W)\)
\(R_b \mathrel{+}= r\)
\(W \mathrel{-}= r\)
\(K_b \mathrel{+}= D - r\)
Restores previously withdrawn returns from the global tracker \(W\) first, then adds the remainder to capital \(K_b\).

WITHDRAWAL
\(X > 0\)
\(k = \min(X,\, K_b)\)
\(K_b \mathrel{-}= k\)
\(\rho = \min(X - k,\, R_b)\)
\(R_b \mathrel{-}= \rho\)
\(W \mathrel{+}= \rho\)
Consumes capital \(K_b\) first, then moves remaining returns \(\rho\) to the global tracker \(W\).

DIVIDEND / INTEREST
\(I > 0\)
\(R_b \mathrel{+}= I\) Yields directly increase the returns pool \(R_b\).

FEE / TAX
\(F > 0\)
\(R_b \mathrel{-}= F\)
\(\text{if } R_b < 0\text{: } K_b \mathrel{+}= R_b,\; R_b = 0\)
Consumes returns \(R_b\) first; if \(R_b\) becomes negative, it drains from capital \(K_b\).

BUY
\(B > 0\)
\(\rho = \min(B,\, R_b)\)
\(R_b \mathrel{-}= \rho\)
\(K_b \mathrel{-}= (B - \rho)\)
Consumes returns \(R_b\) first, then drains the rest from capital \(K_b\).

SELL
\(G = P - C\)
\(K_b \mathrel{+}= C\)
\(R_b \mathrel{+}= G\)
\(\text{if } R_b < 0\text{: } K_b \mathrel{+}= R_b, \quad R_b = 0\)
Cost basis $C =

CASH TRANSFER
(Internal, \(s \to d\), \(X > 0\))
Departure Leg (\(s\)):
\(\rho = \min(X,\, R_s)\)
\(R_s \mathrel{-}= \rho\)
\(\kappa = X - \rho\)
\(K_s \mathrel{-}= \kappa\)

Arrival Leg (\(d\)):
\(K_d \mathrel{+}= \kappa\)
\(R_d \mathrel{+}= \rho\)
Internal cash transfers move pool allocations (\(R_s \to R_d\), \(K_s \to K_d\)) proportional to the departure balance.
The global tracker \(W\) is never touched (capital remains inside the system).

If departure and arrival dates differ, the transfer is in-transit: subtracted from \(s\) at departure day, added to \(d\) at arrival day. Between those dates, \(\sum K_b + \sum R_b < \mathrm{Cash}_{\text{like}}\) by the in-transit amount โ€” handled by proportional reconciliation.

๐Ÿ’ฑ Exchange Result on Foreign Cash

Each cash movement enters the broker's cash, and its pools, at the rate of its own date; the balance held afterwards follows the rate (ยง5). On every day \(t\) on which the rate of a foreign currency held as cash in \(S\) changes โ€” its first known rate included โ€” after that day's cash movements and before that day's transactions update the pools, each broker \(b\) holding foreign cash is revalued:

\[ \varepsilon_b(t) = \sum_{c} N_{b,c}(t) \cdot \mathrm{fx}(c, C^*, t) - \mathrm{Cash}^{\text{replay}}_b(t) \]
\[ \mathrm{Cash}^{\text{replay}}_b \mathrel{+}= \varepsilon_b, \qquad R_b \mathrel{+}= \varepsilon_b, \qquad \text{if } R_b < 0\text{: } K_b \mathrel{+}= R_b,\; R_b = 0 \]

\(\mathrm{Cash}^{\text{replay}}_b(t)\) is the cash the replay carries for \(b\): its value at the previous revaluation plus every later movement at the rate of its own date. A gain goes to the returns; a loss consumes them first and then the capital, exactly like a FEE. The capital baseline does not move: an exchange result is a result, not a contribution, so it is in NAV, Total P&L, ROI, TWRR and MWRR, never in the deposited capital.

๐Ÿงฎ Aggregation for output

\[ \mathrm{CashFromCapital}(t) = \sum_{b \in S} K_b(t) \]
\[ \mathrm{CashFromReturns}(t) = \sum_{b \in S} R_b(t) \]

โš–๏ธ Reconciliation invariant

\[ \mathrm{Cash}_{\text{like}}(t) \approx \sum_{b \in S} K_b(t) + \sum_{b \in S} R_b(t) \]

Both pools are first clamped at zero per broker. If the pools are not empty and the drift still exceeds \(0.01\), the summed pools are scaled to \(\mathrm{Cash}_{\text{like}}\), the cash plus the cash in transit โ€” an aggregate scaling, not a per-broker one, each result floored at zero:

\[ \mathrm{CashFromCapital}(t) = \sum_{b \in S} K_b(t) \cdot \frac{\mathrm{Cash}_{\text{like}}(t)}{\sum_{b \in S} K_b(t) + \sum_{b \in S} R_b(t)}, \qquad \mathrm{CashFromReturns}(t) = \mathrm{Cash}_{\text{like}}(t) - \mathrm{CashFromCapital}(t) \]

Otherwise \(\mathrm{CashFromCapital}(t) = \min\bigl(\sum_b K_b(t),\, \mathrm{Cash}_{\text{like}}(t)\bigr)\) and \(\mathrm{CashFromReturns}\) is the rest, floored at zero. The exchange results on cash at rest are booked into the pools (above), so the drift left comes from in-transit timing: cash in transit โ€” and the move of its exchange rate while it travels โ€” is in no broker's pools.


๐Ÿ“ 7. Period Contribution

For period \([t_0, t_1]\), per-position \((a,b)\):

\[ \Delta\mathrm{UGL}(a,b) = \mathrm{UGL}(a,b,t_1) - \mathrm{UGL}(a,b,t_0) \]
\[ \mathrm{PnL}(a,b) = \Delta\mathrm{UGL}(a,b) + \mathrm{Realized}(a,b) + \mathrm{Income}(a,b) - \mathrm{FeesTaxes}(a,b) \]

Contribution position set:

\[ \mathcal{P} = \text{positions with BUY/SELL/ADJUSTMENT/TRANSFER activity or boundary quantity} \]

Portfolio-level period P&L also exposes a residual:

\[ \mathrm{Other} = \mathrm{PnL}_{period} - \Delta\mathrm{UGL} - \mathrm{Realized} - \mathrm{Income} + \mathrm{FeesTaxes} \]

Since UGL cannot contain the exchange result on cash (ยง5), Other splits into each broker's exchange result over the period and what remains:

\[ \mathrm{Other} = \sum_{b \in S} \mathrm{FXonCash}_b + \mathrm{Residual} \]

Unallocated fees/income without asset_id are grouped per broker as other period effects, and so is \(\mathrm{FXonCash}_b\): one row per broker holding foreign cash, shown when its result is not zero. See Period P&L โ€” FX on Cash.


๐Ÿ“ 8. Realized Gain/Loss

On SELL of \(|q_s|\) units from position \((a,b)\) on date \(t\):

\[ C = |q_s| \cdot w_{\text{pre}}(a,b) \]
\[ \mathrm{Realized} = P_{\text{sell}} \cdot \mathrm{fx}(\mathrm{ccy}_{\text{sell}}, C^*, t) - C \]

Where \(w_{\text{pre}}\) is the WAC in \(C^*\) before the pool reduction (same value used by 3-pool SELL rule above): the units sold leave at their historical cost, with no conversion at the sale date. A sale whose proceeds cannot be converted, or drawn from a position whose cost is incomplete, is left out of Realized.


๐Ÿ“ 9. Pre-Frame / Frame Architecture

Phase Date range Computes
Pre-frame \([t_{\mathrm{first}},\ t_0)\) Cash, qty, WAC, pools โ€” no market evaluation
Frame \([t_0,\ t_1]\) Full daily: prices, FX, position states, portfolio states

Pre-frame transactions update accumulators (cash ledger, WAC pools, 3-pool K/R/W) without any market evaluation: no price is read and no position is valued. Each movement is converted at the rate of its own date, as in the frame, and on every day the rate of a held foreign currency changes, the cash is revalued at that day's rate and the exchange result enters the pools (ยง6), so the pools are correct at \(t_0\). This enables efficient range-based caching.


๐Ÿ“ 10. Performance Metrics (Layer 2)

Computed after daily states, as a separate pass:

Metric Formula Reference
Total PnL \(\mathrm{NAV}(t) - \text{CapitalBaseline}(t)\) Deposited Capital
Period PnL \(\mathrm{NAV}(t_1) - \mathrm{NAV}(t_0) - \text{ECF}_{[t_0,t_1]}\) Period P&L
TWRR \(\prod_i (1 + r_i) - 1\) (sub-period chain) TWRR
MWRR XIRR solving \(\sum \frac{CF_i}{(1+r)^{d_i/365}} = 0\) MWRR
Simple ROI \((\mathrm{NAV} - \text{NetInvested}) / \text{NetInvested}\) ROI
Net annualized return \((1+r_{\mathrm{net}})^{365/d}-1\), suppressed below 30 days Net Annualized Return
Yield on Cost Trailing-365-day gross transaction income per eligible historical unit, divided by residual unit WAC at \(t_1\) Yield on Cost
Timing Effect \(\text{MWRR}_{\text{cum}} - \text{TWRR}_{\text{cum}}\) Timing Effect