diff --git a/fire_planner/simulator.py b/fire_planner/simulator.py index 9c49950..e269fcf 100644 --- a/fire_planner/simulator.py +++ b/fire_planner/simulator.py @@ -223,7 +223,17 @@ def simulate( w = max(0.0, min(w, float(portfolio[p]))) tax_breakdown = regime_at(y).compute_tax(bucket_split(w, y)) t = float(tax_breakdown.total) - portfolio[p] = portfolio[p] - w + # Drain BOTH withdrawal AND tax from the portfolio. Mental + # model: `w` is what the user takes home to spend; the tax + # is an additional drag that comes out of the same pool. A + # high-tax jurisdiction therefore drains the portfolio + # faster and lowers the success rate, matching user intuition + # (Cyprus non-dom should beat UK on outcome, not just on a + # summary stat). Pre-2026-05-10 the engine recorded `t` but + # didn't subtract it, so jurisdiction only changed the + # `median_lifetime_tax_gbp` cell while the fan chart and + # success rate were identical across regimes. + portfolio[p] = max(0.0, portfolio[p] - w - t) withdrawal_hist[p, y] = w tax_hist[p, y] = t last_withdrawal[p] = w diff --git a/frontend/src/pages/ScenarioNew.tsx b/frontend/src/pages/ScenarioNew.tsx index 3333895..30eaf39 100644 --- a/frontend/src/pages/ScenarioNew.tsx +++ b/frontend/src/pages/ScenarioNew.tsx @@ -2,8 +2,8 @@ * Create a user scenario via POST /scenarios. Redirects to the detail * page on success. */ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { api, type ScenarioCreateBody } from '@/api/client'; @@ -27,10 +27,19 @@ const DEFAULTS: ScenarioCreateBody = { export function ScenarioNew() { const [form, setForm] = useState(DEFAULTS); + const [nwAutoFilled, setNwAutoFilled] = useState(false); const [nameError, setNameError] = useState(null); const navigate = useNavigate(); const qc = useQueryClient(); + // Same pattern as What-If: seed nw_seed_gbp from the live NW snapshot. + const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current }); + useEffect(() => { + if (nwAutoFilled || !nw.data || nw.data.accounts.length === 0) return; + setForm((f) => ({ ...f, nw_seed_gbp: nw.data!.total_gbp })); + setNwAutoFilled(true); + }, [nw.data, nwAutoFilled]); + const create = useMutation({ mutationFn: (body: ScenarioCreateBody) => api.scenarios.create(body), onSuccess: (s) => { diff --git a/frontend/src/pages/WhatIf.tsx b/frontend/src/pages/WhatIf.tsx index d012383..0c8f6ef 100644 --- a/frontend/src/pages/WhatIf.tsx +++ b/frontend/src/pages/WhatIf.tsx @@ -2,8 +2,8 @@ * What-If — interactive Monte Carlo. Form on the left, fan chart on the * right. Hits POST /simulate (no DB write); ~1-3s for 5k paths. */ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api, type SimulateRequest, type SimulateResult } from '@/api/client'; @@ -30,9 +30,21 @@ const DEFAULTS: SimulateRequest = { export function WhatIf() { const [form, setForm] = useState(DEFAULTS); + const [nwAutoFilled, setNwAutoFilled] = useState(false); const navigate = useNavigate(); const qc = useQueryClient(); + // Pre-fill NW seed from the latest Wealthfolio snapshot the first + // time it loads, so opening /what-if always starts from real numbers. + // The user can still edit; we won't clobber their input on later + // refetches. + const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current }); + useEffect(() => { + if (nwAutoFilled || !nw.data || nw.data.accounts.length === 0) return; + setForm((f) => ({ ...f, nw_seed_gbp: nw.data!.total_gbp })); + setNwAutoFilled(true); + }, [nw.data, nwAutoFilled]); + const sim = useMutation({ mutationFn: (req: SimulateRequest) => api.simulate(req), });