fire-planner: life-event spending bumps now reflected in fan + auto-
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

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.
This commit is contained in:
Viktor Barzin 2026-05-10 19:17:57 +00:00
parent f9084d1a15
commit eb0dd3ddbf
4 changed files with 199 additions and 49 deletions

View file

@ -111,6 +111,7 @@ def _project(req: SimulateRequest, paths: np.ndarray) -> tuple[SimulationResult,
cashflow_adjustments = None
discretionary_outflows = None
extra_outflows = None
if req.life_events:
engine_events = [
EventInput(
@ -126,6 +127,12 @@ def _project(req: SimulateRequest, paths: np.ndarray) -> tuple[SimulationResult,
cashflow_adjustments = events_to_cashflow_array(engine_events, req.horizon_years)
category_outflows = events_to_category_outflows(engine_events, req.horizon_years)
discretionary_outflows = category_outflows.get("discretionary")
# extra_outflows feeds the withdrawal-trace display: total of
# essential + discretionary spending events surfaces alongside
# the strategy's draw on the chart.
essential = category_outflows.get("essential")
if essential is not None and discretionary_outflows is not None:
extra_outflows = essential + discretionary_outflows
engine_flex = [
EngineFlexRule(
@ -175,6 +182,7 @@ def _project(req: SimulateRequest, paths: np.ndarray) -> tuple[SimulationResult,
income_inflows=income_inflows,
income_taxable=income_taxable,
discretionary_outflows=discretionary_outflows,
extra_outflows=extra_outflows,
flex_rules=engine_flex,
)
elapsed = time.perf_counter() - started