Zivoe · Protocol data

How yield30dComp365 is calculated, and every other yield the indexer offers

Traced from source in centrifuge/api-v3 (the Ponder indexer our app queries), centrifuge/sdk, and the legacy centrifuge/pools-subql.

Trailing 30 days Compounded · 365-day basis NAV per share Ray · 1e27 23 yield fields per snapshot Null before day 30

One number, derived from one series

yield30dComp365 is the trailing 30-day share-price return, compounded and annualized on a 365-day basis. The indexer takes the share class’s NAV-per-share 30 days ago (Pstart) and now (Pend) and computes:

yield30dComp365 = ( Pend / Pstart )365 / 30 − 1 Read as: “if the last 30 days repeated all year, with returns compounding, this is the annual rate.”

Key facts

  • Price source: NAV per share (navPoolPerShare) reported on-chain by the pool’s ShareClassManager — not a market price. Yield reflects NAV appreciation only.
  • It is backward-looking, purely price-derived, and recomputed on every daily snapshot and every on-chain price update.
  • Units: stored as Ray (27-decimal fixed point). Divide by 1e25 to get percent — the SDK does this for you (e.g. 5.5 = 5.5%).
  • Alternatives: the same snapshot carries 23 yield fields — simple total returns and annualized (360/365-basis) variants over 1/7/15/30/90/180-day windows, plus TTM, YTD and since-inception. Centrifuge’s own invest app lets each pool choose which one to display as “APY”, or pin a hardcoded target via apyPercentage.
  • Null until mature: a share class must have a price at least 30 days old before yield30dComp365 is non-null. Young pools show nothing — which is why the “fixed” APY mode exists.
Trailing 30d Compounded Ray-scaled null before day 30

Where the numbers come from

The indexer never computes yield from loan-level interest. Everything derives from one series: the share class token price over time.

On-chain ShareClassManager emits UpdateShareClass / UpdatePricePoolPerShare with NAV per share (18-decimal, pool currency).
Indexer Stores it as tokenPrice; writes a TokenSnapshot on every price update and every 24h UTC period roll.
Yield calc At each snapshot, loads the price history and computes all 23 yield columns from (Pstart, Pend) pairs.
Consumers tokenSnapshots GraphQL query → SDK PoolShareYieldsReport → our app reads yield30dComp365.
How the window start is picked For each rolling window the start price is chosen conservatively: the last strictly positive price at or before (now − N days). Prices of zero or null are skipped. Pend is the live token price at snapshot time.

The exact yield30dComp365 calculation

From compoundAnnualizedRayFromPrices() in src/helpers/tokenYields.ts, with spec { periodDays: 30, compounded: true, dayCount: 365 }.

yield = exp( 365d · ln PendPstart ) − 1 ,  d = 30 Mathematically identical to (Pend/Pstart)365/30 − 1. Implemented with ln/exp Taylor series on 54-decimal fixed-point BigInts — no floating point anywhere, so results are deterministic and exact to well beyond the stored 27 decimals.
Worked example

NAV per share moved from 1.0000 to 1.0040 over the last 30 days (+0.40%):

Field Formula Result
yield30d 0.0040 / 1.0000 0.400%
yield30d360 0.0040 × 360/30 4.800%
yield30d365 0.0040 × 365/30 4.867%
yield30dComp360 1.0040360/30 − 1 4.907%
yield30dComp365 1.0040365/30 − 1 4.977%

Compounding makes the annualized figure slightly higher than the simple version (interest-on-interest), and the gap widens as the 30-day return grows. The compounded 365 variant is the most “APY-like” of the five — it matches the standard APY definition of a compounded annual rate.

Every yield field the indexer offers

The columns are generated from a config (src/config/tokenYield.ts): six rolling windows × three flavors, two compounded 30-day variants, and three fixed anchors — 23 fields per snapshot, all Ray-scaled.

Window Simple total return Annualized · 360-basis Annualized · 365-basis
1 day yield1d yield1d360 yield1d365
7 days yield7d yield7d360 yield7d365
15 days yield15d yield15d360 yield15d365
30 days yield30d yield30d360 yield30d365
90 days yield90d yield90d360 yield90d365
180 days yield180d yield180d360 yield180d365
30 days · compounded yield30dComp360 yield30dComp365
Family Formula Notes
Simple
yieldNd
(PendPstart) / Pstart Raw total return over the window. Not annualized.
Simple annualized
yieldNd360 / 365
(PendPstart) / Pstart × basis/N Linear extrapolation — an APR, no compounding. 360-basis mirrors money-market convention; 365 mirrors ACT/365.
Compound annualized
yield30dComp360 / 365
(Pend / Pstart)basis/30 − 1 Geometric extrapolation — a true APY. Only offered for the 30-day window.
Fixed anchors
yieldTtm · yieldYtd · yieldSinceInception
(PendPstart) / Pstart Simple total return from: 365 days ago · Jan 1 UTC · first positive price. None are annualized (YTD in March ≠ an annual rate).

What Centrifuge lets a pool display as “APY”

Centrifuge’s SDK defines which of these a pool may display as its “APY” (ApyMode in sdk/src/types/poolMetadata.ts). This is the full menu Centrifuge itself supports, and therefore the natural option space for our app too.

Mode Backed by Character
7d365 · 7d360 yield7d365/360 Responsive, noisy — one odd NAV mark swings it hard.
30d365 · 30d360 yield30d365/360 The common default; legacy modes (target, 7day, 30day, 90day, automatic) all fall back to 30d365.
30dComp365 · 30dComp360 yield30dComp365/360 What we use. Same window as 30d but compounded — a true APY, reads slightly higher.
90d365/360 · 180d365/360 yield90d… / yield180d… Smoother, slower to reflect rate changes.
ttm · ytd · sinceInception yieldTtm / yieldYtd / yieldSinceInception Total-return figures, not annual rates — display them as “return”, not “APY”.
fixed apyPercentage in pool metadata A hardcoded, manager-declared number. Used for young pools with no 30-day history. Not measured.
none Hide APY entirely.

What to keep in mind before putting a number on the page

NAV-based, not market-based

The yield tracks the manager-reported NAV per share (accrued interest and revaluations net of fees baked into price). Secondary-market price or redemption friction is invisible to it. Any yield paid out as distributions rather than accrued into NAV would not appear.

Start-price lookup is “at or before”

If snapshots are sparse, Pstart can actually be older than 30 days while the exponent stays 365/30 — the figure can slightly overstate annualized yield across gaps in price history.

Stale NAV freezes the yield

If the manager stops posting NAV updates, daily snapshots keep re-using the last price; the 30-day window slowly flattens to zero rather than showing “unknown”.

Nulls are expected, not errors

Before a window has history (e.g. yield180d… for a three-month-old share class), the field is null. Handle it in the UI.

360 vs 365 basis is purely conventional

360-basis reads ~1.4% lower relatively (e.g. 4.907% vs 4.977% in the worked example). Pick one and stay consistent across the product.

The V2 indexer, for comparison

pools-subql (Centrifuge chain / V2 pools) The older indexer exposed yield7DaysAnnualized, yield30DaysAnnualized and yield90DaysAnnualized, plus MTD/QTD/YTD/since-inception on tranche snapshots. Its annualized figures were simple (linear) annualization with a millisecond-precision day count — there was no compounded variant at all. yield30dComp365 is new to the V3 (api-v3) indexer.