retiretui 0.2.0

Retirement planning application for the terminal.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# RetireTui plan schema (version 1)

A plan is one TOML document. All amounts are annual today's dollars at plan
start - the dollars of `start_year`, whatever year the tax tables were published
for; each item escalates per its `cola`. The engine projects in nominal dollars
and reports a per-year `deflator` (nominal ÷ deflator = today's dollars).

## Document layout

```toml
schema = 1        # required, must be 1

[plan]            # required
[household]       # required
[medicare]        # optional; enables IRMAA surcharge modeling
[[events]]        # optional, repeated
[[accounts]]      # optional, repeated
[[income]]        # optional, repeated
[[expenses]]      # optional, repeated
[[cliffs]]        # optional, repeated
[[transfers]]     # optional, repeated
[[conversions]]   # optional, repeated
[[contributions]] # optional, repeated
[[residency]]     # optional, repeated; prices state income tax
[market]          # optional; what the market tools assume
```

Unknown keys are rejected everywhere.

## [plan]

- `name` (string, optional) - display name.
- `start_year` (integer, required) - first projected calendar year.
- `horizon_age` (integer, required) - projection runs until the oldest person
  reaches this age.
- `inflation` (float, required) - annual plan inflation rate, `-0.10..=0.50`.
- `wage_growth` (float, optional) - annual growth of the national average wage
  past the last year the tax table's index carries, over which a computed Social
  Security benefit is indexed; same bounds as `inflation`. Default the tax
  table's own assumption.
- `withdrawal_order` (array of treatment classes, optional) - classes drained in
  order to cover shortfalls; default `["taxable", "deferred", "roth", "hsa"]`.
- `surplus_to` (account id, optional) - receives unspent income; defaults to the
  first cash account.

## [household]

- `filing` (required) - `"single"` or `"married-joint"`.
- `[[household.people]]` (required) - one person for `single`, two for
  `married-joint`. Each has a unique `id` (string) and `birth` (TOML date, e.g.
  `1980-06-01`), and may carry a `name` - the person as shown, where the `id` is
  only what other items reference - and `earnings` - covered (FICA) wages by
  calendar year, nominal, as the Social Security statement records them, e.g.
  `earnings = { 1995 = 4200, 2024 = 168600 }`. The `import_earnings` tool fills
  it from the statement's XML, spreading a row that states several years as one
  sum evenly over them; a `social-security` income without an `amount` is
  computed from it. A person with no `earnings` at all is taken to have worked
  from 22 until the plan at the salary its first year pays them, scaled back by
  the average wage index; a stated record, even a partial one, is used as it
  stands.

## Triggers

Anywhere a field answers "when", it takes an inline table with exactly one
basis:

```toml
{ date = 2033-01-01 }             # the year containing the date
{ age = 65, owner = "jordan" }    # the year the owner reaches the age
{ event = "house-sale" }          # a named event's year
{ income = "pension", offset = 1 }  # an income's start year, shifted
```

`offset` (whole years, may be negative) is only valid with `event` or `income`.
Reference chains must not form a cycle.

## Escalation (`cola`)

On income, expenses, conversions, and contributions:

- `true` (default) - follows plan inflation.
- `false` - frozen nominal amount.
- a rate, e.g. `0.0125` - fixed annual rate of its own, compounded from plan
  start, same bounds as `inflation`.

## [[accounts]]

- `id` (string, required, unique) - referenced by transfers, conversions, and
  `surplus_to`.
- `name` (string, optional) - display name; defaults to the id.
- `kind` (required) - `"401k"`, `"403b"`, `"457b"` (penalty-exempt), `"414k"`,
  `"ira"`, `"sep-ira"`, `"simple-ira"`, `"hsa"`, `"brokerage"`, `"cash"`.
- `roth` (bool, default false) - Roth treatment; valid on `401k`, `403b`,
  `457b`, and `ira`.
- `owner` (person id, required).
- `balance` (dollars, required) - at plan start.
- `basis` (dollars, optional) - the untaxed part of the balance at plan start:
  cost basis on a brokerage, defaulting to the balance; after-tax money on a
  tax-deferred account, defaulting to nothing, which comes back untaxed pro rata
  on every withdrawal, distribution and conversion - per person across their
  IRAs, per account for a workplace plan.
- `expected_return` (float, optional) - a fixed annual return that never varies;
  absent earns nothing. Refused beside `allocation`.
- `allocation` (optional) - what the account is invested in: a mix
  `{ stocks = 0.6, bonds = 0.4 }` (shares of `stocks`, `bonds` and `cash`, an
  unstated one none, adding up to 1), rebalanced every year, or a glide path - a
  list of steps `{ from = <trigger>, stocks = .., bonds = .., cash = .. }`, each
  held from its trigger until a later step fires, the first held until any does.
  The account then earns its mix: in the ledger, each class's `mean` from
  `[market]`, blended.
- `locked_until` (trigger, optional) - until it fires, the account cannot be
  drained or transferred from.
- `drain_priority` (integer, optional) - drains before every treatment class;
  lower values first.

Tax treatment by kind: `brokerage`/`cash` are taxable, `hsa` is HSA, everything
else is deferred unless `roth = true`.

## [[income]]

- `id` (string, required, unique) - what scenarios and triggers name it by.
- `name` (string, optional) - display name; defaults to the id.
- `kind` (required) - `"salary"`, `"pension"`, `"annuity"`, `"rental"`,
  `"social-security"`, `"windfall"` (untaxed one-time), `"other"`. The kind
  decides tax treatment; social security uses the provisional-income rules.
- `owner` (person id, required).
- `amount` (dollars) - annual today's dollars; required for every kind but
  `social-security`, where it is the annual benefit at the claim age and may be
  left out: the benefit is then computed at the claim from the owner's
  `earnings` record, extended with the owner's `salary` incomes through the year
  before, reduced or credited for each month the claim falls before or after
  full retirement age, so the claim must be in or after the month 62 is
  attained. Credits earned in the claim year are paid from the next January,
  except at 70.
- `start` / `end` (triggers, optional) - the receiving window; absent means plan
  start / horizon. A `social-security` income with a `start` is paid, in its
  first year, for the months from the one the start falls in, as SSA pays it: a
  `date`'s month, the month an age is attained, and for an `event` or `income`
  reference the month of the date or age it rests on (January where there is
  none). A claim in the month 62 is attained is paid from the first month 62 is
  held throughout.
- `on` (trigger, optional) - one-time receipt; excludes `start`/`end`.
- `cola` (see escalation). On a computed `social-security` benefit it is also
  the COLA SSA adds each year from the owner's age-62 year, before and after the
  claim, for every year SSA has not yet published a COLA for.

## [[expenses]]

- `id` (string, required, unique) - what a scenario addresses the item by.
- `name` (string, optional) - what the expense is shown as; defaults to the id.
- `amount` (dollars, required), `start`/`end` or `on` (triggers), `cola` - same
  window semantics as income.

## [medicare]

Opt-in IRMAA modeling: from the year each person turns 65 the engine spends that
year's Medicare surcharge, determined by the household's MAGI from two years
earlier (the statutory lookback). Base Part B/D premiums are **not** modeled -
keep them in your own `[[expenses]]`; only the surcharge above the standard
premium is added.

- `prior_magi` (array of dollars, optional) - MAGI for up to the two calendar
  years before plan start, oldest first, seeding the lookback for the first
  projected years; missing years count below every tier.
- `part_d` (bool, default true) - include Part D surcharges beside Part B.

## [market]

What the market tools assume and how they run. Every field is optional; an
unstated one takes the built-in default shown.

- `leave_at_least` (dollars, optional) - besides never falling short, what a run
  must end with, in today's dollars, to count as a success.
- `stocks`, `bonds`, `cash` - `{ mean, volatility }`: the compound yearly return
  (the median year's) and its standard deviation. Defaults 6% ± 16%, 4% ± 6%,
  2.5% ± 1%.
- `inflation` - `{ volatility, persistence }`: the yearly shock around the
  plan's `inflation`, and the share of a year's deviation carried into the next.
  Defaults 1.5% and 0.6.
- `correlation` - `stocks_bonds`, `stocks_cash`, `stocks_inflation`,
  `bonds_cash`, `bonds_inflation`, `cash_inflation`, each -1 to 1 and together
  possible. Defaults 0.1, 0, -0.1, 0, -0.2, 0.5.
- `monte_carlo` - `draw` (`"assumptions"` or `"history"`, the historical years
  drawn at random), `trials` (1 to 100000, default 1000), `seed` (default 42),
  `block_years` (history drawn this many consecutive years at a time, default
  1).
- `historical` - `from`/`to` (the start years tried, default 1871 to 2025) and
  `wrap` (a history reaching past the data goes on from its start, default
  true).

## [[cliffs]]

A MAGI-triggered cost - the ACA premium-credit cliff's shape, with your own
numbers:

- `id` (string, required, unique) - what a scenario addresses the item by.
- `name` (string, optional) - what the cliff is shown as; defaults to the id.
- `magi_over` (dollars, required) - a year whose MAGI exceeds this incurs the
  cost that same year.
- `cost` (dollars, required) - annual cost when crossed.
- `start` / `end` (triggers, optional) - the active window; absent means plan
  start through the year before the youngest person turns 65.
- `cola` (see escalation) - scales both the threshold and the cost.

## [[events]]

- `id` (string, required, unique) - referenced by triggers as `event`.
- `name` (string, optional) - what the event is shown as; defaults to the id.
- `trigger` (trigger, required) - when the event fires.

## [[transfers]]

One-time account-to-account moves (e.g. a rollover when a linked pension
starts):

- `id` (string, required, unique) - what a scenario addresses the item by.
- `name` (string, optional) - what the transfer is shown as; defaults to the id.
- `from` / `to` (account ids, required), `on` (trigger, required).
- `amount` (dollars, optional) - today's dollars; absent moves the whole
  balance.

Tax treatment is resolved by the engine: a same-treatment move is not taxable;
deferred to taxable is a taxable distribution.

## [[conversions]]

Roth conversion schedules; the converted amount is ordinary income in the year
converted:

- `id` (string, required, unique) - what a scenario addresses the item by.
- `name` (string, optional) - what the conversion is shown as; defaults to the
  id.
- `from` (deferred account id) / `to` (Roth account id), required.
- `amount` (dollars per year, required), `start`/`end` or `on` (triggers),
  `cola`.

## [[contributions]]

Money paid into an account, one item per stream, so an account may take
several - a deferral and a match, or a second stream from a later year.

- `id` (string, required, unique) - what a scenario addresses the item by.
- `name` (string, optional) - what the contribution is shown as; defaults to the
  id.
- `to` (required) - account id.
- `by` - `"employee"` (default; out of cash flow, deducted where the kind defers
  tax), `"employer"` (landing in the account directly), or `"after-tax"` (the
  employee's after-tax money into a 401(k) or 403(b), kept as basis and filled
  last into the plan's yearly cap). Employer contributions are valid on employer
  plans and HSAs; SEP IRA takes employer only.
- The amount, stated one way: `amount` (annual today's dollars, escalated by
  `cola`); `rate` with `of` (a share of the named income's gross each year; `of`
  is an income id), optionally with `step = { add = 0.01, up_to = 0.15 }`
  raising the rate each year after the first until it reaches `up_to`;
  `max = true` (the year's employee limit for the kind and the owner's age,
  employee only); or `match = { rate = 0.5, up_to = 0.06 }` with `of` (an
  employer's match of half the employee's contributions to the account, up to 6%
  of the income's gross; employer only).
- `start`/`end` triggers, or `on` for a single year.

Employee contributions are held to the year's IRS limit (with age-50 and 60-63
catch-ups) per person across the accounts that share one - 401(k) and 403(b)
together, 457(b), SIMPLE, traditional and Roth IRA together, HSA (employer HSA
contributions count) - filled in the order the items are listed; each account's
employee plus employer total is held to the overall plan cap, the employer share
giving way. What was held is said in the year's actions, as is a Roth IRA
contribution in a year whose MAGI is over the Roth income band, and the part of
a traditional IRA contribution a workplace-plan-covered person cannot deduct -
which stays in the account as basis.

## [[residency]]

Where the household lives, which decides the state income tax inside each year's
taxes (`taxes.state`, part of `taxes.total`). Without a residency, and abroad,
only federal tax is projected. There is no end date - each residency lasts until
the next begins:

- `country` (required): an ISO 3166-1 two-letter code, such as `us` or `pt`. A
  U.S. territory is its own country (`pr`, `gu`, `vi`, `as`, `mp`).
- `state`: required when `country` is `us` and refused otherwise - a two-letter
  state code or `dc`.
- `from` (trigger): when the move happens. With any residencies at all, exactly
  one has no `from`: where the household lives when the plan starts.

Codes are read in any case and stored lowercase.

A move is taxed by the new state for the whole year its `from` resolves to; of
two moves in one year, the one listed later stands. State tax is the state's
brackets over ordinary income and capital gains alike, less its standard
deduction, with Social Security taxed only where the state taxes it. Not
modeled: retirement-income exclusions, credits, local taxes, Oregon's federal
tax subtraction (so Oregon reads somewhat high), and Washington's excise on
long-term gains.

Only states with a table validate: the states without an income tax (`ak`, `fl`,
`nv`, `nh`, `sd`, `tn`, `tx`, `wa`, `wy`) and `or`. Any other U.S. state is
refused at `residency[i].state` until a parameter file supplies `[states.xx]` -
and a parameter file overriding a year must carry the states lived in that year;
the `tax_parameters` tool shows each year's tables.

## Worked example

```toml
schema = 1

[plan]
start_year = 2026
horizon_age = 95
inflation = 0.03

[household]
filing = "single"

[[household.people]]
id = "alex"
birth = 1970-06-15

[[accounts]]
id = "ira"
kind = "ira"
owner = "alex"
balance = 500000
expected_return = 0.06

[[accounts]]
id = "roth"
kind = "ira"
roth = true
owner = "alex"
balance = 50000
expected_return = 0.06

[[accounts]]
id = "cash"
kind = "cash"
owner = "alex"
balance = 40000

[[income]]
id = "ssa"
kind = "social-security"
owner = "alex"
amount = 30000
start = { age = 67, owner = "alex" }
cola = 0.024

[[expenses]]
id = "living"
amount = 48000

[[conversions]]
id = "roth-ladder"
name = "Roth ladder"
from = "ira"
to = "roth"
amount = 20000
start = { date = 2027-01-01 }
end = { age = 66, owner = "alex" }

[[contributions]]
id = "roth-savings"
to = "roth"
amount = 7000
end = { age = 66, owner = "alex" }
```

## Scenarios

A scenario is a TOML document that overlays a base plan with deltas. It is
recognized by its `base` key and resolves wherever a plan path is accepted;
`base` may name another scenario (chains are followed bottom-up, cycles
refused).

```toml
schema = 1               # required, must be 1
base = "plan.toml"       # required; relative to this file's directory

[plan]
name = "retire-2030"     # [plan] and [household] fields replace the base's

[[income]]
id = "salary"            # items match by id; stated fields replace the
end = { date = 2030-06-01 }  # base item's, unstated fields survive

[[expenses]]
id = "travel"            # ids are unique within their section
remove = true            # delete the matched item; error if unmatched

[[expenses]]
id = "sabbatical"        # no match -> appended as a new item
amount = 12000
on = { date = 2029-06-01 }
```

Identity keys: every listed item - people, events, accounts, income, expenses,
cliffs, transfers, conversions and contributions - matches by `id`, which every
item must carry; `name` is only what an item is shown as; `residency` replaces
wholesale; `[market]` merges like `[plan]`, one level deep, so a stated `stocks`
or `monte_carlo` table replaces the base's whole. Within a matched item each
stated field replaces the base field entirely - triggers never merge internally.
`replace = true` substitutes the stated item wholesale, which is also how an
optional field is cleared. `write_plan` accepts scenario documents and validates
them fully resolved; `optimize_conversions` and `optimize_claims` emit one.