Author: cs224
Last updated: 2025-12-15
Python implementation: CPython
Python version : 3.12.9
IPython version : 9.8.0
numpy : 2.3.5
pandas : 2.3.3
scipy : 1.16.3
nbdev : 2.4.6
matplotlib: 3.10.8
Core
Imports + tiny helpers
We’ll standardize dates (datetime/date/pandas Timestamp) to date.
We’ll use ACT/365F for year fractions.
assert _to_date(pd.Timestamp("2025-01-01")) == date(2025,1,1)Domain types + sign convention
Investor view: deposits negative, withdrawals positive.
Portfolio view: opposite sign.
Flow(when, amount) is “external flow” whose sign depends on view.
ParsedInputs
ParsedInputs (df:pandas.core.frame.DataFrame, start_date:datetime.date, end_date:datetime.date, start_value:float, end_value:float)
Flow
Flow (when:datetime.date, amount:float)
f1 = Flow(_to_date(pd.Timestamp("2025-01-01")), float(10.5))
f2 = Flow(_to_date(pd.Timestamp("2025-02-01")), float(-10.1))
assert _ensure_sorted([f2, f1]) == [f1, f2]Day count
We use ACT/365F \[ \operatorname{year\_fraction}(d_0, d_1) = \frac{(d_1 - d_0)_{\text{days}}}{365}. \]
year_fraction
year_fraction (d0:datetime.date, d1:datetime.date)
assert year_fraction(date(2025,1,1), date(2026,1,1)) == 1.0
assert year_fraction(date(2025,1,1), date(2025,12,31)) == 364/365Modified Dietz
Assumptions: * portfolio-view flows * cashflows on the start date are not allowed under the post-flow convention (omit them from the inputs)
formula: \[ R_{MD}=\frac{V_T - V_0 - \sum CF_i}{V_0 + \sum w_i CF_i},\quad w_i=\frac{T-t_i}{T-0} \]
modified_dietz
modified_dietz (start:datetime.date, start_value:float, flows:List[__main__.Flow], end:datetime.date, end_value:float)
Modified Dietz with portfolio-view flows; start-date cashflows should be omitted under the post-flow convention.
md = modified_dietz(_to_date(pd.Timestamp("2025-01-01")), 100.0, [], _to_date(pd.Timestamp("2025-12-31")), 110.0)
assert md == approx(0.1, **APPROX)Money-weighted return (XIRR)
Solve \(NPV(r)=\sum \frac{CF_i}{(1+r)^{\Delta t_i}}=0\)
If there are multiple sign changes the solution might not be unique; we warn but return one solution.
f1 = Flow(_to_date(pd.Timestamp("2025-01-01")), 10.0)
f2 = Flow(_to_date(pd.Timestamp("2025-12-31")), 20.0)
cfs = [f1, f2]
assert _npv(cfs, 0.0) == sum([f.amount for f in cfs])money_weighted_return
money_weighted_return (cashflows:Sequence[Tuple[datetime.date,float]])
XIRR on dated cashflows (negative investments, positive distributions).
cashflows = [(_to_date(pd.Timestamp("2025-01-01")), - 100.0)] + [] + [(_to_date(pd.Timestamp("2025-12-31")), 110.0)]
mwr = money_weighted_return(cashflows)
assert mwr == approx(0.1002880629803653, **APPROX)
mwr_period = (1.0 + mwr) ** year_fraction(pd.Timestamp("2025-01-01"), pd.Timestamp("2025-12-31")) - 1.0
assert mwr_period == approx(0.1, **APPROX)Time-weighted return (post-flow valuations)
Assumptions: * valuations are post-flow at each valuation date * requires valuation on each cashflow date (strict mode) * chain-link:
\[ r_i=\frac{V_i - CF_i}{V_{i-1}}-1,\quad TWR=\prod_i(1+r_i)-1 \]
where (CF_i) is portfolio-view flow occurring on date (i).
time_weighted_return
time_weighted_return (valuations:Sequence[Tuple[datetime.date,float]], flows:Iterable[__main__.Flow])
Chain-linked return assuming valuations are post-flow (end-of-day).
portfolio_flows = []
valuations = [(_to_date(pd.Timestamp("2025-01-01")), 100.0), (_to_date(pd.Timestamp("2025-12-31")), 110.0)]
twr = time_weighted_return(valuations, portfolio_flows)
assert twr == approx(0.1, **APPROX)
twr_ann = (1.0 + twr) ** (1.0 / year_fraction(pd.Timestamp("2025-01-01"), pd.Timestamp("2025-12-31"))) - 1.0
assert twr_ann == approx(0.1002880629803653, **APPROX)