๐ธ Yield on Cost (YOC)
Yield on Cost (YOC) measures the non-negative gross recorded income produced per current unit over the trailing 365 calendar dates, relative to the open position's average purchase price per unit (Weighted Average Cost, or WAC).
LibreFolio computes it separately for every \((a,b)\) pair:
The same asset held at two brokers therefore has two independent YOC values.
Gross recorded income โ not a tax-net return
YOC uses only non-negative cash amounts from asset-linked DIVIDEND and INTEREST transactions. Exact zero is accepted; negative income amounts are not supported. Separate TAX and FEE transactions are not subtracted, so the result must not be read as after-tax or net income performance.
๐งญ Scope and trailing window
Let \(T\) be the selected report end date. YOC always uses the inclusive window:
This window contains 365 calendar dates and is independent of the report start date. Moving the start of a dashboard report does not change YOC when \(T\) remains unchanged.
Only non-negative, asset-linked entries from the personal transaction ledger qualify:
| Included | Excluded |
|---|---|
Asset-linked Transaction rows of type DIVIDEND or INTEREST, with cash amount equal to or greater than zero |
Separate TAX, FEE, and ADJUSTMENT transactions |
| The non-negative cash amount, currency, date, asset, and paying broker | Provider or manual AssetEvent income |
| Income assigned to the exact \((a,b)\) pair | Income without an asset |
The transaction schema accepts an exact-zero DIVIDEND or INTEREST cash amount but rejects a negative one. An ADJUSTMENT can affect replayed quantity, WAC, or linked-split inputs, but it never enters the YOC income numerator.
The calculation has no asset-class allowlist. It applies to every open long holding type represented by the portfolio engine, including crypto and manual assets, provided the required ledger, WAC, split, and FX inputs are valid.
๐งฎ Mathematical definition
For each qualifying income transaction \(j\):
| Symbol | Meaning |
|---|---|
| \(D_j\) | Transaction date |
| \(I_j\) | Non-negative gross transaction cash amount in currency \(c_j\), with \(I_j\geq0\) |
| \(C^*\) | Selected report currency |
| \(q_j\) | Eligible long quantity at the paying broker at end of day \(D_j-1\) |
| \(s_j\) | Product of linked split ratios dated from \(D_j\) through \(T\), inclusive |
| \(w_{a,b,T}\) | Average purchase price per unit (WAC) of pair \((a,b)\) at \(T\), kept in \(C^*\) at historical rates |
๐ฑ Income conversion
Each income amount is converted to the report currency at its own transaction date:
๐งฌ Current-unit normalization
Income per unit is first allocated over the eligible prior-day quantity, then normalized for every valid linked split on or after the income date:
For a 2-for-1 split, \(s_j=2\): historical income per old unit is divided by two so it is comparable with current units. A linked split dated on \(D_j\) is included as well.
๐ Average-purchase-price denominator
The denominator is the position's average purchase price per unit, already expressed in the report currency: when the average was built, each acquisition entered it at the rate of its own date (see the multi-currency section of Weighted Average Cost):
where \(Q_{a,b,T}\) is the quantity of the pair's average-cost pool at \(T\) and \(C^{*}_{a,b,T}\) its historical cost in \(C^*\): each acquisition \(i\) added \(P_i \cdot \mathrm{fx}(\mathrm{ccy}(P_i), C^*, d_i)\) for the amount \(P_i\) paid on date \(d_i\), and each reduction removed its proportional share.
No conversion is applied at the report end date \(T\): the denominator is what was paid, and it does not move with today's exchange rate.
LibreFolio then reports:
The API value is a fraction, and the Holdings table renders it as a percentage. An available value may be positive or exactly zero.
๐ Quantity, custody, transfer, and split rules
The eligible quantity \(q_j\) is deliberately evaluated at end of day before payment:
- only
LONGquantity counts; - a same-day BUY is excluded;
- a same-day SELL is included, because those units existed at end of day \(D_j-1\);
- broker custody is respected throughout transfer replay;
- an in-transit fragment still counts for its source broker, never for the destination before arrival.
Income remains attached to the broker that recorded it. Moving units from broker A to broker B does not automatically carry A's historical income into B's YOC. Later income recorded at B uses B's eligible prior-day quantity and B's average purchase price.
Subsequent and same-day linked splits rescale earlier per-unit income into the units that exist at \(T\). LibreFolio uses explicit linked split rows; it does not infer an asset-wide restatement from a split recorded only at another broker. If a connected cross-broker replay contains a split but lacks a matching split row for the income/current broker, the normalization is ambiguous and YOC fails closed. An invalid, duplicated, mismatched, or non-positive linked split likewise makes the affected pair unavailable instead of producing an approximate result.
ADJUSTMENT transactions remain replay inputs only: they can change quantity or WAC and can carry a linked split, but their amounts never count as income.
Same-day trades and a later split
Assume a โฌ20 dividend is recorded on June 30. The paying broker held 100 eligible long units at end of day June 29. A buy or sell on June 30 does not change that denominator.
A linked 2-for-1 split occurs between June 30 and \(T\), so:
If the average purchase price (WAC) at \(T\) is โฌ4.00 per current unit:
๐ FX resolution and provenance
YOC uses the portfolio's current historical FX policy:
- income requests FX for \(D_j\);
- the average purchase price (WAC) needs no FX at \(T\): each of its acquisitions was converted at its own date when the average was built;
- when the exact date is absent, the latest stored rate on or before that date is used;
- no forward rate is substituted.
Provenance preserves both the requested date and the actual rate date of each income conversion, plus the currency pair and days carried backward. The Holdings tooltip can therefore show, for example, that a June 30 income conversion used the most recent June 28 rate.
If any required income conversion cannot be resolved, the whole pair is unavailable. If an acquisition's cost cannot be converted โ or a transfer or adjustment has no cost basis โ the average purchase price itself is unavailable, and so is YOC. LibreFolio does not silently omit the affected transaction or reuse an unrelated value.
๐ฆ Availability and fail-closed behavior
YOC distinguishes a valid absence of income from a calculation that cannot be trusted.
| State | Meaning | Holdings cell |
|---|---|---|
| Available | At least one qualifying income transaction exists and every required input is valid. The result is positive when any qualifying amount is positive; it is exactly zero when one or more qualifying rows are recorded at amount zero and none has a positive amount. Provenance marks that exact-zero case with net_zero=true. No minimum pair age applies. |
Percentage with two decimals: for example, 2.50% or 0.00%. |
| No income | No qualifying income row exists in the window, and the pair has a full 365-date ledger history. The API value is zero, but this state is distinct from an available zero. | - with explanatory tooltip and no warning icon. |
| Unavailable | The no-income pair is too young, or a required ledger/calculation input failed validation. Its value is null and its reason identifies the failure. |
- with an info icon and custom reason tooltip. |
The outer yield_on_cost field is required and non-null for every holding. An unavailable result is therefore represented by the result object's unavailable status and null value, not by a missing or null outer field.
Let \(F_{a,b}\) be the first-ever asset-linked transaction date for the pair. A no-income result becomes valid only when:
Equivalently, \(F_{a,b} \leq T-364\). Closing and later reopening the position does not reset this age: the original first pair transaction remains the anchor.
This one-year gate is evaluated only when the trailing window contains no qualifying income transactions. It is not an annualization rule: a younger pair with valid recorded income can have an available YOC because the metric simply sums the income observed inside \([T-364,T]\).
The calculation fails closed for any of these conditions:
- income has no eligible long quantity at end of day \(D_j-1\);
- transaction or transfer replay is inconsistent;
- linked split data is invalid or inconsistent;
- required historical FX is missing;
- average purchase price (WAC) is missing or non-positive โ missing includes an acquisition whose cost could not be converted or has no cost basis.
One bad required input makes the entire pair unavailable. There is no partial sum, asset-event substitute, provider-income fallback, or current-quantity approximation.
๐ฅ๏ธ Reading YOC in LibreFolio
YOC appears in the shared Holdings table used by both the Dashboard Positions tab and each broker's Positions tab:
- visible by default immediately beside Annualized;
- available values, including exact zero, are fixed to two decimal places;
- positive values have no leading
+; - an available zero appears as
0.00%and hasnet_zero=truein its provenance; - a normal no-income dash has no warning icon;
- an unavailable dash has an info icon whose tooltip explains the specific reason and available provenance;
- show/hide choices are shared between Dashboard and broker views and persist across sessions.
Each row remains broker-specific. In the Dashboard's multi-broker scope, two rows for the same asset can therefore show different values. See Positions & Analysis for the user-facing table guide.
โ๏ธ What YOC is not
| Metric | Denominator and horizon | Why it differs from LibreFolio YOC |
|---|---|---|
| Market dividend yield | Annual dividend per share divided by current market price | Market/provider measure; YOC uses only recorded personal income and average purchase price (WAC). |
| Cumulative cash yield | Lifetime total income divided by current total cost basis | YOC uses only \([T-364,T]\), reconstructs income per eligible historical unit, and does not divide lifetime cash by today's aggregate basis. |
| Net annualized return / CAGR | Compounded net total return over the holding window | Includes market performance, income, fees, and taxes; YOC isolates gross recorded income and is not annualized from a cumulative return. |
| Bond current yield | Annual coupon divided by current bond price | Security-level coupon/price measure; YOC uses the investor's recorded broker ledger and WAC. |
| Yield to maturity (YTM) | Discount rate equating a bond's future coupons and redemption value to price | Forward-looking bond IRR; YOC is trailing, transaction-based, and applies to every holding type. |
๐ Related
- ๐ Weighted Average Cost โ average-purchase-price denominator
- ๐ฅ๏ธ Dashboard Positions โ column states, formatting, and shared preference
- ๐ฐ Dividend & Interest Transactions โ qualifying personal ledger entries
- โ๏ธ Portfolio Engine โ position state and metric layer