glidepath 0.5.0__tar.gz → 0.6.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.5.0 → glidepath-0.6.0}/PKG-INFO +8 -3
- {glidepath-0.5.0 → glidepath-0.6.0}/README.md +7 -2
- {glidepath-0.5.0 → glidepath-0.6.0}/pyproject.toml +1 -1
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/__init__.py +16 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/backtest.py +3 -3
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/copy.py +27 -13
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/display.py +33 -7
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/forms.py +276 -12
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/montecarlo.py +5 -3
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/outlook.py +125 -30
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/plan.py +33 -2
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/__init__.py +4 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/entities.py +12 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/results.py +7 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/withdrawals.py +65 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/forms.py +322 -53
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/style.py +17 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/widgets.py +7 -3
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/decode.py +25 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/document.py +6 -1
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/encode.py +14 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/migrations.py +16 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/values.py +9 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/LICENSE +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/LICENSE-DATA +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/__init__.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/charts.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/drawdown.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/example.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/exports.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/files.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/firstrun.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/inspector.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/labels.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/retirement.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/scenarios.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/shell.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/app/tables.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/annuities.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/backtest.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/comparison.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/config.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/contributions.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/engine.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/glide.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/investments.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/money.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/montecarlo.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/pensions.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/periods.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/provenance.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/randomness.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/region.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/reporting.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/retirement.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/returns.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/scenarios.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/state_pension.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/tax.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/core/wrappers.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/__init__.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_128.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_16.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_24.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_256.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_32.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_48.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/icon_64.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/assets/wordmark.png +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/charts.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/inspector.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/main.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/scenarios.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/gui/tableview.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/__init__.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/persistence/assumptions.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/py.typed +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/__init__.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/__init__.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/ages.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/contributions.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/data/age_rules.toml +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/data/assumptions_default.toml +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/data/returns_history.toml +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/data/tax_year_2026_27.toml +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/extension.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/loader.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/region.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/schema.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/state_pension.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/tax.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/wrappers.py +0 -0
- {glidepath-0.5.0 → glidepath-0.6.0}/src/glidepath/regions/uk/years.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glidepath
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Desktop retirement and investment planner (UK-first, region-extensible).
|
|
5
5
|
Keywords: retirement,pension,financial-planning,investment,uk,sipp,isa,monte-carlo
|
|
6
6
|
Author: James Williams
|
|
@@ -32,10 +32,12 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
<img src="https://raw.githubusercontent.com/williajm/glidepath/main/src/glidepath/gui/assets/wordmark.png" alt="glidepath" width="420">
|
|
33
33
|
</p>
|
|
34
34
|
|
|
35
|
+
[](https://pypi.org/project/glidepath/)
|
|
35
36
|
[](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
|
|
36
37
|
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
37
38
|
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
38
|
-
[](https://scorecard.dev/viewer/?uri=github.com/williajm/glidepath)
|
|
40
|
+
[](https://pypi.org/project/glidepath/)
|
|
39
41
|
[](https://github.com/astral-sh/uv)
|
|
40
42
|
[](https://github.com/astral-sh/ruff)
|
|
41
43
|
[](https://mypy-lang.org/)
|
|
@@ -203,7 +205,10 @@ carrying its notes from
|
|
|
203
205
|
[`CHANGELOG.md`](https://github.com/williajm/glidepath/blob/main/CHANGELOG.md)
|
|
204
206
|
with the same artifacts attached, each carrying signed build
|
|
205
207
|
provenance — verify a downloaded file with
|
|
206
|
-
`gh attestation verify <file> -R williajm/glidepath`.
|
|
208
|
+
`gh attestation verify <file> -R williajm/glidepath`. The signed
|
|
209
|
+
provenance bundle is attached to each release too
|
|
210
|
+
(`glidepath-X.Y.Z-provenance.intoto.jsonl`), so verification also
|
|
211
|
+
works offline with `--bundle`. There are no
|
|
207
212
|
packaged binary builds (installer/exe) yet — install from PyPI as
|
|
208
213
|
above.
|
|
209
214
|
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
<img src="https://raw.githubusercontent.com/williajm/glidepath/main/src/glidepath/gui/assets/wordmark.png" alt="glidepath" width="420">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
[](https://pypi.org/project/glidepath/)
|
|
7
8
|
[](https://github.com/williajm/glidepath/actions/workflows/ci.yml)
|
|
8
9
|
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
9
10
|
[](https://sonarcloud.io/summary/new_code?id=williajm_glidepath)
|
|
10
|
-
[](https://scorecard.dev/viewer/?uri=github.com/williajm/glidepath)
|
|
12
|
+
[](https://pypi.org/project/glidepath/)
|
|
11
13
|
[](https://github.com/astral-sh/uv)
|
|
12
14
|
[](https://github.com/astral-sh/ruff)
|
|
13
15
|
[](https://mypy-lang.org/)
|
|
@@ -175,7 +177,10 @@ carrying its notes from
|
|
|
175
177
|
[`CHANGELOG.md`](https://github.com/williajm/glidepath/blob/main/CHANGELOG.md)
|
|
176
178
|
with the same artifacts attached, each carrying signed build
|
|
177
179
|
provenance — verify a downloaded file with
|
|
178
|
-
`gh attestation verify <file> -R williajm/glidepath`.
|
|
180
|
+
`gh attestation verify <file> -R williajm/glidepath`. The signed
|
|
181
|
+
provenance bundle is attached to each release too
|
|
182
|
+
(`glidepath-X.Y.Z-provenance.intoto.jsonl`), so verification also
|
|
183
|
+
works offline with `--bundle`. There are no
|
|
179
184
|
packaged binary builds (installer/exe) yet — install from PyPI as
|
|
180
185
|
above.
|
|
181
186
|
|
|
@@ -105,11 +105,17 @@ from glidepath.app.firstrun import (
|
|
|
105
105
|
)
|
|
106
106
|
from glidepath.app.forms import (
|
|
107
107
|
ENTITY_ID_KEY,
|
|
108
|
+
HIDE_ADVANCED_LABEL,
|
|
109
|
+
INCOME_PREFERENCE_ANNUITY,
|
|
110
|
+
INCOME_PREFERENCE_KEY,
|
|
108
111
|
OWNER_KEY,
|
|
112
|
+
REQUIRED_MARKER,
|
|
113
|
+
SHOW_ADVANCED_LABEL,
|
|
109
114
|
ChoiceOption,
|
|
110
115
|
FactsFormData,
|
|
111
116
|
FactsFormResult,
|
|
112
117
|
FactsFormViewModel,
|
|
118
|
+
FactsSubmissionOutcome,
|
|
113
119
|
FieldKind,
|
|
114
120
|
FieldSpec,
|
|
115
121
|
FormError,
|
|
@@ -150,6 +156,7 @@ from glidepath.app.montecarlo import (
|
|
|
150
156
|
state_with_monte_carlo,
|
|
151
157
|
)
|
|
152
158
|
from glidepath.app.outlook import (
|
|
159
|
+
DETERMINISTIC_BASIS_SENTENCE,
|
|
153
160
|
NO_OUTLOOK_MESSAGE,
|
|
154
161
|
OUTLOOK_HEADING,
|
|
155
162
|
OUTLOOK_NO_PLAN_MESSAGE,
|
|
@@ -162,6 +169,7 @@ from glidepath.app.plan import (
|
|
|
162
169
|
PlanState,
|
|
163
170
|
facts_saved_message,
|
|
164
171
|
initial_plan_state,
|
|
172
|
+
plan_run_config,
|
|
165
173
|
state_marked_saved,
|
|
166
174
|
state_with_household,
|
|
167
175
|
state_with_override,
|
|
@@ -219,6 +227,7 @@ __all__ = [
|
|
|
219
227
|
"DEFAULT_CHART_BASIS",
|
|
220
228
|
"DEFAULT_COMPARISON_METRIC_KEY",
|
|
221
229
|
"DEFAULT_RUN_MODE",
|
|
230
|
+
"DETERMINISTIC_BASIS_SENTENCE",
|
|
222
231
|
"DISCLAIMER_ACCEPT_LABEL",
|
|
223
232
|
"DISCLAIMER_BODY",
|
|
224
233
|
"DISCLAIMER_DECLINE_LABEL",
|
|
@@ -233,6 +242,9 @@ __all__ = [
|
|
|
233
242
|
"HELP_GUIDE_SECTIONS",
|
|
234
243
|
"HELP_GUIDE_TITLE",
|
|
235
244
|
"HELP_MENU_LABEL",
|
|
245
|
+
"HIDE_ADVANCED_LABEL",
|
|
246
|
+
"INCOME_PREFERENCE_ANNUITY",
|
|
247
|
+
"INCOME_PREFERENCE_KEY",
|
|
236
248
|
"MONTE_CARLO_CHART_TITLE",
|
|
237
249
|
"MONTE_CARLO_NO_PLAN_MESSAGE",
|
|
238
250
|
"MONTE_CARLO_PATHS_MESSAGE",
|
|
@@ -253,6 +265,7 @@ __all__ = [
|
|
|
253
265
|
"OWNER_KEY",
|
|
254
266
|
"REPORT_EXPORT_FAILED_PREFIX",
|
|
255
267
|
"REPORT_NOT_WRITTEN_MESSAGE",
|
|
268
|
+
"REQUIRED_MARKER",
|
|
256
269
|
"RETIREMENT_NO_INCOME_MESSAGE",
|
|
257
270
|
"RETIREMENT_NO_PLAN_MESSAGE",
|
|
258
271
|
"RETIREMENT_RATE_MESSAGE",
|
|
@@ -260,6 +273,7 @@ __all__ = [
|
|
|
260
273
|
"RETIREMENT_STALE_MESSAGE",
|
|
261
274
|
"RETIREMENT_SUCCESS_MESSAGE",
|
|
262
275
|
"RUN_FAILED_PREFIX",
|
|
276
|
+
"SHOW_ADVANCED_LABEL",
|
|
263
277
|
"TABLE_VIEW_LABEL",
|
|
264
278
|
"UNSAVED_CHANGES_PROMPT",
|
|
265
279
|
"UNSAVED_CHANGES_TITLE",
|
|
@@ -286,6 +300,7 @@ __all__ = [
|
|
|
286
300
|
"FactsFormData",
|
|
287
301
|
"FactsFormResult",
|
|
288
302
|
"FactsFormViewModel",
|
|
303
|
+
"FactsSubmissionOutcome",
|
|
289
304
|
"FieldKind",
|
|
290
305
|
"FieldSpec",
|
|
291
306
|
"FileMenuViewModel",
|
|
@@ -357,6 +372,7 @@ __all__ = [
|
|
|
357
372
|
"path_pool",
|
|
358
373
|
"plan_display_name",
|
|
359
374
|
"plan_entity_ids",
|
|
375
|
+
"plan_run_config",
|
|
360
376
|
"record_disclaimer_acknowledged",
|
|
361
377
|
"record_last_plan_path",
|
|
362
378
|
"report_exported_message",
|
|
@@ -19,8 +19,8 @@ from typing import TYPE_CHECKING, Any, Final
|
|
|
19
19
|
|
|
20
20
|
from glidepath.app.display import format_money, format_percent
|
|
21
21
|
from glidepath.app.montecarlo import BAND_SPECS, SUCCESS_RATE_LABEL
|
|
22
|
-
from glidepath.app.plan import PlanState, region_for, replanned_state
|
|
23
|
-
from glidepath.core import Money,
|
|
22
|
+
from glidepath.app.plan import PlanState, plan_run_config, region_for, replanned_state
|
|
23
|
+
from glidepath.core import Money, run_windows
|
|
24
24
|
from glidepath.regions.uk import load_returns_history
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -122,7 +122,7 @@ def state_with_backtest(state: PlanState, *, today: date) -> PlanState:
|
|
|
122
122
|
today=today,
|
|
123
123
|
modified=state.modified,
|
|
124
124
|
)
|
|
125
|
-
config =
|
|
125
|
+
config = plan_run_config(state.household, today=today)
|
|
126
126
|
try:
|
|
127
127
|
series = load_returns_history().series
|
|
128
128
|
result = run_windows(
|
|
@@ -58,15 +58,23 @@ HELP_GUIDE_SECTIONS: Final[tuple[tuple[str, str], ...]] = (
|
|
|
58
58
|
"pension, SIPP, ISA, LISA, general account, or cash) with its "
|
|
59
59
|
"balance, contributions, and optionally its own equity "
|
|
60
60
|
"allocation percentage — blank follows the de-risking glide "
|
|
61
|
-
"path, 100 models an all-equity wrapper — any defined "
|
|
62
|
-
"benefit pension
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
61
|
+
"path, 100 models an all-equity wrapper — and any defined "
|
|
62
|
+
"benefit pension. "
|
|
63
|
+
"The Retirement income section holds two choices: a "
|
|
64
|
+
"preference between drawdown only (the default — everything "
|
|
65
|
+
"stays invested and is withdrawn as needed) and an annuity "
|
|
66
|
+
"or a mix, which adds a section right below to plan annuity "
|
|
67
|
+
"purchases — converting a percentage of your pension pot "
|
|
68
|
+
"into lifetime income at an age you choose, 100% "
|
|
69
|
+
"annuitising the whole pot and several purchases at "
|
|
70
|
+
"different ages annuitising in stages — and a drawdown withdrawal "
|
|
71
|
+
"strategy: fixed real spending (the default), a fixed "
|
|
72
|
+
"percentage of the pot, guardrails, or natural yield. "
|
|
73
|
+
"Add one wrapper, DB, or annuity section per item. "
|
|
74
|
+
"Fields marked * are required, hovering any field repeats "
|
|
75
|
+
'its guidance, and each section\'s "More options" reveals '
|
|
76
|
+
"its rarely needed fields — anything already filled in, or "
|
|
77
|
+
"in error, is shown automatically. Dates can "
|
|
70
78
|
"be typed or picked from the calendar assist, and the "
|
|
71
79
|
'"as of" dates on balances and your state pension forecast '
|
|
72
80
|
"default to today when left blank. Your state pension needs "
|
|
@@ -74,8 +82,8 @@ HELP_GUIDE_SECTIONS: Final[tuple[tuple[str, str], ...]] = (
|
|
|
74
82
|
"the app never re-derives what DWP has already computed. "
|
|
75
83
|
'Press "Save facts and project" to run the '
|
|
76
84
|
"projection — if anything cannot be read, the message under "
|
|
77
|
-
"the buttons
|
|
78
|
-
"until it parses."
|
|
85
|
+
"the buttons and a note under each affected field say what "
|
|
86
|
+
"to fix, and nothing is saved until it parses."
|
|
79
87
|
),
|
|
80
88
|
),
|
|
81
89
|
(
|
|
@@ -133,7 +141,10 @@ HELP_GUIDE_SECTIONS: Final[tuple[tuple[str, str], ...]] = (
|
|
|
133
141
|
"In the projection, income already in payment — defined "
|
|
134
142
|
"benefit pension, state pension, annuity income — meets your "
|
|
135
143
|
"net spending need first, after the tax it bears. Whatever "
|
|
136
|
-
"remains is withdrawn from your wrappers
|
|
144
|
+
"remains is withdrawn from your wrappers. How *much* is "
|
|
145
|
+
"drawn each year follows the withdrawal strategy you chose "
|
|
146
|
+
"on the Facts tab (fixed real spending by default); which "
|
|
147
|
+
"wrapper it comes from follows one fixed order: "
|
|
137
148
|
"general accounts and cash first (every pound left in them "
|
|
138
149
|
"keeps accruing income tax), then ISAs and LISAs, then "
|
|
139
150
|
"already-crystallised pension funds, and uncrystallised "
|
|
@@ -143,7 +154,10 @@ HELP_GUIDE_SECTIONS: Final[tuple[tuple[str, str], ...]] = (
|
|
|
143
154
|
"which would amount to tax advice. In some years a "
|
|
144
155
|
"different order could use an allowance this one leaves "
|
|
145
156
|
"idle, so treat the projection as a consistent baseline, "
|
|
146
|
-
"not the best achievable outcome."
|
|
157
|
+
"not the best achievable outcome. The 'When can I retire?' "
|
|
158
|
+
"and 'How much can I draw down?' cards always answer their "
|
|
159
|
+
"questions in fixed real spending terms, whatever strategy "
|
|
160
|
+
"the plan itself runs."
|
|
147
161
|
),
|
|
148
162
|
),
|
|
149
163
|
(
|
|
@@ -12,7 +12,7 @@ from decimal import Decimal
|
|
|
12
12
|
from enum import Enum
|
|
13
13
|
from typing import Final
|
|
14
14
|
|
|
15
|
-
from glidepath.core import LifeStage, Money
|
|
15
|
+
from glidepath.core import LifeStage, Money, WithdrawalRule, WithdrawalRuleKind
|
|
16
16
|
|
|
17
17
|
_MAX_STRUCTURED_LENGTH = 120
|
|
18
18
|
|
|
@@ -24,6 +24,14 @@ _ENUM_LABELS: Final[Mapping[Enum, str]] = {
|
|
|
24
24
|
"""Enum members whose copy the generic underscore rule would mangle
|
|
25
25
|
("Go go"); the retirement sub-stages read hyphenated (issue #114)."""
|
|
26
26
|
|
|
27
|
+
WITHDRAWAL_RULE_NAMES: Final[Mapping[WithdrawalRuleKind, str]] = {
|
|
28
|
+
WithdrawalRuleKind.FIXED_REAL: "Fixed real spending",
|
|
29
|
+
WithdrawalRuleKind.FIXED_PERCENT: "Fixed percentage of pot",
|
|
30
|
+
WithdrawalRuleKind.GUARDRAILS: "Guardrails (cut or raise on crossings)",
|
|
31
|
+
WithdrawalRuleKind.NATURAL_YIELD: "Natural yield only",
|
|
32
|
+
}
|
|
33
|
+
"""Display names for the withdrawal-strategy decision (planning §5.1)."""
|
|
34
|
+
|
|
27
35
|
WRAPPER_KIND_NAMES: Final[Mapping[str, str]] = {
|
|
28
36
|
"uk.workplace_dc": "Workplace DC",
|
|
29
37
|
"uk.sipp": "SIPP",
|
|
@@ -86,6 +94,14 @@ def format_assumption_key(key: object) -> str:
|
|
|
86
94
|
return ASSUMPTION_NAMES.get(text, text)
|
|
87
95
|
|
|
88
96
|
|
|
97
|
+
def _format_withdrawal_rule(rule: WithdrawalRule) -> str:
|
|
98
|
+
"""The withdrawal-strategy decision's value as display text (10.3)."""
|
|
99
|
+
name = WITHDRAWAL_RULE_NAMES[rule.kind]
|
|
100
|
+
if rule.rate is None:
|
|
101
|
+
return name
|
|
102
|
+
return f"{name} ({format_share(rule.rate.value)} a year)"
|
|
103
|
+
|
|
104
|
+
|
|
89
105
|
def _format_mapping(value: Mapping[object, object]) -> str:
|
|
90
106
|
"""A structured table as compact ``key=value`` pairs, truncated."""
|
|
91
107
|
rendered = "; ".join(f"{key}={format_value(entry)}" for key, entry in value.items())
|
|
@@ -143,15 +159,25 @@ def format_value(value: object) -> str:
|
|
|
143
159
|
numbers, enum choices, policy strings, and structured tables
|
|
144
160
|
(rendered as compact ``key=value`` pairs, truncated when long).
|
|
145
161
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return format_money(value)
|
|
150
|
-
if isinstance(value, date):
|
|
151
|
-
return _format_temporal(value)
|
|
162
|
+
special = _format_special(value)
|
|
163
|
+
if special is not None:
|
|
164
|
+
return special
|
|
152
165
|
if isinstance(value, Enum):
|
|
153
166
|
generic = str(value.name).replace("_", " ").capitalize()
|
|
154
167
|
return _ENUM_LABELS.get(value, generic)
|
|
155
168
|
if isinstance(value, Mapping):
|
|
156
169
|
return _format_mapping(value)
|
|
157
170
|
return str(value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _format_special(value: object) -> str | None:
|
|
174
|
+
"""The non-enum, non-table special cases; ``None`` to fall through."""
|
|
175
|
+
if isinstance(value, bool): # before any numeric type: bool is an int
|
|
176
|
+
return "Yes" if value else "No"
|
|
177
|
+
if isinstance(value, WithdrawalRule):
|
|
178
|
+
return _format_withdrawal_rule(value)
|
|
179
|
+
if isinstance(value, Money):
|
|
180
|
+
return format_money(value)
|
|
181
|
+
if isinstance(value, date):
|
|
182
|
+
return _format_temporal(value)
|
|
183
|
+
return None
|