Methodology
Last updated: 2026-06-01
This page documents how every number on Fortfolio is computed. Each section cites the file and line in our source code that implements the calculation, so the maths is auditable. If you find a discrepancy between this page and the code, the code wins — please email support@fortfolio.app so we can update the page.
1. Data sources
US-listed instruments: yfinance (Yahoo Finance), accessed via a Python Flask microservice that mediates requests so the front end never hits yfinance directly. Indian-market instruments (NSE, BSE): EODHD for the broader universe with yfinance as a fallback. All return calculations use adjusted close prices, so dividends and corporate-action splits are reflected. Currency series for FX conversion are end-of-day from yfinance.
Crisis-window data for the ten pre-seeded historical scenarios is fetched once via scripts/fetch-historical-data.py and bulk-loaded into the TickerReturn table via scripts/import-returns.ts. At user-request time we read from this table only — no external API is hit. This is what makes a stress test deterministic and sub-second.
2. Weighted portfolio return
For a portfolio with holdings i:
Where totalReturn is the percentage change in adjusted close price across the crisis window (e.g. 2008-09-15 to 2009-03-09 for the GFC scenario). Weights must sum to 1.0 within a tolerance of ±1% server-side; the client enforces an exact 100% before enabling the Run button.
Source: lib/calculations.ts:11–30 (function portfolioReturn), with weight validation at lib/calculations.ts:31–36.
3. Weighted max drawdown
We weight each ticker’s individual max drawdown by its allocation:
This is a deliberate approximation. The mathematically precise portfolio drawdown requires daily portfolio-value reconstruction during the crisis window, which we do show on the per-scenario detail page but do not aggregate at the summary level. The weighted-average is a reasonable first-order approximation for portfolios with correlated drawdowns (i.e. most portfolios in a crisis), and it has the property that no portfolio can show a smaller drawdown than its worst-drawn constituent — which matches user intuition.
Source: lib/calculations.ts:21–30.
4. Sharpe and Sortino ratios
Computed against a constant 2% annualised risk-free rate. Sharpe uses the standard deviation of monthly returns; Sortino uses the downside- deviation (negative returns only). Both ratios are annualised by scaling with √12.
The 2% assumption is documented in lib/disclaimers.ts at the BACKTEST_DISCLAIMER constant and is shown next to every Sharpe number on the relevant tool pages. Change the assumption and the ratio changes; we use 2% to match Portfolio Visualizer and the CFA curriculum convention.
Source: lib/backtestCalcs.ts:67–105 (function metricsFromReturns).
5. Scenario date windows
The ten crisis scenarios use the date ranges below. These are the spec values seeded by prisma/seed.ts and are consistent across every stress test on the site. Where literature quotes a slightly different range we have erred toward including the full recovery window so users see how long it took for portfolios to climb back, not just how deep they fell.
- 1973 Oil Shock — 1973-01-11 to 1974-10-03
- Black Monday 1987 — 1987-08-25 to 1987-12-04
- 1990 Recession — 1990-07-16 to 1990-10-11
- Dot-com bust — 2000-03-24 to 2002-10-09
- Global Financial Crisis — 2007-10-09 to 2009-03-09
- Eurozone debt crisis — 2011-05-02 to 2011-10-03
- China devaluation — 2015-08-10 to 2016-02-11
- COVID crash — 2020-02-19 to 2020-03-23
- 2022 rate-hike drawdown — 2022-01-03 to 2022-10-12
- 2025 tariff shock — 2025-02-19 to 2025-04-08
6. Monte Carlo (block bootstrap)
For the Monte Carlo tool, we draw 1-month blocks of historical monthly returns with replacement from the chosen training window, stitch them in random order to fill the target horizon, and repeat the process for 10,000 iterations by default. The output is a distribution of terminal portfolio values from which we report the 5th, 25th, 50th, 75th and 95th percentile bands.
We use block bootstrap rather than i.i.d. resampling so we preserve short-run autocorrelation (a crash month is more likely to be followed by another crash month than chance). We do not parametrise with a normal distribution — the resampling preserves fat tails as observed historically. Tail risk in real markets is still consistently underestimated by historical-resampling methods. See the MONTE_CARLO_DISCLAIMER constant in lib/disclaimers.ts.
Source: lib/flowMonteCarlo.ts:148–232 (function runFlowMonteCarlo); parametric variant at lib/flowMonteCarlo.ts:291–405; regime-conditioned variant at lib/flowMonteCarlo.ts:459–560.
7. Retirement-withdrawal simulation
The retirement tool runs a Monte Carlo over the user’s withdrawal plan: a starting corpus, an annual withdrawal amount (constant-dollar or inflation-adjusted), an asset allocation, and a horizon. For each iteration we apply the resampled monthly return sequence to the surviving corpus and subtract withdrawals month by month, recording whether the portfolio survived to the end of the horizon. The output is a Safe Withdrawal Rate (SWR) curve and a success-rate band across the requested withdrawal grid.
We model sequence-of-returns risk: early-retirement drawdowns matter much more than late-retirement ones, because withdrawals during a drawdown lock in losses. This is the canonical reason the 4% rule is more fragile in practice than its long-run statistics suggest, and we surface the survival cliff in the charts. We do not model taxes on withdrawals, RMDs, social security, or Indian-tax sequencing; these are user-specific and should be modelled with a qualified adviser.
8. Resilience score
The resilience score is a composite 0–100 metric used on the stress-test summary card. It blends four components:
- Drawdown component (weight 0.40): proximity of the portfolio max drawdown to a 60% benchmark threshold.
- Recovery component (weight 0.25): median recovery time across scenarios; null recovery counts as worst-possible.
- Beat-benchmark component (weight 0.20): how often the portfolio outperformed its benchmark across the seeded scenarios.
- CVaR component (weight 0.15): the conditional value-at-risk at the 95% confidence interval, computed from daily returns within crises.
Any null component yields a null score, per the binding constraint documented in feedback/DIRECTION-priya-abrar-directive.md (“a score that lies is worse than no score”). Bands are Resilient, Defensive, Aggressive and Vulnerable; the band cut-offs are documented in the source.
Source: lib/calculations.ts:49–120 (function computeResilienceScore); band tooltips at lib/calculations.ts:118–125.
9. Efficient frontier
We compute the mean–variance frontier across the chosen ticker basket using historical monthly returns. The frontier is sampled at twenty target-return points between the minimum-variance portfolio and the maximum-return portfolio; each point is the result of a quadratic-program solve that minimises portfolio variance subject to (a) weights summing to 1.0 and (b) all weights non-negative (no short-selling). We display the Sharpe-maximising point and the minimum-variance point as labelled markers. Standard Markowitz caveats apply — the frontier is highly sensitive to the input estimates of expected return and covariance, and small input changes can produce large weight shifts. Use it for intuition, not for exact target weights.
10. India-portfolio currency handling
When a portfolio mixes USD and INR positions, returns are computed in the user’s selected display currency (controlled by the fortfolio-currency cookie). Each ticker’s native-currency return is converted using the end-of-day FX rate at the start and end of the window; we do not apply intraperiod hedging. This means that an INR-denominated US position will reflect both the underlying equity return and the USD/INR move over the window — which is the true investor experience, not a hedged-return abstraction.
11. Known limitations
- Pre-tax, pre-fee, pre-slippage. Real outcomes will be worse than every number on this site.
- Survivorship bias. Our ticker universe is the set of tickers that exist today.
- Look-ahead bias. Crisis dates were chosen with the benefit of hindsight.
- Data quality. yfinance and EODHD are best-effort upstream sources; corporate-action errors do propagate.
- No intraday data. All calculations use end-of-day adjusted close.
The full risk language is on the Disclaimer page.