Compute the annualised money-weighted return of an ASX ETF / share portfolio from the list of dated unit purchases. Reports a nominal XIRR plus a deflated XIRR using either Australian CPI (→ real return) or a sovereign bond yield (→ excess return over the risk-free rate).
Dependencies are declared in pyproject.toml and managed by
uv:
uv run perf.py --helpuv syncs pandas, scipy, and yfinance into .venv on the first run.
You supply only dates and unit counts. Prices, distributions, CPI, and bond yields are all fetched from public sources.
A purchase is TICKER:DATE:UNITS, e.g. VGS.AX:2021-03-15:120. If the
ticker is omitted, --ticker is used (default VGS.AX). Bare tickers
with no exchange suffix (e.g. VGS) are assumed ASX-listed and
normalised to .AX; supply an explicit suffix to override.
Either repeat --purchase, or pass a CSV via --purchases:
ticker,date,units
VGS.AX,2021-03-15,120
VGS.AX,2022-08-02,80
VAS.AX,2022-01-10,50Two-column rows (date,units) are also accepted and inherit --ticker.
The header is optional.
Single holding, real return via CPI:
uv run perf.py --purchase 2021-03-15:120 --purchase 2022-08-02:80Multi-ticker portfolio:
uv run perf.py \
--purchase VGS.AX:2021-03-15:120 \
--purchase VAS.AX:2022-01-10:50Assume DRP-style reinvestment at the ex-date close:
uv run perf.py --purchases holdings.csv --reinvestExcess return vs 3-year Australian govt bond:
uv run perf.py --purchases holdings.csv --risk-free au --risk-free-tenor 3Excess return vs 10-year US Treasury:
uv run perf.py --purchases holdings.csv --risk-free us| Flag | Default | Purpose |
|---|---|---|
--ticker |
VGS.AX |
Fallback ticker for purchases that omit one |
--purchase [TICKER:]DATE:UNITS |
— | Repeatable purchase spec |
--purchases FILE |
— | CSV of purchases |
--as-of YYYY-MM-DD |
today | Valuation date |
--reinvest |
off | Convert distributions into units at ex-date close |
--cpi-series |
GCPIAG |
RBA Table G1 series id (All Groups CPI) |
--risk-free {au,us} |
off (use CPI) | Use a sovereign bond yield as deflator |
--risk-free-tenor YEARS |
10 |
Tenor of the bond series |
AU tenors: 2, 3, 5, 10 (RBA series FCMYGBAG{tenor}).
US tenors: 1, 2, 3, 5, 7, 10, 20, 30 (FRED series DGS{tenor}).
- Prices & distributions: Yahoo Finance via
yfinance. Close price on the purchase date is used as the execution price. Distributions are the per-unit cash amounts on the ex-date. - Australian CPI: RBA Statistical Table G1, series
GCPIAG(All Groups, original — sourced from the ABS). - Australian bond yields: RBA Statistical Table F2 (daily).
- US Treasury yields: FRED
DGS{tenor}series (daily).
- Each purchase emits a dated cashflow of
-units × close(date). - Each ex-date dividend emits
+units_held × dist_per_unit, unless--reinvestis set, in which case the cash is divided by that day's close and added to units held. - A terminal cashflow of
+total_units × latest_closeis added on the last available trading day for each ticker. - All per-ticker cashflows are concatenated;
XIRR(Brent root-find on the NPV equation, actual/365 daycount) is computed on the combined series. - For the deflated XIRR, every cashflow is scaled by
numéraire(valuation_date) / numéraire(cashflow_date). The numéraire is either the CPI index directly or a daily-compounded index built from the bond yield (actual/365, held constant between observations).
- All tickers are assumed AUD-denominated (ASX-listed). With
--risk-free us, cashflows are converted to USD via daily AUD/USD spot (Yahoo FinanceAUDUSD=X) before computing the excess return, so the reported excess is a USD figure and an additional USD nominal return is shown. All other modes stay entirely in AUD. - Yahoo's distribution history for ASX ETFs is occasionally incomplete
or mis-dated (especially the EOFY top-up declared in July). Sanity-check
Total distributionsagainst the issuer's published history before trusting the output. - Franking credits, foreign income tax offsets, and AMIT attribution are not modelled — the figure is a pre-tax cash-distribution return.
- The DRP model uses the ex-date close as the reinvestment price, which is a rough proxy for real DRP pricing (typically a VWAP, sometimes at a small discount).
- RBA table URLs occasionally change; if
f2-data.csvorg1-data.csvstarts 404ing, updateRBA_F2_URL/RBA_G1_URLat the top of the file.