manyfutures 0.1.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.
- manyfutures-0.1.0/.gitignore +16 -0
- manyfutures-0.1.0/CHANGELOG.md +43 -0
- manyfutures-0.1.0/GLOSSARY.md +64 -0
- manyfutures-0.1.0/LICENSE +21 -0
- manyfutures-0.1.0/PKG-INFO +106 -0
- manyfutures-0.1.0/README.md +83 -0
- manyfutures-0.1.0/docs/adr/0001-end-age-is-the-only-way-a-death-enters-the-model.md +42 -0
- manyfutures-0.1.0/docs/adr/README.md +8 -0
- manyfutures-0.1.0/docs/decisions.md +1072 -0
- manyfutures-0.1.0/prek.toml +17 -0
- manyfutures-0.1.0/pyproject.toml +115 -0
- manyfutures-0.1.0/scripts/ag2026_guard.py +128 -0
- manyfutures-0.1.0/scripts/clean_install_check.py +80 -0
- manyfutures-0.1.0/src/manyfutures/__init__.py +32 -0
- manyfutures-0.1.0/src/manyfutures/analytics.py +96 -0
- manyfutures-0.1.0/src/manyfutures/constants.py +96 -0
- manyfutures-0.1.0/src/manyfutures/data.py +127 -0
- manyfutures-0.1.0/src/manyfutures/life_table.csv +9220 -0
- manyfutures-0.1.0/src/manyfutures/models.py +856 -0
- manyfutures-0.1.0/src/manyfutures/mortgage.py +184 -0
- manyfutures-0.1.0/src/manyfutures/paths.py +493 -0
- manyfutures-0.1.0/src/manyfutures/places/__init__.py +31 -0
- manyfutures-0.1.0/src/manyfutures/places/at.py +134 -0
- manyfutures-0.1.0/src/manyfutures/places/es.py +133 -0
- manyfutures-0.1.0/src/manyfutures/places/fr.py +92 -0
- manyfutures-0.1.0/src/manyfutures/places/it.py +97 -0
- manyfutures-0.1.0/src/manyfutures/places/nl.py +145 -0
- manyfutures-0.1.0/src/manyfutures/places/uk.py +116 -0
- manyfutures-0.1.0/src/manyfutures/py.typed +0 -0
- manyfutures-0.1.0/src/manyfutures/readouts.py +49 -0
- manyfutures-0.1.0/src/manyfutures/scenario.py +852 -0
- manyfutures-0.1.0/src/manyfutures/simulation.py +1807 -0
- manyfutures-0.1.0/src/manyfutures/tax.py +218 -0
- manyfutures-0.1.0/src/manyfutures/us_cpi.csv +113 -0
- manyfutures-0.1.0/tests/__init__.py +0 -0
- manyfutures-0.1.0/tests/example_household.py +59 -0
- manyfutures-0.1.0/tests/test_ag2026_guard.py +186 -0
- manyfutures-0.1.0/tests/test_clean_install.py +45 -0
- manyfutures-0.1.0/tests/test_mc_portfolio.py +4744 -0
- manyfutures-0.1.0/tests/test_package.py +18 -0
- manyfutures-0.1.0/tests/test_portfolio_engine_boundary.py +66 -0
- manyfutures-0.1.0/tests/test_portfolio_scenario.py +1840 -0
- manyfutures-0.1.0/tests/test_readme.py +38 -0
- manyfutures-0.1.0/uv.lock +507 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What each release changed, and whether it moved the headline, the chance of running short.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 (2026-10-26)
|
|
6
|
+
|
|
7
|
+
The first release: the engine, moved here from the private dashboard it was built for, with the projected life
|
|
8
|
+
table of Statistics Netherlands (CBS).
|
|
9
|
+
|
|
10
|
+
### The engine, moved
|
|
11
|
+
|
|
12
|
+
- **The Scenario seam** at the package's top level: `run`, `return_band`, `compare` and `inputs_that_matter` run a
|
|
13
|
+
`Scenario`, the household's decisions, from a `Start`, its money and debts on a date and the market's past
|
|
14
|
+
returns, on `Assumptions`, its facts and the model's constants, and give an `Outcome` or a `ReturnBand`.
|
|
15
|
+
- **The rest under the module names it had in the dashboard**: `models`, `constants`, `data`, `places` (nine places
|
|
16
|
+
in six countries, each tax regime with its source and date), `tax`, `mortgage`, `paths`, `simulation`,
|
|
17
|
+
`analytics` and `readouts`.
|
|
18
|
+
- **The household's facts have no default** (decision 9): a run is always told whose life it runs on. The model's
|
|
19
|
+
constants keep their defaults, and the Scenario's defaults are a made-up example household's choices.
|
|
20
|
+
- **It reaches out to nothing**: it depends on numpy and pydantic alone, fetches nothing (the caller hands it the
|
|
21
|
+
return history), and its measures give plain records rather than tables.
|
|
22
|
+
- **US inflation** bundled as `us_cpi.csv`: the CPI-U each December, FRED series CPIAUCNS (public domain), each
|
|
23
|
+
year's own taken off its return (decision 2).
|
|
24
|
+
- **Its words and reasons**: the glossary (`GLOSSARY.md`), modelling decisions 1 to 35 (`docs/decisions.md`) and
|
|
25
|
+
ADR 0001, the End Age (`docs/adr/`). The README runs the example household's plan in under twenty lines.
|
|
26
|
+
|
|
27
|
+
### The CBS life table
|
|
28
|
+
|
|
29
|
+
- CBS's projected life table, StatLine 86246NED (the Kernprognose 2025-2070, published 16 December 2025, CC BY 4.0),
|
|
30
|
+
bundled as `life_table.csv` with its source, date, licence and reduction in its header: the chance of dying
|
|
31
|
+
before the next birthday for men and for women, at ages 0 to 99 or older, in each year from 2025 to 2070.
|
|
32
|
+
- Each adult's chances are read along their own birth year at run time, so the End Age is drawn for any two birth
|
|
33
|
+
years the table covers (decision 26).
|
|
34
|
+
- It replaces the actuaries' table AG2026, which is all rights reserved: AG2026 never entered this repository, and
|
|
35
|
+
a check in CI keeps it out.
|
|
36
|
+
|
|
37
|
+
### The headline
|
|
38
|
+
|
|
39
|
+
- The move changed no number: before its tests moved to the example household, the moved engine passed the
|
|
40
|
+
dashboard's own engine tests on the dashboard's household, 324 of 324. The CBS table, which the dashboard took up
|
|
41
|
+
just before the move, moved the End Ages a little and the headline with them.
|
|
42
|
+
- The tests here are pinned on the example household and the CBS table. Its default plan runs short in about one
|
|
43
|
+
future in five: 21.7% ± 0.6 at the default 5,000 futures.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# manyfutures
|
|
2
|
+
|
|
3
|
+
The engine's words: what a plan, its futures and its measures are called here, in the code, the docstrings, the
|
|
4
|
+
tests and the docs. Each is defined once. The modelling decisions in [docs/decisions.md](docs/decisions.md) and the
|
|
5
|
+
ADRs in [docs/adr/](docs/adr/) use them. The words came with the engine from the dashboard it was built for.
|
|
6
|
+
|
|
7
|
+
## Language
|
|
8
|
+
|
|
9
|
+
**Scenario**:
|
|
10
|
+
The household's decisions for one plan: when to retire, what to spend in retirement and how much of its spending
|
|
11
|
+
it must be able to pay (Must-have Spending), the Bridge Income and until when, whether, when and where to
|
|
12
|
+
downsize, and whether to sell the home as a last resort (the Last-resort Sale).
|
|
13
|
+
_Avoid_: config, inputs
|
|
14
|
+
|
|
15
|
+
**Retirement**:
|
|
16
|
+
Leaving full-time work, at the Scenario's retirement age. From then the household's salaries fall to its Bridge
|
|
17
|
+
Income and its spending becomes the Scenario's annual spending.
|
|
18
|
+
_Avoid_: stopping work (the household keeps earning Bridge Income)
|
|
19
|
+
|
|
20
|
+
**Bridge Income**:
|
|
21
|
+
The household's gross earnings after Retirement, from consulting, contract work or a partner's job, in today's
|
|
22
|
+
euros, until an age the Scenario sets.
|
|
23
|
+
_Avoid_: post-retirement salary, pt salary, part-time salary
|
|
24
|
+
|
|
25
|
+
**End Age**:
|
|
26
|
+
The age, in whole years, at which the later of the two deaths falls on one simulated path, drawn at random for that
|
|
27
|
+
path from CBS's projected life table: the plan has to last to the end of the year it falls in. The one way a death
|
|
28
|
+
enters the model: nothing else in the plan changes with it.
|
|
29
|
+
_Avoid_: life expectancy, horizon, death age, survivor
|
|
30
|
+
|
|
31
|
+
**Must-have Spending**:
|
|
32
|
+
The part of the household's baseline spending it must fund every year, in today's euros. Housing, children, the
|
|
33
|
+
student loan and taxes are always paid and sit outside it.
|
|
34
|
+
_Avoid_: floor, essential spending
|
|
35
|
+
|
|
36
|
+
**Nice-to-have Spending**:
|
|
37
|
+
Baseline spending above Must-have Spending.
|
|
38
|
+
_Avoid_: discretionary spending
|
|
39
|
+
|
|
40
|
+
**Money Left at the End**:
|
|
41
|
+
What a simulated future leaves at the end of the year its End Age falls in, in today's euros: its liquid money (the
|
|
42
|
+
portfolio and the cash reserve) plus its home's value less what is still owed on it.
|
|
43
|
+
_Avoid_: terminal wealth, bequest, estate, final liquid (that is the portfolio alone)
|
|
44
|
+
|
|
45
|
+
**Running Short**:
|
|
46
|
+
A year in which more than a tenth of Must-have Spending goes unfunded.
|
|
47
|
+
_Avoid_: shortfall, failure, depletion
|
|
48
|
+
|
|
49
|
+
**Spending Rule**:
|
|
50
|
+
How the household's spending is set each year from Retirement: Fixed (the chosen amount every year) or Sustainable
|
|
51
|
+
(recomputed each year from what the money can carry).
|
|
52
|
+
_Avoid_: amortisation, guardrail, withdrawal policy
|
|
53
|
+
|
|
54
|
+
**Return Band**:
|
|
55
|
+
The same run at the assumed average return and at one point lower and one point higher.
|
|
56
|
+
_Avoid_: parameter band, prior, sensitivity
|
|
57
|
+
|
|
58
|
+
**Last-resort Sale**:
|
|
59
|
+
Selling the home when liquid money cannot cover next year's need, and renting from then on.
|
|
60
|
+
_Avoid_: equity release, forced sale
|
|
61
|
+
|
|
62
|
+
**Assumptions**:
|
|
63
|
+
The household facts and model constants a run reads, as distinct from the Scenario's decisions.
|
|
64
|
+
_Avoid_: parameters, config
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Pinder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: manyfutures
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A household's retirement plan run over many simulated futures: the chance it runs short, and what moves it.
|
|
5
|
+
Project-URL: Repository, https://github.com/thomaspinder/manyfutures
|
|
6
|
+
Project-URL: Issues, https://github.com/thomaspinder/manyfutures/issues
|
|
7
|
+
Author: Thomas Pinder
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: numpy>=2.4.4
|
|
21
|
+
Requires-Dist: pydantic>=2.12.5
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# manyfutures
|
|
25
|
+
|
|
26
|
+
Runs a household's retirement plan over many simulated futures: how often the plan runs short, the money left at
|
|
27
|
+
the end, and which of its inputs matter most.
|
|
28
|
+
|
|
29
|
+
The engine was built for a private dashboard and moved here in 0.1.0: the plan and its assumptions, the simulated
|
|
30
|
+
futures and the measures over them, with the projected life table of Statistics Netherlands (CBS) for how long each
|
|
31
|
+
future lasts. Every amount is in today's euros. The household lives in Amsterdam, under Dutch tax, unless its plan
|
|
32
|
+
moves it to one of the other places the engine knows.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install manyfutures
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
or `uv add manyfutures`. Python 3.11 or later; the only dependencies are numpy and pydantic.
|
|
41
|
+
|
|
42
|
+
## An example
|
|
43
|
+
|
|
44
|
+
A made-up household: two adults born in 1990 and 1992, earning 180,000 a year between them and spending 40,000, with
|
|
45
|
+
one child, a home bought in 2024 with a 350,000 mortgage, and a student loan of £20,000. Its plan is the `Scenario`'s
|
|
46
|
+
defaults: retiring at 58 on 40,000 a year, 30,000 of it Must-have Spending, with 40,000 a year of Bridge Income until
|
|
47
|
+
the pension at 67 and a downsize to a 350,000 home.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from datetime import date
|
|
51
|
+
|
|
52
|
+
import numpy as np
|
|
53
|
+
from manyfutures import Assumptions, Scenario, Start, readouts, run
|
|
54
|
+
from manyfutures.models import Adult, MortgageTerms, Position, StudentLoanTerms
|
|
55
|
+
|
|
56
|
+
household = Assumptions( # its facts; every other Assumption is one of the engine's own model constants
|
|
57
|
+
first_adult=Adult(born=date(1990, 1, 1), life_series="men"), partner=Adult(born=1992, life_series="women"),
|
|
58
|
+
gross_income=180_000, working_years_spending=40_000, pension=20_000, child_birth_ages=(33,),
|
|
59
|
+
daycare_days_per_week=2, daycare_monthly_per_day=500, childcare_reimbursement_rate=0.33, after_school_monthly=300,
|
|
60
|
+
mortgage=MortgageTerms(rate=0.035, term=30, start=date(2024, 1, 1)),
|
|
61
|
+
student_loan=StudentLoanTerms(rate=0.03, repayment=500, repayment_day=1),
|
|
62
|
+
)
|
|
63
|
+
position = Position(portfolio=350_000, crypto=0, cash_buffer=30_000, home_value=520_000, mortgage_balance=340_000,
|
|
64
|
+
student_loan=20_000)
|
|
65
|
+
returns = np.random.default_rng(1).normal(0.07, 0.17, 120) # made up: one a year, 1906 to 2025
|
|
66
|
+
start = Start(today=date(2026, 6, 1), position=position, returns=returns, return_years=np.arange(1906, 2026))
|
|
67
|
+
metrics = run(Scenario(), start, assumptions=household).metrics
|
|
68
|
+
print("Chance of running short:", readouts.chance(metrics.chance_of_running_short, metrics.chance_of_running_short_se))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It prints:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
Chance of running short: 21.7% ± 0.6
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
About one future in five runs short: in at least one of its years more than a tenth of the Must-have Spending goes
|
|
78
|
+
unfunded. The ± is one standard error of the simulation, in points, over the 5,000 futures a `Scenario` runs by
|
|
79
|
+
default.
|
|
80
|
+
|
|
81
|
+
The three inputs:
|
|
82
|
+
|
|
83
|
+
- **`Assumptions`**: the household's facts, which have no default, and the model's constants, which do (inflation,
|
|
84
|
+
investment costs, the life table, the Running Short threshold and the rest).
|
|
85
|
+
- **`Start`**: where the plan begins, the household's money and debts on a date (a `Position`) and the market's past
|
|
86
|
+
returns, each with its calendar year. Here the returns are made up; a real run is handed a history such as the
|
|
87
|
+
S&P 500's. The engine fetches nothing.
|
|
88
|
+
- **`Scenario`**: the household's decisions, each with a default, so a Scenario names only those that differ.
|
|
89
|
+
|
|
90
|
+
`run` gives an `Outcome`: its `metrics` (the chance of running short, how deep and for how long, the money left at
|
|
91
|
+
the end, the same readouts over the futures that last to 95), the End Ages and the portfolio at each age.
|
|
92
|
+
`return_band` runs the same plan with every year's return a point lower and a point higher, `compare` runs it in every
|
|
93
|
+
place, and `inputs_that_matter` moves one hand-set input at a time.
|
|
94
|
+
|
|
95
|
+
## The engine's words and decisions
|
|
96
|
+
|
|
97
|
+
- [GLOSSARY.md](GLOSSARY.md): the words, each defined once.
|
|
98
|
+
- [docs/decisions.md](docs/decisions.md): the modelling decisions, numbered as they were made, each with its rule,
|
|
99
|
+
its reason and the code and tests that hold it.
|
|
100
|
+
- [docs/adr/](docs/adr/): the decisions that are not to be reopened, the End Age's among them.
|
|
101
|
+
- [CHANGELOG.md](CHANGELOG.md): what each release changed, and whether it moved the headline.
|
|
102
|
+
|
|
103
|
+
## Licence
|
|
104
|
+
|
|
105
|
+
MIT: see [LICENSE](LICENSE). The bundled life table is CBS's (CC BY 4.0) and the US inflation the BLS's, through FRED
|
|
106
|
+
(public domain); each file's header says where it comes from.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# manyfutures
|
|
2
|
+
|
|
3
|
+
Runs a household's retirement plan over many simulated futures: how often the plan runs short, the money left at
|
|
4
|
+
the end, and which of its inputs matter most.
|
|
5
|
+
|
|
6
|
+
The engine was built for a private dashboard and moved here in 0.1.0: the plan and its assumptions, the simulated
|
|
7
|
+
futures and the measures over them, with the projected life table of Statistics Netherlands (CBS) for how long each
|
|
8
|
+
future lasts. Every amount is in today's euros. The household lives in Amsterdam, under Dutch tax, unless its plan
|
|
9
|
+
moves it to one of the other places the engine knows.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install manyfutures
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or `uv add manyfutures`. Python 3.11 or later; the only dependencies are numpy and pydantic.
|
|
18
|
+
|
|
19
|
+
## An example
|
|
20
|
+
|
|
21
|
+
A made-up household: two adults born in 1990 and 1992, earning 180,000 a year between them and spending 40,000, with
|
|
22
|
+
one child, a home bought in 2024 with a 350,000 mortgage, and a student loan of £20,000. Its plan is the `Scenario`'s
|
|
23
|
+
defaults: retiring at 58 on 40,000 a year, 30,000 of it Must-have Spending, with 40,000 a year of Bridge Income until
|
|
24
|
+
the pension at 67 and a downsize to a 350,000 home.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from datetime import date
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from manyfutures import Assumptions, Scenario, Start, readouts, run
|
|
31
|
+
from manyfutures.models import Adult, MortgageTerms, Position, StudentLoanTerms
|
|
32
|
+
|
|
33
|
+
household = Assumptions( # its facts; every other Assumption is one of the engine's own model constants
|
|
34
|
+
first_adult=Adult(born=date(1990, 1, 1), life_series="men"), partner=Adult(born=1992, life_series="women"),
|
|
35
|
+
gross_income=180_000, working_years_spending=40_000, pension=20_000, child_birth_ages=(33,),
|
|
36
|
+
daycare_days_per_week=2, daycare_monthly_per_day=500, childcare_reimbursement_rate=0.33, after_school_monthly=300,
|
|
37
|
+
mortgage=MortgageTerms(rate=0.035, term=30, start=date(2024, 1, 1)),
|
|
38
|
+
student_loan=StudentLoanTerms(rate=0.03, repayment=500, repayment_day=1),
|
|
39
|
+
)
|
|
40
|
+
position = Position(portfolio=350_000, crypto=0, cash_buffer=30_000, home_value=520_000, mortgage_balance=340_000,
|
|
41
|
+
student_loan=20_000)
|
|
42
|
+
returns = np.random.default_rng(1).normal(0.07, 0.17, 120) # made up: one a year, 1906 to 2025
|
|
43
|
+
start = Start(today=date(2026, 6, 1), position=position, returns=returns, return_years=np.arange(1906, 2026))
|
|
44
|
+
metrics = run(Scenario(), start, assumptions=household).metrics
|
|
45
|
+
print("Chance of running short:", readouts.chance(metrics.chance_of_running_short, metrics.chance_of_running_short_se))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
It prints:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Chance of running short: 21.7% ± 0.6
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
About one future in five runs short: in at least one of its years more than a tenth of the Must-have Spending goes
|
|
55
|
+
unfunded. The ± is one standard error of the simulation, in points, over the 5,000 futures a `Scenario` runs by
|
|
56
|
+
default.
|
|
57
|
+
|
|
58
|
+
The three inputs:
|
|
59
|
+
|
|
60
|
+
- **`Assumptions`**: the household's facts, which have no default, and the model's constants, which do (inflation,
|
|
61
|
+
investment costs, the life table, the Running Short threshold and the rest).
|
|
62
|
+
- **`Start`**: where the plan begins, the household's money and debts on a date (a `Position`) and the market's past
|
|
63
|
+
returns, each with its calendar year. Here the returns are made up; a real run is handed a history such as the
|
|
64
|
+
S&P 500's. The engine fetches nothing.
|
|
65
|
+
- **`Scenario`**: the household's decisions, each with a default, so a Scenario names only those that differ.
|
|
66
|
+
|
|
67
|
+
`run` gives an `Outcome`: its `metrics` (the chance of running short, how deep and for how long, the money left at
|
|
68
|
+
the end, the same readouts over the futures that last to 95), the End Ages and the portfolio at each age.
|
|
69
|
+
`return_band` runs the same plan with every year's return a point lower and a point higher, `compare` runs it in every
|
|
70
|
+
place, and `inputs_that_matter` moves one hand-set input at a time.
|
|
71
|
+
|
|
72
|
+
## The engine's words and decisions
|
|
73
|
+
|
|
74
|
+
- [GLOSSARY.md](GLOSSARY.md): the words, each defined once.
|
|
75
|
+
- [docs/decisions.md](docs/decisions.md): the modelling decisions, numbered as they were made, each with its rule,
|
|
76
|
+
its reason and the code and tests that hold it.
|
|
77
|
+
- [docs/adr/](docs/adr/): the decisions that are not to be reopened, the End Age's among them.
|
|
78
|
+
- [CHANGELOG.md](CHANGELOG.md): what each release changed, and whether it moved the headline.
|
|
79
|
+
|
|
80
|
+
## Licence
|
|
81
|
+
|
|
82
|
+
MIT: see [LICENSE](LICENSE). The bundled life table is CBS's (CC BY 4.0) and the US inflation the BLS's, through FRED
|
|
83
|
+
(public domain); each file's header says where it comes from.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-10-25
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The End Age is the only way a death enters the model
|
|
7
|
+
|
|
8
|
+
A review of the household-finance literature, made for the dashboard the engine was first built in, argued, with
|
|
9
|
+
every strand and both of its critics behind it, that a household plan should carry two lives: a survivor phase with
|
|
10
|
+
its own income fall, lower needs, a sale of the home at the first death, and life cover sized to the survivor's
|
|
11
|
+
living standard. We decided against all of it. Each simulated path draws one **End Age** from a life table (the later
|
|
12
|
+
of two draws, one per adult, from CBS's projected life table, StatLine 86246NED) and the plan has to last to it;
|
|
13
|
+
nothing else in the plan changes when a death occurs, and the model never asks who died. The household the engine
|
|
14
|
+
was first built for judged the survivor case not a money problem for planning, and held that if one of them died
|
|
15
|
+
there would be more important things to worry about as a household than the plan. The random End Age is kept
|
|
16
|
+
because the horizon still has to be random: stopping every path at a fixed age moved the headline a long way and
|
|
17
|
+
hid the long-lived futures in which running short happens.
|
|
18
|
+
|
|
19
|
+
The decision was made while the engine was part of that dashboard, and it travelled with the engine to this
|
|
20
|
+
package in 0.1.0. Decision 26 in [the decisions](../decisions.md) holds how the End Age is drawn and what it does.
|
|
21
|
+
|
|
22
|
+
## Considered options
|
|
23
|
+
|
|
24
|
+
- Two lives with a survivor phase (the review's recommendation): rejected as above; also the survivor-income
|
|
25
|
+
figures the review found are American and would have been hand-set.
|
|
26
|
+
- A fixed horizon at a life-expectancy age (the engine's prior behaviour): rejected because the horizon is the
|
|
27
|
+
single largest driver of the headline and the long-lived tail is where the risk sits.
|
|
28
|
+
- The End Age from the current-year CBS period table: rejected because it ignores future gains in longevity and
|
|
29
|
+
cuts about five years off the life of an adult in their thirties. CBS's projected table allows for them, and is
|
|
30
|
+
what the End Age is drawn from.
|
|
31
|
+
- The actuaries' projection table AG2026, which the End Age was first drawn from: rejected and replaced on
|
|
32
|
+
2026-10-25 by CBS's projected table, because AG2026 is all rights reserved and the engine is published, and CBS's
|
|
33
|
+
table is open (CC BY 4.0). The End Ages moved little. AG2026 never entered this repository, and a check in CI
|
|
34
|
+
keeps it out (`scripts/ag2026_guard.py`).
|
|
35
|
+
|
|
36
|
+
## Consequences
|
|
37
|
+
|
|
38
|
+
This is not to be reopened: the household the engine was first built for asked that it not be, and that holds here
|
|
39
|
+
too. No survivor rules, no first-death sale, no life-cover lever, however strongly a source argues for them. The End
|
|
40
|
+
Age is capped at the model's last age, 100 (`MAX_MODEL_AGE`), which truncates about one future in five of the
|
|
41
|
+
example household's (`test_three_in_five_futures_last_to_95_and_six_in_seven_to_90`); lifting the cap is a
|
|
42
|
+
one-constant change if wanted.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Architecture decision records
|
|
2
|
+
|
|
3
|
+
A decision recorded here is settled: it says what was decided, what else was considered, and why it is not to be
|
|
4
|
+
reopened. The modelling decisions, each a rule with its reason and the code that holds it, are in
|
|
5
|
+
[the decisions](../decisions.md).
|
|
6
|
+
|
|
7
|
+
- [0001 The End Age is the only way a death enters the model](0001-end-age-is-the-only-way-a-death-enters-the-model.md)
|
|
8
|
+
(accepted, 2026-10-25)
|