glidepath 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. glidepath-0.2.0/LICENSE +21 -0
  2. glidepath-0.2.0/LICENSE-DATA +28 -0
  3. glidepath-0.2.0/PKG-INFO +189 -0
  4. glidepath-0.2.0/README.md +178 -0
  5. glidepath-0.2.0/pyproject.toml +153 -0
  6. glidepath-0.2.0/src/glidepath/__init__.py +3 -0
  7. glidepath-0.2.0/src/glidepath/app/__init__.py +364 -0
  8. glidepath-0.2.0/src/glidepath/app/backtest.py +281 -0
  9. glidepath-0.2.0/src/glidepath/app/charts.py +759 -0
  10. glidepath-0.2.0/src/glidepath/app/copy.py +174 -0
  11. glidepath-0.2.0/src/glidepath/app/display.py +148 -0
  12. glidepath-0.2.0/src/glidepath/app/drawdown.py +436 -0
  13. glidepath-0.2.0/src/glidepath/app/example.py +66 -0
  14. glidepath-0.2.0/src/glidepath/app/exports.py +487 -0
  15. glidepath-0.2.0/src/glidepath/app/files.py +249 -0
  16. glidepath-0.2.0/src/glidepath/app/firstrun.py +114 -0
  17. glidepath-0.2.0/src/glidepath/app/forms.py +1750 -0
  18. glidepath-0.2.0/src/glidepath/app/inspector.py +506 -0
  19. glidepath-0.2.0/src/glidepath/app/labels.py +66 -0
  20. glidepath-0.2.0/src/glidepath/app/montecarlo.py +399 -0
  21. glidepath-0.2.0/src/glidepath/app/plan.py +354 -0
  22. glidepath-0.2.0/src/glidepath/app/retirement.py +446 -0
  23. glidepath-0.2.0/src/glidepath/app/scenarios.py +831 -0
  24. glidepath-0.2.0/src/glidepath/app/shell.py +185 -0
  25. glidepath-0.2.0/src/glidepath/app/tables.py +138 -0
  26. glidepath-0.2.0/src/glidepath/core/__init__.py +390 -0
  27. glidepath-0.2.0/src/glidepath/core/annuities.py +240 -0
  28. glidepath-0.2.0/src/glidepath/core/backtest.py +514 -0
  29. glidepath-0.2.0/src/glidepath/core/comparison.py +278 -0
  30. glidepath-0.2.0/src/glidepath/core/config.py +82 -0
  31. glidepath-0.2.0/src/glidepath/core/contributions.py +337 -0
  32. glidepath-0.2.0/src/glidepath/core/engine.py +2811 -0
  33. glidepath-0.2.0/src/glidepath/core/entities.py +264 -0
  34. glidepath-0.2.0/src/glidepath/core/glide.py +289 -0
  35. glidepath-0.2.0/src/glidepath/core/investments.py +175 -0
  36. glidepath-0.2.0/src/glidepath/core/money.py +107 -0
  37. glidepath-0.2.0/src/glidepath/core/montecarlo.py +609 -0
  38. glidepath-0.2.0/src/glidepath/core/pensions.py +298 -0
  39. glidepath-0.2.0/src/glidepath/core/periods.py +367 -0
  40. glidepath-0.2.0/src/glidepath/core/provenance.py +271 -0
  41. glidepath-0.2.0/src/glidepath/core/randomness.py +128 -0
  42. glidepath-0.2.0/src/glidepath/core/region.py +46 -0
  43. glidepath-0.2.0/src/glidepath/core/reporting.py +231 -0
  44. glidepath-0.2.0/src/glidepath/core/results.py +504 -0
  45. glidepath-0.2.0/src/glidepath/core/retirement.py +291 -0
  46. glidepath-0.2.0/src/glidepath/core/returns.py +312 -0
  47. glidepath-0.2.0/src/glidepath/core/scenarios.py +579 -0
  48. glidepath-0.2.0/src/glidepath/core/state_pension.py +264 -0
  49. glidepath-0.2.0/src/glidepath/core/tax.py +139 -0
  50. glidepath-0.2.0/src/glidepath/core/withdrawals.py +461 -0
  51. glidepath-0.2.0/src/glidepath/core/wrappers.py +278 -0
  52. glidepath-0.2.0/src/glidepath/gui/__init__.py +6 -0
  53. glidepath-0.2.0/src/glidepath/gui/assets/icon_128.png +0 -0
  54. glidepath-0.2.0/src/glidepath/gui/assets/icon_16.png +0 -0
  55. glidepath-0.2.0/src/glidepath/gui/assets/icon_24.png +0 -0
  56. glidepath-0.2.0/src/glidepath/gui/assets/icon_256.png +0 -0
  57. glidepath-0.2.0/src/glidepath/gui/assets/icon_32.png +0 -0
  58. glidepath-0.2.0/src/glidepath/gui/assets/icon_48.png +0 -0
  59. glidepath-0.2.0/src/glidepath/gui/assets/icon_64.png +0 -0
  60. glidepath-0.2.0/src/glidepath/gui/assets/wordmark.png +0 -0
  61. glidepath-0.2.0/src/glidepath/gui/charts.py +829 -0
  62. glidepath-0.2.0/src/glidepath/gui/forms.py +359 -0
  63. glidepath-0.2.0/src/glidepath/gui/inspector.py +186 -0
  64. glidepath-0.2.0/src/glidepath/gui/main.py +51 -0
  65. glidepath-0.2.0/src/glidepath/gui/scenarios.py +402 -0
  66. glidepath-0.2.0/src/glidepath/gui/style.py +376 -0
  67. glidepath-0.2.0/src/glidepath/gui/tableview.py +67 -0
  68. glidepath-0.2.0/src/glidepath/gui/widgets.py +989 -0
  69. glidepath-0.2.0/src/glidepath/persistence/__init__.py +48 -0
  70. glidepath-0.2.0/src/glidepath/persistence/assumptions.py +112 -0
  71. glidepath-0.2.0/src/glidepath/persistence/decode.py +747 -0
  72. glidepath-0.2.0/src/glidepath/persistence/document.py +101 -0
  73. glidepath-0.2.0/src/glidepath/persistence/encode.py +433 -0
  74. glidepath-0.2.0/src/glidepath/persistence/migrations.py +158 -0
  75. glidepath-0.2.0/src/glidepath/persistence/values.py +298 -0
  76. glidepath-0.2.0/src/glidepath/py.typed +0 -0
  77. glidepath-0.2.0/src/glidepath/regions/__init__.py +7 -0
  78. glidepath-0.2.0/src/glidepath/regions/uk/__init__.py +189 -0
  79. glidepath-0.2.0/src/glidepath/regions/uk/ages.py +156 -0
  80. glidepath-0.2.0/src/glidepath/regions/uk/contributions.py +717 -0
  81. glidepath-0.2.0/src/glidepath/regions/uk/data/age_rules.toml +78 -0
  82. glidepath-0.2.0/src/glidepath/regions/uk/data/assumptions_default.toml +170 -0
  83. glidepath-0.2.0/src/glidepath/regions/uk/data/returns_history.toml +150 -0
  84. glidepath-0.2.0/src/glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
  85. glidepath-0.2.0/src/glidepath/regions/uk/extension.py +479 -0
  86. glidepath-0.2.0/src/glidepath/regions/uk/loader.py +704 -0
  87. glidepath-0.2.0/src/glidepath/regions/uk/region.py +160 -0
  88. glidepath-0.2.0/src/glidepath/regions/uk/schema.py +563 -0
  89. glidepath-0.2.0/src/glidepath/regions/uk/state_pension.py +129 -0
  90. glidepath-0.2.0/src/glidepath/regions/uk/tax.py +466 -0
  91. glidepath-0.2.0/src/glidepath/regions/uk/wrappers.py +283 -0
  92. glidepath-0.2.0/src/glidepath/regions/uk/years.py +92 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 williajm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Data licence notice
2
+ ===================
3
+
4
+ Everything in this repository is MIT-licensed (see LICENSE) EXCEPT the
5
+ following bundled data file:
6
+
7
+ src/glidepath/regions/uk/data/returns_history.toml
8
+
9
+ That file is a derived work of the JST Macrohistory Database
10
+ (https://www.macrohistory.net/database/) — © Òscar Jordà, Moritz
11
+ Schularick, Alan M. Taylor and co-authors — and is distributed under the
12
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
13
+ licence (CC BY-NC-SA 4.0):
14
+
15
+ https://creativecommons.org/licenses/by-nc-sa/4.0/
16
+
17
+ In particular, the figures in that file may be used and redistributed
18
+ only non-commercially, with attribution, and under the same licence
19
+ terms. Cite Jordà, Schularick & Taylor (2017), "Macrofinancial History
20
+ and the New Business Cycle Facts", and — for the return series — Jordà,
21
+ Knoll, Kuvshinov, Schularick & Taylor (2019), "The Rate of Return on
22
+ Everything, 1870-2015", The Quarterly Journal of Economics
23
+ (https://doi.org/10.1093/qje/qjz012).
24
+
25
+ The MIT licence grant does not extend to that file. To obtain a
26
+ distribution free of CC BY-NC-SA material, delete the file and
27
+ regenerate a replacement from a source of your choosing with
28
+ scripts/build_returns_history.py as a starting point.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: glidepath
3
+ Version: 0.2.0
4
+ Summary: Desktop retirement and investment planner (UK-first, region-extensible).
5
+ License-Expression: MIT AND CC-BY-NC-SA-4.0
6
+ License-File: LICENSE
7
+ License-File: LICENSE-DATA
8
+ Requires-Dist: pyside6>=6.11.1
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ <p align="center">
13
+ <img src="src/glidepath/gui/assets/wordmark.png" alt="glidepath" width="420">
14
+ </p>
15
+
16
+ [![CI](https://github.com/williajm/glidepath/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
17
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=williajm_glidepath&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
18
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=williajm_glidepath&metric=coverage)](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
19
+ [![Python 3.14](https://img.shields.io/badge/python-3.14-blue)](https://github.com/williajm/glidepath/blob/main/.python-version)
20
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
21
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
22
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
23
+ [![License: MIT](https://img.shields.io/github/license/williajm/glidepath)](LICENSE)
24
+
25
+ <p align="center">
26
+ <a href="https://williajm.github.io/glidepath/"><b>williajm.github.io/glidepath</b></a>
27
+ </p>
28
+
29
+ A desktop retirement and investment planner. UK-first, built so other
30
+ regions can be added later. Every number in a plan is a **fact** you
31
+ stated, a **decision** you made, or an **assumption** the app defaulted
32
+ — always inspectable, never silently guessed. All data stays local;
33
+ nothing is transmitted.
34
+
35
+ <p align="center">
36
+ <img src="docs/screenshots/charts.png"
37
+ alt="The charts tab: the Monte Carlo fan chart — nested percentile bands deepening toward the median line — beside the success-rate readout and the retirement-age and backtest cards"
38
+ width="800">
39
+ </p>
40
+
41
+ *(All screenshots show example data, not anyone's real finances.)*
42
+
43
+ What it models today (single person, UK):
44
+
45
+ - **Wrappers** — workplace DC, SIPP, S&S ISA, LISA, GIA and cash, with
46
+ UK contribution relief mechanics and dividend/savings taxation.
47
+ - **Defined benefit pensions** — deferred entitlements or active
48
+ CARE-style accrual, with revaluation, early/late factors and
49
+ commutation; **state pension** from your official DWP forecast,
50
+ including deferral.
51
+ - **Tax** — rUK and Scottish income tax from verified 2026/27 data
52
+ files; pension allowances (AA/taper/MPAA, lump-sum allowance).
53
+ - **Projection** — deterministic or Monte Carlo runs from the app:
54
+ success rate, probability of ruin, ending-pot percentiles, and a
55
+ probability fan chart on its own tab, reproducible from a seed. With
56
+ a de-risking glide path, tax-aware decumulation with optional
57
+ go-go/slow-go/no-go retirement spending multipliers, and annuity
58
+ purchases entered in the facts form. (The engine also models
59
+ alternative withdrawal strategies — fixed %, guardrails, natural
60
+ yield — and tax-free-cash strategies; the app currently runs the
61
+ fixed-real defaults, with no strategy picker in the UI yet.)
62
+ - **"When can I retire?"** — a solver for the earliest retirement age
63
+ that sustains a target income (a replacement rate you choose, 66% of
64
+ employment income by default), met deterministically or at a Monte
65
+ Carlo success target.
66
+ - **Historical backtesting** — replays the plan over every rolling
67
+ window of world market history since 1900 (global equities in
68
+ sterling terms, UK gilts and cash, deflated by UK inflation):
69
+ the share of historical starting years the plan survives, the worst
70
+ starting year, and the range of outcomes as chart bands —
71
+ sequence-of-returns risk that independent Monte Carlo draws miss.
72
+ - **Scenarios** — named what-ifs over your decisions and assumptions,
73
+ with a side-by-side comparison; plans saved as a local JSON file.
74
+
75
+ ## More screenshots
76
+
77
+ Facts entry — everything on this screen is either a fact you state or
78
+ a choice you make; anything estimated lives in the assumptions
79
+ inspector instead:
80
+
81
+ <p align="center">
82
+ <img src="docs/screenshots/facts.png"
83
+ alt="The facts tab: the About you, Household spending, and State pension cards of the entry form, with the example plan's values filled in"
84
+ width="800">
85
+ </p>
86
+
87
+ Stated vs assumed — the provenance view: the facts you stated, the
88
+ choices in effect, and every assumption the run used with its value,
89
+ default/overridden status, source, and date:
90
+
91
+ <p align="center">
92
+ <img src="docs/screenshots/stated_vs_assumed.png"
93
+ alt="The stated-vs-assumed tab: tables of stated facts, choices in effect, assumptions used with sources, and the plan structure"
94
+ width="800">
95
+ </p>
96
+
97
+ ## Disclaimer
98
+
99
+ Glidepath is a personal modelling tool for exploring retirement scenarios.
100
+ It is not financial advice and is not regulated; its outputs depend on
101
+ assumptions that will not match reality. Do not make financial decisions
102
+ based solely on this tool.
103
+
104
+ ## Run
105
+
106
+ Requires only [uv](https://docs.astral.sh/uv/). Install the latest
107
+ release [from PyPI](https://pypi.org/project/glidepath/) and launch it:
108
+
109
+ ```sh
110
+ uv tool install glidepath
111
+ glidepath
112
+ ```
113
+
114
+ (`pipx install glidepath` works too. Glidepath is a desktop app
115
+ distributed through PyPI, not a library — importing `glidepath`
116
+ modules is not a supported API.)
117
+
118
+ Or run straight from a checkout:
119
+
120
+ ```sh
121
+ git clone https://github.com/williajm/glidepath.git
122
+ cd glidepath
123
+ uv run glidepath
124
+ ```
125
+
126
+ The first run from a checkout creates a virtual environment and
127
+ installs the locked dependencies automatically, then launches the
128
+ desktop app. Accept the
129
+ disclaimer and a fresh install
130
+ opens with an example plan already projected, so every tab has
131
+ something to show — replace its values with your own facts, or clear
132
+ the form and start blank. Charts label each bar with the tax year and
133
+ your age and switch between today's money and nominal; Help → "How to
134
+ use glidepath" walks through every tab. Save your plan from the File
135
+ menu as a `.glidepath.json` file you own, stored wherever you choose;
136
+ the next launch reopens your last plan automatically.
137
+
138
+ ## Developing
139
+
140
+ Development additionally requires GNU Make, which drives every workflow
141
+ command:
142
+
143
+ ```sh
144
+ make sync # create the platform venv from the lockfile (fails if the lock has drifted)
145
+ make hooks # install the pre-commit hooks (required before committing)
146
+ ```
147
+
148
+ One-time per machine: set `UV_PROJECT_ENVIRONMENT` user-wide (`.venv-win`
149
+ on Windows, `.venv-wsl` in WSL) so bare `uv` commands and the git hooks use
150
+ the same venv as make. See `CLAUDE.md`. (Skipping this only matters for a
151
+ shared Windows/WSL checkout — for trying the app, uv's default `.venv` is
152
+ fine.)
153
+
154
+ ## Everyday commands
155
+
156
+ ```sh
157
+ make check # all merge gates: ruff, format, mypy --strict, pytest (>=90% cov), dep age
158
+ make fix # auto-fix lint issues and reformat
159
+ make test # tests with coverage
160
+ make deps # the ONLY way to add/upgrade dependencies (7-day cooldown lock)
161
+ make audit # pip-audit the lockfile for known CVEs
162
+ ```
163
+
164
+ Dependencies are never added with plain `uv add`/`uv lock`: run `make deps`
165
+ so the 7-day supply-chain cooldown is applied. CI runs every `make check`
166
+ gate plus `make audit` and the SonarCloud quality gate on each PR — so a
167
+ clean `make check` locally does not quite guarantee a green pipeline. See
168
+ `CLAUDE.md` for the full policies.
169
+
170
+ ## Releases
171
+
172
+ Releases are `vX.Y.Z` tags on `main`; each GitHub Release carries its
173
+ notes from [`CHANGELOG.md`](CHANGELOG.md). There are no packaged builds
174
+ yet — run from source as above.
175
+
176
+ ## Data licences
177
+
178
+ The code is MIT-licensed (see `LICENSE`). One data file is not: the
179
+ historical return series
180
+ (`src/glidepath/regions/uk/data/returns_history.toml`) is derived from
181
+ the [JST Macrohistory Database](https://www.macrohistory.net/database/)
182
+ (Jordà, Schularick & Taylor; return series per Jordà, Knoll, Kuvshinov,
183
+ Schularick & Taylor 2019) and is distributed under
184
+ [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) —
185
+ attribution required, non-commercial use only, share-alike. The package
186
+ metadata declares `MIT AND CC-BY-NC-SA-4.0` accordingly; `LICENSE-DATA`
187
+ and the file's own header carry the full notice, and
188
+ `scripts/build_returns_history.py` regenerates the file from the
189
+ upstream dataset.
@@ -0,0 +1,178 @@
1
+ <p align="center">
2
+ <img src="src/glidepath/gui/assets/wordmark.png" alt="glidepath" width="420">
3
+ </p>
4
+
5
+ [![CI](https://github.com/williajm/glidepath/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
6
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=williajm_glidepath&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
7
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=williajm_glidepath&metric=coverage)](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
8
+ [![Python 3.14](https://img.shields.io/badge/python-3.14-blue)](https://github.com/williajm/glidepath/blob/main/.python-version)
9
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
10
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
11
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
12
+ [![License: MIT](https://img.shields.io/github/license/williajm/glidepath)](LICENSE)
13
+
14
+ <p align="center">
15
+ <a href="https://williajm.github.io/glidepath/"><b>williajm.github.io/glidepath</b></a>
16
+ </p>
17
+
18
+ A desktop retirement and investment planner. UK-first, built so other
19
+ regions can be added later. Every number in a plan is a **fact** you
20
+ stated, a **decision** you made, or an **assumption** the app defaulted
21
+ — always inspectable, never silently guessed. All data stays local;
22
+ nothing is transmitted.
23
+
24
+ <p align="center">
25
+ <img src="docs/screenshots/charts.png"
26
+ alt="The charts tab: the Monte Carlo fan chart — nested percentile bands deepening toward the median line — beside the success-rate readout and the retirement-age and backtest cards"
27
+ width="800">
28
+ </p>
29
+
30
+ *(All screenshots show example data, not anyone's real finances.)*
31
+
32
+ What it models today (single person, UK):
33
+
34
+ - **Wrappers** — workplace DC, SIPP, S&S ISA, LISA, GIA and cash, with
35
+ UK contribution relief mechanics and dividend/savings taxation.
36
+ - **Defined benefit pensions** — deferred entitlements or active
37
+ CARE-style accrual, with revaluation, early/late factors and
38
+ commutation; **state pension** from your official DWP forecast,
39
+ including deferral.
40
+ - **Tax** — rUK and Scottish income tax from verified 2026/27 data
41
+ files; pension allowances (AA/taper/MPAA, lump-sum allowance).
42
+ - **Projection** — deterministic or Monte Carlo runs from the app:
43
+ success rate, probability of ruin, ending-pot percentiles, and a
44
+ probability fan chart on its own tab, reproducible from a seed. With
45
+ a de-risking glide path, tax-aware decumulation with optional
46
+ go-go/slow-go/no-go retirement spending multipliers, and annuity
47
+ purchases entered in the facts form. (The engine also models
48
+ alternative withdrawal strategies — fixed %, guardrails, natural
49
+ yield — and tax-free-cash strategies; the app currently runs the
50
+ fixed-real defaults, with no strategy picker in the UI yet.)
51
+ - **"When can I retire?"** — a solver for the earliest retirement age
52
+ that sustains a target income (a replacement rate you choose, 66% of
53
+ employment income by default), met deterministically or at a Monte
54
+ Carlo success target.
55
+ - **Historical backtesting** — replays the plan over every rolling
56
+ window of world market history since 1900 (global equities in
57
+ sterling terms, UK gilts and cash, deflated by UK inflation):
58
+ the share of historical starting years the plan survives, the worst
59
+ starting year, and the range of outcomes as chart bands —
60
+ sequence-of-returns risk that independent Monte Carlo draws miss.
61
+ - **Scenarios** — named what-ifs over your decisions and assumptions,
62
+ with a side-by-side comparison; plans saved as a local JSON file.
63
+
64
+ ## More screenshots
65
+
66
+ Facts entry — everything on this screen is either a fact you state or
67
+ a choice you make; anything estimated lives in the assumptions
68
+ inspector instead:
69
+
70
+ <p align="center">
71
+ <img src="docs/screenshots/facts.png"
72
+ alt="The facts tab: the About you, Household spending, and State pension cards of the entry form, with the example plan's values filled in"
73
+ width="800">
74
+ </p>
75
+
76
+ Stated vs assumed — the provenance view: the facts you stated, the
77
+ choices in effect, and every assumption the run used with its value,
78
+ default/overridden status, source, and date:
79
+
80
+ <p align="center">
81
+ <img src="docs/screenshots/stated_vs_assumed.png"
82
+ alt="The stated-vs-assumed tab: tables of stated facts, choices in effect, assumptions used with sources, and the plan structure"
83
+ width="800">
84
+ </p>
85
+
86
+ ## Disclaimer
87
+
88
+ Glidepath is a personal modelling tool for exploring retirement scenarios.
89
+ It is not financial advice and is not regulated; its outputs depend on
90
+ assumptions that will not match reality. Do not make financial decisions
91
+ based solely on this tool.
92
+
93
+ ## Run
94
+
95
+ Requires only [uv](https://docs.astral.sh/uv/). Install the latest
96
+ release [from PyPI](https://pypi.org/project/glidepath/) and launch it:
97
+
98
+ ```sh
99
+ uv tool install glidepath
100
+ glidepath
101
+ ```
102
+
103
+ (`pipx install glidepath` works too. Glidepath is a desktop app
104
+ distributed through PyPI, not a library — importing `glidepath`
105
+ modules is not a supported API.)
106
+
107
+ Or run straight from a checkout:
108
+
109
+ ```sh
110
+ git clone https://github.com/williajm/glidepath.git
111
+ cd glidepath
112
+ uv run glidepath
113
+ ```
114
+
115
+ The first run from a checkout creates a virtual environment and
116
+ installs the locked dependencies automatically, then launches the
117
+ desktop app. Accept the
118
+ disclaimer and a fresh install
119
+ opens with an example plan already projected, so every tab has
120
+ something to show — replace its values with your own facts, or clear
121
+ the form and start blank. Charts label each bar with the tax year and
122
+ your age and switch between today's money and nominal; Help → "How to
123
+ use glidepath" walks through every tab. Save your plan from the File
124
+ menu as a `.glidepath.json` file you own, stored wherever you choose;
125
+ the next launch reopens your last plan automatically.
126
+
127
+ ## Developing
128
+
129
+ Development additionally requires GNU Make, which drives every workflow
130
+ command:
131
+
132
+ ```sh
133
+ make sync # create the platform venv from the lockfile (fails if the lock has drifted)
134
+ make hooks # install the pre-commit hooks (required before committing)
135
+ ```
136
+
137
+ One-time per machine: set `UV_PROJECT_ENVIRONMENT` user-wide (`.venv-win`
138
+ on Windows, `.venv-wsl` in WSL) so bare `uv` commands and the git hooks use
139
+ the same venv as make. See `CLAUDE.md`. (Skipping this only matters for a
140
+ shared Windows/WSL checkout — for trying the app, uv's default `.venv` is
141
+ fine.)
142
+
143
+ ## Everyday commands
144
+
145
+ ```sh
146
+ make check # all merge gates: ruff, format, mypy --strict, pytest (>=90% cov), dep age
147
+ make fix # auto-fix lint issues and reformat
148
+ make test # tests with coverage
149
+ make deps # the ONLY way to add/upgrade dependencies (7-day cooldown lock)
150
+ make audit # pip-audit the lockfile for known CVEs
151
+ ```
152
+
153
+ Dependencies are never added with plain `uv add`/`uv lock`: run `make deps`
154
+ so the 7-day supply-chain cooldown is applied. CI runs every `make check`
155
+ gate plus `make audit` and the SonarCloud quality gate on each PR — so a
156
+ clean `make check` locally does not quite guarantee a green pipeline. See
157
+ `CLAUDE.md` for the full policies.
158
+
159
+ ## Releases
160
+
161
+ Releases are `vX.Y.Z` tags on `main`; each GitHub Release carries its
162
+ notes from [`CHANGELOG.md`](CHANGELOG.md). There are no packaged builds
163
+ yet — run from source as above.
164
+
165
+ ## Data licences
166
+
167
+ The code is MIT-licensed (see `LICENSE`). One data file is not: the
168
+ historical return series
169
+ (`src/glidepath/regions/uk/data/returns_history.toml`) is derived from
170
+ the [JST Macrohistory Database](https://www.macrohistory.net/database/)
171
+ (Jordà, Schularick & Taylor; return series per Jordà, Knoll, Kuvshinov,
172
+ Schularick & Taylor 2019) and is distributed under
173
+ [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) —
174
+ attribution required, non-commercial use only, share-alike. The package
175
+ metadata declares `MIT AND CC-BY-NC-SA-4.0` accordingly; `LICENSE-DATA`
176
+ and the file's own header carry the full notice, and
177
+ `scripts/build_returns_history.py` regenerates the file from the
178
+ upstream dataset.
@@ -0,0 +1,153 @@
1
+ [project]
2
+ name = "glidepath"
3
+ version = "0.2.0"
4
+ description = "Desktop retirement and investment planner (UK-first, region-extensible)."
5
+ readme = "README.md"
6
+ # MIT code plus one CC BY-NC-SA 4.0 data file (returns_history.toml —
7
+ # scope and attribution in LICENSE-DATA).
8
+ license = "MIT AND CC-BY-NC-SA-4.0"
9
+ license-files = ["LICENSE", "LICENSE-DATA"]
10
+ requires-python = ">=3.14"
11
+ dependencies = [
12
+ "pyside6>=6.11.1",
13
+ ]
14
+
15
+ [project.gui-scripts]
16
+ glidepath = "glidepath.gui.main:main"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "hypothesis",
21
+ "mypy",
22
+ "pip-audit",
23
+ "pre-commit",
24
+ "pytest",
25
+ "pytest-cov",
26
+ "pytest-timeout",
27
+ "ruff",
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.11,<0.13"]
32
+ build-backend = "uv_build"
33
+
34
+ # --- uv ---------------------------------------------------------------------
35
+ [tool.uv]
36
+ # Supply-chain cooldown: resolution ignores anything published to PyPI after
37
+ # this timestamp (now minus 7 days). Updated ONLY by `make deps`.
38
+ exclude-newer = "2026-07-30T09:40:45Z"
39
+
40
+ # PyPI is the ONLY permitted index (supply-chain policy; see CLAUDE.md).
41
+ [[tool.uv.index]]
42
+ name = "pypi"
43
+ url = "https://pypi.org/simple"
44
+ default = true
45
+
46
+ # --- ruff -------------------------------------------------------------------
47
+ [tool.ruff]
48
+ src = ["src", "tests", "scripts"]
49
+
50
+ [tool.ruff.lint]
51
+ select = ["ALL"]
52
+ ignore = [
53
+ "COM812", # Redundant with (and conflicts with) the ruff formatter.
54
+ "ISC001", # Redundant with (and conflicts with) the ruff formatter.
55
+ "CPY001", # No per-file copyright headers; repository-level licensing applies.
56
+ ]
57
+
58
+ [tool.ruff.lint.pydocstyle]
59
+ convention = "google"
60
+
61
+ [tool.ruff.lint.mccabe]
62
+ max-complexity = 10
63
+
64
+ [tool.ruff.lint.per-file-ignores]
65
+ "src/glidepath/core/randomness.py" = [
66
+ "S311", # Seeded Monte Carlo simulation draws, never security material (planning §4.6).
67
+ ]
68
+ "src/glidepath/core/montecarlo.py" = [
69
+ "PLR0913", # run_paths/sustainable_income take the §5.2 engine quartet plus keyword-only run knobs; bundling would obscure the run(plan, assumptions, region, config) shape.
70
+ ]
71
+ "src/glidepath/core/backtest.py" = [
72
+ "PLR0913", # run_windows and its chunk worker mirror the §5.2 runner signature (see core/montecarlo.py).
73
+ ]
74
+ "scripts/build_returns_history.py" = [
75
+ "S314", # xml.etree parses the JST workbook the developer supplies locally, never untrusted input.
76
+ ]
77
+ "src/glidepath/core/retirement.py" = [
78
+ "PLR0913", # earliest_retirement_age mirrors the §5.2 runner signature (see core/montecarlo.py).
79
+ ]
80
+ "src/glidepath/app/montecarlo.py" = [
81
+ "BLE001", # The slow-run transition must fold ANY failure into state (§4.7): an exception escaping past the shell's worker thread holds the in-flight guard forever.
82
+ ]
83
+ "src/glidepath/app/retirement.py" = [
84
+ "BLE001", # Same in-flight-guard rationale as app/montecarlo.py.
85
+ ]
86
+ "src/glidepath/app/drawdown.py" = [
87
+ "BLE001", # Same in-flight-guard rationale as app/montecarlo.py.
88
+ ]
89
+ "src/glidepath/app/backtest.py" = [
90
+ "BLE001", # Same in-flight-guard rationale as app/montecarlo.py.
91
+ ]
92
+ "tests/**" = [
93
+ "S101", # pytest tests use bare assert by design.
94
+ "PLR2004", # Literal expected values in assertions are idiomatic in tests.
95
+ "INP001", # tests/ is intentionally not a package (pytest src-layout).
96
+ "PLR0913", # Test-data factories take many optional keyword knobs by design.
97
+ ]
98
+ "scripts/**" = [
99
+ "T201", # Standalone CLI scripts report via print.
100
+ "INP001", # scripts/ is intentionally not a package.
101
+ ]
102
+
103
+ # --- mypy -------------------------------------------------------------------
104
+ [tool.mypy]
105
+ strict = true
106
+ warn_unused_ignores = true
107
+ python_version = "3.14"
108
+ files = ["src", "tests", "scripts"]
109
+ # Lets the tests' top-level script imports (e.g. `import check_dep_age`)
110
+ # resolve to scripts/ — mirrors pytest's pythonpath below.
111
+ mypy_path = ["scripts"]
112
+
113
+ # --- pytest / coverage ------------------------------------------------------
114
+ [tool.pytest.ini_options]
115
+ addopts = [
116
+ "-ra",
117
+ "--strict-markers",
118
+ "--strict-config",
119
+ "--cov=glidepath",
120
+ "--cov=scripts",
121
+ "--cov-report=term-missing",
122
+ "--cov-report=xml",
123
+ ]
124
+ testpaths = ["tests"]
125
+ # scripts/ is not a package; tests import the operational scripts (the
126
+ # gates behind `make deps`/`make bump`/releases) as top-level modules.
127
+ pythonpath = ["scripts"]
128
+ # Fail a stuck test fast instead of hanging CI for the runner limit: the
129
+ # GUI suite waits on worker pools for up to 60s, so the ceiling sits
130
+ # safely above that while still cutting a wedged run short.
131
+ timeout = 120
132
+ markers = [
133
+ "gui: exercises the PySide6 shell on the offscreen Qt platform",
134
+ "slow: long-running engine sweeps (Monte Carlo, backtest, solvers); deselect with -m 'not slow'",
135
+ ]
136
+
137
+ [tool.coverage.run]
138
+ branch = true
139
+ # The Monte Carlo performance harness is a manual measurement tool
140
+ # (planning §4.6), run by hand and read by a human — the only scripts/
141
+ # entry outside the coverage gate.
142
+ omit = ["scripts/measure_mc_performance.py"]
143
+
144
+ [tool.coverage.report]
145
+ # The suite sits at ~98%; a floor close behind it means an entire module
146
+ # losing its tests fails CI instead of coasting on the old 90% margin.
147
+ fail_under = 96
148
+ show_missing = true
149
+ exclude_lines = [
150
+ "pragma: no cover",
151
+ # Script entry-point guards: the tests call main() directly.
152
+ 'if __name__ == "__main__":',
153
+ ]
@@ -0,0 +1,3 @@
1
+ """glidepath: a desktop retirement and investment planner (UK-first)."""
2
+
3
+ __version__ = "0.1.0"