How Time-Weighted Return (TWR) Works — A Step-by-Step Guide
Time-weighted return isolates manager skill from cash-flow timing by chain-linking sub-period returns. A precise, worked walkthrough with code.
- TWR is the geometric chain-link of sub-period returns, where each sub-period ends at an external cash flow.
- It removes the distorting effect of deposit and withdrawal timing, isolating the trader's skill on the assets they actually controlled.
- GIPS, the global standard for fund performance reporting, requires TWR for comparability across managers.
- The math is simple: split the period at every external flow, compute each sub-period return, multiply (1 + r_i) together, subtract 1.
- NakedPnL computes TWR daily from raw exchange NAV snapshots and publishes the result in an append-only, hash-chained registry.
Time-weighted return (TWR) is the standard methodology for measuring the performance of an investment manager when the size of the portfolio changes over time due to deposits and withdrawals. It answers a specific question: how would one dollar invested at the start have grown if the manager were never given more capital and never lost capital to withdrawals?
That question matters because raw account growth conflates two completely different things — what the trader did with the money they had, and how lucky they got with the timing of new capital. TWR strips out the second factor. The result is a number that is comparable across traders, across funds, and across measurement windows.
Why TWR exists in the first place
Consider two traders. Both start with $10,000. Both end the year with a portfolio worth $14,000. Trader A made every dollar from trading. Trader B started the year flat, then a friend wired in $4,000 on December 30th — the trading account itself produced zero return. Their final balance is identical. Their skill is not.
A naive percentage change ((14000 − 10000) / 10000 = 40%) treats them as equals. TWR does not. By splitting the year at the December 30th deposit and computing the return of each sub-period independently, TWR correctly assigns Trader A a 40% return and Trader B a 0% return. This is the entire point of the metric.
The formula
Let the measurement period contain n external cash flows, dividing it into n+1 sub-periods. For each sub-period i, let V_start be the portfolio value immediately after the previous flow (or at the very start), and let V_end be the portfolio value immediately before the next flow (or at the very end). The sub-period return is:
r_i = (V_end_i - V_start_i) / V_start_iThe total time-weighted return over the full period is the geometric chain-link of those sub-period returns:
TWR = (1 + r_1) * (1 + r_2) * ... * (1 + r_n+1) - 1The geometric form (multiplying growth factors) is essential. Adding sub-period returns produces an arithmetic average, which systematically overstates performance whenever returns are volatile. The geometric chain mirrors the actual compounding behaviour of capital.
What counts as an external cash flow
An external cash flow is any movement of money into or out of the portfolio that is not the result of the trader's investment decisions. These must terminate sub-periods. Internal transactions — buying a token, closing a position, paying exchange fees — never trigger a new sub-period; they are simply reflected in the next NAV snapshot.
- Deposit of fiat or stablecoin to the exchange account
- Withdrawal of any asset off the exchange
- Internal transfer to a different sub-account
- Airdrops or external token grants (treated as deposits)
- Exchange-paid promotional rebates credited to the spot wallet
Why daily snapshots, not monthly
GIPS requires sub-period termination at every external flow above a materiality threshold. The more granular the snapshots, the smaller the residual error from approximation between flow events. Monthly snapshots can mis-attribute up to several percentage points of return when a large flow lands mid-month and the portfolio is volatile.
Daily snapshots are the practical sweet spot for crypto and 24/7 markets: they bound the worst-case error to a single trading day's drift between flow time and snapshot time, and they align with how exchange APIs publish account valuations. NakedPnL's daily cron at 23:55 UTC pulls a NAV reading from each connected venue and feeds it directly into the TWR engine.
A worked example
Suppose a trader starts on 2026-01-01 with $10,000 in their Binance spot account. The portfolio drifts up and down, and on 2026-01-15 they deposit another $5,000. The values look like this:
| Date | NAV before flow | External flow | NAV after flow |
|---|---|---|---|
| 2026-01-01 | $10,000 | — | $10,000 |
| 2026-01-14 | $11,500 | — | $11,500 |
| 2026-01-15 | $11,200 | +$5,000 | $16,200 |
| 2026-01-31 | $17,820 | — | $17,820 |
Sub-period 1 runs from $10,000 to $11,200, the value immediately before the deposit. Sub-period 2 runs from $16,200, the value immediately after the deposit, to $17,820 at month-end.
r_1 = (11200 - 10000) / 10000 = 0.12000
r_2 = (17820 - 16200) / 16200 = 0.10000
TWR = (1 + 0.12) * (1 + 0.10) - 1
= 1.12 * 1.10 - 1
= 1.232 - 1
= 0.232 => 23.2%Compare that to the naive percentage change. The trader contributed $15,000 of capital total and ended with $17,820, suggesting a return of (17820 − 15000) / 15000 = 18.8%. The naive number is wrong: it under-states performance because it averages the two sub-periods on a dollar-weighted basis, ignoring that the trader earned 12% on the first $10,000 over two weeks before the new capital arrived.
Reference implementation
Below is a minimal Python implementation of the GIPS-required TWR computation. NakedPnL's production engine in lib/calculation/twr-engine.ts follows the same algorithm but uses Decimal.js to avoid IEEE 754 floating-point error across thousands of compounded daily steps.
from decimal import Decimal, getcontext
from dataclasses import dataclass
from typing import List
getcontext().prec = 28
@dataclass
class Snapshot:
date: str
nav: Decimal # NAV before any flow on that date
flow: Decimal # +deposit, -withdrawal, 0 if none
def twr(snapshots: List[Snapshot]) -> Decimal:
"""Geometric chain-linked TWR per GIPS methodology.
Each snapshot's `nav` is taken before the flow that day,
so a sub-period ends on the day-before's nav and resumes
from (nav + flow) on the flow day.
"""
if len(snapshots) < 2:
return Decimal(0)
growth = Decimal(1)
start = snapshots[0].nav + snapshots[0].flow # post-flow base
for prev, curr in zip(snapshots, snapshots[1:]):
end = curr.nav # value immediately before today's flow
if start <= 0:
raise ValueError(f"non-positive sub-period base on {curr.date}")
sub_return = (end - start) / start
growth *= (Decimal(1) + sub_return)
# next sub-period starts post-flow
start = curr.nav + curr.flow
return growth - Decimal(1)
# Example matching the worked table above
snaps = [
Snapshot('2026-01-01', Decimal('10000'), Decimal('0')),
Snapshot('2026-01-15', Decimal('11200'), Decimal('5000')),
Snapshot('2026-01-31', Decimal('17820'), Decimal('0')),
]
print(twr(snaps)) # -> 0.232 (23.2%)Handling missing or stale NAVs
Real exchange data is messy. APIs go down. Maintenance windows skip a snapshot. A market freeze (Binance outage during the May 2021 cascade liquidations is the canonical example) leaves the account un-snapshottable for hours. The honest response is to record the gap explicitly rather than fabricate a value.
NakedPnL's engine refuses to interpolate between known NAVs. If a daily snapshot is missing, the gap is recorded in the calculation hash chain and the affected sub-period is flagged. The verification page at /verify/chain/[handle] surfaces these gaps to anyone re-checking the math, so a track record with hidden gaps cannot be quietly smoothed over.
Why TWR is the metric regulators chose
The Global Investment Performance Standards (GIPS), published by the CFA Institute, are the reporting framework most institutional asset managers worldwide adopt. GIPS Standards 2020 require firms to present TWR for all composites, and explicitly disallow money-weighted return as a primary metric except in tightly defined private-market contexts.
“Time-weighted returns must be used to remove the effects of external cash flows, which are generally client-driven. By removing the effects of external cash flows, time-weighted returns reflect the firm's investment management decisions.”
The reasoning is straightforward: when comparing managers, the comparison must control for things the manager did not choose. Cash-flow timing is one of those things. NakedPnL publishes TWR for the same reason GIPS requires it — comparability is impossible without it.
How to verify a TWR figure independently
Every NavSnapshot row in the NakedPnL registry is hashed (SHA-256 over the canonicalized raw exchange response) and chain-linked to the previous day. The published TWR is reproducible by any third party with the chain export and a working hash function. The /docs/verification page contains complete reference snippets in Python and JavaScript.