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
glidepath/__init__.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""UI-agnostic application layer (roadmap 8.1 to 8.3; planning §4.7).
|
|
2
|
+
|
|
3
|
+
View models, user-facing copy (including the §1 disclaimer), display
|
|
4
|
+
formatting, form parsing, session state, and first-run state for any
|
|
5
|
+
UI shell. Everything here is plain typed Python over the scenario
|
|
6
|
+
layer and engine — no Qt imports (guard test), no assumption of a
|
|
7
|
+
desktop — so a future web shell can reuse it unchanged.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from glidepath.app.backtest import (
|
|
11
|
+
BACKTEST_NO_PLAN_MESSAGE,
|
|
12
|
+
BACKTEST_RUNNING_MESSAGE,
|
|
13
|
+
BACKTEST_STALE_MESSAGE,
|
|
14
|
+
NO_BACKTEST_MESSAGE,
|
|
15
|
+
BacktestMetric,
|
|
16
|
+
BacktestPanelViewModel,
|
|
17
|
+
build_backtest_panel,
|
|
18
|
+
state_with_backtest,
|
|
19
|
+
)
|
|
20
|
+
from glidepath.app.charts import (
|
|
21
|
+
CHART_VIEW_LABEL,
|
|
22
|
+
DEFAULT_CHART_BASIS,
|
|
23
|
+
MONTE_CARLO_CHART_TITLE,
|
|
24
|
+
NO_PROJECTION_MESSAGE,
|
|
25
|
+
RUN_FAILED_PREFIX,
|
|
26
|
+
TABLE_VIEW_LABEL,
|
|
27
|
+
ChartBand,
|
|
28
|
+
ChartBasisOption,
|
|
29
|
+
ChartFill,
|
|
30
|
+
ChartSeries,
|
|
31
|
+
ChartSpec,
|
|
32
|
+
ChartsViewModel,
|
|
33
|
+
ChartTable,
|
|
34
|
+
bar_tooltip,
|
|
35
|
+
basis_from_key,
|
|
36
|
+
build_charts_view_model,
|
|
37
|
+
chart_table,
|
|
38
|
+
fill_tooltip,
|
|
39
|
+
)
|
|
40
|
+
from glidepath.app.copy import (
|
|
41
|
+
ABOUT_TITLE,
|
|
42
|
+
APP_NAME,
|
|
43
|
+
DATE_PICKER_TOOLTIP,
|
|
44
|
+
DISCLAIMER_ACCEPT_LABEL,
|
|
45
|
+
DISCLAIMER_BODY,
|
|
46
|
+
DISCLAIMER_DECLINE_LABEL,
|
|
47
|
+
DISCLAIMER_TITLE,
|
|
48
|
+
HELP_GUIDE_INTRO,
|
|
49
|
+
HELP_GUIDE_SECTIONS,
|
|
50
|
+
HELP_GUIDE_TITLE,
|
|
51
|
+
HELP_MENU_LABEL,
|
|
52
|
+
)
|
|
53
|
+
from glidepath.app.display import (
|
|
54
|
+
format_date,
|
|
55
|
+
format_money,
|
|
56
|
+
format_percent,
|
|
57
|
+
format_recorded,
|
|
58
|
+
format_value,
|
|
59
|
+
)
|
|
60
|
+
from glidepath.app.drawdown import (
|
|
61
|
+
DRAWDOWN_AGE_MESSAGE,
|
|
62
|
+
DRAWDOWN_NO_PLAN_MESSAGE,
|
|
63
|
+
DRAWDOWN_RUNNING_MESSAGE,
|
|
64
|
+
DRAWDOWN_STALE_MESSAGE,
|
|
65
|
+
DRAWDOWN_SUCCESS_MESSAGE,
|
|
66
|
+
NO_DRAWDOWN_MESSAGE,
|
|
67
|
+
DrawdownAnswer,
|
|
68
|
+
DrawdownPanelViewModel,
|
|
69
|
+
DrawdownRequest,
|
|
70
|
+
build_drawdown_panel,
|
|
71
|
+
state_with_drawdown,
|
|
72
|
+
)
|
|
73
|
+
from glidepath.app.example import example_facts_form_data
|
|
74
|
+
from glidepath.app.exports import (
|
|
75
|
+
NOTHING_TO_EXPORT_MESSAGE,
|
|
76
|
+
REPORT_EXPORT_FAILED_PREFIX,
|
|
77
|
+
REPORT_NOT_WRITTEN_MESSAGE,
|
|
78
|
+
ExportOutcome,
|
|
79
|
+
PlanReport,
|
|
80
|
+
ReportRequest,
|
|
81
|
+
build_plan_report,
|
|
82
|
+
cash_flow_csv,
|
|
83
|
+
chart_resource_name,
|
|
84
|
+
export_cash_flow_csv,
|
|
85
|
+
plan_display_name,
|
|
86
|
+
report_exported_message,
|
|
87
|
+
)
|
|
88
|
+
from glidepath.app.files import (
|
|
89
|
+
NOTHING_TO_SAVE_MESSAGE,
|
|
90
|
+
UNSAVED_CHANGES_PROMPT,
|
|
91
|
+
UNSAVED_CHANGES_TITLE,
|
|
92
|
+
LoadOutcome,
|
|
93
|
+
SaveOutcome,
|
|
94
|
+
document_from_state,
|
|
95
|
+
has_unsaved_changes,
|
|
96
|
+
load_plan_state,
|
|
97
|
+
save_plan_state,
|
|
98
|
+
)
|
|
99
|
+
from glidepath.app.firstrun import (
|
|
100
|
+
FirstRunState,
|
|
101
|
+
default_state_path,
|
|
102
|
+
load_state,
|
|
103
|
+
record_disclaimer_acknowledged,
|
|
104
|
+
record_last_plan_path,
|
|
105
|
+
)
|
|
106
|
+
from glidepath.app.forms import (
|
|
107
|
+
ENTITY_ID_KEY,
|
|
108
|
+
ChoiceOption,
|
|
109
|
+
FactsFormData,
|
|
110
|
+
FactsFormResult,
|
|
111
|
+
FactsFormViewModel,
|
|
112
|
+
FieldKind,
|
|
113
|
+
FieldSpec,
|
|
114
|
+
FormError,
|
|
115
|
+
PlanEntityIds,
|
|
116
|
+
SectionSpec,
|
|
117
|
+
build_facts_form_view_model,
|
|
118
|
+
facts_form_data_from_household,
|
|
119
|
+
form_cannot_represent,
|
|
120
|
+
format_form_errors,
|
|
121
|
+
parse_facts_form,
|
|
122
|
+
plan_entity_ids,
|
|
123
|
+
)
|
|
124
|
+
from glidepath.app.inspector import (
|
|
125
|
+
AssumptionRow,
|
|
126
|
+
DecisionRow,
|
|
127
|
+
FactRow,
|
|
128
|
+
InspectorViewModel,
|
|
129
|
+
RollForwardRow,
|
|
130
|
+
StructureRow,
|
|
131
|
+
build_inspector_view_model,
|
|
132
|
+
)
|
|
133
|
+
from glidepath.app.montecarlo import (
|
|
134
|
+
DEFAULT_RUN_MODE,
|
|
135
|
+
MONTE_CARLO_NO_PLAN_MESSAGE,
|
|
136
|
+
MONTE_CARLO_PATHS_MESSAGE,
|
|
137
|
+
MONTE_CARLO_RUNNING_MESSAGE,
|
|
138
|
+
MONTE_CARLO_SEED_MESSAGE,
|
|
139
|
+
MONTE_CARLO_STALE_MESSAGE,
|
|
140
|
+
NO_MONTE_CARLO_MESSAGE,
|
|
141
|
+
MonteCarloMetric,
|
|
142
|
+
MonteCarloPanelViewModel,
|
|
143
|
+
RunModeOption,
|
|
144
|
+
monte_carlo_running_status,
|
|
145
|
+
path_pool,
|
|
146
|
+
run_mode_from_key,
|
|
147
|
+
run_mode_key,
|
|
148
|
+
state_with_monte_carlo,
|
|
149
|
+
)
|
|
150
|
+
from glidepath.app.plan import (
|
|
151
|
+
OVERRIDE_SOURCE,
|
|
152
|
+
OverrideOutcome,
|
|
153
|
+
PlanState,
|
|
154
|
+
facts_saved_message,
|
|
155
|
+
initial_plan_state,
|
|
156
|
+
state_marked_saved,
|
|
157
|
+
state_with_household,
|
|
158
|
+
state_with_override,
|
|
159
|
+
state_with_scenarios,
|
|
160
|
+
)
|
|
161
|
+
from glidepath.app.retirement import (
|
|
162
|
+
NO_RETIREMENT_MESSAGE,
|
|
163
|
+
RETIREMENT_NO_INCOME_MESSAGE,
|
|
164
|
+
RETIREMENT_NO_PLAN_MESSAGE,
|
|
165
|
+
RETIREMENT_RATE_MESSAGE,
|
|
166
|
+
RETIREMENT_RUNNING_MESSAGE,
|
|
167
|
+
RETIREMENT_STALE_MESSAGE,
|
|
168
|
+
RETIREMENT_SUCCESS_MESSAGE,
|
|
169
|
+
RetirementAnswer,
|
|
170
|
+
RetirementPanelViewModel,
|
|
171
|
+
RetirementRequest,
|
|
172
|
+
build_retirement_panel,
|
|
173
|
+
state_with_retirement,
|
|
174
|
+
)
|
|
175
|
+
from glidepath.app.scenarios import (
|
|
176
|
+
DEFAULT_COMPARISON_METRIC_KEY,
|
|
177
|
+
ComparisonMetricOption,
|
|
178
|
+
OverrideRow,
|
|
179
|
+
ScenarioItem,
|
|
180
|
+
ScenariosViewModel,
|
|
181
|
+
TargetOption,
|
|
182
|
+
build_scenarios_view_model,
|
|
183
|
+
metric_from_key,
|
|
184
|
+
scenario_target_options,
|
|
185
|
+
state_with_scenario_added,
|
|
186
|
+
state_with_scenario_override,
|
|
187
|
+
state_without_scenario,
|
|
188
|
+
state_without_scenario_override,
|
|
189
|
+
)
|
|
190
|
+
from glidepath.app.shell import (
|
|
191
|
+
AboutViewModel,
|
|
192
|
+
DisclaimerViewModel,
|
|
193
|
+
FileMenuViewModel,
|
|
194
|
+
HelpGuideViewModel,
|
|
195
|
+
HelpSectionViewModel,
|
|
196
|
+
ShellViewModel,
|
|
197
|
+
build_shell_view_model,
|
|
198
|
+
should_show_disclaimer,
|
|
199
|
+
)
|
|
200
|
+
from glidepath.core import ReportBasis
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"ABOUT_TITLE",
|
|
204
|
+
"APP_NAME",
|
|
205
|
+
"BACKTEST_NO_PLAN_MESSAGE",
|
|
206
|
+
"BACKTEST_RUNNING_MESSAGE",
|
|
207
|
+
"BACKTEST_STALE_MESSAGE",
|
|
208
|
+
"CHART_VIEW_LABEL",
|
|
209
|
+
"DATE_PICKER_TOOLTIP",
|
|
210
|
+
"DEFAULT_CHART_BASIS",
|
|
211
|
+
"DEFAULT_COMPARISON_METRIC_KEY",
|
|
212
|
+
"DEFAULT_RUN_MODE",
|
|
213
|
+
"DISCLAIMER_ACCEPT_LABEL",
|
|
214
|
+
"DISCLAIMER_BODY",
|
|
215
|
+
"DISCLAIMER_DECLINE_LABEL",
|
|
216
|
+
"DISCLAIMER_TITLE",
|
|
217
|
+
"DRAWDOWN_AGE_MESSAGE",
|
|
218
|
+
"DRAWDOWN_NO_PLAN_MESSAGE",
|
|
219
|
+
"DRAWDOWN_RUNNING_MESSAGE",
|
|
220
|
+
"DRAWDOWN_STALE_MESSAGE",
|
|
221
|
+
"DRAWDOWN_SUCCESS_MESSAGE",
|
|
222
|
+
"ENTITY_ID_KEY",
|
|
223
|
+
"HELP_GUIDE_INTRO",
|
|
224
|
+
"HELP_GUIDE_SECTIONS",
|
|
225
|
+
"HELP_GUIDE_TITLE",
|
|
226
|
+
"HELP_MENU_LABEL",
|
|
227
|
+
"MONTE_CARLO_CHART_TITLE",
|
|
228
|
+
"MONTE_CARLO_NO_PLAN_MESSAGE",
|
|
229
|
+
"MONTE_CARLO_PATHS_MESSAGE",
|
|
230
|
+
"MONTE_CARLO_RUNNING_MESSAGE",
|
|
231
|
+
"MONTE_CARLO_SEED_MESSAGE",
|
|
232
|
+
"MONTE_CARLO_STALE_MESSAGE",
|
|
233
|
+
"NOTHING_TO_EXPORT_MESSAGE",
|
|
234
|
+
"NOTHING_TO_SAVE_MESSAGE",
|
|
235
|
+
"NO_BACKTEST_MESSAGE",
|
|
236
|
+
"NO_DRAWDOWN_MESSAGE",
|
|
237
|
+
"NO_MONTE_CARLO_MESSAGE",
|
|
238
|
+
"NO_PROJECTION_MESSAGE",
|
|
239
|
+
"NO_RETIREMENT_MESSAGE",
|
|
240
|
+
"OVERRIDE_SOURCE",
|
|
241
|
+
"REPORT_EXPORT_FAILED_PREFIX",
|
|
242
|
+
"REPORT_NOT_WRITTEN_MESSAGE",
|
|
243
|
+
"RETIREMENT_NO_INCOME_MESSAGE",
|
|
244
|
+
"RETIREMENT_NO_PLAN_MESSAGE",
|
|
245
|
+
"RETIREMENT_RATE_MESSAGE",
|
|
246
|
+
"RETIREMENT_RUNNING_MESSAGE",
|
|
247
|
+
"RETIREMENT_STALE_MESSAGE",
|
|
248
|
+
"RETIREMENT_SUCCESS_MESSAGE",
|
|
249
|
+
"RUN_FAILED_PREFIX",
|
|
250
|
+
"TABLE_VIEW_LABEL",
|
|
251
|
+
"UNSAVED_CHANGES_PROMPT",
|
|
252
|
+
"UNSAVED_CHANGES_TITLE",
|
|
253
|
+
"AboutViewModel",
|
|
254
|
+
"AssumptionRow",
|
|
255
|
+
"BacktestMetric",
|
|
256
|
+
"BacktestPanelViewModel",
|
|
257
|
+
"ChartBand",
|
|
258
|
+
"ChartBasisOption",
|
|
259
|
+
"ChartFill",
|
|
260
|
+
"ChartSeries",
|
|
261
|
+
"ChartSpec",
|
|
262
|
+
"ChartTable",
|
|
263
|
+
"ChartsViewModel",
|
|
264
|
+
"ChoiceOption",
|
|
265
|
+
"ComparisonMetricOption",
|
|
266
|
+
"DecisionRow",
|
|
267
|
+
"DisclaimerViewModel",
|
|
268
|
+
"DrawdownAnswer",
|
|
269
|
+
"DrawdownPanelViewModel",
|
|
270
|
+
"DrawdownRequest",
|
|
271
|
+
"ExportOutcome",
|
|
272
|
+
"FactRow",
|
|
273
|
+
"FactsFormData",
|
|
274
|
+
"FactsFormResult",
|
|
275
|
+
"FactsFormViewModel",
|
|
276
|
+
"FieldKind",
|
|
277
|
+
"FieldSpec",
|
|
278
|
+
"FileMenuViewModel",
|
|
279
|
+
"FirstRunState",
|
|
280
|
+
"FormError",
|
|
281
|
+
"HelpGuideViewModel",
|
|
282
|
+
"HelpSectionViewModel",
|
|
283
|
+
"InspectorViewModel",
|
|
284
|
+
"LoadOutcome",
|
|
285
|
+
"MonteCarloMetric",
|
|
286
|
+
"MonteCarloPanelViewModel",
|
|
287
|
+
"OverrideOutcome",
|
|
288
|
+
"OverrideRow",
|
|
289
|
+
"PlanEntityIds",
|
|
290
|
+
"PlanReport",
|
|
291
|
+
"PlanState",
|
|
292
|
+
"ReportBasis",
|
|
293
|
+
"ReportRequest",
|
|
294
|
+
"RetirementAnswer",
|
|
295
|
+
"RetirementPanelViewModel",
|
|
296
|
+
"RetirementRequest",
|
|
297
|
+
"RollForwardRow",
|
|
298
|
+
"RunModeOption",
|
|
299
|
+
"SaveOutcome",
|
|
300
|
+
"ScenarioItem",
|
|
301
|
+
"ScenariosViewModel",
|
|
302
|
+
"SectionSpec",
|
|
303
|
+
"ShellViewModel",
|
|
304
|
+
"StructureRow",
|
|
305
|
+
"TargetOption",
|
|
306
|
+
"bar_tooltip",
|
|
307
|
+
"basis_from_key",
|
|
308
|
+
"build_backtest_panel",
|
|
309
|
+
"build_charts_view_model",
|
|
310
|
+
"build_drawdown_panel",
|
|
311
|
+
"build_facts_form_view_model",
|
|
312
|
+
"build_inspector_view_model",
|
|
313
|
+
"build_plan_report",
|
|
314
|
+
"build_retirement_panel",
|
|
315
|
+
"build_scenarios_view_model",
|
|
316
|
+
"build_shell_view_model",
|
|
317
|
+
"cash_flow_csv",
|
|
318
|
+
"chart_resource_name",
|
|
319
|
+
"chart_table",
|
|
320
|
+
"default_state_path",
|
|
321
|
+
"document_from_state",
|
|
322
|
+
"example_facts_form_data",
|
|
323
|
+
"export_cash_flow_csv",
|
|
324
|
+
"facts_form_data_from_household",
|
|
325
|
+
"facts_saved_message",
|
|
326
|
+
"fill_tooltip",
|
|
327
|
+
"form_cannot_represent",
|
|
328
|
+
"format_date",
|
|
329
|
+
"format_form_errors",
|
|
330
|
+
"format_money",
|
|
331
|
+
"format_percent",
|
|
332
|
+
"format_recorded",
|
|
333
|
+
"format_value",
|
|
334
|
+
"has_unsaved_changes",
|
|
335
|
+
"initial_plan_state",
|
|
336
|
+
"load_plan_state",
|
|
337
|
+
"load_state",
|
|
338
|
+
"metric_from_key",
|
|
339
|
+
"monte_carlo_running_status",
|
|
340
|
+
"parse_facts_form",
|
|
341
|
+
"path_pool",
|
|
342
|
+
"plan_display_name",
|
|
343
|
+
"plan_entity_ids",
|
|
344
|
+
"record_disclaimer_acknowledged",
|
|
345
|
+
"record_last_plan_path",
|
|
346
|
+
"report_exported_message",
|
|
347
|
+
"run_mode_from_key",
|
|
348
|
+
"run_mode_key",
|
|
349
|
+
"save_plan_state",
|
|
350
|
+
"scenario_target_options",
|
|
351
|
+
"should_show_disclaimer",
|
|
352
|
+
"state_marked_saved",
|
|
353
|
+
"state_with_backtest",
|
|
354
|
+
"state_with_drawdown",
|
|
355
|
+
"state_with_household",
|
|
356
|
+
"state_with_monte_carlo",
|
|
357
|
+
"state_with_override",
|
|
358
|
+
"state_with_retirement",
|
|
359
|
+
"state_with_scenario_added",
|
|
360
|
+
"state_with_scenario_override",
|
|
361
|
+
"state_with_scenarios",
|
|
362
|
+
"state_without_scenario",
|
|
363
|
+
"state_without_scenario_override",
|
|
364
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Historical backtest in the shell: transition and panel (roadmap 9.18).
|
|
2
|
+
|
|
3
|
+
The core window runner (:func:`~glidepath.core.run_windows`) surfaced
|
|
4
|
+
per planning §4.7 as a charts-screen card next to the Monte Carlo
|
|
5
|
+
panel: an explicit Run action replays the plan over every rolling
|
|
6
|
+
window of the shipped historical return series
|
|
7
|
+
(``returns_history.toml``, the §5.3 provenance pattern), and the
|
|
8
|
+
readout mirrors the Monte Carlo metrics — success rate over windows,
|
|
9
|
+
the worst starting year, ending-pot percentiles — so the two surfaces
|
|
10
|
+
read side by side. Results are reproducible: no randomness is
|
|
11
|
+
involved, and every plan-changing transition routes through
|
|
12
|
+
:func:`~glidepath.app.plan.replanned_state`, which drops the held
|
|
13
|
+
result, so a stale backtest can never be shown against a changed
|
|
14
|
+
plan.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, replace
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
19
|
+
|
|
20
|
+
from glidepath.app.display import format_money, format_percent
|
|
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, RunConfig, run_windows
|
|
24
|
+
from glidepath.regions.uk import load_returns_history
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from datetime import date
|
|
28
|
+
from decimal import Decimal
|
|
29
|
+
|
|
30
|
+
from glidepath.core import BacktestResult, WindowOutcome
|
|
31
|
+
|
|
32
|
+
BACKTEST_HEADING: Final = "Historical backtest"
|
|
33
|
+
|
|
34
|
+
RUN_BACKTEST_LABEL: Final = "Run backtest"
|
|
35
|
+
|
|
36
|
+
NO_BACKTEST_MESSAGE: Final = (
|
|
37
|
+
"No backtest yet — press Run backtest to replay the plan over every"
|
|
38
|
+
" historical starting year."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
BACKTEST_NO_PLAN_MESSAGE: Final = (
|
|
42
|
+
"Save facts on the Facts tab before running a backtest."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
BACKTEST_FAILED_PREFIX: Final = "The backtest failed: "
|
|
46
|
+
|
|
47
|
+
BACKTEST_RUNNING_MESSAGE: Final = "Running historical backtest…"
|
|
48
|
+
|
|
49
|
+
BACKTEST_STALE_MESSAGE: Final = (
|
|
50
|
+
"Backtest result discarded — the plan changed while it ran."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
WINDOWS_LABEL: Final = "Windows"
|
|
54
|
+
|
|
55
|
+
WORST_WINDOW_LABEL: Final = "Worst starting year"
|
|
56
|
+
|
|
57
|
+
BEST_WINDOW_LABEL: Final = "Best starting year"
|
|
58
|
+
|
|
59
|
+
BACKTEST_YEAR_LABEL: Final = "Show starting year"
|
|
60
|
+
|
|
61
|
+
BACKTEST_YEAR_TOOLTIP_NO_RUN: Final = (
|
|
62
|
+
"Run the backtest first, then enter a historical starting year to"
|
|
63
|
+
" draw its balance path on the balances chart."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class BacktestMetric:
|
|
69
|
+
"""One backtest readout row: a label and its formatted value."""
|
|
70
|
+
|
|
71
|
+
label: str
|
|
72
|
+
value: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class BacktestPanelViewModel:
|
|
77
|
+
"""The historical-backtest card on the charts screen (roadmap 9.18).
|
|
78
|
+
|
|
79
|
+
``message`` carries the no-run or failure copy; it is blank
|
|
80
|
+
whenever ``metrics`` is populated. The card's one input is the
|
|
81
|
+
starting-year picker: ``year_value`` echoes the raw text the shell
|
|
82
|
+
captured (presentation state, not part of any run),
|
|
83
|
+
``year_placeholder`` shows the valid span greyed inside the empty
|
|
84
|
+
box (what input is expected), ``year_tooltip`` explains what an
|
|
85
|
+
entry will draw (what the control is for), and ``year_message``
|
|
86
|
+
says why a non-blank entry drew no trajectory — it never displaces
|
|
87
|
+
the metrics.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
heading: str
|
|
91
|
+
run_label: str
|
|
92
|
+
year_label: str
|
|
93
|
+
year_value: str
|
|
94
|
+
year_placeholder: str
|
|
95
|
+
year_tooltip: str
|
|
96
|
+
year_message: str
|
|
97
|
+
metrics: tuple[BacktestMetric, ...]
|
|
98
|
+
message: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def state_with_backtest(state: PlanState, *, today: date) -> PlanState:
|
|
102
|
+
"""The state after replaying the plan over every historical window.
|
|
103
|
+
|
|
104
|
+
Anything unusable — no plan, a horizon the series cannot cover, an
|
|
105
|
+
engine rejection — folds into ``backtest_error`` on the returned
|
|
106
|
+
state, never raising at a shell (the :mod:`glidepath.app.plan`
|
|
107
|
+
rule). Before the windows run, the base projection (and the
|
|
108
|
+
scenario runs with it) is recomputed at the same ``today`` through
|
|
109
|
+
:func:`~glidepath.app.plan.replanned_state` — the 9.13 rule: the
|
|
110
|
+
backtest bands and the deterministic chart they overlay must share
|
|
111
|
+
one anchor date. The run is serial by design: a full backtest is
|
|
112
|
+
around a second of work (each window is one deterministic engine
|
|
113
|
+
pass), well under the process-pool threshold the Monte Carlo
|
|
114
|
+
transition uses.
|
|
115
|
+
"""
|
|
116
|
+
if state.household is None:
|
|
117
|
+
return _with_backtest_error(state, BACKTEST_NO_PLAN_MESSAGE)
|
|
118
|
+
base = replanned_state(
|
|
119
|
+
state.assumptions,
|
|
120
|
+
state.household,
|
|
121
|
+
state.scenarios,
|
|
122
|
+
today=today,
|
|
123
|
+
modified=state.modified,
|
|
124
|
+
)
|
|
125
|
+
config = RunConfig(today=today)
|
|
126
|
+
try:
|
|
127
|
+
series = load_returns_history().series
|
|
128
|
+
result = run_windows(
|
|
129
|
+
state.household,
|
|
130
|
+
state.assumptions,
|
|
131
|
+
region_for(state.assumptions),
|
|
132
|
+
config,
|
|
133
|
+
series=series,
|
|
134
|
+
)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
# Broad by design, mirroring the Monte Carlo transition: an
|
|
137
|
+
# escape past the shell's worker thread would leave the
|
|
138
|
+
# in-flight guard held (buttons disabled, spinner running)
|
|
139
|
+
# forever, so every failure folds into the state (§4.7).
|
|
140
|
+
return _with_backtest_error(base, BACKTEST_FAILED_PREFIX + str(exc))
|
|
141
|
+
changes: dict[str, Any] = {"backtest": result, "backtest_error": None}
|
|
142
|
+
return replace(base, **changes) if changes else base
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def selected_window(
|
|
146
|
+
result: BacktestResult, year_text: str
|
|
147
|
+
) -> tuple[WindowOutcome | None, str]:
|
|
148
|
+
"""The window the raw starting-year text picks, or why none does.
|
|
149
|
+
|
|
150
|
+
A blank entry picks nothing silently; anything else must name one
|
|
151
|
+
of the result's starting years. The message names the valid span,
|
|
152
|
+
so a miss teaches the range.
|
|
153
|
+
"""
|
|
154
|
+
text = year_text.strip()
|
|
155
|
+
if not text:
|
|
156
|
+
return None, ""
|
|
157
|
+
first = result.outcomes[0].start_year
|
|
158
|
+
last = result.outcomes[-1].start_year
|
|
159
|
+
miss = f"No window starts in {text} — starting years run {first} to {last}."
|
|
160
|
+
try:
|
|
161
|
+
year = int(text, 10)
|
|
162
|
+
except ValueError:
|
|
163
|
+
return None, miss
|
|
164
|
+
outcome = next(
|
|
165
|
+
(entry for entry in result.outcomes if entry.start_year == year), None
|
|
166
|
+
)
|
|
167
|
+
if outcome is None:
|
|
168
|
+
return None, miss
|
|
169
|
+
return outcome, ""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_backtest_panel(
|
|
173
|
+
state: PlanState,
|
|
174
|
+
*,
|
|
175
|
+
ending_pot_deflator: Decimal | None,
|
|
176
|
+
basis_suffix: str,
|
|
177
|
+
year_text: str = "",
|
|
178
|
+
) -> BacktestPanelViewModel:
|
|
179
|
+
"""The historical-backtest card for the charts screen (9.18).
|
|
180
|
+
|
|
181
|
+
``ending_pot_deflator`` is the final period's balance deflator in
|
|
182
|
+
the screen's basis (1 under nominal) — every window shares the
|
|
183
|
+
run's one assumed CPI path (planning §5.2), so the deterministic
|
|
184
|
+
report's deflator presents the window pots too; ``None`` (no
|
|
185
|
+
projection to read it from) leaves the metrics unbuilt.
|
|
186
|
+
``basis_suffix`` names the basis on the money labels. ``year_text``
|
|
187
|
+
is the starting-year picker's raw text; with a held result it is
|
|
188
|
+
vetted through :func:`selected_window` so the card can say why a
|
|
189
|
+
miss drew nothing.
|
|
190
|
+
"""
|
|
191
|
+
result = state.backtest
|
|
192
|
+
metrics: tuple[BacktestMetric, ...] = ()
|
|
193
|
+
message = ""
|
|
194
|
+
year_placeholder = ""
|
|
195
|
+
year_tooltip = BACKTEST_YEAR_TOOLTIP_NO_RUN
|
|
196
|
+
year_message = ""
|
|
197
|
+
if state.backtest_error is not None:
|
|
198
|
+
message = state.backtest_error
|
|
199
|
+
elif result is None or ending_pot_deflator is None:
|
|
200
|
+
message = NO_BACKTEST_MESSAGE
|
|
201
|
+
else:
|
|
202
|
+
metrics = _metrics(result, ending_pot_deflator, basis_suffix)
|
|
203
|
+
first = result.outcomes[0].start_year
|
|
204
|
+
last = result.outcomes[-1].start_year
|
|
205
|
+
year_placeholder = f"{first}-{last}"
|
|
206
|
+
year_tooltip = (
|
|
207
|
+
"Draw one historical starting year's balance path on the"
|
|
208
|
+
" balances chart, alongside the worst and best starting"
|
|
209
|
+
f" years. Enter a year from {first} to {last}."
|
|
210
|
+
)
|
|
211
|
+
_, year_message = selected_window(result, year_text)
|
|
212
|
+
return BacktestPanelViewModel(
|
|
213
|
+
heading=BACKTEST_HEADING,
|
|
214
|
+
run_label=RUN_BACKTEST_LABEL,
|
|
215
|
+
year_label=BACKTEST_YEAR_LABEL,
|
|
216
|
+
year_value=year_text,
|
|
217
|
+
year_placeholder=year_placeholder,
|
|
218
|
+
year_tooltip=year_tooltip,
|
|
219
|
+
year_message=year_message,
|
|
220
|
+
metrics=metrics,
|
|
221
|
+
message=message,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _window_value(
|
|
226
|
+
outcome: WindowOutcome, ending_pot_deflator: Decimal, basis_suffix: str
|
|
227
|
+
) -> str:
|
|
228
|
+
"""One starting year with how its window ended.
|
|
229
|
+
|
|
230
|
+
A ruined window names the plan-calendar year the money ran out —
|
|
231
|
+
the year the charts' x axis carries — and a surviving one its
|
|
232
|
+
ending pot, so the row reads against the chart either way.
|
|
233
|
+
"""
|
|
234
|
+
shortfall = outcome.first_shortfall_period
|
|
235
|
+
if shortfall is not None:
|
|
236
|
+
return f"{outcome.start_year} — money ran out in {shortfall.start.year}"
|
|
237
|
+
presented = Money(outcome.ending_balance.amount / ending_pot_deflator).quantized()
|
|
238
|
+
return (
|
|
239
|
+
f"{outcome.start_year} — ending pot {format_money(presented)} ({basis_suffix})"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _metrics(
|
|
244
|
+
result: BacktestResult, ending_pot_deflator: Decimal, basis_suffix: str
|
|
245
|
+
) -> tuple[BacktestMetric, ...]:
|
|
246
|
+
"""The backtest readout rows, mirroring the Monte Carlo metrics."""
|
|
247
|
+
first = result.outcomes[0].start_year
|
|
248
|
+
last = result.outcomes[-1].start_year
|
|
249
|
+
rows = [
|
|
250
|
+
BacktestMetric(
|
|
251
|
+
label=SUCCESS_RATE_LABEL, value=format_percent(result.success_rate)
|
|
252
|
+
),
|
|
253
|
+
BacktestMetric(
|
|
254
|
+
label=WINDOWS_LABEL,
|
|
255
|
+
value=f"{result.window_count} starting years ({first} to {last})",
|
|
256
|
+
),
|
|
257
|
+
BacktestMetric(
|
|
258
|
+
label=WORST_WINDOW_LABEL,
|
|
259
|
+
value=_window_value(result.worst_window, ending_pot_deflator, basis_suffix),
|
|
260
|
+
),
|
|
261
|
+
BacktestMetric(
|
|
262
|
+
label=BEST_WINDOW_LABEL,
|
|
263
|
+
value=_window_value(result.best_window, ending_pot_deflator, basis_suffix),
|
|
264
|
+
),
|
|
265
|
+
]
|
|
266
|
+
for spec in BAND_SPECS:
|
|
267
|
+
pot = result.ending_pot_percentile(spec.percentile)
|
|
268
|
+
presented = Money(pot.amount / ending_pot_deflator).quantized()
|
|
269
|
+
rows.append(
|
|
270
|
+
BacktestMetric(
|
|
271
|
+
label=f"{spec.ending_pot_label} ({basis_suffix})",
|
|
272
|
+
value=format_money(presented),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
return tuple(rows)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _with_backtest_error(state: PlanState, message: str) -> PlanState:
|
|
279
|
+
"""The state carrying a backtest failure, any held result dropped."""
|
|
280
|
+
changes: dict[str, Any] = {"backtest": None, "backtest_error": message}
|
|
281
|
+
return replace(state, **changes) if changes else state
|