Metrics & formulas

Definitions and formulas for every metric the app renders, verified against the engine source and referenced to the standard they implement.

These definitions back the KPI strips on Performance, the columns in Attribution, the metrics in each position drawer, and the strategy desk. Where Syncrone departs from a textbook definition, the departure is stated next to the formula.

The metrics engine#

The metrics engine is a deterministic computation layer. It takes snapshot timelines as input and produces metrics as output, with no I/O and no clock.

ArithmeticEngine metrics use decimal.js. XIRR and benchmark risk statistics are solved in 64-bit floating point (see Precision).
DeterminismThe same input produces the same output, independent of when or where it runs.
Dependency graphMetrics declare their dependencies. The engine orders them into levels and runs each level in turn.
Null over zeroA metric that cannot be measured is published as empty (null), never as 0. A 0 always means a measured zero.

Returns#

Time-weighted return (TWR)

GIPS defines TWR as a method that "reflects the change in value and negates the effects of external cash flows." Syncrone revalues the position at every external cash flow (each deposit or withdrawal produces an interaction snapshot), so each interval's return is exact rather than approximated.

twr_md
rt=NAVt−FtNAVt−1−1TWRt=∏i≤t(1+ri)−1r_t = \frac{\text{NAV}_t - F_t}{\text{NAV}_{t-1}} - 1 \qquad \text{TWR}_t = \prod_{i \le t} (1 + r_i) - 1

F_t is the external flow booked at snapshot t (positive = deposit). The snapshot is post-flow, so subtracting F_t isolates the performance of the interval. Stored as a percentage. An opening NAV of zero or below one cent carries the index forward; a negative opening NAV (debt above collateral) or a missing NAV ends the chain, and every metric built on it becomes empty until the position is rebuilt.

Example: NAV $100 → $102 (no flow) → $153 after a $50 deposit: r₁ = 2%, r₂ = (153 − 50)/102 − 1 = 0.98%, TWR = 1.02 × 1.0098 − 1 = 3.0%

Modified Dietz ROI (money-weighted)

The since-inception return on the capital actually at work. Each flow is weighted by the share of the window it was invested, as in the GIPS Handbook's Modified Dietz formula. Computed over the whole window rather than linked sub-periods, it is a money-weighted return: it rewards having more capital deployed when performance was good.

roi_md
ROIMD=NAVt−NAV0−∑FiNAV0+∑wiFi,wi=t−tit−t0\text{ROI}_{MD} = \frac{\text{NAV}_t - \text{NAV}_0 - \sum F_i}{\text{NAV}_0 + \sum w_i F_i}, \qquad w_i = \frac{t - t_i}{t - t_0}

Weights use exact timestamps, not whole days. Stored as a percentage. The opening flow is already in NAV₀ and is not counted again. No segment reset: a deposit does not restart the return. Empty when the denominator is ≤ 0. After a full exit the value is frozen at the exit, so it does not drift as time passes.

APR (MD) and annualised ROI (MD)

apr_md (simple)
APRMD=ROIMD×365D\text{APR}_{MD} = \text{ROI}_{MD} \times \frac{365}{D}

D is exact fractional days since the Modified Dietz anchor (inception). Computed on any window length. Below one year it is an indicative run-rate, not annualised performance in the GIPS sense. Capped at ±1,000,000%.

annualized_roi_md (compounded, GIPS-gated)
(1+ROIMD)365/D−1\left(1 + \text{ROI}_{MD}\right)^{365/D} - 1

Empty until one full calendar year has passed since the anchor, per GIPS 2.A.12. Empty when ROI ≤ −100%.

CAGR

cagr
CAGR=(1+TWRMD)365/D−1\text{CAGR} = \left(1 + \text{TWR}_{MD}\right)^{365/D} - 1

Compound annual growth of the flow-adjusted TWR index, with D in exact fractional days from the first priced interval. Unlike annualised ROI (MD), CAGR is not gated at one year, so on a short window it is an extrapolation. Stored as a decimal ratio (0.1187 = 11.87%). Capped at ±1,000,000 (as a ratio); a total loss is −1.

XIRR

The money-weighted internal rate of return on dated cash flows, using the same convention as Excel's XIRR.

XIRR
∑iCFi(1+r)(di−d0)/365=0\sum_i \frac{CF_i}{(1 + r)^{(d_i - d_0)/365}} = 0

Deposits are negative, withdrawals positive, and the closing NAV is a final positive flow. For a bounded period the opening NAV is a negative first flow; for ITD the first real deposit anchors the timeline. Solved by Newton–Raphson with a bisection fallback. Empty when there are fewer than two flows, no sign change, no convergence, or |r| > 10,000%. Portfolio XIRR concatenates position flow books; holdings are not included.

Classic (segment) ROI, APR, APY

The original return family, still the default on reports and the API. Each deposit or withdrawal starts a new segment; ROI blends segments with an exponentially weighted average, and APR annualises from the segment start. Because a deposit restarts the clock, a fresh segment can annualise to extreme values. Prefer the MD family for comparison.

roi / apr / apy
roit=λ roit−1+(1−λ) stapr=roi×365Dapy=(1+apr365)365−1\text{roi}_t = \lambda \, \text{roi}_{t-1} + (1-\lambda)\, s_t \qquad \text{apr} = \text{roi} \times \frac{365}{D} \qquad \text{apy} = \left(1 + \frac{\text{apr}}{365}\right)^{365} - 1

s_t is the current segment's ROI. λ = previous segment weight / (previous + current segment weight). D is the days since the segment started. Carried unchanged on interaction snapshots. APR capped at ±1,000,000%; APY capped at 1,000,000%. APY uses daily compounding of the APR, the Regulation DD formula with Interest/Principal = APR/365 over each day.

IRR-APR (irr_apr) is the legacy TWR times 365 / days since the position started. Despite the name it is not an IRR: it is a simple annualisation of a time-weighted return. It no longer feeds any risk ratio.

Risk#

Risk methodology settings

Each organisation sets two inputs in Entity Setup. Saving either rebuilds Sharpe and Sortino across all history for every position and strategy. Reports keep the rate they were generated with, so a report you sent stays reproducible; re-run it to pick up a new rate.

SettingMeaning (default)
Risk-free rateAnnual percent used by Sharpe (default 3%). Converted to a daily rate as (pct / 100) / 365.
Sortino MARAnnual minimum acceptable return used by Sortino (default 0%). Converted the same way.

The simple /365 conversion is deliberate: the ratios use the arithmetic mean of daily returns × 365, so a simple daily rate is its exact inverse. The DeFi benchmark assigned to a position never enters Sharpe or Sortino.

The daily return series

Sharpe, Sortino and drawdown never read NAV directly. They read the flow-adjusted TWR index, so deposits and withdrawals cannot show up as gains or losses.

Daily observation
Wt=1+TWRMD,trt=WtWt−1−1W_t = 1 + \text{TWR}_{MD,t} \qquad r_t = \frac{W_t}{W_{t-1}} - 1

Only daily snapshots are observations; interaction snapshots advance the index but do not add a sample, so an active position does not get more (and noisier) observations than a quiet one. 365 calendar days per year, because crypto markets do not close.

Sharpe ratio

sharpe_ratio
Sharpe=r−rf‾s(r)×365rf=ρ365\text{Sharpe} = \dfrac{\overline{r - r_f}}{s(r)} \times \sqrt{365} \qquad r_f = \dfrac{\rho}{365}

ρ is the annual risk-free rate as a decimal (the 3% default is 0.03). s is the sample standard deviation (n − 1), maintained with Welford's online algorithm. Requires at least 2 daily observations. Empty when volatility is zero or the return chain is broken. Since inception. The √365 scaling assumes daily returns are serially uncorrelated (Sharpe, 1994).

Sortino ratio

sortino_ratio
Sortino=r−m‾σd×365σd=1n∑i=1nmin⁡(ri−m, 0)2\text{Sortino} = \dfrac{\overline{r - m}}{\sigma_d} \times \sqrt{365} \qquad \sigma_d = \sqrt{\dfrac{1}{n}\sum_{i=1}^{n}\min(r_i - m,\, 0)^2}

m is the daily minimum acceptable return: annual percent / (100 × 365), so the 0% default leaves m at 0. σ_d is the target downside deviation: n counts every daily observation, including days at or above the MAR (Rollinger & Hoffman). Requires at least 2 observations; empty when there is no shortfall below the MAR.

Maximum drawdown

max_drawdown
DDt=Wtmax⁡s≤tWs−1,MaxDD=min⁡tDDt\text{DD}_t = \frac{W_t}{\max_{s \le t} W_s} - 1, \qquad \text{MaxDD} = \min_t \text{DD}_t

Measured on the flow-adjusted index W, not on NAV: a withdrawal of half the capital is not a 50% drawdown, and a deposit does not reset the peak. Observed on daily snapshots only. Stored as a negative ratio (−0.20 = −20%).

Example: Index 1.00 → 1.25 → 1.00: max drawdown = 1.00 / 1.25 − 1 = −20%, whatever was deposited or withdrawn in between.

A secondary NAV-based family (max_drawdown_usd) answers "how far below its peak value is this money". It is the only dollar-denominated drawdown, it does move with flows, and it is never used as the headline risk figure.

Calmar ratio

calmar_ratio
Calmar=CAGR∣MaxDD∣\text{Calmar} = \frac{\text{CAGR}}{\lvert \text{MaxDD} \rvert}

Both terms come from the same flow-adjusted index, since inception. Nothing is subtracted (no risk-free rate, no MAR). Empty when max drawdown is exactly 0; no threshold is applied, so a tiny real drawdown can produce a very large ratio, capped at ±1,000,000. Young's original 1991 Calmar uses a trailing 36-month window; a since-inception ratio is strictly the MAR ratio (Bacon), which suits short-lived DeFi positions better.

Volatility

Annualised volatility
σann=s(r)×365\sigma_{ann} = s(r) \times \sqrt{365}

Sample standard deviation of the daily returns above. Shown on strategies (at least 20 observations) and benchmarks (at least 2 consecutive-day returns).

P&L#

P&L is computed per asset leg, then rolled up to positions, holdings and the fund. Cost basis uses the average-cost (pro-rata) method: a withdrawal realises the exited fraction of unrealised P&L and writes down invested capital by the same fraction.

Total P&L
P&L=Realised+Unrealised\text{P\&L} = \text{Realised} + \text{Unrealised}

For a period, the change in inception-to-date P&L between the period start and end. At fund level: Σ positions + Σ holdings.

Unrealised P&L
Unrealised=NAV−C\text{Unrealised} = \text{NAV} - C

C is invested capital: the proportionally written-down cost basis, not the gross amount ever deposited.

Realised P&L
Realised+=Uprior×outflowNAVprior\text{Realised} \mathrel{+}= U_{\text{prior}} \times \dfrac{\text{outflow}}{\text{NAV}_{\text{prior}}}

Booked on withdrawal, for the exited fraction. Unchanged when there is no outflow or the prior NAV was zero. This is average cost, not per-lot.

Reward P&L
Reward=∑child reward-asset P&L\text{Reward} = \sum \text{child reward-asset P\&L}

Claimed and still-accruing incentive legs, including Merkl. Already included in total P&L: never add it on top.

Debt legs carry negative value, so a borrow position's P&L rolls up as collateral minus debt.

P&L decomposition#

Total P&L is split along two independent axes. Each isolates one component and treats the remainder as a residual, so both reconcile exactly to total_pnl. For what the components mean and how to read them, see PnL decomposition.

Quantity axis
strategy=∑(Δunits×Pend)market=total−strategy\text{strategy} = \sum (\Delta \text{units} \times P_{\text{end}}) \qquad \text{market} = \text{total} - \text{strategy}

Strategy P&L marks the change in token units at the period-end price. Market P&L is the residual, so the two sum to total P&L.

Income axis
yield=∑child balance P&Lprice effect=total−yield\text{yield} = \sum \text{child balance P\&L} \qquad \text{price effect} = \text{total} - \text{yield}

Yield is the child DEFAULT-leg balance P&L, in USD at each day's price. Price effect is the residual.

Temporal variants

All four metrics exist in two variants. Stored figures always carry a prefix.

VariantWindow
itd_*_pnl_usdInception to date: cumulative, all-time.
range_*_pnl_usdThe selected period: the value at the period end minus the value at the period start.

Strategy P&L

Fieldsitd_strategy_pnl_usdrange_strategy_pnl_usd
Asset level
B=Brealised+BunrealisedITD=Bend×PendRange=(Bend−Bstart)×PendB = B_{\text{realised}} + B_{\text{unrealised}} \qquad \text{ITD} = B_{\text{end}} \times P_{\text{end}} \qquad \text{Range} = (B_{\text{end}} - B_{\text{start}}) \times P_{\text{end}}

B is balance P&L in token units. ITD uses the closing balance; Range uses the change over the window. Both are stored as itd_strategy_pnl_usd and range_strategy_pnl_usd.

Every leg is priced on the last day of the period; there is no opening price. Both variants are 0 when that price is missing or ≤ 0. Holdings are always 0.

Yield P&L

Fieldsitd_yield_pnl_usdrange_yield_pnl_usd
Asset level
yield=Crealised+Cunrealised\text{yield} = C_{\text{realised}} + C_{\text{unrealised}}

C is the child DEFAULT-leg balance P&L in USD. Stored as itd_yield_pnl_usd and range_yield_pnl_usd.

The same balance change as strategy P&L, but valued at each day's price rather than the period-end price. Holdings are always 0.

Market P&L and price effect P&L

Residuals
market=total−strategyprice effect=total−yield\text{market} = \text{total} - \text{strategy} \qquad \text{price effect} = \text{total} - \text{yield}

Stored as market_pnl_usd and price_effect_pnl_usd, in both ITD and range variants.

Position rollup

Strategy and yield use the same rule, driven by the NAV policy (or the Receipt / Underlying unwrap toggle on screen). There is no fallback: under the receipt policy a receipt leg of 0 is a valid answer and is reported as 0.

ConditionSource
Policy = underlying and the receipt has leaf underlyingsΣ leaf underlying values (intermediate wrappers are skipped to avoid double counting)
OtherwiseThe receipt (default) leg
Position has no receipt legΣ all of its legs

Fund rollup

Fund rollup
M=∑pMp+∑hPnLhE=∑pEp+∑hPnLhY=∑pYpM = \sum_p M_p + \sum_h \text{PnL}_h \qquad E = \sum_p E_p + \sum_h \text{PnL}_h \qquad Y = \sum_p Y_p

M is market P&L, E is price effect, Y is yield. Holdings add their total P&L to market and price effect, and contribute 0 to yield.

Report period returns#

Reports compute period returns per fund, wallet, position and asset. They default to the classic family; a report can be generated with the Modified Dietz family instead.

Period and ITD return
r=PnLV0+12(I−O)rITD=PnLITDCr = \dfrac{\text{PnL}}{V_0 + \tfrac{1}{2}(I - O)} \qquad r_{\text{ITD}} = \dfrac{\text{PnL}_{\text{ITD}}}{C}

Classic default. V_0 is the opening value, I inflows and O outflows, with every flow treated as mid-period (Original Dietz). C is cost basis, or the opening value when there is no cost basis.

Period TWR, APR and APY
TWR=1+TWR11+TWR0−1APR=r×365dAPY=(1+r)365/d−1\text{TWR} = \dfrac{1 + \text{TWR}_1}{1 + \text{TWR}_0} - 1 \qquad \text{APR} = r \times \dfrac{365}{d} \qquad \text{APY} = (1 + r)^{365/d} - 1

Position TWR links the stored TWR from the window start to its end. For APR and APY, r is that period TWR on positions and the period return otherwise. Capped at ±1,000,000%.

Precision & arithmetic#

decimal.jsEngine metrics (NAV, P&L, TWR, Modified Dietz, Sharpe, Sortino, drawdown, CAGR, Calmar) use arbitrary-precision decimal arithmetic, so rounding does not compound across tens of thousands of snapshots.
Float64XIRR is solved in 64-bit floating point to a relative rate tolerance of 1e-12. Benchmark risk statistics and strategy desk statistics (volatility, VaR, skew, K-ratio) are also float64. Both are read at well below float64 precision.
Welford's algorithmSharpe keeps a running mean and M2 of daily returns, so the variance is numerically stable and appendable without replaying history.
CapsAPR, APR (MD), APY and report APR/APY: ±1,000,000%. IRR-APR, CAGR and Calmar: ±1,000,000 in their own units. A capped value is a short-window artefact, not a rate.
Methodology changesEach row records the risk-methodology version it was computed with. A change forces a rebuild instead of blending two methodologies into one series.

Sources#

The standards and primary references these definitions follow:

Where these numbers appear#