RetireForecast

← Back to Portfolio

Jun 2026 – Present

A UK retirement and downsizing forecast engine: pension lump-sum tax, buy-cheaper versus rent, and run-out-of-money risk under Monte Carlo. The engine reproduces HMRC worked examples to the penny before any figure reaches a screen. Deliberately kept off the public internet for regulatory reasons.

Tech Stack

Laravel PHP 8.4 PostgreSQL Livewire Filament PHPUnit Monte Carlo ApexCharts

About this project

What it is

A UK retirement forecast engine, built around one question. An older couple, one still working and one retired, are deciding whether to sell the family home and buy somewhere cheaper outright, or sell and rent and invest the lot. Underneath sit the two things nobody sees coming: what a pension lump sum does to your tax, and whether the money lasts for life.

The code is public. The running tool is not, on purpose, and there is a section on that below.

The bar I set first

In this domain, nearly right is worthless. A forecast that is close is still a number somebody makes an irreversible six-figure decision on. So the correctness bar went in before any interface did: the engine reproduces published HMRC worked examples to the penny, proven by unit tests, before a single figure reaches a screen.

  • Money is integer pence, never a float, GBP only, rates in integer basis points. Ages come from a date of birth and a reference date, never stored.
  • The engine has no framework in it. Its own package, no input or output, no clock, and a test fails the build if it ever reaches for one.
  • Tax figures are versioned per tax year, each carrying its source and the date it was verified, not constants somebody edits every April.
  • Every reported figure is net of the mortgage, defined once and inherited by the screen, the PDF, the CSV and the Monte Carlo, so print cannot drift from screen.

The bug worth writing up

The forecast sweeps a household through a set of levers, each producing a modified copy. Seven of them rebuilt the underlying object positionally, so a field added to the object but forgotten in one lever was silently dropped from every swept forecast. No error, no failing test, just a slightly wrong number in a tool whose value is being right.

I did not fix the seven call sites. Each object now has one rebuild path, and a test reads its own properties by reflection, so adding a field without handling it fails the build.

Refusing to give a wrong answer

  • Scotland throws rather than guesses. Scottish income tax bands differ and the engine does not model them yet, so asked for a Scottish taxpayer it refuses, rather than return an English figure that looks plausible.
  • No invisible figures. Any default applied to a projection is shown on screen with its value and its reason. An audit command sweeps every stored scenario for correctness and disclosure, and exits non-zero so it can gate a release.
  • The known limits are written down, including the ones that make the tool conservative: the ISA allowance is enforced but not yet used, so sell-and-invest plans are understated.

A single deterministic number would imply a precision that does not exist, so the headline output is a Monte Carlo: joint-life mortality, sequence-of-returns risk, and house-price, salary and care-cost paths modelled as random. It is reproducible under a fixed seed, which took moving to PostgreSQL, because SQLite could not handle the concurrent access the queue driver needed.

Why you cannot try it

The public posture is education and guidance, never regulated advice, and the build enforces it: a test fails if any output phrases a personal recommendation. The version I run for myself has that walled-off capability switched on, behind a flag that has to be off before any public release.

Putting it online would mean putting advice-shaped output about pensions and property in front of strangers. That is a regulated activity I am not authorised for, so it stays on my machine.

Where it is

Feature-complete for its purpose across roughly five weeks and 346 commits, covering income tax, National Insurance, the pension lump-sum suite including the Month 1 emergency tax overpayment and its reclaim, State Pension, stamp duty, capital gains, private residence relief, means-tested benefits, inheritance tax and care costs.

What I would do differently

  1. Write the compliance test before the compliance wording. The line is enforced by a failing build, which is right, but it arrived after the copy it governs.
  2. Reach for reflection-guarded tests earlier. That test is a handful of lines and retires a category of silent drift.