{"id":51239893,"url":"https://github.com/jmcph4/perf-py","last_synced_at":"2026-06-28T23:31:55.242Z","repository":{"id":352332517,"uuid":"1214752139","full_name":"jmcph4/perf-py","owner":"jmcph4","description":"Compute the annualised money-weighted return of an ASX ETF / share portfolio from the list of dated unit purchases","archived":false,"fork":false,"pushed_at":"2026-04-19T04:00:44.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-19T04:16:52.782Z","etag":null,"topics":["asx","australia","finance","personal-finance"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jmcph4.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["jmcph4"]}},"created_at":"2026-04-19T02:06:13.000Z","updated_at":"2026-04-19T04:00:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jmcph4/perf-py","commit_stats":null,"previous_names":["jmcph4/perf-py"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/jmcph4/perf-py","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmcph4%2Fperf-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmcph4%2Fperf-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmcph4%2Fperf-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmcph4%2Fperf-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jmcph4","download_url":"https://codeload.github.com/jmcph4/perf-py/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmcph4%2Fperf-py/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34907985,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asx","australia","finance","personal-finance"],"created_at":"2026-06-28T23:31:53.491Z","updated_at":"2026-06-28T23:31:55.235Z","avatar_url":"https://github.com/jmcph4.png","language":"Python","funding_links":["https://github.com/sponsors/jmcph4"],"categories":[],"sub_categories":[],"readme":"# perf.py\n\nCompute the annualised money-weighted return of an ASX ETF / share portfolio\nfrom the list of dated unit purchases. Reports a nominal XIRR plus a\ndeflated XIRR using either Australian CPI (→ real return) or a sovereign\nbond yield (→ excess return over the risk-free rate).\n\n## Install \u0026 run\n\nDependencies are declared in `pyproject.toml` and managed by\n[`uv`](https://docs.astral.sh/uv/):\n\n```sh\nuv run perf.py --help\n```\n\n`uv` syncs `pandas`, `scipy`, and `yfinance` into `.venv` on the first run.\n\n## Inputs\n\nYou supply only **dates and unit counts**. Prices, distributions, CPI, and\nbond yields are all fetched from public sources.\n\nA purchase is `TICKER:DATE:UNITS`, e.g. `VGS.AX:2021-03-15:120`. If the\nticker is omitted, `--ticker` is used (default `VGS.AX`). Bare tickers\nwith no exchange suffix (e.g. `VGS`) are assumed ASX-listed and\nnormalised to `.AX`; supply an explicit suffix to override.\n\nEither repeat `--purchase`, or pass a CSV via `--purchases`:\n\n```csv\nticker,date,units\nVGS.AX,2021-03-15,120\nVGS.AX,2022-08-02,80\nVAS.AX,2022-01-10,50\n```\n\nTwo-column rows (`date,units`) are also accepted and inherit `--ticker`.\nThe header is optional.\n\n## Examples\n\nSingle holding, real return via CPI:\n\n```sh\nuv run perf.py --purchase 2021-03-15:120 --purchase 2022-08-02:80\n```\n\nMulti-ticker portfolio:\n\n```sh\nuv run perf.py \\\n  --purchase VGS.AX:2021-03-15:120 \\\n  --purchase VAS.AX:2022-01-10:50\n```\n\nAssume DRP-style reinvestment at the ex-date close:\n\n```sh\nuv run perf.py --purchases holdings.csv --reinvest\n```\n\nExcess return vs 3-year Australian govt bond:\n\n```sh\nuv run perf.py --purchases holdings.csv --risk-free au --risk-free-tenor 3\n```\n\nExcess return vs 10-year US Treasury:\n\n```sh\nuv run perf.py --purchases holdings.csv --risk-free us\n```\n\n## CLI reference\n\n| Flag | Default | Purpose |\n|---|---|---|\n| `--ticker` | `VGS.AX` | Fallback ticker for purchases that omit one |\n| `--purchase [TICKER:]DATE:UNITS` | — | Repeatable purchase spec |\n| `--purchases FILE` | — | CSV of purchases |\n| `--as-of YYYY-MM-DD` | today | Valuation date |\n| `--reinvest` | off | Convert distributions into units at ex-date close |\n| `--cpi-series` | `GCPIAG` | RBA Table G1 series id (All Groups CPI) |\n| `--risk-free {au,us}` | off (use CPI) | Use a sovereign bond yield as deflator |\n| `--risk-free-tenor YEARS` | `10` | Tenor of the bond series |\n\nAU tenors: 2, 3, 5, 10 (RBA series `FCMYGBAG{tenor}`).\nUS tenors: 1, 2, 3, 5, 7, 10, 20, 30 (FRED series `DGS{tenor}`).\n\n## Data sources\n\n- **Prices \u0026 distributions**: Yahoo Finance via `yfinance`. Close price on\n  the purchase date is used as the execution price. Distributions are the\n  per-unit cash amounts on the ex-date.\n- **Australian CPI**: RBA Statistical Table G1, series `GCPIAG` (All\n  Groups, original — sourced from the ABS).\n- **Australian bond yields**: RBA Statistical Table F2 (daily).\n- **US Treasury yields**: FRED `DGS{tenor}` series (daily).\n\n## Method\n\n1. Each purchase emits a dated cashflow of `-units × close(date)`.\n2. Each ex-date dividend emits `+units_held × dist_per_unit`, unless\n   `--reinvest` is set, in which case the cash is divided by that day's\n   close and added to units held.\n3. A terminal cashflow of `+total_units × latest_close` is added on the\n   last available trading day for each ticker.\n4. All per-ticker cashflows are concatenated; `XIRR` (Brent root-find on\n   the NPV equation, actual/365 daycount) is computed on the combined\n   series.\n5. For the deflated XIRR, every cashflow is scaled by\n   `numéraire(valuation_date) / numéraire(cashflow_date)`. The numéraire\n   is either the CPI index directly or a daily-compounded index built\n   from the bond yield (actual/365, held constant between observations).\n\n## Caveats\n\n- All tickers are assumed AUD-denominated (ASX-listed). With `--risk-free us`,\n  cashflows are converted to USD via daily AUD/USD spot (Yahoo Finance\n  `AUDUSD=X`) before computing the excess return, so the reported excess\n  is a USD figure and an additional USD nominal return is shown. All other\n  modes stay entirely in AUD.\n- Yahoo's distribution history for ASX ETFs is occasionally incomplete\n  or mis-dated (especially the EOFY top-up declared in July). Sanity-check\n  `Total distributions` against the issuer's published history before\n  trusting the output.\n- Franking credits, foreign income tax offsets, and AMIT attribution are\n  not modelled — the figure is a pre-tax cash-distribution return.\n- The DRP model uses the ex-date close as the reinvestment price, which\n  is a rough proxy for real DRP pricing (typically a VWAP, sometimes at\n  a small discount).\n- RBA table URLs occasionally change; if `f2-data.csv` or `g1-data.csv`\n  starts 404ing, update `RBA_F2_URL` / `RBA_G1_URL` at the top of the\n  file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmcph4%2Fperf-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjmcph4%2Fperf-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmcph4%2Fperf-py/lists"}