A free, no-login portfolio stress-testing tool. Paste your holdings, watch them replayed through the four worst markets of the last 25 years, and read the damage in plain English.
FastAPI · React 19 · yfinance · pandas · NumPy · Claude
- Introduction
- Key features
- Tech stack
- Architecture
- Project structure
- Installation
- Usage
- API reference
- Environment variables
- How the numbers are made
- Security and privacy
- Accessibility
- Contributing
- License
- Support and contact
Every investing app shows you what your portfolio did. Almost none honestly show you what it could lose. Real stress-testing tools exist, but they are built for financial advisors, hidden behind logins and sales calls, and priced in the thousands per year.
IndexCheck closes that gap for everyone else. You type in tickers and weights, and it replays that exact mix through the 2008 financial crisis, the 2020 COVID crash, the 2022 rate-hike selloff, and the 2000 dot-com bust, using real historical prices. It reports the deepest drawdown, how long recovery took, which holding hurt most, and how concentrated you really are. A language model then explains it all like a smart friend, not a whitepaper.
Note
IndexCheck is an educational tool. It does not give investment advice, make recommendations, or predict the future. Results are historical replays and statistics, not forecasts.
| Feature | What it does |
|---|---|
| Historical crash replay | Runs your portfolio through 4 real crises using split- and dividend-adjusted closing prices. |
| Max drawdown and recovery | Reports the deepest peak-to-trough fall and the time to climb back to breakeven. |
| Correlated Monte Carlo | Projects 1,000 forward paths with a covariance-aware simulation (API today, chart UI next). |
| Concentration analysis | Flags holdings that move together and scores diversification 0 to 100. |
| Plain-English narration | Claude turns the raw statistics into a readable summary, with a template fallback when no key is set. |
| Honest coverage | A holding with no history for a window is excluded and disclosed, never silently filled in. |
| Zero friction | No account, no signup, no database. Your portfolio is never stored. |
| Backend | Python · FastAPI · Uvicorn · yfinance · pandas · NumPy · slowapi · Anthropic SDK |
| Frontend | React 19 · Vite 7 · Tailwind CSS 4 |
| AI layer | Claude (claude-sonnet-4-20250514 by default) |
| Data | Yahoo Finance daily bars via yfinance |
Browser ──POST /analyze──▶ FastAPI ──▶ scenarios.py (historical replay)
▲ │ ──▶ monte_carlo.py (correlated sims)
│ │ ──▶ correlation.py (diversification)
│ │ ──▶ narrate.py (Claude explanation)
└───────JSON results───────┘ │
data.py (validated fetch + LRU cache)
│
Yahoo Finance
The backend is stateless: requests are computed in memory and returned. Price history is cached briefly by ticker (never by portfolio). Nothing about a user or a portfolio is persisted anywhere.
Click to expand the full tree
.
├── backend/
│ ├── main.py # FastAPI app: routes, middleware, security, validation
│ ├── config.py # All env-driven config and safety limits
│ ├── data.py # Ticker validation + yfinance fetch + bounded LRU cache
│ ├── scenarios.py # Historical crisis replay and drawdown/recovery math
│ ├── monte_carlo.py # Correlated geometric Brownian motion simulation
│ ├── correlation.py # Correlation matrix + diversification score
│ ├── narrate.py # Claude narration with deterministic fallback
│ ├── requirements.txt
│ └── .env.example
├── frontend/
│ ├── src/
│ │ ├── App.jsx # Router + landing page + results flow
│ │ ├── main.jsx # Entry point, applies saved a11y preference
│ │ ├── index.css # Design tokens, a11y (focus, skip link, high contrast)
│ │ ├── disclaimers.js # Single source of truth for legal copy
│ │ ├── components/
│ │ │ ├── CrashCanvas.jsx # Procedural candlestick crash animation
│ │ │ ├── PortfolioInput.jsx # Add/remove holding rows
│ │ │ ├── ScenarioCard.jsx # One scenario result with coverage disclosure
│ │ │ ├── NarrationPanel.jsx # Typewriter plain-English read
│ │ │ ├── HowItWorks.jsx # Methodology, named algorithms
│ │ │ └── Footer.jsx # Disclaimer, legal links, contrast toggle
│ │ └── pages/
│ │ ├── Terms.jsx # /terms
│ │ ├── Privacy.jsx # /privacy
│ │ └── LegalLayout.jsx # Shared legal page frame
│ ├── vercel.json # SPA rewrites for client routing
│ └── vite.config.js # Dev proxy /api -> :8000
├── dev.sh # Start backend + frontend together
├── LICENSE
├── README.md
└── README.mdx
| Folder | Purpose |
|---|---|
backend/ |
Stateless FastAPI service. Each analysis type is its own module. |
frontend/ |
Vite + React single-page app. Institutional, editorial design. |
frontend/src/components/ |
Presentational and interactive UI pieces. |
frontend/src/pages/ |
Legal pages served via a tiny built-in router. |
Important
You need Python 3.11+ and Node 18+. Start the backend first, then the frontend.
cd backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env # optional: add ANTHROPIC_API_KEY for richer narration
.venv/bin/uvicorn main:app --reload --port 8000Open http://127.0.0.1:8000/docs for the interactive API.
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api -> :8000Tip
After the one-time setup above, run ./dev.sh from the repo root to start
the backend and frontend together (it stops the backend when you quit).
No API key? The app still works fully. Narration falls back to clear,
deterministic templates and every number is computed the same way.
- Open the app and scroll to Your portfolio.
- Add holdings as rows, for example
AAPL 40,MSFT 30,SPY 20,CASH 10. Weights are relative and auto-normalized. - Click Run the stress test.
- Read the results, worst crisis first: max drawdown, recovery time, biggest drag, and a concentration score.
Query the API directly:
curl -s -X POST http://127.0.0.1:8000/analyze \
-H "Content-Type: application/json" \
-d '{
"holdings": [
{"ticker": "SPY", "weight": 60},
{"ticker": "QQQ", "weight": 40}
],
"scenario": "2008 Financial Crisis",
"horizon_years": 5
}'Sample response (trimmed)
{
"scenario": {
"scenario": "2008 Financial Crisis",
"max_drawdown_pct": -44.97,
"trough_date": "2009-03-09",
"recovery": { "recovered": true, "years": 1.6 },
"worst_holding": { "ticker": "SPY", "total_return": -0.3713 },
"covered_weight": 1.0,
"skipped_tickers": []
},
"correlation": { "diversification_score": 4, "high_correlation_pairs": [ ... ] },
"monte_carlo": { "outcomes": { "5yr": { "median_return_pct": 186.09 } } }
}Base URL in dev: http://127.0.0.1:8000
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Liveness plus whether narration is live or fallback. |
GET |
/scenarios |
List the four historical crisis windows. |
POST |
/scenario |
Run one historical scenario against a portfolio. |
POST |
/monte-carlo |
Forward projection: fan-chart bands and outcome stats. |
POST |
/correlation |
Correlation matrix and diversification score. |
POST |
/analyze |
All three analyses in one call. This is what the UI uses. |
Request body (analysis routes):
{
"holdings": [{ "ticker": "AAPL", "weight": 40 }],
"scenario": "2008 Financial Crisis",
"horizon_years": 5
}Note
CASH is a supported synthetic holding that holds its value through any
crash. Weights do not need to sum to 100; they are normalized for you.
All backend config is read from the environment. Only ANTHROPIC_API_KEY is
sensitive, and it is never exposed to the frontend.
| Variable | Default | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
(unset) | Enables Claude narration. Falls back to templates if unset. |
ANTHROPIC_MODEL |
claude-sonnet-4-20250514 |
Model used for narration. |
ALLOWED_ORIGINS |
local dev URLs | Comma-separated CORS allowlist. Set to your deployed frontend. |
MAX_HOLDINGS |
30 |
Max holdings per request. |
MAX_TICKER_LEN |
10 |
Max ticker length. |
MC_MAX_SIMS |
2000 |
Monte Carlo simulation cap. |
MC_SEED |
42 |
Seed for reproducible simulations. |
RATE_LIMIT_DEFAULT |
60/minute |
Global per-IP limit. |
RATE_LIMIT_ANALYZE |
12/minute |
Per-IP limit on analysis routes. |
YF_TIMEOUT_SECONDS |
20 |
Timeout for market-data fetches. |
ANTHROPIC_TIMEOUT_SECONDS |
30 |
Timeout for narration calls. |
PRICE_CACHE_MAX_ENTRIES |
512 |
Bounded LRU price cache size. |
MAX_REQUEST_BYTES |
32768 |
Request body size cap. |
Frontend: set VITE_API_URL to the deployed backend URL in production
(otherwise the dev proxy handles /api).
No black box. Each method, by name:
- Crash replay — a buy-and-hold weighted index,
V(t) = Σ wᵢ · Pᵢ(t) / Pᵢ(0), with maximum drawdown measured against a running peak. - Recovery — first passage back to the prior peak, capped at six years.
- Biggest drag — contribution weighted (weight × return), so a tiny position cannot outrank a large one.
- Concentration — pairwise Pearson correlation of daily returns over two years; score is
(1 − average correlation) × 100, credited for cash. - Monte Carlo — correlated geometric Brownian motion: per-asset drift and volatility from three years of log returns, cross-asset correlation applied via a Cholesky decomposition of the covariance matrix.
Warning
The Monte Carlo drift is estimated from the recent past, so after a long bull run the median projection leans optimistic. Treat simulated ranges as possibilities, not predictions.
- Stateless. No database, no accounts, no stored portfolios.
- Strict input validation. Tickers are whitelisted (
^[A-Z0-9][A-Z0-9.\-^=]{0,9}$), with caps on holdings, weights, and body size. - Rate limiting per IP via slowapi.
- Security headers:
nosniff,X-Frame-Options: DENY, strict CSP,no-store. - No error leakage. Internal exceptions return generic messages.
- Secrets from env only.
.envis gitignored; nothing sensitive reaches the browser bundle.
Caution
Market data comes from Yahoo Finance via the unofficial yfinance library.
It has no SLA and its terms restrict redistribution. Fine for a personal
project; revisit with a licensed data vendor before commercial scale.
- Body text meets WCAG AA contrast; a high-contrast mode raises everything further.
- High contrast activates automatically with the OS
prefers-contrastsetting, or manually via the footer toggle (the only thing stored locally). - Respects
prefers-reduced-motion(the crash animation stops). - Visible keyboard focus rings, a skip-to-content link, labeled inputs, and semantic landmarks.
Contributions are welcome.
- Fork and branch:
git checkout -b feature/your-idea. - Backend: keep modules single-purpose and preserve the verified reference number (60/40 SPY/QQQ through 2008 = −44.97%).
- Frontend: run
npm run buildbefore opening a PR; keep the design tokens and avoid em dashes in copy. - Open a pull request describing the change and any new endpoints or env vars.
Released under the MIT License. Market data belongs to its respective providers.
Questions, bugs, or ideas: shricharansriram@berkeley.edu
Built as an educational tool. Not financial advice.