Simulator methodology
Technical transparency to understand paths, percentiles and advanced tools.
Last updated: May 2026
1. Summary: what the simulator does
My FIRE Simulator projects financial independence (FIRE) plans using thousands of monthly paths (Monte Carlo or historical), configurable taxes and withdrawal strategies. The numerical core is written in C++ and runs in your browser as WebAssembly (WASM) inside a Web Worker: your main plan figures are not sent to our servers for calculation.
This page describes the logic for users who want to understand—or mentally audit—the engine. It does not replace professional financial advice.
2. In-browser architecture
The technical flow of the main simulator is:
- Interface (HTML + JavaScript): reads the form, validates phases, taxes and portfolio; builds a configuration JSON (
store.js). - Web Worker (
simulation_worker.js): receives the JSON and delegates to the appropriate WASM binary without blocking the UI. - WASM modules compiled from C++: main simulation, inverse goals, heatmap and SWR search.
- Results: percentiles, charts (Chart.js), annual table and inheritance histogram are rendered on the client.
The quick calculator on the landing page (“Calculate your goal”) is the exception: it uses deterministic compound-interest JavaScript only, without WASM, for an instant response.
3. Main simulation engine
By default 5,000 independent iterations run (iterations in the payload), each with the same configuration but different random paths.
Time granularity
The simulation advances month by month over the defined horizon. Each month applies, in this order: return selection, portfolio growth, broker fee, debt payments, contribution/withdrawal netting, net contributions, tax-aware withdrawals, and capital recording.
Monthly return — custom mode (log-normal GBM)
In custom mode the engine generates a monthly return using Geometric Brownian Motion (GBM). The random number generator is a Mersenne Twister (mt19937) seeded from hardware (std::random_device):
R_monthly = exp( (μ − σ²/2) × Δt + σ × √Δt × Z ) − 1
where:
Δt = 1/12 (monthly step)
Z ~ N(0, 1) (standard normal distribution)
μ = expected annual portfolio return
σ = annual portfolio volatilityMonthly return — historical mode (block sampling)
In historical mode, each simulated year draws a consecutive block from the JST series. Annual returns are converted to monthly and combined by portfolio weights:
R_month_eq = (1 + R_annual_eq)^(1/12) − 1
R_month_bond = (1 + R_annual_bond)^(1/12) − 1
R_month_fx = (1 + R_annual_fx)^(1/12) − 1
R_portfolio = w_stocks × R_month_eq + w_bonds × R_month_bond + w_crypto × R_crypto_lognormal
If NOT hedged (unhedged currency):
R_final = (1 + R_portfolio) × (1 + R_month_fx) − 1Statistical outputs
- P10 / P50 / P90: interpolated percentiles of capital (nominal and real) each month and at the end.
- Success probability / ruin risk: fraction of paths that deplete capital before the horizon.
- Tax breakdown (P50): accumulated fees and taxes in the median scenario when the engine calculates taxes dynamically.
Interpolated percentile calculation
We do not assume normality of final capital. Percentiles are computed with order statistics directly on the 5,000-result vectors:
index = (N − 1) × p / 100
lower = ⌊index⌋
frac = index − lower
percentile = arr[lower] × (1 − frac) + arr[lower + 1] × frac
(partial sort via std::nth_element, O(N) complexity)4. Data sources: custom vs historical
Custom (pure Monte Carlo)
Generates random monthly returns with a log-normal distribution (GBM) using the configured mean mu and volatility sigma. Inflation is constant throughout the simulation.
Historical (block sampling — Jordà–Schularick–Taylor)
The engine uses the Jordà–Schularick–Taylor (JST) database, one of the most cited academic macro-financial data sources. Each annual record contains four fields per country:
eq— Total equity return (price + dividends).bond— Long-term government bond return.inf— Annual inflation rate.fx— Exchange rate change vs the user's currency.
Available regions: US, Europe (Germany proxy pre-1999), Japan, United Kingdom. Default period: modern era 1950–2020. Panic button: extends to 1870+ including world wars and the Great Depression.
Block sampling algorithm
To preserve autocorrelation between consecutive years, the engine draws blocks of N years (configurable as blockSize, default 5). Each block starts at a random year in the series and advances sequentially, wrapping around:
At block start:
start_index = random in [0, series_length − 1]
Within block (consecutive years):
current_index = (start_index + offset) % series_length
When block years exhausted → new random indexCurrency hedge (Hedged): when enabled, the fx impact is ignored. Otherwise, return is adjusted: (1 + R_portfolio) × (1 + R_fx) − 1.
5. Portfolio, volatility and glidepath
In custom mode, the UI estimates mu and sigma with Markowitz-style heuristics. The constants used in the engine for each asset class are:
| Asset | Expected μ | Volatility σ |
|---|---|---|
| Stocks | 9% | 15% |
| Bonds | 4% | 5% |
| Crypto | 40% | 75% |
| Correlation | ρ |
|---|---|
| Stocks – Bonds | 0.05 |
| Stocks – Crypto | 0.20 |
| Bonds – Crypto | 0.05 |
Portfolio variance formula
μ_portfolio = w_s × μ_s + w_b × μ_b + w_c × μ_c
σ²_portfolio = w_s² × σ_s² + w_b² × σ_b² + w_c² × σ_c²
+ 2 × w_s × w_b × σ_s × σ_b × ρ_sb
+ 2 × w_s × w_c × σ_s × σ_c × ρ_sc
+ 2 × w_b × w_c × σ_b × σ_c × ρ_bcGlidepath (portfolio transition)
When enabled, stock/bond/crypto weights are linearly interpolated month by month between user-defined control points. At each month t, the engine recalculates mu and sigma with the Markowitz formula above using that month's interpolated weights.
Stress events
Stress events apply a multiplier factor to the return in the chosen month: stressFactor = 1 − drop. A 40% drop equals stressFactor = 0.6, simulating a point-in-time market crash.
6. Taxation and capital buckets
Dynamic tax logic runs only when the engine needs the full monthly loop (needsDynamicCalc): non-fixed withdrawal, historical source, taxes enabled, configured brackets, or FIFO mode. The plan JSON includes generalCapital, taxFreeCapital, deferredCapital, taxStrategy (average | fifo), withdrawalOrder, taxBrackets[], flatTaxRate and latentGainsPct.
… are in English for international readers. The JSON built by store.js and the C++ engine still use legacy Spanish keys (e.g. generalCapital in docs = capGeneral in source).Three capital buckets
- General (broker): contributions in
cashFlowPhaseswithaccount: "general". Only bucket with modelled tax cost (average or FIFO). - Tax-free (libre):
account: "libre". Withdrawals assume 100% taxable gain (ratio = 1.0); no FIFO. - Deferred (diferido):
account: "diferido". Same tax treatment as libre on withdrawal.
Withdrawal order (withdrawalOrder)
- Optimal (default): fixed sequence
general → libre → diferido(code indices 0 → 2 → 1). - Prorated (
prorated): splits net withdrawal by bucket weight; shortfalls cascade to the next bucket in the same order.
Average mode (taxStrategy: "average")
Keeps an aggregate tax cost basis (generalCostBasis) on the general bucket. On withdrawal, taxable gain is proportional. Tax can be flat (flatTaxRate) or progressive brackets (taxBrackets with max and rate). Gains accumulate in annualGainYTD and reset each calendar year.
gain_ratio = (marketValue − costBasis) / marketValue
taxable_gain = grossWithdrawal × gain_ratio
Flat tax: tax = taxable_gain × flatTaxRate
Brackets: For each bracket with (max, rate):
capacity = bracket.max − annualGainYTD
chunk = min(remainingGain, capacity)
tax += chunk × bracket.rate
Engine solves gross needed for a target net withdrawal:
gross = net / (1 − gain_ratio × marginal_rate)FIFO mode (taxStrategy: "fifo")
Applies only to the general bucket. Each contribution adds a { cost, units } lot to a deque; an index price (indexPrice) updates monthly with portfolio return and broker fee. Sales consume lots first-in-first-out. If the deque exceeds 120 lots, the oldest 12 are merged into one lot to cap memory.
Monthly index price update:
indexPrice *= (1 + R_portfolio) × (1 − brokerFee)
For each lot consumed (FIFO order):
lot_marketValue = units × indexPrice
cost_per_unit = original_cost / original_units
gain = (indexPrice − cost_per_unit) × units_sold
tax += apply_brackets(gain)7. Withdrawal strategies
Strategy is sent as withdrawalStrategy: fixed (default), guyton or vpw. Planned withdrawals arrive as negative values in cashFlowPhases[].val and are applied inside the dynamic monthly loop.
Fixed (fixed)
Each planned withdrawal month: withdrawal = baseWithdrawals[t] × withdrawalInflationFactor. With adjustFlowsForInflation, the factor is multiplied by (1 + annual_inflation) once per year — classic inflation-adjusted spending.
Guyton–Klinger (guyton)
UI params: gkThreshold (%) and gkAdjustment (%). The engine calculates the initialWithdrawalRate on the first withdrawal. Each January applies two guardrail rules:
initialWithdrawalRate = plannedAnnualWithdrawal / totalCapital (1st withdrawal only)
Each January:
1. Prosperity rule:
If totalCapital ≥ capitalAtYearStart → apply inflation to spending
Otherwise → freeze spending (no inflation bump)
2. Guardrail rules:
currentRate = annualSpending / totalCapital
If currentRate > initialRate × (1 + threshold) → factor × (1 − adjustment) [cut]
If currentRate < initialRate × (1 − threshold) → factor × (1 + adjustment) [raise]VPW — Variable Percentage Withdrawal (vpw)
Parameter vpwRate (expected annual rate, e.g. 4%). Each January (or at first withdrawal) recalculates an annuity on live capital. Not a fixed % of capital, but a variable annuity similar to a reverse mortgage:
remainingYears = (totalMonths − t + 1) / 12
If vpwRate > 0:
pmtRate = vpwRate / (1 − (1 + vpwRate)^(−remainingYears))
If vpwRate = 0:
pmtRate = 1 / remainingYears
Monthly_withdrawal = totalCapital × pmtRate / 128. Advanced tools
The three tools in «Advanced tools» do not read the main form: they build their own payloads and call the Web Worker with types goals, heatmap and swr.
Goals (simulate_goals.cpp)
mode: "time": years to reachtargetwithinitialand monthlymonthlysavings (cap 80 years per path).mode: "money": constant real monthly savings needed overyearsto hit the target.- Always 5,000 iterations. Real returns: custom log-normal minus inflation; historical random start year then sequential series from injected
historicalSeriesData. - Percentiles: pessimistic = worse — time mode P10 = 90th percentile of years; money mode P10 = higher savings required.
Heatmap (simulate_heatmap.cpp)
- Simplified vs main engine: no phases, taxes, glidepath, debts or GK/VPW.
- Grid: retirement age (
ageMin…ageMax, stepageStep) × monthly spending (expMin…expMax, stepexpStep). Per cell: fixed panel capital, horizon95 − ageyears, annual spend = monthly×12 revalued by inflation each year, annual return (MC log-normal with portfolio μ/σ heuristics, or historical index walk year by year). - 5,000 simulations per cell; success = capital > 0 at end. Result:
rate = successes / 5000 × 100. - UI limit:
rows × columns ≤ 120; minimum spending step 10, age step ≥ 1.
SWR — safe withdrawal rate
- Clones the main payload but forces a pure scenario:
withdrawalStrategy: fixed, no extra phases, debts, crises or mortgages. - In the worker: binary search between 1% and 15% (7 iterations). Each test injects a single monthly withdrawal phase =
(initialCapital × rate/100) / 12for the full horizon (years). - Each test calls the same
simulate_wasmas the main plan (taxes, portfolio, historical data, etc.). Target: ≥ 95% of paths without ruin ((iterations − bankruptcies) / iterations). - Does not model future contributions, public pensions or extraordinary income — only «retire today with inflation-adjusted fixed withdrawal».
simulate_wasm. See simulation_worker.js → handleSWR.9. What does run on the server
The main simulations run entirely in your browser (WASM + Web Worker). Data is only sent to a server in these cases:
- Google account / Supabase: when you save a plan to the cloud, share a link, or participate in the community gallery (comments, likes).
- AI advisor: sends a plan summary to Google Gemini via Netlify Function. Data is discarded after generating the response.
- Contact form: Formspree (name, email and message only).
- Analytics: Simple Analytics (no cookies, no personal data).
- Error logs: if the WASM engine crashes, the error is logged (no plan data) for debugging.
10. Limitations and assumptions
Every simulation simplifies reality. It is important to know the model's assumptions:
- The future can be worse than historical P10. The JST dataset does not cover extreme hyperinflation, asset confiscation, or unprecedented global systemic collapse.
- Constant correlations. Asset correlations (stocks–bonds, stocks–crypto) are fixed. In reality, correlations change during crises (they tend to increase).
- Implicit rebalancing without costs. The engine assumes the portfolio maintains its target weights every month without modelling transaction costs from rebalancing.
- Constant inflation in custom mode. In custom mode, inflation does not vary between iterations. In historical mode it does fluctuate year by year.
- Tax rules and fees change. The tax brackets and broker fees you set are static throughout the simulation.
- No advanced sequence-of-returns risk. Block sampling preserves autocorrelation within each block, but does not capture long-duration market regimes.
- Educational tool, not investment advice. Always consult a qualified financial advisor before making decisions about your assets.