/** * 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 } from '@tanstack/react-query'; import { useState } from 'react'; import { api, type SimulateRequest, type SimulateResult } from '@/api/client'; import { FanChart } from '@/components/FanChart'; import { gbp, pct } from '@/lib/format'; const JURISDICTIONS = ['uk', 'cyprus', 'bulgaria', 'malaysia', 'thailand', 'uae', 'nomad']; const STRATEGIES = ['trinity', 'guyton_klinger', 'vpw', 'vpw_floor']; const GLIDES = ['rising', 'static_60_40']; const DEFAULTS: SimulateRequest = { jurisdiction: 'cyprus', strategy: 'guyton_klinger', leave_uk_year: 2, glide_path: 'rising', spending_gbp: '60000', nw_seed_gbp: '1500000', savings_per_year_gbp: '0', horizon_years: 60, floor_gbp: null, n_paths: 5000, seed: 42, }; export function WhatIf() { const [form, setForm] = useState(DEFAULTS); const sim = useMutation({ mutationFn: (req: SimulateRequest) => api.simulate(req), }); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); sim.mutate({ ...form, floor_gbp: form.strategy === 'vpw_floor' ? form.floor_gbp : null, }); }; const update = (key: K, value: SimulateRequest[K]) => setForm((f) => ({ ...f, [key]: value })); return (

What if…

Run a single Monte Carlo against the engine. No data persisted.

update('strategy', v)} options={STRATEGIES} /> onChange(e.target.value)} className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400" > {options.map((o) => ( ))} ); } function NumberInput({ value, onChange, min, max, step = 1, }: { value: number; onChange: (v: number) => void; min?: number; max?: number; step?: number; }) { return ( { const n = Number(e.target.value); if (Number.isFinite(n)) onChange(n); }} className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-slate-400" /> ); }