Reconcile time-weighted and money-weighted stock returns
An account balance can rise while its investments lose money when the owner adds capital. To explain the result, reconstruct the same ledger in two ways: chain returns between external cash flows for time-weighted return, TWR, and solve a dated investor cash-flow equation for money-weighted return, MWR. The two measures answer different questions and must share the same valuation boundary, period and currency.
The GIPS handbook explains geometric linking of subperiod returns and IRR as one method of calculating MWR that reflects cash-flow size and timing. The following is an original personal-account calculation, not a claim of GIPS compliance. It is an external method; read-only aggregation or a simulated account does not establish that CoTrading already calculates these measures.
Define the account boundary and gather inputs
Choose one stock portfolio in one currency. Include its cash and securities in the valuation boundary, and decide whether the result is net of specified fees and taxes. Obtain opening and closing total values, daily valuations where available, and every external contribution or withdrawal with an effective timestamp. Preserve quantities, price timestamps and any receivables needed to reconstruct each value.
A stock purchase exchanges internal cash for a security; it is not an external contribution. A sale reverses that exchange. A dividend retained as portfolio cash remains inside the boundary, while cash distributed out of the account becomes an external withdrawal when it crosses the boundary. Count each economic event once. Otherwise the same dividend can enter both the valuation return and a fictitious contribution adjustment.
Keep cost treatment consistent. In this example, observed account values are assumed net of the costs included in the ledger, and there are no additional outside-paid fees. If a real account has costs paid externally, document how the performance boundary handles them instead of automatically classifying every cash movement as investor capital. Missing fee or income information limits the meaning of a purported net return.
Make cash-flow timing visible
For an external flow at time t, record the value immediately before the flow, the flow amount and the value immediately after it. A deposit should satisfy after = before + deposit when nothing else occurs simultaneously. With multiple same-day events, retain their order or an explicit beginning/end-of-day policy. A closing balance alone cannot identify whether a deposit participated in that day’s investment movement.
If a flow occurs intraday and only daily closing marks exist, an exact flow-time TWR is unavailable. Obtain an appropriate valuation or use an explicitly labeled approximation under a stated policy. Do not call a weighted-flow approximation exact geometric slicing. If the opening base is zero, percentage return for that segment is undefined; start a new funded segment rather than divide by zero.
Our invented ledger has two intervals of exactly 365 days:
| Date and event | Value before flow | External deposit | Value after flow |
|---|---|---|---|
| 2023-01-01: open | 0 | 100 | 100 |
| 2024-01-01: add funds after valuation | 110 | 90 | 200 |
| 2024-12-31: terminal valuation | 180 | 0 | 180 |
The terminal date is deliberate: 2024 is a leap year, but the second interval ends on December 31, so both elapsed intervals are 365 days. This makes the numeric demonstration simple while the dated implementation still computes actual day differences. The terminal value is included as a hypothetical liquidation receipt for MWR, without implying a real sale occurred.
Calculate the TWR chain
The first subperiod starts with 100 and ends at 110 before the additional deposit, giving 10%. The second starts with 200 after the deposit and ends at 180, giving −10%. Chain growth factors: (110/100) × (180/200) − 1 = −1%. Adding 10% and −10% would incorrectly produce zero.
The 90 deposit appears in the opening value of the second subperiod, not in its profit. The account balance grows from 100 to 180, an unadjusted 80% increase, yet net contributions are 190 and the investment gain is −10. Those three figures describe different quantities. The ledger bridge is opening 100 + later contribution 90 + gain (−10) = terminal 180.
For more flows, repeat the before/after bridge at every boundary and multiply every subperiod growth factor. Do not sum account values across dates. A withdrawal reduces the next opening base, not the return of the preceding investment segment. If valuations and flows fail to reconcile, stop and investigate before presenting a TWR percentage.
Build the dated MWR equation
Use investor signs: contributions paid into the account are negative, withdrawals received are positive, and the final portfolio value is a positive terminal receipt. The cash flows are −100 at the opening, −90 after 365 days and +180 after 730 days. With an actual-days/365 convention, the annualized rate r solves:
-100 - 90/(1+r) + 180/(1+r)^2 = 0
r > -1
Set x = 1+r. Then 100x² + 90x − 180 = 0. The positive x solution gives annualized MWR −3.490283%. Over the full 730-day span its equivalent compounded return is (1+r)^2 − 1 = −6.858745%. TWR −1% above is cumulative over that same span; compare it with this full-period equivalent rather than silently comparing cumulative and annualized numbers.
MWR is lower here because more capital participates in the second, losing interval. It is not a second estimate that must numerically equal TWR, nor evidence that one calculation is wrong. This example does not infer a general ability to time deposits well or badly from a single account path.
Implement a bounded solver with explicit statuses
The core dated present-value function is:
def xnpv(rate, flows):
if rate <= -1:
raise ValueError("rate must exceed -1")
origin = flows[0][0]
return sum(amount / (1 + rate) ** ((day-origin).days / 365)
for day, amount in flows)
The complete script uses Python date differences, described in the datetime documentation, and arithmetic checks from math. It accepts the conventional pattern of negative contributions followed by nonnegative receipts, checks a rate bracket from −0.9999 to 10, and bisects 180 times. It verifies the resulting equation residual rather than trusting a displayed percentage.
Reconcile real flows before calling it. This date-based solver sorts dates and nets flows on the same calendar day; use a timestamp-aware extension when intraday distinctions matter. It reports invalid_empty for an absent ledger, undefined_zero_flows for all-zero or fully netted-away flows, and undefined_zero_period when there are fewer than two effective dates. Same-day −100 and +100 cannot establish a unique annualized return: their present value is zero at every permitted rate. These input states must never enter bisection. A bracket failure returns not_bracketed; this means the implementation has not bracketed a root, not that mathematics proves no solution exists. The example is not a general-purpose IRR library or a comprehensive solver for every irregular cash-flow pattern.
Handle no solution and multiple solutions separately
The first counterexample has only negative flows, −100 and −10. Every discounted term is negative for r greater than −1, so there is no finite root. The script returns no_root_same_sign. In an ordinary account report, this often signals an incomplete terminal receipt or another ledger problem. Do not manufacture a zero return or silently omit the account.
The different-mechanism counterexample is −100, +230 and −132 at the same annual intervals. It represents an abstract receipt followed by a later payment obligation, not the standard long-only positive-terminal account above. Its equation becomes 100x² − 230x + 132 = 0; x can be 1.1 or 1.2. Thus both 10% and 20% annual rates make NPV zero.
The general conventional solver refuses this sign pattern with nonconventional_needs_root_analysis; the script verifies the two roots analytically for this particular quadratic. It does not choose whichever answer a numerical starting guess finds. A coarse grid scan can miss tangent roots or roots outside the scanned interval, so “one root found” is not a universal uniqueness proof. Preserve all established roots and report that a single MWR interpretation is ambiguous.
Reproduce and accept the report
Run python docs/growth/research/round-5/examples/R633.py. Actual output includes TWR −1.0000%, annual MWR −3.490283%, full-period MWR equivalent −6.858745%, roots 10.000000% and 20.000000%, no_root_same_sign, and a rejected zero-base TWR. Assertions check the chain, analytic reference solution, equation residuals and failure statuses.
Accept the report only when the external-flow ledger reconciles, valuation timing is explicit, currencies remain separate and annualization labels are visible. Keep the cash-flow equation and solver status beside the result. Recalculate when transactions, classifications, timestamps or valuations are corrected, retaining the earlier version. These synthetic checks verify the demonstrated arithmetic and handling rules; they do not verify real account data, real historical performance or a product workflow.
Preserve an interpretation table alongside the percentages
Use separate report rows for balance change, investment gain, cumulative TWR, annualized MWR and full-period MWR equivalent. Attach the same account, currency and start/end dates to each row. In our example, the amounts are 80 of balance increase and a loss of 10; the percentages answer different denominator and timing questions. If a later reconciliation discovers that part of the 90 deposit was actually an internal transfer between cash subaccounts already inside the boundary, correct the classification and recompute both chains. Do not patch only the displayed percentage, because the valuation bridge and cash-flow equation would then contradict each other.
Download the complete example. Use Python 3.12 or later and run python R633.py in the download directory. It uses only the standard library and synthetic inputs.
FAQ
Should buying shares be entered as a negative investor flow?
Not when cash and shares are inside the same portfolio boundary. The purchase is an internal exchange; an investor contribution crosses the boundary.
Does solver failure mean the investment earned zero?
No. Missing brackets, incomplete data, no mathematical root and multiple roots require distinct statuses. None should be replaced with a convenient zero.