refresh on scenario edits
Two fixes for the user's report that adding a £100k life-event spend
didn't change the chart:
Engine (simulator.py)
- New `extra_outflows` param. cashflow_adjustments still drains the
portfolio at start-of-year as before, but the simulator now ALSO
records the spending in `withdrawal_hist[p, y]` so the chart's red
median-withdrawal trace shows the bump. Without this, the £100k
silently came out of the portfolio but the user-facing withdrawal
trace stayed at the strategy's flat 4% draw.
- simulate.py wires extra_outflows = essential + discretionary
category outflows from life events.
UX (ScenarioDetail.tsx)
- New auto-refresh: when life events / income streams / flex rules
change for a scenario, the page fires `/simulate` automatically
with 2,000 paths and uses the result as the primary fan/year-stats
source. The persisted MC run is only consulted as a fallback for
scenarios with no overrides.
- Fan chart title gains a "live preview · Xs · Ny" pill while a sim
is current, and "re-running…" while a fresh one is in flight.
- Removed the now-redundant "Live preview run" duplicate card lower
down — the main chart IS the live preview.
- Year-stats badge row reads from sim.data when available so changes
propagate immediately to NW / Δ NW / Spending / Taxes.
247 pytest pass (+1 new); mypy + ruff clean; frontend typecheck/test/
build green.
Until now life events were stored but ignored by the engine — pure
metadata. Now they actually move portfolios.
Engine:
- simulator.simulate() takes optional cashflow_adjustments: a (n_years,)
real-GBP array applied each year *after* savings + return but
*before* withdrawal. Positive = inflow, negative = outflow.
- New fire_planner/life_events.py with EventInput dataclass +
events_to_cashflow_array(events, horizon). Handles ranged deltas,
one-time amounts, disabled events, year clipping past horizon,
negative year_start (clipped to 0), and summing multiple events.
API:
- /simulate accepts optional life_events list. Server converts each
to EventInput, builds cashflow_adjustments, passes to simulate().
- Frontend Run-now on scenario detail now fetches the scenario's
life events and includes them in the request — projections finally
reflect "retire at 50, kid born at y3, inheritance at y22".
Tests: 11 events helper + 4 end-to-end engine + 1 API integration =
16 new tests. 188 total (was 172). mypy strict + ruff clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>