glidepath 0.2.0__py3-none-any.whl
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/__init__.py +3 -0
- glidepath/app/__init__.py +364 -0
- glidepath/app/backtest.py +281 -0
- glidepath/app/charts.py +759 -0
- glidepath/app/copy.py +174 -0
- glidepath/app/display.py +148 -0
- glidepath/app/drawdown.py +436 -0
- glidepath/app/example.py +66 -0
- glidepath/app/exports.py +487 -0
- glidepath/app/files.py +249 -0
- glidepath/app/firstrun.py +114 -0
- glidepath/app/forms.py +1750 -0
- glidepath/app/inspector.py +506 -0
- glidepath/app/labels.py +66 -0
- glidepath/app/montecarlo.py +399 -0
- glidepath/app/plan.py +354 -0
- glidepath/app/retirement.py +446 -0
- glidepath/app/scenarios.py +831 -0
- glidepath/app/shell.py +185 -0
- glidepath/app/tables.py +138 -0
- glidepath/core/__init__.py +390 -0
- glidepath/core/annuities.py +240 -0
- glidepath/core/backtest.py +514 -0
- glidepath/core/comparison.py +278 -0
- glidepath/core/config.py +82 -0
- glidepath/core/contributions.py +337 -0
- glidepath/core/engine.py +2811 -0
- glidepath/core/entities.py +264 -0
- glidepath/core/glide.py +289 -0
- glidepath/core/investments.py +175 -0
- glidepath/core/money.py +107 -0
- glidepath/core/montecarlo.py +609 -0
- glidepath/core/pensions.py +298 -0
- glidepath/core/periods.py +367 -0
- glidepath/core/provenance.py +271 -0
- glidepath/core/randomness.py +128 -0
- glidepath/core/region.py +46 -0
- glidepath/core/reporting.py +231 -0
- glidepath/core/results.py +504 -0
- glidepath/core/retirement.py +291 -0
- glidepath/core/returns.py +312 -0
- glidepath/core/scenarios.py +579 -0
- glidepath/core/state_pension.py +264 -0
- glidepath/core/tax.py +139 -0
- glidepath/core/withdrawals.py +461 -0
- glidepath/core/wrappers.py +278 -0
- glidepath/gui/__init__.py +6 -0
- glidepath/gui/assets/icon_128.png +0 -0
- glidepath/gui/assets/icon_16.png +0 -0
- glidepath/gui/assets/icon_24.png +0 -0
- glidepath/gui/assets/icon_256.png +0 -0
- glidepath/gui/assets/icon_32.png +0 -0
- glidepath/gui/assets/icon_48.png +0 -0
- glidepath/gui/assets/icon_64.png +0 -0
- glidepath/gui/assets/wordmark.png +0 -0
- glidepath/gui/charts.py +829 -0
- glidepath/gui/forms.py +359 -0
- glidepath/gui/inspector.py +186 -0
- glidepath/gui/main.py +51 -0
- glidepath/gui/scenarios.py +402 -0
- glidepath/gui/style.py +376 -0
- glidepath/gui/tableview.py +67 -0
- glidepath/gui/widgets.py +989 -0
- glidepath/persistence/__init__.py +48 -0
- glidepath/persistence/assumptions.py +112 -0
- glidepath/persistence/decode.py +747 -0
- glidepath/persistence/document.py +101 -0
- glidepath/persistence/encode.py +433 -0
- glidepath/persistence/migrations.py +158 -0
- glidepath/persistence/values.py +298 -0
- glidepath/py.typed +0 -0
- glidepath/regions/__init__.py +7 -0
- glidepath/regions/uk/__init__.py +189 -0
- glidepath/regions/uk/ages.py +156 -0
- glidepath/regions/uk/contributions.py +717 -0
- glidepath/regions/uk/data/age_rules.toml +78 -0
- glidepath/regions/uk/data/assumptions_default.toml +170 -0
- glidepath/regions/uk/data/returns_history.toml +150 -0
- glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
- glidepath/regions/uk/extension.py +479 -0
- glidepath/regions/uk/loader.py +704 -0
- glidepath/regions/uk/region.py +160 -0
- glidepath/regions/uk/schema.py +563 -0
- glidepath/regions/uk/state_pension.py +129 -0
- glidepath/regions/uk/tax.py +466 -0
- glidepath/regions/uk/wrappers.py +283 -0
- glidepath/regions/uk/years.py +92 -0
- glidepath-0.2.0.dist-info/METADATA +189 -0
- glidepath-0.2.0.dist-info/RECORD +93 -0
- glidepath-0.2.0.dist-info/WHEEL +4 -0
- glidepath-0.2.0.dist-info/entry_points.txt +3 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE-DATA +28 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""UK wrapper kinds and rules (roadmap 3.1, 9.2; planning §4.2, §5.1, §6).
|
|
2
|
+
|
|
3
|
+
Implements the core :class:`~glidepath.core.WrapperRuleset` protocol for
|
|
4
|
+
the UK wrapper kinds — workplace DC, SIPP, ISA, LISA, GIA and cash.
|
|
5
|
+
Every figure — the pension tax-free lump-sum fraction, the ISA and LISA
|
|
6
|
+
allowances, the LISA bonus rate — comes from the tax-year data files
|
|
7
|
+
(§5.3); nothing is hardcoded here (guard-tested). The kind-to-rules
|
|
8
|
+
*structure* (which kinds are pensions, which relief mechanics each may
|
|
9
|
+
operate) is scheme mechanics, not policy figures:
|
|
10
|
+
|
|
11
|
+
- Workplace DC schemes operate either relief mechanic: relief at source
|
|
12
|
+
or a net pay arrangement, per the employer's scheme.
|
|
13
|
+
- A SIPP is a personal pension, so contributions get relief at source.
|
|
14
|
+
- ISAs and LISAs attract no contribution relief; growth and withdrawals
|
|
15
|
+
are tax-free. The LISA's 25% government bonus is a contribution
|
|
16
|
+
bonus, not tax relief (it never extends tax bands), and its
|
|
17
|
+
contributions are age-windowed (18 to 50, pro-rated by whole months).
|
|
18
|
+
- GIA and cash accounts are bare taxable accounts: paid from taxed
|
|
19
|
+
income, growth taxable as it arises (dividends and interest feed the
|
|
20
|
+
§6 savings/dividend layers), withdrawals themselves tax-free. A cash
|
|
21
|
+
savings account prices no platform or fund charges, so the cash kind
|
|
22
|
+
alone is exempt from the shipped default fee assumptions
|
|
23
|
+
(``bears_default_fees``); the shipped cash return default is
|
|
24
|
+
therefore the account's whole return.
|
|
25
|
+
- Pension money in is tax-relieved, grows tax-free, and comes out as
|
|
26
|
+
taxable income after the tax-free lump-sum fraction (EET).
|
|
27
|
+
|
|
28
|
+
Access follows the §4.1 gate convention: pension kinds are gated by the
|
|
29
|
+
NMPA (via :class:`~glidepath.regions.uk.ages.UkAgeRules`, including the
|
|
30
|
+
2028 step-up); LISAs by the charge-free access age of 60 — the engine
|
|
31
|
+
never models a charged early withdrawal, so the 25% withdrawal charge
|
|
32
|
+
ships as data but no draw ever bears it (planning §5.2 gates are
|
|
33
|
+
errors, never silent draws); ISAs, GIAs and cash have no access age.
|
|
34
|
+
Contribution caps: the ISA annual allowance is a per-person cap shared
|
|
35
|
+
across all ISAs and LISAs through the ``uk.isa`` allowance group, and
|
|
36
|
+
the LISA allowance is a sub-cap inside it (its own group); pension
|
|
37
|
+
kinds have no per-kind cap here — the annual allowance is a
|
|
38
|
+
cross-pension measure of pension input amounts and lands in roadmap
|
|
39
|
+
3.3, the member relief limit with contribution schedules in 3.2.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
44
|
+
|
|
45
|
+
from glidepath.core import (
|
|
46
|
+
ContributionCap,
|
|
47
|
+
ContributionTaxTreatment,
|
|
48
|
+
ContributionTerms,
|
|
49
|
+
GrowthTaxTreatment,
|
|
50
|
+
ReliefMechanic,
|
|
51
|
+
WithdrawalTaxTreatment,
|
|
52
|
+
WrapperKindId,
|
|
53
|
+
WrapperTaxTreatment,
|
|
54
|
+
)
|
|
55
|
+
from glidepath.regions.uk.ages import UkAgeRules
|
|
56
|
+
from glidepath.regions.uk.loader import available_tax_years, load_tax_year
|
|
57
|
+
from glidepath.regions.uk.years import TaxYearSeries, UkTaxYearError
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from collections.abc import Mapping
|
|
61
|
+
from datetime import date
|
|
62
|
+
|
|
63
|
+
from glidepath.core import Money, Period
|
|
64
|
+
from glidepath.regions.uk.extension import FutureYearsExtension
|
|
65
|
+
from glidepath.regions.uk.schema import TaxYearFile
|
|
66
|
+
|
|
67
|
+
WORKPLACE_DC_KIND = WrapperKindId("uk.workplace_dc")
|
|
68
|
+
"""An employer-sponsored defined-contribution pension scheme."""
|
|
69
|
+
|
|
70
|
+
SIPP_KIND = WrapperKindId("uk.sipp")
|
|
71
|
+
"""A self-invested personal pension."""
|
|
72
|
+
|
|
73
|
+
ISA_KIND = WrapperKindId("uk.isa")
|
|
74
|
+
"""A (stocks-and-shares or cash) individual savings account."""
|
|
75
|
+
|
|
76
|
+
LISA_KIND = WrapperKindId("uk.lisa")
|
|
77
|
+
"""A lifetime ISA: bonus-bearing, age-windowed, gated until 60."""
|
|
78
|
+
|
|
79
|
+
GIA_KIND = WrapperKindId("uk.gia")
|
|
80
|
+
"""A general investment account: bare, taxable as growth arises."""
|
|
81
|
+
|
|
82
|
+
CASH_KIND = WrapperKindId("uk.cash")
|
|
83
|
+
"""A cash savings account outside any wrapper: interest is taxable."""
|
|
84
|
+
|
|
85
|
+
ISA_ALLOWANCE_GROUP = "uk.isa"
|
|
86
|
+
"""Allowance group of the overall ISA annual allowance (ISA + LISA)."""
|
|
87
|
+
|
|
88
|
+
LISA_ALLOWANCE_GROUP = "uk.lisa"
|
|
89
|
+
"""Allowance group of the LISA sub-allowance inside the overall one."""
|
|
90
|
+
|
|
91
|
+
_PENSION_KINDS = frozenset({WORKPLACE_DC_KIND, SIPP_KIND})
|
|
92
|
+
_TAX_FREE_SAVINGS_KINDS = frozenset({ISA_KIND, LISA_KIND})
|
|
93
|
+
_TAXABLE_KINDS = frozenset({GIA_KIND, CASH_KIND})
|
|
94
|
+
_ALL_KINDS = _PENSION_KINDS | _TAX_FREE_SAVINGS_KINDS | _TAXABLE_KINDS
|
|
95
|
+
|
|
96
|
+
_RELIEF_MECHANICS: Mapping[WrapperKindId, frozenset[ReliefMechanic]] = {
|
|
97
|
+
WORKPLACE_DC_KIND: frozenset(
|
|
98
|
+
{ReliefMechanic.RELIEF_AT_SOURCE, ReliefMechanic.NET_PAY}
|
|
99
|
+
),
|
|
100
|
+
SIPP_KIND: frozenset({ReliefMechanic.RELIEF_AT_SOURCE}),
|
|
101
|
+
ISA_KIND: frozenset(),
|
|
102
|
+
LISA_KIND: frozenset(),
|
|
103
|
+
GIA_KIND: frozenset(),
|
|
104
|
+
CASH_KIND: frozenset(),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UkWrapperError(ValueError):
|
|
109
|
+
"""A wrapper query the shipped UK data cannot answer."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _unknown_kind(kind: WrapperKindId) -> NoReturn:
|
|
113
|
+
"""Reject a wrapper kind the UK region does not define."""
|
|
114
|
+
msg = f"unknown UK wrapper kind {kind!r}"
|
|
115
|
+
raise UkWrapperError(msg)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class UkWrapperRuleset:
|
|
120
|
+
"""UK implementation of the core ``WrapperRuleset`` protocol.
|
|
121
|
+
|
|
122
|
+
Holds the tax-year files its figures come from, plus the age rules
|
|
123
|
+
behind the pension access gate. Figure queries (``tax_treatment``,
|
|
124
|
+
``annual_contribution_limit``) share the tax system's coverage
|
|
125
|
+
semantics: shipped data beats extrapolation, and the query's tax
|
|
126
|
+
year is always resolved — even when the answer needs no yearly
|
|
127
|
+
figure — so a query outside coverage fails loudly rather than
|
|
128
|
+
answering from the wrong year. Access gates read the effective-dated
|
|
129
|
+
age rules instead and carry no tax-year coverage constraint.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
tax_years: tuple[TaxYearFile, ...]
|
|
133
|
+
ages: UkAgeRules
|
|
134
|
+
future_years: FutureYearsExtension | None = None
|
|
135
|
+
|
|
136
|
+
def __post_init__(self) -> None:
|
|
137
|
+
"""Require at least one year, ascending and non-overlapping."""
|
|
138
|
+
try:
|
|
139
|
+
self._series()
|
|
140
|
+
except UkTaxYearError as exc:
|
|
141
|
+
raise UkWrapperError(str(exc)) from exc
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_shipped_data(
|
|
145
|
+
cls, future_years: FutureYearsExtension | None = None
|
|
146
|
+
) -> UkWrapperRuleset:
|
|
147
|
+
"""Build a ruleset over every shipped data file."""
|
|
148
|
+
years = available_tax_years()
|
|
149
|
+
return cls(
|
|
150
|
+
tax_years=tuple(load_tax_year(year) for year in years),
|
|
151
|
+
ages=UkAgeRules.from_shipped_data(),
|
|
152
|
+
future_years=future_years,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def tax_treatment(self, kind: WrapperKindId, period: Period) -> WrapperTaxTreatment:
|
|
156
|
+
"""The in/during/out treatment of ``kind`` during ``period`` (§4.2).
|
|
157
|
+
|
|
158
|
+
Pension kinds are EET with the year's tax-free lump-sum
|
|
159
|
+
fraction on the way out; ISAs and LISAs are TEE; GIA and cash
|
|
160
|
+
accounts are taxed as growth arises, with withdrawals
|
|
161
|
+
themselves tax-free (module docstring).
|
|
162
|
+
"""
|
|
163
|
+
year = self._year_for(period)
|
|
164
|
+
if kind in _PENSION_KINDS:
|
|
165
|
+
return WrapperTaxTreatment(
|
|
166
|
+
contributions=ContributionTaxTreatment.TAX_RELIEVED,
|
|
167
|
+
growth=GrowthTaxTreatment.TAX_FREE,
|
|
168
|
+
withdrawals=WithdrawalTaxTreatment.PARTIALLY_TAX_FREE,
|
|
169
|
+
tax_free_fraction=year.pension.tax_free_lump_sum_fraction,
|
|
170
|
+
)
|
|
171
|
+
if kind in _TAX_FREE_SAVINGS_KINDS:
|
|
172
|
+
return WrapperTaxTreatment(
|
|
173
|
+
contributions=ContributionTaxTreatment.FROM_TAXED_INCOME,
|
|
174
|
+
growth=GrowthTaxTreatment.TAX_FREE,
|
|
175
|
+
withdrawals=WithdrawalTaxTreatment.TAX_FREE,
|
|
176
|
+
)
|
|
177
|
+
if kind in _TAXABLE_KINDS:
|
|
178
|
+
return WrapperTaxTreatment(
|
|
179
|
+
contributions=ContributionTaxTreatment.FROM_TAXED_INCOME,
|
|
180
|
+
growth=GrowthTaxTreatment.TAXABLE,
|
|
181
|
+
withdrawals=WithdrawalTaxTreatment.TAX_FREE,
|
|
182
|
+
)
|
|
183
|
+
_unknown_kind(kind)
|
|
184
|
+
|
|
185
|
+
def contribution_terms(
|
|
186
|
+
self, kind: WrapperKindId, date_of_birth: date, period: Period
|
|
187
|
+
) -> ContributionTerms:
|
|
188
|
+
"""The contribution terms of ``kind`` for one person and period.
|
|
189
|
+
|
|
190
|
+
The ISA annual allowance is per person per tax year, shared
|
|
191
|
+
across all their ISAs and LISAs through the ``uk.isa`` group;
|
|
192
|
+
the LISA allowance is a sub-cap inside it, and LISA
|
|
193
|
+
contributions carry the year's bonus rate within the exact
|
|
194
|
+
18-to-50 contribution window (planning §6) — the engine
|
|
195
|
+
intersects the window with the period and the run window and
|
|
196
|
+
pro-rates by whole months. Pension, GIA and cash kinds are
|
|
197
|
+
uncapped
|
|
198
|
+
here — the pension annual allowance is a cross-pension measure
|
|
199
|
+
of pension input amounts (roadmap 3.3), not a per-kind cap.
|
|
200
|
+
"""
|
|
201
|
+
year = self._year_for(period)
|
|
202
|
+
if kind == ISA_KIND:
|
|
203
|
+
return ContributionTerms(
|
|
204
|
+
caps=(ContributionCap(ISA_ALLOWANCE_GROUP, year.isa.annual_allowance),)
|
|
205
|
+
)
|
|
206
|
+
if kind == LISA_KIND:
|
|
207
|
+
return ContributionTerms(
|
|
208
|
+
caps=(
|
|
209
|
+
ContributionCap(LISA_ALLOWANCE_GROUP, year.isa.lisa_allowance),
|
|
210
|
+
ContributionCap(ISA_ALLOWANCE_GROUP, year.isa.annual_allowance),
|
|
211
|
+
),
|
|
212
|
+
bonus_rate=year.isa.lisa_bonus_rate,
|
|
213
|
+
window=self.ages.lisa_contribution_window(date_of_birth),
|
|
214
|
+
)
|
|
215
|
+
if kind in _PENSION_KINDS or kind in _TAXABLE_KINDS:
|
|
216
|
+
return ContributionTerms()
|
|
217
|
+
_unknown_kind(kind)
|
|
218
|
+
|
|
219
|
+
def permitted_relief_mechanics(
|
|
220
|
+
self, kind: WrapperKindId
|
|
221
|
+
) -> frozenset[ReliefMechanic]:
|
|
222
|
+
"""The relief mechanics ``kind`` may operate (module docstring)."""
|
|
223
|
+
mechanics = _RELIEF_MECHANICS.get(kind)
|
|
224
|
+
if mechanics is None:
|
|
225
|
+
_unknown_kind(kind)
|
|
226
|
+
return mechanics
|
|
227
|
+
|
|
228
|
+
def bears_default_fees(self, kind: WrapperKindId) -> bool:
|
|
229
|
+
"""Whether ``kind`` bears the default platform/fund fees (§7).
|
|
230
|
+
|
|
231
|
+
Cash savings accounts price no platform or fund charges — the
|
|
232
|
+
rate on the account is the whole deal — so the cash kind is
|
|
233
|
+
exempt from the shipped fee defaults (issue #118); every other
|
|
234
|
+
UK kind is a platform-administered account those defaults
|
|
235
|
+
describe. Cash *held inside* another wrapper (a SIPP or ISA
|
|
236
|
+
cash slice) still bears that wrapper's fees.
|
|
237
|
+
"""
|
|
238
|
+
if kind not in _ALL_KINDS:
|
|
239
|
+
_unknown_kind(kind)
|
|
240
|
+
return kind != CASH_KIND
|
|
241
|
+
|
|
242
|
+
def lump_sum_allowance(self, period: Period) -> Money | None:
|
|
243
|
+
"""The year's lump sum allowance (planning §6; roadmap 5.2).
|
|
244
|
+
|
|
245
|
+
The per-person lifetime cap on pension tax-free cash — PCLS
|
|
246
|
+
and the tax-free elements of UFPLS payments. The engine tracks
|
|
247
|
+
usage across the run; this only answers the period's figure,
|
|
248
|
+
with the usual coverage semantics (class docstring).
|
|
249
|
+
"""
|
|
250
|
+
return self._year_for(period).pension.lump_sum_allowance
|
|
251
|
+
|
|
252
|
+
def is_access_open(
|
|
253
|
+
self, kind: WrapperKindId, date_of_birth: date, period: Period
|
|
254
|
+
) -> bool:
|
|
255
|
+
"""Whether *new* access to ``kind`` may open in ``period``.
|
|
256
|
+
|
|
257
|
+
Pension kinds follow the NMPA access gate (§4.1), including the
|
|
258
|
+
2028 step-up; LISAs the charge-free access age of 60 (module
|
|
259
|
+
docstring); ISAs, GIAs and cash are always accessible. Only
|
|
260
|
+
*new* pension access (crystallisation, UFPLS) is gated — funds
|
|
261
|
+
already crystallised and benefits already in payment are never
|
|
262
|
+
re-gated (planning §5.1), so someone who crystallised at 55
|
|
263
|
+
before the 2028 step keeps drawing at 56 even while this gate
|
|
264
|
+
is shut.
|
|
265
|
+
"""
|
|
266
|
+
if kind in _PENSION_KINDS:
|
|
267
|
+
return self.ages.is_pension_access_open(date_of_birth, period)
|
|
268
|
+
if kind == LISA_KIND:
|
|
269
|
+
return self.ages.is_lisa_access_open(date_of_birth, period)
|
|
270
|
+
if kind == ISA_KIND or kind in _TAXABLE_KINDS:
|
|
271
|
+
return True
|
|
272
|
+
_unknown_kind(kind)
|
|
273
|
+
|
|
274
|
+
def _series(self) -> TaxYearSeries:
|
|
275
|
+
"""The shared year-resolution series over this ruleset's files."""
|
|
276
|
+
return TaxYearSeries(tax_years=self.tax_years, future_years=self.future_years)
|
|
277
|
+
|
|
278
|
+
def _year_for(self, period: Period) -> TaxYearFile:
|
|
279
|
+
"""The shipped or synthesized file fully containing ``period``."""
|
|
280
|
+
try:
|
|
281
|
+
return self._series().year_for(period)
|
|
282
|
+
except UkTaxYearError as exc:
|
|
283
|
+
raise UkWrapperError(str(exc)) from exc
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Tax-year data resolution shared by the UK rule implementations (§5.3).
|
|
2
|
+
|
|
3
|
+
Both the tax system (roadmap 2.3) and the wrapper ruleset (roadmap 3.1)
|
|
4
|
+
answer period queries from the same per-tax-year data files, with the
|
|
5
|
+
same coverage semantics: shipped data always beats extrapolation, the
|
|
6
|
+
future-years extension (roadmap 2.5) only reaches *past* the last
|
|
7
|
+
shipped year, and a query outside coverage fails loudly. This module
|
|
8
|
+
holds that resolution once; the consumers wrap
|
|
9
|
+
:class:`UkTaxYearError` in their own error types.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from itertools import pairwise
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from glidepath.regions.uk.extension import extend_tax_year
|
|
17
|
+
from glidepath.regions.uk.schema import tax_year_start_year
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from datetime import date
|
|
21
|
+
|
|
22
|
+
from glidepath.core import Period
|
|
23
|
+
from glidepath.regions.uk.extension import FutureYearsExtension
|
|
24
|
+
from glidepath.regions.uk.schema import TaxYearFile
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UkTaxYearError(ValueError):
|
|
28
|
+
"""A query no shipped or synthesized tax-year data can answer."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class TaxYearSeries:
|
|
33
|
+
"""An ascending run of tax-year files plus an optional extension.
|
|
34
|
+
|
|
35
|
+
The single source of "which year's figures govern this period" for
|
|
36
|
+
every UK ruleset (planning §5.3).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
tax_years: tuple[TaxYearFile, ...]
|
|
40
|
+
future_years: FutureYearsExtension | None = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
"""Require at least one year, ascending and non-overlapping."""
|
|
44
|
+
if not self.tax_years:
|
|
45
|
+
msg = "at least one tax-year file is required"
|
|
46
|
+
raise UkTaxYearError(msg)
|
|
47
|
+
for previous, current in pairwise(self.tax_years):
|
|
48
|
+
if current.meta.start_date <= previous.meta.end_date:
|
|
49
|
+
msg = "tax-year files must be in ascending, non-overlapping order"
|
|
50
|
+
raise UkTaxYearError(msg)
|
|
51
|
+
|
|
52
|
+
def year_for(self, period: Period) -> TaxYearFile:
|
|
53
|
+
"""The shipped or synthesized file fully containing ``period``.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
UkTaxYearError: If no file covers the period's start, or the
|
|
57
|
+
period extends beyond that file's tax year.
|
|
58
|
+
"""
|
|
59
|
+
year = self.year_containing(period.start)
|
|
60
|
+
if period.end > year.meta.end_date:
|
|
61
|
+
msg = (
|
|
62
|
+
f"period {period.start}..{period.end} extends beyond"
|
|
63
|
+
f" tax year {year.meta.tax_year}"
|
|
64
|
+
)
|
|
65
|
+
raise UkTaxYearError(msg)
|
|
66
|
+
return year
|
|
67
|
+
|
|
68
|
+
def year_containing(self, day: date) -> TaxYearFile:
|
|
69
|
+
"""The shipped file covering ``day``, or an extension past the last.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
UkTaxYearError: If ``day`` falls outside shipped coverage and
|
|
73
|
+
the extension cannot reach it.
|
|
74
|
+
"""
|
|
75
|
+
for year in self.tax_years:
|
|
76
|
+
if year.meta.start_date <= day <= year.meta.end_date:
|
|
77
|
+
return year
|
|
78
|
+
last = self.tax_years[-1]
|
|
79
|
+
if self.future_years is not None and day > last.meta.end_date:
|
|
80
|
+
return extend_tax_year(
|
|
81
|
+
last,
|
|
82
|
+
tax_year_start_year(day),
|
|
83
|
+
policy=self.future_years.policy,
|
|
84
|
+
cpi=self.future_years.cpi,
|
|
85
|
+
)
|
|
86
|
+
problem = (
|
|
87
|
+
"the future-years extension only reaches past the last shipped year"
|
|
88
|
+
if self.future_years is not None
|
|
89
|
+
else "no future-years extension is configured (planning §5.3)"
|
|
90
|
+
)
|
|
91
|
+
msg = f"no shipped tax-year data covers {day}; {problem}"
|
|
92
|
+
raise UkTaxYearError(msg)
|
|
@@ -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,93 @@
|
|
|
1
|
+
glidepath/__init__.py,sha256=Vi-6JCIWmCK4aG7z8SdSFe3dgy472Aw2GdB7oXREBYM,96
|
|
2
|
+
glidepath/app/__init__.py,sha256=Z7avWMx9HBGF7U3NhYrdQLPm-zgxTN-ek_DUT_8AN6w,9102
|
|
3
|
+
glidepath/app/backtest.py,sha256=An1Pt-7dZVvAZZYVimBjSWijfbQZlhSRO3gEZuihwSc,10375
|
|
4
|
+
glidepath/app/charts.py,sha256=ekEGk_IpkL7vcclOWLXS4jhLbLE1fq6wIqIgnVIktX0,26623
|
|
5
|
+
glidepath/app/copy.py,sha256=p9WuE5btMaOs8GLwZakdaHVzN1QJHvZx0bUc_rBpA8k,8567
|
|
6
|
+
glidepath/app/display.py,sha256=UiJ6jpa1OdGj1e_kYQjJF3QfVg9bDPV_gMzZB2Z55hk,5333
|
|
7
|
+
glidepath/app/drawdown.py,sha256=RAK-kInYpm8qlNBGjhi08bWk-Z0r-Rla6zHbDmRmWiw,15406
|
|
8
|
+
glidepath/app/example.py,sha256=mk1qG5QeItZ0TrwEQbp2jQLiV9TTfSIDvey6wdJmaXI,2659
|
|
9
|
+
glidepath/app/exports.py,sha256=gByQk6TyLOvJuRj9FqlB5qfSolfulCwBolcK9IRja8c,17226
|
|
10
|
+
glidepath/app/files.py,sha256=OgbwsLICeM-fMIs5vNowRvwWGt4TMDvwlYq7IEmUIGM,8492
|
|
11
|
+
glidepath/app/firstrun.py,sha256=8X9LsiwCcJAR8lgjFpk1e2osCO89DW4OSf8xYNpTJeE,3977
|
|
12
|
+
glidepath/app/forms.py,sha256=FRYJ-BDMUsB7fZ4XgCGRDO4qIL2eppTK82qfxzmf2lA,66976
|
|
13
|
+
glidepath/app/inspector.py,sha256=SVikOMErDnrJ9guNcS-ifW_J9k13h5mJqNdkG66BhwE,18009
|
|
14
|
+
glidepath/app/labels.py,sha256=ldY_Vjn00KoDwt8YWX5xR1FFtAjMPO6IDq15J5w2Tr0,2327
|
|
15
|
+
glidepath/app/montecarlo.py,sha256=14MabwPhhOyDUR3MKfgh_Wc7MyidKLZDPR6Su7sMBXY,14044
|
|
16
|
+
glidepath/app/plan.py,sha256=jAjSqmlCiFkDtFvpN5w6aaL-mVGJwfTI-_cN-PLKzcA,12691
|
|
17
|
+
glidepath/app/retirement.py,sha256=pkcAok4UKKK7ES5wFeNSe62d1u08Uvwtjtegilvd3T0,15855
|
|
18
|
+
glidepath/app/scenarios.py,sha256=QaWj-uStyW1v7_CdVb4OPkOknPrGz0phhK1JKmzlpLI,28316
|
|
19
|
+
glidepath/app/shell.py,sha256=LtSlMMGijRulXWU8SAf1HcZDi_FLTKzMgk6Ay7lQT3E,5338
|
|
20
|
+
glidepath/app/tables.py,sha256=Cf-9c7bt7BstdG2Tzfe9FNBfafGyEziZtAg8Zb7ZlNY,4869
|
|
21
|
+
glidepath/core/__init__.py,sha256=xUVGH8pt8qLlk-XM8j9gHN1DBMHx4B6VlR3DeCA00Q8,9118
|
|
22
|
+
glidepath/core/annuities.py,sha256=zs2oOsoCWhyEnPJrxPXzcGt-nDxkHKdrLk8n3zfAOgA,9477
|
|
23
|
+
glidepath/core/backtest.py,sha256=Xc0v3rD4L-gogNowt-xZDnBxqL_YU5UGyIn3PwMT5lY,20265
|
|
24
|
+
glidepath/core/comparison.py,sha256=6iuSZMA_Fk8UI05oBd4xF5ZJ29Bqn4lzgQ5jZkp9noU,10419
|
|
25
|
+
glidepath/core/config.py,sha256=1hotIfVhzbXvZ17G5Mp0up4DQ_SG6vRU4_i8ZgNyhAM,3022
|
|
26
|
+
glidepath/core/contributions.py,sha256=o3QT77_oO-Ki5ckOpuWBi_a2ULzu1QNYcdwToKU_430,13721
|
|
27
|
+
glidepath/core/engine.py,sha256=muElq7dHscX5VekhpytQCnnD6bLmpD_eQT0jtCYOjdc,126490
|
|
28
|
+
glidepath/core/entities.py,sha256=gg0JKjHmxjv-Ef2cU-D6f66qnu-kaCnlDwkQxuObcB0,10452
|
|
29
|
+
glidepath/core/glide.py,sha256=Hrm2hF9zllolTgwPxzWhpv7LoiwkYxqTaUD3o72w71Q,11427
|
|
30
|
+
glidepath/core/investments.py,sha256=s6XefinlQiMim76b7-4lEmkHVCOixh2sntURdVFJLFk,6177
|
|
31
|
+
glidepath/core/money.py,sha256=Lng5B4HAKWAopo-d-U_N7SdjpU0F3UStytSCDfe1BIM,3812
|
|
32
|
+
glidepath/core/montecarlo.py,sha256=oyiC3qjk45gSWY_rTdrWnsszd-Y3-yYcx89nOJYKwQc,25350
|
|
33
|
+
glidepath/core/pensions.py,sha256=YXoFgkkFXrSN-yHOm8SM6Bqbt3N8_b5KtjkIZ7kDnUQ,12407
|
|
34
|
+
glidepath/core/periods.py,sha256=L1ecdl--bFW8kWWxFrxO4fUuANO3myv2-eAJL733bJs,13978
|
|
35
|
+
glidepath/core/provenance.py,sha256=BMflWxqO0zNwwhLdQx3Gz-m0AFG8RtmmLOvmBujBkrE,9272
|
|
36
|
+
glidepath/core/randomness.py,sha256=gM28ebJWV7Yi5mPiHIc5vhaF2yw-FRiGwaNySSljbIo,5470
|
|
37
|
+
glidepath/core/region.py,sha256=zaw5dBI_62dYhnmDtIs4J0zhNkg1z1oeXeXAyERhDU8,1742
|
|
38
|
+
glidepath/core/reporting.py,sha256=qeHw0QrKs-N1vTVc9uYlh1oA_JcqKBEh3pmZm2U0VIE,8552
|
|
39
|
+
glidepath/core/results.py,sha256=r1BseA-Grv-gnorHCjAhcV__tMrS26t1lsKrfxdHEWI,20518
|
|
40
|
+
glidepath/core/retirement.py,sha256=eFpf0_vofntqlN-23MoDWhijEaUHhty4R2ErZD60uzA,12453
|
|
41
|
+
glidepath/core/returns.py,sha256=JpBaV7sAt6jNKrBTe6MyIfbLMsqzUGOzUk-_359GEDA,12584
|
|
42
|
+
glidepath/core/scenarios.py,sha256=YZ1lkS9M8Y28wmbJVkM4v9DG0ij4btwC_1RJpFlD9PI,22466
|
|
43
|
+
glidepath/core/state_pension.py,sha256=vmBksTi2ZEd8mfW5H7tLjF9zY7PfeqnpLQpYGGQGL-Y,10557
|
|
44
|
+
glidepath/core/tax.py,sha256=1befR5b1p7knvyKSavvo9qwnwmOJeDpLsz1Z0O1IDs4,5134
|
|
45
|
+
glidepath/core/withdrawals.py,sha256=LlQ-7GgsjV-_yalTd1OdxwLUqL3NdoECQ2VPJNqqSHU,19640
|
|
46
|
+
glidepath/core/wrappers.py,sha256=DSg4leOBmrcvPhjuOFNeez6h6NAFuOxFlp92srAk6xc,11213
|
|
47
|
+
glidepath/gui/__init__.py,sha256=MNnse5aChZ4X-xWHc646jfYxQjwdJoVnyKT8OcWXuHM,266
|
|
48
|
+
glidepath/gui/assets/icon_128.png,sha256=Mo1TMH1xskDBFBUh2vr4s695OIVdAhdf4yXgycgfAIM,12367
|
|
49
|
+
glidepath/gui/assets/icon_16.png,sha256=npOGODf3xu2MZ0H3pvs7lQPY1WwP2sBeWOIVPly59vM,524
|
|
50
|
+
glidepath/gui/assets/icon_24.png,sha256=7VOticK7lUklv7gfQfSaqtCWAUFdyqZXjxFitDt3r3M,910
|
|
51
|
+
glidepath/gui/assets/icon_256.png,sha256=LC9Qfqyn5y9Q1XXRUQSpo8D9xePPso0M0LN5MU4hH8k,47043
|
|
52
|
+
glidepath/gui/assets/icon_32.png,sha256=sOYguevAWV3iVOfhw_V12y3YMvJY9cuR0a3eI5VyF2I,1349
|
|
53
|
+
glidepath/gui/assets/icon_48.png,sha256=ZOV6Eb-2hIVKgMcsYQv28t2aF4PuaWX9rNIqM37_5nQ,2476
|
|
54
|
+
glidepath/gui/assets/icon_64.png,sha256=HIXqq4oiEN0560296QzhBAZ1TjlmwCKnm2LoeRHw1NM,3886
|
|
55
|
+
glidepath/gui/assets/wordmark.png,sha256=rzZvG2fyq3V5O3LRBaDxALlA7JVEK0NtFPLE5hIoGs0,139710
|
|
56
|
+
glidepath/gui/charts.py,sha256=s6MA1SW9VO6AA613oOZsB1X-CcBaKhx5wCV0Voorkqg,34168
|
|
57
|
+
glidepath/gui/forms.py,sha256=UBENTCZrice_SndzhsOSj6_CgT4C1pEyDMYJdOC7LSE,14087
|
|
58
|
+
glidepath/gui/inspector.py,sha256=8aljbZaC5qWXiaA30ASoKuiP3dKMF5l6cmSZOiE-Bf4,6990
|
|
59
|
+
glidepath/gui/main.py,sha256=H14yrEdQS5ZwnRpF85KGlFapk80wwEO0PBfjWPG7Kko,1829
|
|
60
|
+
glidepath/gui/scenarios.py,sha256=Ozal-Tm7b7i65E-DavzGuccYxUMLrRRvyUFnKoJ2ctE,16651
|
|
61
|
+
glidepath/gui/style.py,sha256=GrEEnMzV8emmKu1sOx6xxVcJoIDHzDO7sxZF8BJ0rIk,10036
|
|
62
|
+
glidepath/gui/tableview.py,sha256=ePJE1vNRWq7p_lDTqSRf5R88WAdlMj_koIiDrD1koig,2331
|
|
63
|
+
glidepath/gui/widgets.py,sha256=cdQl6pf6hjXjSxcT6cd83B_pJsRa2pKW9xSv2SDQ8fs,39509
|
|
64
|
+
glidepath/persistence/__init__.py,sha256=g84BDLvNuz9nIF1dYMEWmyd4__qNTDlPu-FTZyeXFjU,1364
|
|
65
|
+
glidepath/persistence/assumptions.py,sha256=jhwIxznXOFrmzNOjWrKAxTXvrQVMEReg3DI-Iowb_7I,4328
|
|
66
|
+
glidepath/persistence/decode.py,sha256=lSXEsyhoTw6yG6WXx436IydlCA6JiAT7eVRZeaAJ5Ig,25173
|
|
67
|
+
glidepath/persistence/document.py,sha256=6QRWaKy6yZtWemBW_hoQu0utBxFsnLYGr1NK-s6UtKU,3998
|
|
68
|
+
glidepath/persistence/encode.py,sha256=5qNEsHawdb-N6uiFPnCTPlOClZCeKsiwsQtQGXPq6ag,15209
|
|
69
|
+
glidepath/persistence/migrations.py,sha256=kAzbfZX9Qvu3YxDMD2afrr3TdDgLGgV0_7-O4iO-qlA,6461
|
|
70
|
+
glidepath/persistence/values.py,sha256=KbdVMnSCkgBZOAC3GHTysclTH5dbqq0rUQbhJwvU2NA,9857
|
|
71
|
+
glidepath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
|
+
glidepath/regions/__init__.py,sha256=KVe-NFgzrNCKjCLUpGkyilNCZywKNXGr0P811vorztk,362
|
|
73
|
+
glidepath/regions/uk/__init__.py,sha256=Ma0wbPoMbS66pCUhL71OlJDbISiaCHBDVTZWr9Qa0HA,4542
|
|
74
|
+
glidepath/regions/uk/ages.py,sha256=Syr0DZ2IhEWkIcpnRoFVXOO5sWpxn2dHYdyVt4FPP1Q,6434
|
|
75
|
+
glidepath/regions/uk/contributions.py,sha256=yCs3uJv8b41ZHxLheRjVLecNtQ1ZyDyGMQUdbATS7aY,30879
|
|
76
|
+
glidepath/regions/uk/data/age_rules.toml,sha256=l8AiAJl27VmkgNthjnYJJ92biFz2c3hHq3l4BlFehCA,4114
|
|
77
|
+
glidepath/regions/uk/data/assumptions_default.toml,sha256=tjB0Nv9006yrfyCnsc2dMP3mi8J_1ob1iHNBMAJ5HyU,6515
|
|
78
|
+
glidepath/regions/uk/data/returns_history.toml,sha256=x0I5PgFRv_iGQNpxRA04bmm6RENXDrcDWkY9I_cWZ88,13003
|
|
79
|
+
glidepath/regions/uk/data/tax_year_2026_27.toml,sha256=bIeF1z8JdmhFZAjkwZAI7hNvdAyCWiBhXoTbc1p0A3w,4301
|
|
80
|
+
glidepath/regions/uk/extension.py,sha256=xrTxlEm1FA028AJR9OAjM4qffwKEzJ84jMgHE59icmE,19720
|
|
81
|
+
glidepath/regions/uk/loader.py,sha256=OMi0qyuLEKjxjPQ9vByBEho3Q1xL-jF908Mumv-698Y,25812
|
|
82
|
+
glidepath/regions/uk/region.py,sha256=OWAM1-jIqybX3nqMNHL8-m64lrDUgU5GO3x2mCfHq40,6629
|
|
83
|
+
glidepath/regions/uk/schema.py,sha256=ojCp-8PFstxIcoQpcxRtaUD-5GD_zERJTspfZWuU9EI,19878
|
|
84
|
+
glidepath/regions/uk/state_pension.py,sha256=Pcaq-TiMp4zkN9EMfLwMTwHI2Ntb4B_ybiGhQcIb8Ao,5102
|
|
85
|
+
glidepath/regions/uk/tax.py,sha256=hYnQRz8kLGqn3iUqHkdc-lTTA4RZnTODBj6BJRI66vE,18666
|
|
86
|
+
glidepath/regions/uk/wrappers.py,sha256=dKW8QMC0BFeHII02lC7XdVsx4sRRaqVNf6g_GdOX-sU,12319
|
|
87
|
+
glidepath/regions/uk/years.py,sha256=3J-AuuugkWif-vn-RTwQ8S2C8CWByAgan0VikxH1Jkw,3544
|
|
88
|
+
glidepath-0.2.0.dist-info/licenses/LICENSE,sha256=jZIkRQTP4p9jTFpLIOYNhSic-5vp3eK7RGTe3Qpcvbo,1065
|
|
89
|
+
glidepath-0.2.0.dist-info/licenses/LICENSE-DATA,sha256=dCGRR-wYyTIvTWSZ_L9Za6Hy2VaZoMFZyfT7EVqZgVU,1256
|
|
90
|
+
glidepath-0.2.0.dist-info/WHEEL,sha256=l3MmIxu8qaet7ng2J9fFoJnYGj8IREj7jXTbgsuzmy4,81
|
|
91
|
+
glidepath-0.2.0.dist-info/entry_points.txt,sha256=PqQpL7yA9dpYn3ebDjHc3e0rpNfV4D5pD5BL9-sH0uc,51
|
|
92
|
+
glidepath-0.2.0.dist-info/METADATA,sha256=il6iMNMrsYnLZJD6vzfLBiKbKEeXdHsnJ9gS_CK3MFs,8646
|
|
93
|
+
glidepath-0.2.0.dist-info/RECORD,,
|