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.

Privacy by design: we only send data to Supabase or other services described in the privacy policy if you save a plan, share a link or use the AI advisor.

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:

  1. Interface (HTML + JavaScript): reads the form, validates phases, taxes and portfolio; builds a configuration JSON (store.js).
  2. Web Worker (simulation_worker.js): receives the JSON and delegates to the appropriate WASM binary without blocking the UI.
  3. WASM modules compiled from C++: main simulation, inverse goals, heatmap and SWR search.
  4. 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 volatility

Monthly 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) − 1

Statistical outputs

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:

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 index

Currency 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:

AssetExpected μVolatility σ
Stocks9%15%
Bonds4%5%
Crypto40%75%

Correlationρ
Stocks – Bonds0.05
Stocks – Crypto0.20
Bonds – Crypto0.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 × ρ_bc

Glidepath (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.

English identifiers: all technical names inside 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

Withdrawal order (withdrawalOrder)

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)
Country templates in the UI are educational approximations. Always verify against your real tax rules; the engine does not model every nuance (wash sales, personal allowances, etc.).

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 / 12

8. 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)

Heatmap (simulate_heatmap.cpp)

SWR — safe withdrawal rate

Audit trail: dedicated WASM for goals/heatmap; SWR reuses full simulate_wasm. See simulation_worker.jshandleSWR.

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:

10. Limitations and assumptions

Every simulation simplifies reality. It is important to know the model's assumptions:

Open the simulator