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.
Files changed (93) hide show
  1. glidepath/__init__.py +3 -0
  2. glidepath/app/__init__.py +364 -0
  3. glidepath/app/backtest.py +281 -0
  4. glidepath/app/charts.py +759 -0
  5. glidepath/app/copy.py +174 -0
  6. glidepath/app/display.py +148 -0
  7. glidepath/app/drawdown.py +436 -0
  8. glidepath/app/example.py +66 -0
  9. glidepath/app/exports.py +487 -0
  10. glidepath/app/files.py +249 -0
  11. glidepath/app/firstrun.py +114 -0
  12. glidepath/app/forms.py +1750 -0
  13. glidepath/app/inspector.py +506 -0
  14. glidepath/app/labels.py +66 -0
  15. glidepath/app/montecarlo.py +399 -0
  16. glidepath/app/plan.py +354 -0
  17. glidepath/app/retirement.py +446 -0
  18. glidepath/app/scenarios.py +831 -0
  19. glidepath/app/shell.py +185 -0
  20. glidepath/app/tables.py +138 -0
  21. glidepath/core/__init__.py +390 -0
  22. glidepath/core/annuities.py +240 -0
  23. glidepath/core/backtest.py +514 -0
  24. glidepath/core/comparison.py +278 -0
  25. glidepath/core/config.py +82 -0
  26. glidepath/core/contributions.py +337 -0
  27. glidepath/core/engine.py +2811 -0
  28. glidepath/core/entities.py +264 -0
  29. glidepath/core/glide.py +289 -0
  30. glidepath/core/investments.py +175 -0
  31. glidepath/core/money.py +107 -0
  32. glidepath/core/montecarlo.py +609 -0
  33. glidepath/core/pensions.py +298 -0
  34. glidepath/core/periods.py +367 -0
  35. glidepath/core/provenance.py +271 -0
  36. glidepath/core/randomness.py +128 -0
  37. glidepath/core/region.py +46 -0
  38. glidepath/core/reporting.py +231 -0
  39. glidepath/core/results.py +504 -0
  40. glidepath/core/retirement.py +291 -0
  41. glidepath/core/returns.py +312 -0
  42. glidepath/core/scenarios.py +579 -0
  43. glidepath/core/state_pension.py +264 -0
  44. glidepath/core/tax.py +139 -0
  45. glidepath/core/withdrawals.py +461 -0
  46. glidepath/core/wrappers.py +278 -0
  47. glidepath/gui/__init__.py +6 -0
  48. glidepath/gui/assets/icon_128.png +0 -0
  49. glidepath/gui/assets/icon_16.png +0 -0
  50. glidepath/gui/assets/icon_24.png +0 -0
  51. glidepath/gui/assets/icon_256.png +0 -0
  52. glidepath/gui/assets/icon_32.png +0 -0
  53. glidepath/gui/assets/icon_48.png +0 -0
  54. glidepath/gui/assets/icon_64.png +0 -0
  55. glidepath/gui/assets/wordmark.png +0 -0
  56. glidepath/gui/charts.py +829 -0
  57. glidepath/gui/forms.py +359 -0
  58. glidepath/gui/inspector.py +186 -0
  59. glidepath/gui/main.py +51 -0
  60. glidepath/gui/scenarios.py +402 -0
  61. glidepath/gui/style.py +376 -0
  62. glidepath/gui/tableview.py +67 -0
  63. glidepath/gui/widgets.py +989 -0
  64. glidepath/persistence/__init__.py +48 -0
  65. glidepath/persistence/assumptions.py +112 -0
  66. glidepath/persistence/decode.py +747 -0
  67. glidepath/persistence/document.py +101 -0
  68. glidepath/persistence/encode.py +433 -0
  69. glidepath/persistence/migrations.py +158 -0
  70. glidepath/persistence/values.py +298 -0
  71. glidepath/py.typed +0 -0
  72. glidepath/regions/__init__.py +7 -0
  73. glidepath/regions/uk/__init__.py +189 -0
  74. glidepath/regions/uk/ages.py +156 -0
  75. glidepath/regions/uk/contributions.py +717 -0
  76. glidepath/regions/uk/data/age_rules.toml +78 -0
  77. glidepath/regions/uk/data/assumptions_default.toml +170 -0
  78. glidepath/regions/uk/data/returns_history.toml +150 -0
  79. glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
  80. glidepath/regions/uk/extension.py +479 -0
  81. glidepath/regions/uk/loader.py +704 -0
  82. glidepath/regions/uk/region.py +160 -0
  83. glidepath/regions/uk/schema.py +563 -0
  84. glidepath/regions/uk/state_pension.py +129 -0
  85. glidepath/regions/uk/tax.py +466 -0
  86. glidepath/regions/uk/wrappers.py +283 -0
  87. glidepath/regions/uk/years.py +92 -0
  88. glidepath-0.2.0.dist-info/METADATA +189 -0
  89. glidepath-0.2.0.dist-info/RECORD +93 -0
  90. glidepath-0.2.0.dist-info/WHEEL +4 -0
  91. glidepath-0.2.0.dist-info/entry_points.txt +3 -0
  92. glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
  93. glidepath-0.2.0.dist-info/licenses/LICENSE-DATA +28 -0
glidepath/gui/style.py ADDED
@@ -0,0 +1,376 @@
1
+ """The shell's visual theme: palette, stylesheet, and branding (§4.7).
2
+
3
+ Purely presentational Qt mechanics — no copy, no policy. The Fusion
4
+ style gives an identical baseline on every platform; the palette and
5
+ stylesheet build a light look around the brand green — white cards on
6
+ a softly green-tinted window, the accent carrying buttons, focus,
7
+ selections, tab underlines, and headings; the window icon and the
8
+ About wordmark load from ``assets/`` (the icon at every
9
+ taskbar-to-tile size), so refreshed art is a file swap.
10
+
11
+ The chart series palette is a colour-vision-validated categorical
12
+ set (fixed slot order — the ordering is the CVD-safety mechanism);
13
+ the percentile band overlays take an ordered ink ramp so the bands
14
+ read apart from the series fills, and the Monte Carlo fan fills one
15
+ brand-green hue at stepped alphas so probability depth reads by
16
+ lightness alone (9.24).
17
+ """
18
+
19
+ from importlib import resources
20
+ from typing import TYPE_CHECKING
21
+
22
+ from PySide6.QtGui import QColor, QIcon, QPalette, QPixmap
23
+
24
+ if TYPE_CHECKING:
25
+ from PySide6.QtWidgets import QApplication
26
+
27
+ ACCENT = "#1e6d59"
28
+ """The single accent colour — the brand tile's green: buttons, focus,
29
+ selections, the active tab underline."""
30
+ ACCENT_TEXT = "#14503f"
31
+ """The accent stepped darker for text on light surfaces (card titles,
32
+ table headers, the selected tab) — the button green is too light to
33
+ read as small text."""
34
+ _ACCENT_HOVER = "#195b4a"
35
+ _ACCENT_PRESSED = "#144a3c"
36
+ _ACCENT_DISABLED = "#a3c4b9"
37
+ _WINDOW = "#e9eeeb"
38
+ _CARD = "#ffffff"
39
+ _BORDER = "#d3dbd5"
40
+ _TEXT = "#1f2523"
41
+ _MUTED = "#5c6663"
42
+ _DISABLED_TEXT = "#95a09b"
43
+ _DISABLED_FILL = "#f0f3f1"
44
+ _TINT_HOVER = "#e7f0ec"
45
+ _TINT_PRESSED = "#d7e7e0"
46
+ _ALT_ROW = "#f4f8f5"
47
+ _HEADER_BG = "#eef3f0"
48
+ _SCROLL_HANDLE = "#c2ccc6"
49
+ _SCROLL_HANDLE_HOVER = "#9db1a8"
50
+ _TOOLTIP_BG = "#253230"
51
+
52
+ CHART_SURFACE = "#ffffff"
53
+ """The chart plot surface — white, matching the cards the charts sit
54
+ beside, and the surface the series palette is validated against."""
55
+ CHART_GRID = "#e8ece9"
56
+ """Hairline horizontal gridlines — recessive, behind the marks."""
57
+ CHART_AXIS_LINE = "#c9d2cc"
58
+ """The axis baseline pen — a step darker than the grid."""
59
+ CHART_LABEL_INK = _MUTED
60
+ """Axis tick labels and titles — muted ink, never the series colour."""
61
+ CHART_TEXT_INK = _TEXT
62
+ """Legend labels — primary ink; the marker beside them carries the
63
+ series identity."""
64
+
65
+ CHART_SERIES = (
66
+ "#2a78d6",
67
+ "#eb6834",
68
+ "#1baf7a",
69
+ "#eda100",
70
+ "#e87ba4",
71
+ "#008300",
72
+ "#4a3aa7",
73
+ "#e34948",
74
+ )
75
+ """Categorical series colours, assigned to bar sets in this fixed
76
+ order, never cycled or re-ranked. The order is validated for adjacent
77
+ colour-vision-deficiency separation on the white chart surface; the
78
+ sub-3:1-contrast slots are relieved by the per-segment hover
79
+ tooltips, the white segment gaps, and the cash-flow CSV export."""
80
+
81
+ CHART_BAND_INKS = ("#0b0b0b", "#52514e", "#898781")
82
+ """Percentile/backtest band overlays, assigned in band order — one
83
+ neutral ink hue stepped light-ward so the ordered bands read apart
84
+ from every series fill and from each other."""
85
+
86
+ CHART_FAN_FILL = ACCENT
87
+ """The Monte Carlo fan's single fill hue — the brand green; depth
88
+ comes from the alpha ramp, so the fan stays colour-vision-safe
89
+ (lightness alone orders the fills)."""
90
+
91
+ CHART_FAN_ALPHAS = (45, 75, 110, 150)
92
+ """Fill opacities in fan order, outermost first. The nested fills
93
+ overlap, so the stacked alphas deepen naturally toward the median —
94
+ central probability mass reads as colour depth."""
95
+
96
+ _ICON_SIZES = (16, 24, 32, 48, 64, 128, 256)
97
+
98
+ STYLESHEET = f"""
99
+ QGroupBox {{
100
+ background: {_CARD};
101
+ border: 1px solid {_BORDER};
102
+ border-radius: 8px;
103
+ margin-top: 14px;
104
+ padding: 12px 14px 12px 14px;
105
+ }}
106
+ QGroupBox::title {{
107
+ subcontrol-origin: margin;
108
+ left: 12px;
109
+ padding: 0 5px;
110
+ color: {ACCENT_TEXT};
111
+ font-weight: 600;
112
+ }}
113
+ QLineEdit, QComboBox {{
114
+ background: {_CARD};
115
+ border: 1px solid {_BORDER};
116
+ border-radius: 6px;
117
+ padding: 5px 9px;
118
+ selection-background-color: {ACCENT};
119
+ selection-color: white;
120
+ }}
121
+ QLineEdit:focus, QComboBox:focus {{
122
+ border: 1px solid {ACCENT};
123
+ }}
124
+ QLineEdit:disabled, QComboBox:disabled {{
125
+ background: {_DISABLED_FILL};
126
+ color: {_DISABLED_TEXT};
127
+ }}
128
+ QComboBox::drop-down {{
129
+ border: none;
130
+ width: 26px;
131
+ }}
132
+ QComboBox QAbstractItemView {{
133
+ background: {_CARD};
134
+ border: 1px solid {_BORDER};
135
+ padding: 4px;
136
+ selection-background-color: {_TINT_HOVER};
137
+ selection-color: {_TEXT};
138
+ }}
139
+ QPushButton {{
140
+ background: {_CARD};
141
+ border: 1px solid {_BORDER};
142
+ border-radius: 6px;
143
+ padding: 6px 16px;
144
+ }}
145
+ QPushButton:hover {{
146
+ background: {_TINT_HOVER};
147
+ border-color: {ACCENT};
148
+ color: {ACCENT_TEXT};
149
+ }}
150
+ QPushButton:pressed {{
151
+ background: {_TINT_PRESSED};
152
+ }}
153
+ QPushButton:disabled {{
154
+ background: {_DISABLED_FILL};
155
+ border-color: {_BORDER};
156
+ color: {_DISABLED_TEXT};
157
+ }}
158
+ QPushButton#primaryButton {{
159
+ background: {ACCENT};
160
+ border: 1px solid {ACCENT};
161
+ color: white;
162
+ font-weight: bold;
163
+ }}
164
+ QPushButton#primaryButton:hover {{
165
+ background: {_ACCENT_HOVER};
166
+ color: white;
167
+ }}
168
+ QPushButton#primaryButton:pressed {{
169
+ background: {_ACCENT_PRESSED};
170
+ }}
171
+ QPushButton#primaryButton:disabled {{
172
+ background: {_ACCENT_DISABLED};
173
+ border-color: {_ACCENT_DISABLED};
174
+ color: white;
175
+ }}
176
+ QTabWidget::pane {{
177
+ border: 1px solid {_BORDER};
178
+ border-radius: 6px;
179
+ top: -1px;
180
+ }}
181
+ QTabBar::tab {{
182
+ padding: 8px 18px;
183
+ border: 1px solid transparent;
184
+ border-bottom: 2px solid transparent;
185
+ color: {_MUTED};
186
+ margin-right: 2px;
187
+ }}
188
+ QTabBar::tab:hover {{
189
+ color: {_TEXT};
190
+ }}
191
+ QTabBar::tab:selected {{
192
+ color: {ACCENT_TEXT};
193
+ font-weight: bold;
194
+ border-bottom: 2px solid {ACCENT};
195
+ }}
196
+ QTableWidget {{
197
+ background: {_CARD};
198
+ alternate-background-color: {_ALT_ROW};
199
+ border: 1px solid {_BORDER};
200
+ border-radius: 6px;
201
+ gridline-color: {_ALT_ROW};
202
+ selection-background-color: {_TINT_PRESSED};
203
+ selection-color: {_TEXT};
204
+ }}
205
+ QHeaderView::section {{
206
+ background: {_HEADER_BG};
207
+ border: none;
208
+ border-bottom: 1px solid {_BORDER};
209
+ padding: 6px 10px;
210
+ font-weight: 600;
211
+ color: {ACCENT_TEXT};
212
+ }}
213
+ QListWidget {{
214
+ background: {_CARD};
215
+ border: 1px solid {_BORDER};
216
+ border-radius: 6px;
217
+ padding: 3px;
218
+ }}
219
+ QListWidget::item {{
220
+ padding: 5px 8px;
221
+ border-radius: 4px;
222
+ }}
223
+ QListWidget::item:hover {{
224
+ background: {_TINT_HOVER};
225
+ }}
226
+ QListWidget::item:selected {{
227
+ background: {_TINT_PRESSED};
228
+ color: {_TEXT};
229
+ }}
230
+ QScrollArea {{
231
+ border: none;
232
+ }}
233
+ QScrollBar:vertical {{
234
+ background: transparent;
235
+ width: 10px;
236
+ margin: 0;
237
+ }}
238
+ QScrollBar::handle:vertical {{
239
+ background: {_SCROLL_HANDLE};
240
+ border-radius: 5px;
241
+ min-height: 30px;
242
+ }}
243
+ QScrollBar::handle:vertical:hover {{
244
+ background: {_SCROLL_HANDLE_HOVER};
245
+ }}
246
+ QScrollBar:horizontal {{
247
+ background: transparent;
248
+ height: 10px;
249
+ margin: 0;
250
+ }}
251
+ QScrollBar::handle:horizontal {{
252
+ background: {_SCROLL_HANDLE};
253
+ border-radius: 5px;
254
+ min-width: 30px;
255
+ }}
256
+ QScrollBar::handle:horizontal:hover {{
257
+ background: {_SCROLL_HANDLE_HOVER};
258
+ }}
259
+ QScrollBar::add-line, QScrollBar::sub-line {{
260
+ width: 0;
261
+ height: 0;
262
+ }}
263
+ QScrollBar::add-page, QScrollBar::sub-page {{
264
+ background: transparent;
265
+ }}
266
+ QMenuBar {{
267
+ background: transparent;
268
+ padding: 2px 6px;
269
+ }}
270
+ QMenuBar::item {{
271
+ padding: 5px 10px;
272
+ border-radius: 5px;
273
+ color: {_TEXT};
274
+ }}
275
+ QMenuBar::item:selected {{
276
+ background: {_TINT_HOVER};
277
+ color: {ACCENT_TEXT};
278
+ }}
279
+ QMenu {{
280
+ background: {_CARD};
281
+ border: 1px solid {_BORDER};
282
+ padding: 5px;
283
+ }}
284
+ QMenu::item {{
285
+ padding: 6px 22px 6px 14px;
286
+ border-radius: 4px;
287
+ }}
288
+ QMenu::item:selected {{
289
+ background: {_TINT_HOVER};
290
+ color: {ACCENT_TEXT};
291
+ }}
292
+ QMenu::separator {{
293
+ height: 1px;
294
+ background: {_BORDER};
295
+ margin: 5px 8px;
296
+ }}
297
+ QStatusBar {{
298
+ color: {_MUTED};
299
+ }}
300
+ QStatusBar::item {{
301
+ border: none;
302
+ }}
303
+ QToolTip {{
304
+ background-color: {_TOOLTIP_BG};
305
+ color: {_WINDOW};
306
+ border: 1px solid {_TOOLTIP_BG};
307
+ padding: 5px 8px;
308
+ }}
309
+ QRadioButton {{
310
+ spacing: 6px;
311
+ }}
312
+ QLabel#answerLabel {{
313
+ font-size: 13pt;
314
+ font-weight: 600;
315
+ color: {ACCENT_TEXT};
316
+ }}
317
+ """
318
+
319
+
320
+ def _asset_bytes(name: str) -> bytes:
321
+ """One packaged asset's raw bytes."""
322
+ return (resources.files("glidepath.gui") / "assets" / name).read_bytes()
323
+
324
+
325
+ def app_icon() -> QIcon:
326
+ """The brand icon — the glide curve on the green tile — every size."""
327
+ icon = QIcon()
328
+ for size in _ICON_SIZES:
329
+ pixmap = QPixmap()
330
+ pixmap.loadFromData(_asset_bytes(f"icon_{size}.png"))
331
+ icon.addPixmap(pixmap)
332
+ return icon
333
+
334
+
335
+ def wordmark_pixmap() -> QPixmap:
336
+ """The horizontal wordmark, for the About screen."""
337
+ pixmap = QPixmap()
338
+ pixmap.loadFromData(_asset_bytes("wordmark.png"))
339
+ return pixmap
340
+
341
+
342
+ def apply_theme(app: QApplication) -> None:
343
+ """Apply the theme to the whole application (module docstring)."""
344
+ app.setStyle("Fusion")
345
+ palette = app.palette()
346
+ palette.setColor(QPalette.ColorRole.Window, QColor(_WINDOW))
347
+ palette.setColor(QPalette.ColorRole.Base, QColor(_CARD))
348
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor(_ALT_ROW))
349
+ palette.setColor(QPalette.ColorRole.Text, QColor(_TEXT))
350
+ palette.setColor(QPalette.ColorRole.WindowText, QColor(_TEXT))
351
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor(_TEXT))
352
+ palette.setColor(QPalette.ColorRole.Highlight, QColor(ACCENT))
353
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("white"))
354
+ palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(_MUTED))
355
+ app.setPalette(palette)
356
+ app.setStyleSheet(STYLESHEET)
357
+ app.setWindowIcon(app_icon())
358
+
359
+
360
+ __all__ = [
361
+ "ACCENT",
362
+ "ACCENT_TEXT",
363
+ "CHART_AXIS_LINE",
364
+ "CHART_BAND_INKS",
365
+ "CHART_FAN_ALPHAS",
366
+ "CHART_FAN_FILL",
367
+ "CHART_GRID",
368
+ "CHART_LABEL_INK",
369
+ "CHART_SERIES",
370
+ "CHART_SURFACE",
371
+ "CHART_TEXT_INK",
372
+ "STYLESHEET",
373
+ "app_icon",
374
+ "apply_theme",
375
+ "wordmark_pixmap",
376
+ ]
@@ -0,0 +1,67 @@
1
+ """Shared read-only table widgets for view-model rows (§4.7).
2
+
3
+ Every result pane renders pre-formatted app-layer rows the same way;
4
+ these helpers keep that mechanical binding in one place.
5
+ """
6
+
7
+ from PySide6.QtWidgets import (
8
+ QAbstractItemView,
9
+ QTableWidget,
10
+ QTableWidgetItem,
11
+ QWidget,
12
+ )
13
+
14
+ _MAX_COLUMN_WIDTH = 300
15
+ """Widths beyond this elide (the view's default ElideRight) instead of
16
+ forcing the whole table into a horizontal scrollbar; the full text
17
+ stays reachable via the cell tooltip or the override dialog."""
18
+
19
+ _TOOLTIP_LENGTH = 45
20
+ """Cells longer than this get their own text as tooltip — roughly what
21
+ fits in a capped column, so anything elided is hoverable in full."""
22
+
23
+
24
+ def read_only_table(parent: QWidget) -> QTableWidget:
25
+ """A table configured for display-only rows."""
26
+ table = QTableWidget(parent)
27
+ table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
28
+ table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
29
+ table.setAlternatingRowColors(True)
30
+ table.verticalHeader().setVisible(False)
31
+ return table
32
+
33
+
34
+ def fill_table(
35
+ table: QTableWidget,
36
+ columns: tuple[str, ...],
37
+ rows: list[tuple[str, ...]],
38
+ tooltips: list[str] | None = None,
39
+ ) -> None:
40
+ """Replace a table's contents with pre-formatted cells.
41
+
42
+ Columns size to their content up to a cap; longer cells elide.
43
+ ``tooltips`` are per-row descriptions shown on the first (label)
44
+ column only, so an elided cell elsewhere always hovers to its own
45
+ full text, never to the row description.
46
+ """
47
+ table.setColumnCount(len(columns))
48
+ table.setHorizontalHeaderLabels(list(columns))
49
+ table.setRowCount(len(rows))
50
+ for row_index, row in enumerate(rows):
51
+ for column_index, text in enumerate(row):
52
+ item = QTableWidgetItem(text)
53
+ if tooltips is not None and column_index == 0:
54
+ item.setToolTip(tooltips[row_index])
55
+ elif len(text) > _TOOLTIP_LENGTH:
56
+ item.setToolTip(text)
57
+ table.setItem(row_index, column_index, item)
58
+ table.resizeColumnsToContents()
59
+ for index in range(table.columnCount()):
60
+ if table.columnWidth(index) > _MAX_COLUMN_WIDTH:
61
+ table.setColumnWidth(index, _MAX_COLUMN_WIDTH)
62
+
63
+
64
+ __all__ = [
65
+ "fill_table",
66
+ "read_only_table",
67
+ ]