Data source
Prices come from Yahoo Finance via yfinance with auto_adjust=True, so closes are dividend-adjusted (BIL, QQQ, SPY distributions are included). All series are USD daily closes.
CAGR
(end_value / start_value)^(1/years) − 1, where years = trading_days / 252.
Annualized volatility
std(daily_returns) × √252. Population std (divisor = n). Assumes daily returns are independent — leveraged ETFs have mild negative serial autocorrelation, so the true annualized vol is slightly lower than this calculation.
Sharpe ratio
(mean_daily − rfr_daily) / std_daily × √252, where rfr_daily = (1 + rfr_annual)^(1/252) − 1. The risk-free rate is whatever you set in the config above. Default 2% is a rough time-weighted average for 2015–2022 (T-bill yield was near 0% in 2015–2021, ~5% in 2023+). For period-accurate ratios, override to match your chosen backtest window. Also reported with a standard error band: SE(Sharpe) = √((1 + 0.5·Sharpe²) / T_years) (Lo 2002, IID approximation).
Sortino ratio
(mean_daily − rfr_daily) × 252 / downside_dev_annualized, where downside deviation only counts days with returns below rfr_daily, summed-of-squares divided by total n (Sortino & Price 1994 convention).
Maximum drawdown
Largest peak-to-trough percentage loss using daily close prices only. Intraday lows are not captured, so actual lived drawdowns may be a few percent worse.
Monte Carlo
Block-bootstrap resampling of historical daily portfolio returns. Default: 2,000 paths, 10-year horizon, 5-day blocks, RNG seed = 42 on first render (click "Re-run with new seed" to see how stable the bands are). Block-bootstrapping preserves short-term autocorrelation and volatility clustering — it does not assume normal/Gaussian returns. Risk of Ruin = % of paths ending below 50% of the anchor value; Probability of Loss = % ending below the anchor value.
The MC projection anchor is configurable via the toggle above the section. Default: initial capital (matches a fresh investor's mental model: "if I start today with $X, what range of outcomes might I see in 10 years?"). The alternative is anchoring on the backtest's ending value (useful when asking "if I'm already at $X, what's next?").
Limitation: 5-day blocks preserve weekly autocorrelation but not regime/crisis dependencies. Heavier-tailed events than what's in the historical sample won't appear.
Stress tests
Hardcoded historical windows (2018 vol, COVID 2020, post-COVID recovery, 2022 rate hikes, 2022 tech crash, plus GFC 2008 if data exists). Only windows with ≥5 trading days of overlapping data are shown. TQQQ began trading 2010-02-11, so anything before that returns no data.
Transaction frictions
Commission + slippage is applied symmetrically — the sell side receives price × (1 − f), the buy side pays price × (1 + f). Default 0.15% per side ≈ 0.30% round-trip, realistic for Canadian discount brokers on leveraged ETFs.
What's NOT modeled
- Taxes — every rebalance is a taxable event in a non-registered account
- Currency / FX — all returns are USD, ignoring CAD/USD volatility
- Foreign withholding tax on US ETF distributions (treatment depends on account type)
- Inflation — returns are nominal, not real (subtract ~2–3% per year for real CAGR)
- Settlement delays — assumes T+0 cash availability
- Bid-ask spread variability — uses closing price, not actual fill price
Source code
All calculations are open and inspectable in the public repository — Python tools (Monte Carlo, stress tests) and JS in this page itself.