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.
- glidepath-0.2.0/LICENSE +21 -0
- glidepath-0.2.0/LICENSE-DATA +28 -0
- glidepath-0.2.0/PKG-INFO +189 -0
- glidepath-0.2.0/README.md +178 -0
- glidepath-0.2.0/pyproject.toml +153 -0
- glidepath-0.2.0/src/glidepath/__init__.py +3 -0
- glidepath-0.2.0/src/glidepath/app/__init__.py +364 -0
- glidepath-0.2.0/src/glidepath/app/backtest.py +281 -0
- glidepath-0.2.0/src/glidepath/app/charts.py +759 -0
- glidepath-0.2.0/src/glidepath/app/copy.py +174 -0
- glidepath-0.2.0/src/glidepath/app/display.py +148 -0
- glidepath-0.2.0/src/glidepath/app/drawdown.py +436 -0
- glidepath-0.2.0/src/glidepath/app/example.py +66 -0
- glidepath-0.2.0/src/glidepath/app/exports.py +487 -0
- glidepath-0.2.0/src/glidepath/app/files.py +249 -0
- glidepath-0.2.0/src/glidepath/app/firstrun.py +114 -0
- glidepath-0.2.0/src/glidepath/app/forms.py +1750 -0
- glidepath-0.2.0/src/glidepath/app/inspector.py +506 -0
- glidepath-0.2.0/src/glidepath/app/labels.py +66 -0
- glidepath-0.2.0/src/glidepath/app/montecarlo.py +399 -0
- glidepath-0.2.0/src/glidepath/app/plan.py +354 -0
- glidepath-0.2.0/src/glidepath/app/retirement.py +446 -0
- glidepath-0.2.0/src/glidepath/app/scenarios.py +831 -0
- glidepath-0.2.0/src/glidepath/app/shell.py +185 -0
- glidepath-0.2.0/src/glidepath/app/tables.py +138 -0
- glidepath-0.2.0/src/glidepath/core/__init__.py +390 -0
- glidepath-0.2.0/src/glidepath/core/annuities.py +240 -0
- glidepath-0.2.0/src/glidepath/core/backtest.py +514 -0
- glidepath-0.2.0/src/glidepath/core/comparison.py +278 -0
- glidepath-0.2.0/src/glidepath/core/config.py +82 -0
- glidepath-0.2.0/src/glidepath/core/contributions.py +337 -0
- glidepath-0.2.0/src/glidepath/core/engine.py +2811 -0
- glidepath-0.2.0/src/glidepath/core/entities.py +264 -0
- glidepath-0.2.0/src/glidepath/core/glide.py +289 -0
- glidepath-0.2.0/src/glidepath/core/investments.py +175 -0
- glidepath-0.2.0/src/glidepath/core/money.py +107 -0
- glidepath-0.2.0/src/glidepath/core/montecarlo.py +609 -0
- glidepath-0.2.0/src/glidepath/core/pensions.py +298 -0
- glidepath-0.2.0/src/glidepath/core/periods.py +367 -0
- glidepath-0.2.0/src/glidepath/core/provenance.py +271 -0
- glidepath-0.2.0/src/glidepath/core/randomness.py +128 -0
- glidepath-0.2.0/src/glidepath/core/region.py +46 -0
- glidepath-0.2.0/src/glidepath/core/reporting.py +231 -0
- glidepath-0.2.0/src/glidepath/core/results.py +504 -0
- glidepath-0.2.0/src/glidepath/core/retirement.py +291 -0
- glidepath-0.2.0/src/glidepath/core/returns.py +312 -0
- glidepath-0.2.0/src/glidepath/core/scenarios.py +579 -0
- glidepath-0.2.0/src/glidepath/core/state_pension.py +264 -0
- glidepath-0.2.0/src/glidepath/core/tax.py +139 -0
- glidepath-0.2.0/src/glidepath/core/withdrawals.py +461 -0
- glidepath-0.2.0/src/glidepath/core/wrappers.py +278 -0
- glidepath-0.2.0/src/glidepath/gui/__init__.py +6 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_128.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_16.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_24.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_256.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_32.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_48.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/icon_64.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/assets/wordmark.png +0 -0
- glidepath-0.2.0/src/glidepath/gui/charts.py +829 -0
- glidepath-0.2.0/src/glidepath/gui/forms.py +359 -0
- glidepath-0.2.0/src/glidepath/gui/inspector.py +186 -0
- glidepath-0.2.0/src/glidepath/gui/main.py +51 -0
- glidepath-0.2.0/src/glidepath/gui/scenarios.py +402 -0
- glidepath-0.2.0/src/glidepath/gui/style.py +376 -0
- glidepath-0.2.0/src/glidepath/gui/tableview.py +67 -0
- glidepath-0.2.0/src/glidepath/gui/widgets.py +989 -0
- glidepath-0.2.0/src/glidepath/persistence/__init__.py +48 -0
- glidepath-0.2.0/src/glidepath/persistence/assumptions.py +112 -0
- glidepath-0.2.0/src/glidepath/persistence/decode.py +747 -0
- glidepath-0.2.0/src/glidepath/persistence/document.py +101 -0
- glidepath-0.2.0/src/glidepath/persistence/encode.py +433 -0
- glidepath-0.2.0/src/glidepath/persistence/migrations.py +158 -0
- glidepath-0.2.0/src/glidepath/persistence/values.py +298 -0
- glidepath-0.2.0/src/glidepath/py.typed +0 -0
- glidepath-0.2.0/src/glidepath/regions/__init__.py +7 -0
- glidepath-0.2.0/src/glidepath/regions/uk/__init__.py +189 -0
- glidepath-0.2.0/src/glidepath/regions/uk/ages.py +156 -0
- glidepath-0.2.0/src/glidepath/regions/uk/contributions.py +717 -0
- glidepath-0.2.0/src/glidepath/regions/uk/data/age_rules.toml +78 -0
- glidepath-0.2.0/src/glidepath/regions/uk/data/assumptions_default.toml +170 -0
- glidepath-0.2.0/src/glidepath/regions/uk/data/returns_history.toml +150 -0
- glidepath-0.2.0/src/glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
- glidepath-0.2.0/src/glidepath/regions/uk/extension.py +479 -0
- glidepath-0.2.0/src/glidepath/regions/uk/loader.py +704 -0
- glidepath-0.2.0/src/glidepath/regions/uk/region.py +160 -0
- glidepath-0.2.0/src/glidepath/regions/uk/schema.py +563 -0
- glidepath-0.2.0/src/glidepath/regions/uk/state_pension.py +129 -0
- glidepath-0.2.0/src/glidepath/regions/uk/tax.py +466 -0
- glidepath-0.2.0/src/glidepath/regions/uk/wrappers.py +283 -0
- glidepath-0.2.0/src/glidepath/regions/uk/years.py +92 -0
glidepath-0.2.0/LICENSE
ADDED
|
@@ -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.
|
glidepath-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
|
|
17
|
+
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
18
|
+
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
19
|
+
[](https://github.com/williajm/glidepath/blob/main/.python-version)
|
|
20
|
+
[](https://github.com/astral-sh/uv)
|
|
21
|
+
[](https://github.com/astral-sh/ruff)
|
|
22
|
+
[](https://mypy-lang.org/)
|
|
23
|
+
[](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
|
+
[](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
|
|
6
|
+
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
7
|
+
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
8
|
+
[](https://github.com/williajm/glidepath/blob/main/.python-version)
|
|
9
|
+
[](https://github.com/astral-sh/uv)
|
|
10
|
+
[](https://github.com/astral-sh/ruff)
|
|
11
|
+
[](https://mypy-lang.org/)
|
|
12
|
+
[](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
|
+
]
|