The open-source, fund-manager-grade toolkit for backtesting a multi-asset portfolio over 20 years, stress-testing it with Monte Carlo + rolling windows + Markowitz frontier + Italian tax + FIRE projection, and getting an LLM to review the results — either from the CLI or through a beautiful Streamlit dashboard you can deploy in one click.
If you are an investor, quant curious, aspiring FIRE retiree, or data scientist who wants to understand what really happens to a defensive portfolio across GFC 2008, COVID 2020 and Stagflation 2022 — this repo is your sandbox. Clone, pip install, and you're minutes away from a full report. You can run it from the terminal, from a one-click dashboard, or deploy it publicly on Streamlit Cloud / HuggingFace Spaces.
The engine currently runs the Four Umbrellas defensive preset — the strategy from the companion Medium article "The Four Umbrellas Portfolio" — as the default (and only) portfolio; customization for now requires editing src/portfolio.py. Every allocation decision, every metric, every chart in that article is reproducible here. Generic multi-portfolio support (CLI --portfolio flag, interactive asset picker, save/load) is tracked in the upcoming PR2+ refactor.
Python newcomers, read this first. Every
python …command below assumes you've already activated the project virtual environment in your current terminal. If you haven't, the script will stop with a clear error explaining the two ways to fix it. The full step-by-step install (venv creation + activation + deps) is in Installing from scratch — do that once, then come back here.
git clone https://github.com/padosoft/portfolio-backtest.git
cd portfolio-backtest
# 1) Create the virtual environment (once)
python -m venv .venv
# 2) Activate it IN THIS TERMINAL (needed every time you open a new terminal)
# Windows PowerShell: .venv\Scripts\Activate.ps1
# Windows cmd.exe: .venv\Scripts\activate.bat
# macOS / Linux: source .venv/bin/activate
# 3) Install deps (once)
pip install -r requirements.txt
# OPTION A — full CLI report with synthetic data (no data files needed)
python backtest.py --synthetic --monte-carlo
# → open output/REPORT.md — 13 charts, stats tables, MC fan chart
# OPTION B — interactive dashboard (sliders, tabs, AI analysis)
pip install -r requirements-dashboard.txt
streamlit run streamlit_app.py
# → browser opens at http://localhost:8501
# OPTION C — plan your FIRE / retirement
python fire.py --age 45 --sex M --capital 100000 --contributions 1000 \
--fire-age 60 --spending 2500 --pension --pension-amount 1500 --pension-age 67
# OPTION D — have an LLM critique your portfolio
# Put your key in a .env file (see the "AI analysis" section), then:
python backtest.py && python analyze.py --results output/Don't want to activate the venv? You can always invoke the venv's Python directly instead of
python— skip activation entirely and use:
- Windows:
.venv\Scripts\python.exe backtest.py --synthetic- macOS / Linux:
.venv/bin/python backtest.py --syntheticThat form works in any shell, any terminal, with zero configuration.
Jump to: Install · CLI reference · GUI launch · FIRE · AI · Deploy to Cloud/HF
- ⚡ TL;DR — try it in 30 seconds
- 🛠️ Installing from scratch (step-by-step)
- 📋 CLI reference — everything you can do from the terminal
- 🧰 Custom portfolios —
--portfolioflag - What's new in v2.0 🚀
- 🌟 Why you'll actually enjoy using this
- What this is
- Why it exists
- Features
- What it calculates
- Example output
- Quick start (5 minutes)
- Running the interactive dashboard locally
- 📸 Screenshots
- 🚀 Deploy the dashboard publicly
- 🔥 FIRE calculator — plan your retirement
- 🤖 AI analysis — LLM-driven qualitative review
- Advanced analyses (sensitivity, rolling window, frontier)
- Full onboarding
- Usage recipes
- Configuration
- Methodology
- Architecture
- FAQ
- 📸 More screenshots
- Testing
- Roadmap
- Contributing
- Citation
- License & legal
Written for readers who "just want to double-click run"— if you already know what a venv is, skip to the CLI reference.
- Python 3.11, 3.12, 3.13, or 3.14 — download from python.org if you don't have it. On Windows, tick "Add python.exe to PATH" in the installer.
- Git — git-scm.com
- A terminal. On Windows, either PowerShell (built-in) or Git Bash (ships with Git) both work.
Check that Python is installed:
python --version
# expected: Python 3.11.x (or 3.12 / 3.13 / 3.14)git clone https://github.com/padosoft/portfolio-backtest.git
cd portfolio-backtestEverything below assumes your terminal is inside this portfolio-backtest/ folder.
A virtual environment is a private, project-local copy of Python that keeps this project's dependencies isolated from the rest of your system.
python -m venv .venvYou now have a .venv/ folder in the project. Do not edit it; Git ignores it.
This is the step most people miss. You must activate the venv every time you open a new terminal window. If you skip it, the scripts will stop with a clear error and point you back here.
| Terminal | Activation command |
|---|---|
| Windows PowerShell | .venv\Scripts\Activate.ps1 |
| Windows cmd.exe | .venv\Scripts\activate.bat |
| Windows Git Bash / WSL | source .venv/Scripts/activate |
| macOS / Linux | source .venv/bin/activate |
You'll know activation worked when your prompt gets a (.venv) prefix:
(.venv) PS C:\Users\you\portfolio-backtest>
PowerShell says "running scripts is disabled"? Run this once, answer
Y, then retry activation:Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Prefer zero activation? Replace every
python …command in this README with.venv\Scripts\python.exe …(Windows) or.venv/bin/python …(macOS/Linux). It's the same thing — the venv's Python knows to use its own packages.
pip install -r requirements.txtFor the Streamlit dashboard, also install:
pip install -r requirements-dashboard.txtFor running tests locally:
pip install -r requirements-dev.txtpython backtest.py --syntheticYou should see progress logs and output/REPORT.md appear at the end. If this works, you're done installing.
If you want AI analysis, create a .env file at the project root:
# Windows (PowerShell)
Copy-Item .env.example .env
# macOS / Linux
cp .env.example .envThen open .env in any text editor and paste your API key(s). See the AI section for the full guide.
Each new terminal window:
cd portfolio-backtest
.venv\Scripts\Activate.ps1 # (or the right activation command for your shell)
python backtest.py --synthetic --monte-carlo # or any other commandThat's the entire loop. If something ever fails with ModuleNotFoundError: No module named 'pandas', it means step 4 (activation) was skipped in that terminal — just run it again.
The toolkit exposes three top-level commands, each fully --help-able. This table lists the most useful flags; run python <command>.py --help for the complete list.
| Flag | What it does | Example |
|---|---|---|
| (none) | Full 20-year run, options overlay on, no MC | python backtest.py |
--start YYYY-MM-DD / --end YYYY-MM-DD |
Constrain the period | --start 2008-01-01 --end 2023-12-31 |
--nav AMOUNT |
Starting NAV in EUR (default 100 000) | --nav 250000 |
--synthetic |
Use random-walk data (no real files needed) | python backtest.py --synthetic |
--no-options |
Disable the semi-annual put-spread overlay | python backtest.py --no-options |
--monte-carlo |
Run block-bootstrap Monte Carlo — auto-includes Prudente / Mediana / Ottimista (10°/50°/90°) scenarios table + chart (NEW v2) | python backtest.py --monte-carlo --mc-paths 10000 --mc-years 20 |
--mc-paths N · --mc-years Y · --mc-block-size M |
Tune MC (defaults: 5000 paths, auto horizon, 3-month blocks) | --mc-paths 20000 --mc-years 30 --mc-block-size 3 |
--sensitivity PARAM --range LO HI --step S |
Sweep a parameter | --sensitivity gold --range 0.10 0.25 --step 0.025 |
--sensitivity PARAM --values V1 V2 ... |
Sweep specific values | --sensitivity rebalance_freq --values 1 2 4 12 |
--rolling-window --window-years N --step-months M |
Rolling-window stress test | --rolling-window --window-years 10 --step-months 1 |
--efficient-frontier --n-random N |
Markowitz + Dirichlet sampling | --efficient-frontier --n-random 50000 |
--output-dir DIR |
Where to write outputs (default output/) |
--output-dir output/custom-run |
Tax modeling is available programmatically via
src/tax.py(TaxLedger,TaxConfig— 26% CGT with 4-year minusvalenze FIFO carry, 12.5% gov-bond rate). Wiring it behind a--taxCLI flag is on the roadmap; for now,importit from your own script or test to apply tax drag on realized rebalance flows.
python fire.py --age 45 --sex M --capital 100000 --contributions 1000 \
--fire-age 60 --spending 2500 --inflation 0.02 \
--pension --pension-amount 1500 --pension-age 67 --pension-revaluation 0.75 \
--tax-on-withdrawals --simulations 5000See the full FIRE section for every parameter.
# Analyze a REPORT.md you already produced
python analyze.py --results output/
python analyze.py --results output/ --provider anthropic --model claude-opus-4-7
python analyze.py --results output/ --provider local --model kimi-k2
python analyze.py --results output/ --extra output/sensitivity/gold.csvSee the AI analysis section for provider setup.
pip install -r requirements-dashboard.txt
streamlit run streamlit_app.py
# → opens http://localhost:8501See Running the interactive dashboard locally for the tour.
Starting with PR2 the engine accepts any portfolio composition from the CLI, not just the hardcoded Four Umbrellas preset. The default is unchanged — python backtest.py --synthetic still runs the Four Umbrellas preset and produces byte-identical output.
# 1) No flag → default preset (Four Umbrellas). Zero diff from pre-PR2.
python backtest.py --synthetic
# 2) By NAME — resolves to portfolios/<name>.toml. Ship your own TOML
# in portfolios/my_strategy.toml and load it by name:
python backtest.py --synthetic --portfolio my_strategy
# 3) By PATH — any path ending in .toml or containing a /:
python backtest.py --synthetic --portfolio portfolios/four_umbrellas.toml
python backtest.py --synthetic --portfolio /absolute/path/to/some.toml
# 4) Inline JSON — anything starting with '{':
python backtest.py --synthetic --portfolio \
'{"name":"Toy","assets":[{"key":"gold","weight":0.5},{"key":"cash","weight":0.5}]}'
# List every preset in portfolios/ and exit
python backtest.py --list-portfoliosA preset is a plain TOML file. Weights MUST sum to 1.0 ±0.002 — the loader rejects anything else with a clear error. The available asset keys come from data/catalog.toml; add your own catalog entries to register new datasets.
# portfolios/my_strategy.toml
name = "My defensive strategy"
notes = "40/40/20 gold/equity/cash — example"
options_overlay = false # SPY/QQQ put-spread overlay (needs SPY+QQQ+VIX data)
rebalance_months = [1, 7] # January + July (semi-annual)
transaction_cost_bps = 20.0
[[assets]]
key = "gold"
weight = 0.4
[[assets]]
key = "quality"
weight = 0.4
[[assets]]
key = "cash"
weight = 0.2Since PR3 every advanced analysis accepts the --portfolio spec and
operates on that portfolio — no more "uses the default preset" warning:
# Efficient frontier restricted to your portfolio's asset universe
python backtest.py --synthetic --portfolio my_strategy --efficient-frontier
# Rolling 10-year windows simulated against your portfolio
python backtest.py --synthetic --portfolio my_strategy --rolling-window --window-years 10
# Sensitivity sweep on a weight inside your portfolio (not the globals)
python backtest.py --synthetic --portfolio my_strategy --sensitivity gold --range 0.10 0.25 --step 0.05Two caveats on the sensitivity path when a custom portfolio is in use:
--sensitivity options_budgeton a custom portfolio is not yet supported — the options overlay still reads the globalOPTIONSconfig; a per-portfolioOptionsConfigis tracked for a later PR. Workaround: sweepoptions_budgeton the default preset (no--portfolio).- Weight-like params (
gold,dbi,put_write,nasdaq_top30,momentum,quality) require the portfolio to contain both a matching asset and an explicitcashsleeve (the delta is absorbed by cash). The error message points at the specific missing piece.
After a run you can save the portfolio to portfolios/<slug>.toml and
reload it later — either from the CLI or from the Streamlit dashboard.
# Save the just-completed run. NAME is slugified (lowercase + '_') so
# 'My Defensive Strategy' becomes portfolios/my_defensive_strategy.toml.
python backtest.py --synthetic --portfolio my_strategy --save-as "My Defensive Strategy"
# Collision = exit 2 unless you pass --overwrite.
python backtest.py --synthetic --save-as "My Defensive Strategy" --overwrite
# List every saved preset with its cached CAGR / Vol / MaxDD / Period.
python backtest.py --list-portfoliosThe saved TOML includes an optional [metrics] section with CAGR,
annualized vol, max drawdown, the simulated period, and a UTC timestamp
of the run. Reload with --portfolio my_defensive_strategy (bare name)
or via the 📂 Portafogli salvati tab in the dashboard.
Protected names: the shipped four_umbrellas.toml preset is
reserved — the CLI and UI refuse to save/overwrite/delete it. Update it
only via a repo commit.
A massive feature rollout delivered in 11 reviewed-and-merged PRs on top of the 1.0 baseline, 267 tests passing across the matrix:
| # | Feature | What it unlocks |
|---|---|---|
| 1 | 📊 Metrics++ | Average Drawdown, Max DD duration (months), UPI (Ulcer Performance Index, Martin 1989) — drawdown-aware risk assessment |
| 2 | 🎲 MC scenarios | Explicit Prudente (10°) / Mediana (50°) / Ottimista (90°) percentiles with € values and implied CAGR — what clients actually need to see |
| 3 | 🏛️ Classic benchmarks | Golden Butterfly (Tyler), Harry Browne Permanent, Dalio All-Weather (official), Swensen Lazy — compare against the giants |
| 4 | 🎚️ Sensitivity sweep | One CLI flag iterates any parameter (gold, dbi, options_budget, rebalance_freq, put_write, nasdaq_top30, momentum, quality) across a range — instant "what if" analysis |
| 5 | 🌀 Rolling-window backtest | N-year window slid monthly/quarterly/annually — answers "would this portfolio have worked regardless of when I started investing?" |
| 6 | 📐 Efficient Frontier | Markowitz MVO + 50k Dirichlet-sampled random portfolios, Max Sharpe / Min Vol / Max Return markers, Four Umbrellas reference dot. Interactive Plotly chart — hover any point to see its allocation breakdown |
| 7 | 🇮🇹 Italian tax modeling | 26% CGT, 4-year minusvalenze carry-forward with FIFO compensation, 12.5% gov-bond rate, pension exempt during accumulation — proper after-tax numbers |
| 8 | 🔥 FIRE calculator | 2-phase Monte Carlo (accumulation → decumulation) with ISTAT 2023 mortality sampling, INPS-style pension revaluation, CGT on withdrawals, 4 dedicated charts |
| 9 | 🤖 AI analysis | Pluggable LLM provider: OpenRouter (default) / OpenAI / Anthropic / Local (Ollama, vLLM, LM Studio) — structured Italian review |
| 10 | 🖥️ Streamlit dashboard | Full interactive UI: sidebar form, Run/Save buttons, tabs for Summary / Charts / MC / AI. Deploy configs for local, Streamlit Cloud, and HuggingFace Spaces (Dockerfile) |
| 11 | ⚙️ CI/CD + docs | GitHub Actions matrix (3 OS × 2 Python) runs pytest + 6 smoke tests + artifact upload on every PR |
All v2 features are additive and flag-gated — default behavior preserves v1 compatibility. Existing users can git pull with zero surprises.
Most "portfolio backtesters" on the web are either (a) black-box web tools where you cannot see the methodology, (b) deep quant libraries (backtrader, vectorbt) with a steep learning curve for a single-portfolio study, or (c) academic code dumps that require significant rewiring to fit a custom allocation.
This project is deliberately in the middle — and tries very hard to be joyful to use:
- 🎯 One command, full report —
python backtest.py→output/REPORT.mdwith 13 charts and all stats. No plumbing. - 🖱️ Dashboard for the GUI people —
streamlit run streamlit_app.pyand you're clicking sliders. - 📚 Readable source — every business-logic module is < 300 lines. Audit it. Fork it. Teach with it.
- 🔬 Academically grounded — citations for every methodological choice (Israelov & Nielsen on options, Politis & White on block bootstrap, Martin on UPI).
- 🧪 267 tests, CI on Ubuntu / macOS / Windows × Python 3.11 / 3.12. PRs blocked if tests fail.
- 🇮🇹 Localized for Italy — tax rules, ISTAT mortality tables, INPS pension revaluation, Italian LLM output. Also works for anyone else; flag-gate what you don't need.
- 🆓 MIT license on the code. No strings.
If you are building your own portfolio, writing about markets, studying for a CFA, teaching a finance class, or just curious whether all these scenarios actually matter — clone it, run it, fork it, publish your numbers. PRs welcome.
A self-contained Python backtest engine that:
- Simulates a multi-asset portfolio with configurable weights across 19 sleeves (equity factors, gold, managed futures, bonds, crypto, EM satellites, options overlay).
- Benchmarks it against five reference portfolios out of the box: 100% S&P 500 TR (EUR), 100% SWDA (MSCI World EUR), 60/40, All-Weather proxy, and the portfolio without the options overlay.
- Computes 14 performance metrics per portfolio — CAGR, annualized vol, Sharpe, Sortino, Max Drawdown, Calmar, Ulcer Index, CVaR 5%, longest underwater period (months), recovery months, best/worst month, % positive months, total return.
- Generates 13 charts covering equity curves, drawdowns, underwater periods, rolling statistics, crisis zooms, annual returns, risk-return positioning, metric comparisons, sleeve correlations, Monte Carlo simulations.
- Produces a unified Markdown report (
output/REPORT.md) that bundles every chart and table into one consumable document — no data viewer required beyond a Markdown renderer. - Runs a block-bootstrap Monte Carlo simulation (configurable paths and horizon) with a fan chart and terminal-wealth distribution.
- Is fully reproducible: configurable date range, deterministic random seed for the MC simulation, every parameter exposed in one file.
Most "portfolio backtesters" on the web are either (a) black-box web tools where you cannot see the methodology, (b) deep quant libraries (backtrader, vectorbt) with a steep learning curve for a single-portfolio study, or (c) academic code dumps that require significant rewiring to fit a custom allocation.
This project sits deliberately in the middle:
- Opinionated — it implements one portfolio family very well, instead of being a generic framework.
- Auditable — every line of business logic (rebalancing, options pricing, metric computation) is readable in < 300 lines per module.
- Defensible — citations for every methodological choice, alignment with published practitioner literature (AQR, CBOE, SG).
- Redistributable (the code; not the data — see License & legal).
If you are building your own portfolio and want a starting point that is academically grounded but not dogmatic, this is a good scaffold to fork.
- ✅ 19-sleeve allocation with independent weights (configurable in
src/portfolio.py) - ✅ EUR / USD hedging per sleeve (each sleeve flagged hedged or unhedged)
- ✅ TER (annual expense ratio) deduction per sleeve
- ✅ Semi-annual rebalancing with configurable months (default Jan/Jul)
- ✅ Rebalance-by-buying philosophy (flow-driven, not sell-driven) — the alternative to traditional rebalancing is in the article
- ✅ BTC activation date awareness (BTC sleeve is 0% pre-2014, target weight post-2014)
- ✅ Transaction cost modeling (20 bps round-trip, configurable)
- ✅ Options overlay — SPY + QQQ debit put spreads, semi-annual rolls, Black-Scholes + VIX (skew-adjusted) pricing per Israelov & Nielsen (2015)
- ✅ 2×/3× take-profit rule — mechanical daily check, closes 50% at 2× premium, 100% at 3× (solves the whipsaw problem — see article §15)
- ✅ Fixed budget discipline — options overlay hard-capped at 0.30% NAV/year
- ✅ 100% S&P 500 TR EUR — US large-cap equity reference
- ✅ 100% SWDA / MSCI World EUR — global equity reference (iShares Core MSCI World proxy)
- ✅ 60/40 — 60% MSCI World + 40% Bloomberg Euro Aggregate
- ✅ All-Weather proxy — 30/40/15/7.5/7.5 equity/long-bonds/short-bonds/gold/commodities
- ✅ Four Umbrellas (no options) — for isolating the contribution of the options overlay
- ✅ 14 performance metrics (see What it calculates)
- ✅ 13 publication-quality charts (see Example output)
- ✅ Rolling 3-year Sharpe and return series
- ✅ Crisis-period peak-to-trough drawdowns (GFC 2008, COVID 2020, Stagflation 2022)
- ✅ Block-bootstrap Monte Carlo with percentile fan chart and terminal-wealth distribution
- ✅ Configurable date range —
--startand--endCLI flags - ✅ Configurable NAV —
--navCLI flag (default €100,000 — but the math is scale-invariant) - ✅ Synthetic data mode —
--syntheticflag to test the pipeline without real data - ✅ Unified Markdown report —
output/REPORT.mdties all outputs together - ✅ Google Colab notebook — one-click browser-based execution
- ✅ Deterministic seeds for reproducibility
- ✅ MIT license — no strings on the code
| Metric | Formula / interpretation |
|---|---|
| CAGR | Compound Annual Growth Rate: (W_end / W_start)^(1/years) - 1 |
| Annualized Volatility | σ(monthly returns) × √12 |
| Sharpe Ratio | (CAGR - RF) / annualized_vol with annualized excess returns |
| Sortino Ratio | (CAGR - RF) / downside_deviation — penalizes only negative returns |
| Max Drawdown | Largest peak-to-trough loss over the period |
| Calmar Ratio | `CAGR / |
| Ulcer Index | RMS drawdown depth — integrates depth × duration |
| CVaR 5% (Expected Shortfall) | Average return in the worst 5% of months |
| Worst Month / Best Month | Min / max of the monthly return series |
| % Positive Months | Fraction of months with positive returns |
| Total Return | End-to-end cumulative return over the period |
| Longest Underwater Period | Longest consecutive stretch (in months) below the prior all-time high |
| Recovery Months | Months from worst drawdown bottom to a new all-time high |
| Average Drawdown (v2.0) | Mean of underwater values — less noisy than Max DD |
| Max DD Duration (v2.0) | Duration in months of the single worst peak-to-trough event |
| UPI (v2.0) | Ulcer Performance Index: (CAGR - RF) / Ulcer Index — drawdown-aware Sharpe (Martin 1989) |
Peak-to-trough drawdown within each of:
- GFC (Jan 2008 – Jun 2009)
- COVID (2020)
- Stagflation (2022)
...for every portfolio modeled.
- Terminal wealth percentiles: 5th, 25th, 50th (median), 75th, 95th
- Probability of positive nominal return over horizon
- Probability of Max Drawdown exceeding 20%
- Probability of Max Drawdown exceeding 40%
- Median Max Drawdown across all simulated paths
- Worst-5% Max Drawdown (95th percentile of drawdown magnitude)
- Pearson correlation matrix between all sleeves of the main portfolio (monthly returns). Shows diversification quality and concentration risks within the allocation.
Note. Real outputs are generated only after you source the data and run the backtest. Below are the 13 chart types the engine produces, rendered in the order they appear in
output/REPORT.md.
| # | Chart | What you learn |
|---|---|---|
| 1 | equity_curve.png |
Log-scale wealth paths of every portfolio side-by-side over 20 years |
| 2 | drawdown.png |
How deep each portfolio went into drawdown, running through time |
| 3 | underwater.png |
How long each portfolio stayed under the prior all-time high (one subplot per portfolio) |
| 4 | rolling_sharpe.png |
Rolling 3-year Sharpe ratio — shows where the risk-adjusted return came from |
| 5 | rolling_returns.png |
Rolling 3-year cumulative return — helps spot regimes of outperformance |
| 6 | crisis_zoom.png |
Side-by-side comparison during GFC, COVID, and Stagflation 2022 |
| 7 | annual_returns.png |
Grouped bar chart: each year, one bar per portfolio |
| 8 | return_distribution.png |
Overlapped histograms of monthly returns — shows tail behavior |
| 9 | risk_return_scatter.png |
CAGR vs Vol, marker size = |
| 10 | metrics_comparison.png |
Grouped bar chart of CAGR, Vol, |
| 11 | correlation_heatmap.png |
Correlation matrix between all sleeves — diversification check |
| 12 | monte_carlo_fan.png |
Percentile fan chart from N bootstrap paths over a future horizon |
| 13 | monte_carlo_distribution.png |
Terminal-wealth histogram from the MC simulation |
Plus REPORT.md bundling all of them in one Markdown document, and two CSV exports (summary_statistics.csv, monthly_returns.csv) for spreadsheet analysis.
Works end-to-end with synthetic data so you can verify the plumbing before sourcing the real data.
# 1. Clone and install
git clone https://github.com/padosoft/portfolio-backtest.git
cd portfolio-backtest
# 2. Create virtual environment
python -m venv .venv
# macOS / Linux:
source .venv/bin/activate
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# 3. Install dependencies
pip install -r requirements.txt
# 4. Run on synthetic data (2-year, random-walk pricing, for plumbing check ONLY)
python backtest.py --synthetic --start 2023-01-31 --end 2024-12-31
# 5. Inspect outputs
ls output/
# -> REPORT.md + 13 PNG charts + 2 CSVs
# Open output/REPORT.md in any Markdown viewer (VS Code works great).If REPORT.md opens and shows all 13 charts inline, the pipeline works. Now you're ready for real data.
The Streamlit dashboard gives you a full interactive UI with sliders, checkboxes, portfolio selection, tabbed charts, AI analysis button, and a "Save to output/" checkbox. No CLI needed.
pip install -r requirements-dashboard.txt
streamlit run streamlit_app.pyOpens automatically at http://localhost:8501 in your default browser.
Since PR4, the sidebar is just brand + links. Everything happens in the tabs at the top.
⚙️ Impostazioni tab — build your portfolio and run:
- Carica preset dropdown loads any
portfolios/*.toml(Four Umbrellas ships by default). - 🎯 Portafoglio — dynamic asset picker backed by
data/catalog.toml:- Search / select any asset (with its catalog key and start date)
- Add with a weight %
- Edit weights inline (each row editable via
st.data_editor) - 🗑️ per-row delete
- Live Totale badge: 🟢 at 100% ±0.2%, 🔴 otherwise — the Run button is disabled until the sum is valid.
- 📅 Periodo — date range picker. If your start predates any asset's data availability, a yellow
⚠️ warning identifies the constraining asset and the effective start date. - ⚙️ Motore — starting NAV, options overlay toggle + budget (bps/year), rebalance frequency (Annual / Semi-annual / Quarterly / Monthly), transaction-cost bps.
- 🎲 Analisi avanzate — Monte Carlo toggle (paths + horizon + block). Efficient Frontier / FIRE / Walk-forward toggles are present but locked (🔒 arriva in PR5).
- Benchmarks multi-select + ▶ Run backtest + 💾 Save to output/.
Post-run tabs (populated after a successful run):
- 📊 Summary — stats table + crisis-period drawdowns per portfolio.
- 📈 Charts — 10 inline charts (equity curve, drawdown, underwater, rolling Sharpe/returns, crisis zoom, annual returns, return distribution, risk/return scatter, metrics comparison).
- 🎲 Monte Carlo — Prudente / Mediana / Ottimista metric cards + fan chart.
- 🤖 AI Analysis — pick a provider (OpenRouter / OpenAI / Anthropic / Local) and get structured Italian feedback inline.
Note. The screenshots in the next section are from the pre-PR4 sidebar layout; they'll be refreshed once PR5 brings Frontier / FIRE / walk-forward into the UI.
Three deploy targets ship pre-configured. Pick your favorite.
- Push this repo to GitHub (public or private with access granted)
- Go to share.streamlit.io
- New app → select your repo → main file:
streamlit_app.py - Add secrets (optional, for AI analysis):
OPENROUTER_API_KEY = "..." - Deploy. You get a public URL in ~2 minutes.
The bundled .streamlit/config.toml + requirements-dashboard.txt are picked up automatically.
- Create a new Space with SDK: Docker
- Clone this repo into the Space (or push a fork)
- The bundled
Dockerfilebuilds the dashboard on HF's runner (port 7860) - Push — the Space builds and serves at your HF URL automatically
docker build -t four-umbrellas-dashboard .
docker run -p 7860:7860 four-umbrellas-dashboard
# → open http://localhost:7860The Dockerfile is tuned for HuggingFace Spaces (port 7860) but works anywhere Docker runs.
Standalone FIRE (Financial Independence, Retire Early) Monte Carlo simulator with Italian mortality tables (ISTAT 2023) and INPS-style pension revaluation.
python fire.py --age 45 --sex M --capital 100000 --contributions 1000 \
--fire-age 60 --spending 2500 --inflation 0.02 --simulations 1000python fire.py --age 45 --sex M --capital 100000 --contributions 1000 \
--fire-age 60 --spending 2500 --inflation 0.02 \
--pension --pension-amount 1500 --pension-age 67 --pension-revaluation 0.75 \
--tax-on-withdrawals --simulations 5000| Group | Parameters |
|---|---|
| Personal | --age, --sex, --fixed-end-age (skip mortality sampling) |
| Wealth | --capital, --contributions, --frequency (month or year) |
| Goal | --fire-age, --spending, --inflation |
| Pension | --pension, --pension-amount, --pension-age, --pension-revaluation (1.0 = 100% INPS perequazione, 0.75 = 75%, 0 = no adjustment) |
| Tax | --tax-on-withdrawals, --tax-rate |
| Simulation | --simulations, --block-size, --seed |
Four dedicated charts + statistics panel:
- Portfolio projection — percentile bands (5/25/50/75/95) from age to death, with FIRE age + pension start markers
- Success probability — % of simulations where portfolio > 0 at each age
- Failure age distribution — histogram (or confirmation message if all successes)
- Legacy distribution — nominal + real final wealth histograms
- Console summary — success probability, median legacy (nominal + real), worst-case failure age
Send backtest results to an LLM for qualitative analysis (punti di forza, debolezze, raccomandazioni con numeri specifici, caveat metodologici, verdetto finale).
| Provider | Env var for the key | Default model (change with --model) |
|---|---|---|
| OpenRouter (default — one key, many models) | OPENROUTER_API_KEY |
anthropic/claude-opus-4-7 |
| OpenAI | OPENAI_API_KEY |
gpt-4o |
| Anthropic | ANTHROPIC_API_KEY |
claude-opus-4-7 |
| Local (Ollama / vLLM / LM Studio) | — (usually no key) | kimi-k2-0.6b |
For a local provider, optionally set LOCAL_API_BASE_URL (default http://localhost:11434/v1 for Ollama).
The project reads API keys from a plain-text .env file at the project root. This is the easiest path for everyone who isn't already comfortable with export / setx.
At the project root — the same folder that contains backtest.py, fire.py, analyze.py, and requirements.txt:
portfolio-backtest/
├── backtest.py
├── fire.py
├── analyze.py
├── .env ← HERE (you create it)
├── .env.example ← template shipped with the repo
├── requirements.txt
├── src/
└── …
The file is named exactly .env — with the leading dot, no extension. On Windows Explorer, enable "File name extensions" to avoid accidentally creating .env.txt.
.env is already listed in .gitignore, so your keys will never be committed.
# Windows (PowerShell)
Copy-Item .env.example .env
# Windows (cmd.exe)
copy .env.example .env
# macOS / Linux
cp .env.example .envOpen .env in any text editor (Notepad, VS Code, nano, …) and replace the placeholder with your real key. Set ONLY the provider(s) you actually use — leave the others as placeholders or delete their lines.
# Example — OpenRouter (recommended, single key for many models)
OPENROUTER_API_KEY=sk-or-v1-abc123yourrealkeyhere
# If you'd rather use OpenAI directly:
# OPENAI_API_KEY=sk-proj-xxxxxx
# If you'd rather use Anthropic directly:
# ANTHROPIC_API_KEY=sk-ant-xxxxxx
# Local (Ollama), no key needed. Only uncomment if your endpoint isn't the default:
# LOCAL_API_BASE_URL=http://localhost:11434/v1Format rules:
KEY=value, one per line.- No spaces around
=. - Quotes (
"…") are allowed but not required. - Lines starting with
#are comments. Blank lines are ignored. - Real shell environment variables (if you already
exported them) always win over.env, so you can override per-session.
| Provider | Sign up | Key page |
|---|---|---|
| OpenRouter | openrouter.ai | openrouter.ai/keys |
| OpenAI | platform.openai.com | platform.openai.com/api-keys |
| Anthropic | console.anthropic.com | console.anthropic.com/settings/keys |
The --model flag on analyze.py (or the "Model" input in the Streamlit dashboard) lets you switch models without touching the code. If you don't pass --model, the provider's default (see table above) is used.
# Use the provider's default model (OpenRouter → claude-opus-4-7)
python analyze.py --results output/
# Explicit model via OpenRouter — any model IDslug the provider supports
python analyze.py --results output/ --provider openrouter --model anthropic/claude-opus-4-7
python analyze.py --results output/ --provider openrouter --model openai/gpt-4o
python analyze.py --results output/ --provider openrouter --model google/gemini-2.5-pro
python analyze.py --results output/ --provider openrouter --model meta-llama/llama-3.3-70b-instruct
# Direct OpenAI
python analyze.py --results output/ --provider openai --model gpt-4o-mini
# Direct Anthropic
python analyze.py --results output/ --provider anthropic --model claude-sonnet-4-6
# Local (Ollama — just run `ollama pull <model>` first)
python analyze.py --results output/ --provider local --model llama3.1Full list of OpenRouter model slugs: openrouter.ai/models.
# (one-time) Put your key in .env — see section above.
# 1) Generate the report
python backtest.py
# 2) Send it to the LLM — default provider OpenRouter, default model Claude Opus 4.7
python analyze.py --results output/
# Same thing, different provider
python analyze.py --results output/ --provider anthropic
python analyze.py --results output/ --provider local --model kimi-k2
# Include a sensitivity CSV as extra context for the model
python analyze.py --results output/ --extra output/sensitivity/gold.csvOutput: output/AI_ANALYSIS.md — structured Italian Markdown with 5 sections (diagnosi forze, debolezze, raccomandazioni numeriche, caveat, verdetto).
If you prefer not to use a .env file, export the variables directly in your shell. These override anything in .env:
# macOS / Linux / Git Bash
export OPENROUTER_API_KEY="sk-or-v1-..."
# Windows PowerShell (current session only)
$env:OPENROUTER_API_KEY = "sk-or-v1-..."
# Windows PowerShell (persisted across sessions)
[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "sk-or-v1-...", "User")
# Windows cmd.exe (current session only)
set OPENROUTER_API_KEY=sk-or-v1-...When deploying the dashboard, configure the same variable names as secrets in the hosting platform's UI — they land in os.environ automatically and are picked up by the same code path as the local .env. The .env file itself is .gitignored and never reaches the deployment.
Vary one parameter across a range, see how CAGR/MaxDD/Sharpe/Calmar respond:
python backtest.py --sensitivity gold --range 0.10 0.25 --step 0.025
python backtest.py --sensitivity dbi --range 0.03 0.10 --step 0.01
python backtest.py --sensitivity options_budget --range 0.0 0.01 --step 0.001
python backtest.py --sensitivity rebalance_freq --values 1 2 4 12Supported parameters: gold, dbi, options_budget, rebalance_freq, put_write, nasdaq_top30, momentum, quality
Outputs: output/sensitivity/<param>.csv + 2×2 subplot chart (CAGR/MaxDD/Sharpe/Calmar vs param).
Stress-test the strategy across different starting dates:
python backtest.py --rolling-window --window-years 10 --step-months 1 # monthly step (default)
python backtest.py --rolling-window --window-years 15 --step-months 12 # annual step (faster)
python backtest.py --rolling-window --window-years 5 --step-months 3 # quarterly stepOutputs: output/rolling_window/rolling_<N>y.csv + 3-panel chart (CAGR/MaxDD/Sharpe distribution by starting year), console summary with % windows with positive CAGR, % windows with CAGR > 4%, median/worst/best.
Markowitz mean-variance optimization + 50k Dirichlet-sampled random portfolios:
python backtest.py --efficient-frontier --n-random 50000 # default
python backtest.py --efficient-frontier --n-random 10000 # faster for dev iterationsOutput: scatter plot with 50k portfolios colored by Sharpe + smooth Markowitz frontier line + markers for Max Sharpe (★), Min Vol (◆), Max Return (▲) + Four Umbrellas reference position (🔵 circle) for direct comparison.
- Python 3.11 or newer (tested on 3.11, 3.12, 3.13, 3.14)
- 500 MB free disk space for data (more if you store multiple runs)
- Internet connection for the initial data fetch
- Registration accounts (all free) for MSCI, CBOE, SG, S&P, FTSE data portals — see data/README.md
- ~1 hour for first-time data sourcing (mostly navigating providers' websites)
See the full, step-by-step guide in Installing from scratch. The short version (if you already know venvs):
git clone https://github.com/padosoft/portfolio-backtest.git
cd portfolio-backtest
python -m venv .venv
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install -r requirements.txtpython fetch_data.pyThis script downloads ~6 CSV files that are legally auto-fetchable (SPY, QQQ, VIX, BTC-USD from Yahoo Finance; EUR/USD and 3-month Treasury from FRED; computed S&P 500 and Nasdaq 100 total-return series). It then prints explicit instructions for the remaining ~15 files that require manual download from their respective providers.
See data/README.md for the full list. For each missing file:
- Visit the source URL printed by
fetch_data.py - Register (all free) if needed
- Download the monthly time series (2005–today, or your preferred range)
- Save it to
data/raw/with exactly the filename specified indata/README.md
The most common files to fetch manually:
- MSCI indices (Quality, Momentum, World ex-USA, Healthcare, Europe): msci.com/end-of-day-data-search
- CBOE PUT Index: cboe.com/us/indices/dashboard/PUT
- SG CTA Index: wholesale.banking.societegenerale.com/en/prime-services-indices
- LBMA Gold: lbma.org.uk/prices-and-data
- S&P 500 Healthcare TR: spglobal.com/spdji
python -m src.data_loader --validateThis prints OK if every expected file is present, or a list of missing filenames with instructions on where to fetch each one.
# Default: full 20-year run (2005-2024), with options overlay, no Monte Carlo
python backtest.py
# Tailored: custom date range
python backtest.py --start 2008-01-01 --end 2023-12-31
# With Monte Carlo (adds ~1 minute of runtime for 10k paths)
python backtest.py --monte-carlo --mc-paths 10000 --mc-years 20
# All options
python backtest.py --helpOpen output/REPORT.md in any Markdown viewer. Recommended:
- VS Code — built-in Markdown preview (Ctrl/Cmd+Shift+V)
- GitHub — push the output folder to a branch, browse it there
- Typora / Obsidian — nice for long-form reading
- Browser with Markdown extension — e.g., "Markdown Viewer" on Chrome
The report contains:
- Full run configuration (period, NAV, options on/off)
- Summary statistics table
- All 13 charts with captions
- Crisis peak-to-trough table
- Monte Carlo section (if enabled)
- A short disclaimer
python backtest.py --start 2010-01-01 --end 2019-12-31 --output-dir output/2010spython backtest.py --no-options --output-dir output/no-optionspython backtest.py --nav 250000The math is scale-invariant. The NAV only changes the displayed EUR values in charts and reports; all percentages and ratios are unchanged.
# 10,000 paths, 20-year future horizon, 3-month blocks
python backtest.py --monte-carlo --mc-paths 10000 --mc-years 20 --mc-block-size 3
# Longer horizon, more paths (slower)
python backtest.py --monte-carlo --mc-paths 20000 --mc-years 30For now, edit src/portfolio.py (WEIGHTS, EQUITY, OPTIONS, REBALANCE, CASH) and re-run with a different --output-dir. Compare the resulting summary_statistics.csv files across runs. A programmatic sweep tool is on the roadmap — contributions welcome.
python backtest.py --synthetic --start 2023-01-31 --end 2024-12-31Generates random-walk returns. Not real results — useful only to verify that the code runs end-to-end on your machine.
All portfolio parameters live in src/portfolio.py. The main editable dictionaries:
WEIGHTS = {
"pension": 0.11,
"gold": 0.1825,
"equity": 0.4672,
"crypto": 0.0365,
"bonds": 0.0365,
"em_sat": 0.0073,
"dbi": 0.05,
"cash": 0.11,
}EQUITY = {
"put_write": 0.131,
"nasdaq_top30": 0.117,
"hc_us_hedged": 0.022,
"hc_world": 0.051,
"quality": 0.073,
"momentum": 0.058,
"ex_usa": 0.015,
}@dataclass
class OptionsConfig:
enabled: bool = True
budget_nav_per_year: float = 0.003 # 0.30% NAV/year
hedge_ratio_of_equity: float = 0.30 # 30% of equity notional
spy_qqq_split: Tuple[float, float] = (0.70, 0.30)
long_strike_pct: float = 0.87 # long put at 13% OTM
short_strike_pct: float = 0.70 # short put at 30% OTM
tenor_months: int = 6
take_profit_partial_multiple: float = 2.0
take_profit_full_multiple: float = 3.0
commission_per_contract: float = 1.0
iv_skew_adjustment: float = 1.15 # OTM IV ≈ 1.15 × ATM VIX@dataclass
class CashConfig:
target: float = 0.11
floor_hard: float = 0.08
comfort: float = 0.10
upper_band: float = 0.14@dataclass
class RebalanceConfig:
months: Tuple[int, ...] = (1, 7) # January, July
transaction_cost_bps: float = 20.0 # 0.20% round-trip
band_relative_pct: float = 0.20 # ±20% of target weight
band_minimum_absolute: float = 0.005Edit any of these and re-run — there is no "compile" step.
- Monthly granularity for portfolio backtest — standard practice in long-horizon multi-asset research; reduces noise and avoids daily-rebalancing survivorship artifacts.
- Daily granularity for options overlay — required for path-dependent 2×/3× take-profit rule.
- EUR reporting currency — unhedged USD positions converted using monthly end-of-month EUR/USD rates.
- Proxy indices where ETFs did not exist for the full 20-year window (e.g., use CBOE PUT Index pre-2016 instead of PUTW ETF; use SG CTA Index pre-2019 instead of DBMF).
- Transaction costs modeled at 20 bps round-trip per rebalance (reasonable retail approximation).
- TER deducted monthly per sleeve using realistic real-world values.
For historical put spread pricing, we use the Black-Scholes formula with:
- Spot = SPY/QQQ closing price
- Risk-free rate = 3-month US Treasury yield (FRED)
- Dividend yield = 1.5% (conservative approximation of SPY's historical yield)
- Implied volatility = VIX × skew adjustment factor (to model the well-documented OTM put vol premium — the "smile")
This methodology is defensible for relative comparisons and is the standard in practitioner papers that do not have access to CBOE DataShop SPX option chains. See:
Israelov, R. & Nielsen, L. N. (2015). "Still Not Cheap: Portfolio Protection in Calm Markets". AQR / Journal of Portfolio Management.
For full-fidelity options pricing, purchase CBOE DataShop SPX options EOD data (~$200–500 for 20 years) and replace src/options_overlay.py's pricing function.
The MC simulation uses block bootstrap (default block size = 3 months) rather than IID resampling. Rationale: financial returns exhibit short-term autocorrelation (momentum, volatility clustering) that IID resampling destroys. Block bootstrap preserves these features.
Politis, D. N. & White, H. (2004). "Automatic Block-Length Selection for the Dependent Bootstrap". Econometric Reviews.
- Semi-annual (January + July) for macro and intra-sleeve drift
- Bands (±20% of target weight, min ±0.5% NAV) — no rebalance if within band
- Annual for benchmark portfolios (standard convention)
- Flow-driven preference (rebalance-by-buying) — philosophical discussion in article §14
All of these are discussed in article §16 and §18. Briefly:
- Survivorship bias in ETF selection (mitigated by index proxies)
- Black-Scholes + VIX approximation of historical option prices
- Stylized transaction costs
- Pre-tax returns (no tax modeling)
- No slippage during crises
- Idealized rebalancing execution
Net effect: the backtest is slightly optimistic vs real-world execution by ~20–40 bps/year over 20 years.
portfolio-backtest/
├── backtest.py # main entry point; CLI parsing, orchestration
├── fetch_data.py # public-data fetcher (Yahoo + FRED)
├── requirements.txt
├── LICENSE # MIT
├── README.md # this file
├── src/
│ ├── __init__.py
│ ├── portfolio.py # all configurable weights and parameters
│ ├── data_loader.py # CSV ingestion, currency conversion, alignment
│ ├── rebalance.py # semi-annual rebalancing engine
│ ├── options_overlay.py # BS + VIX put-spread simulator with 2×/3× rule
│ ├── metrics.py # CAGR, Sharpe, Max DD, Ulcer, CVaR, underwater
│ ├── plots.py # 13 chart generators (matplotlib)
│ ├── monte_carlo.py # block-bootstrap MC simulator
│ └── report.py # Markdown report generator
├── data/
│ ├── README.md # data sourcing guide (22 CSV manifest)
│ └── raw/ # user populates (.gitignored)
├── notebooks/
│ └── colab_backtest.ipynb # one-click Google Colab version
└── output/ # results go here (.gitignored)
├── REPORT.md # bundled Markdown report
├── summary_statistics.csv
├── monthly_returns.csv
└── *.png # 13 charts
Plus the testing infrastructure:
├── tests/
│ ├── __init__.py
│ ├── conftest.py # shared pytest fixtures
│ ├── test_metrics.py # 25+ tests on CAGR/Sharpe/MaxDD/...
│ ├── test_portfolio.py # 20+ tests on config integrity (weights sum to 1.0, etc.)
│ ├── test_options_overlay.py # Black-Scholes validation, put spread math, IV-from-VIX
│ ├── test_rebalance.py # portfolio + benchmark simulation
│ ├── test_monte_carlo.py # block bootstrap determinism, MC stats
│ ├── test_data_loader.py # synthetic bundle + date slicing
│ ├── test_report.py # Markdown report structure
│ └── test_plots.py # smoke test for all 11 chart generators
├── pytest.ini # pytest configuration
├── requirements-dev.txt # pytest + pytest-cov + runtime deps
└── .github/
└── workflows/
└── tests.yml # CI: 3 OS × 2 Python versions
backtest.py— entry point. Parses CLI, orchestrates the pipeline: data load → portfolio simulation → options overlay → metrics → charts → report.src/portfolio.py— single source of truth for all configurable parameters (weights, option parameters, rebalance rules, cash thresholds, TER, symbol mapping).src/data_loader.py— reads CSVs fromdata/raw/, aligns to end-of-month dates, converts USD returns to EUR where needed, produces a singleDataBundlepassed to the rest of the pipeline.src/rebalance.py— the core portfolio simulation: tracks sleeve weights through time, applies TER, executes rebalance events with transaction costs.src/options_overlay.py— Black-Scholes put spread simulator with daily mark-to-market, 2×/3× take-profit, semi-annual rolls, hard budget discipline.src/metrics.py— every performance metric. Pure functions; easy to test; each metric in < 20 lines.src/plots.py— 13 chart generators, all matplotlib, all saved as 120-DPI PNG.src/monte_carlo.py— block-bootstrap engine with configurable block size; generates wealth paths and summary statistics.src/report.py— assemblesoutput/REPORT.mdfrom tables and PNG paths.
A: No. All data sources are free with registration (MSCI, CBOE, SG, S&P, FTSE) or fully public (Yahoo Finance, FRED, LBMA). If you want higher-fidelity options pricing, CBOE DataShop is optional and paid.
A: The backtest uses your CSV files' actual date ranges. If you downloaded MSCI data only through 2023, the backtest will end at 2023. Re-download with fresher ranges to extend.
A: Data can differ based on source (MSCI vs iShares-ETF-proxy, for instance), on whether "Net TR" or "Gross TR" was selected, on weekend/holiday handling, and on fetching date. Differences of 10–30 bps in CAGR are normal.
A: The Black-Scholes + VIX approximation is less accurate for the deep historical period because implied-vol skew behavior differed materially pre-GFC. The approximation is sufficient for relative comparisons but not for precise P&L replication of any specific year.
A: Yes. Add the return series to data/raw/, add an entry in SYMBOL_MAP, HEDGED, TER_ANNUAL, and the appropriate WEIGHTS/EQUITY/other dict in src/portfolio.py. Make sure total weights sum to 1.0 (check in build_target_weights() in src/rebalance.py).
A: Not yet. See Roadmap. For now, all returns are pre-tax. For Italian residents, approximate after-tax results by applying 26% on realized capital gains at each rebalance (hard to do exactly because lots matter).
A: Yes, all three. Tested on Windows 11, macOS Sonoma, Ubuntu 24.04. The only OS-specific thing is the venv activation command.
A: Absolutely not. This is a research and educational tool. It is not a portfolio management system, not a trading system, and not a source of personalized investment advice. See License & legal.
A: Accessibility. More data scientists, quants, and curious readers know Python than the alternatives. The algorithms are straightforward to port if you prefer another language.
A: Realistic enough for relative comparisons and order-of-magnitude sizing. Not realistic enough for precise trade execution forecasting. If you plan to actually run the overlay, validate your specific broker's behavior on a few test trades first.
The project ships with a pytest suite covering every module. Tests run on every push to main and on every pull request via GitHub Actions (Ubuntu, macOS, Windows × Python 3.11, 3.12).
# Install dev dependencies (includes pytest + pytest-cov)
pip install -r requirements-dev.txt
# Run the full suite
pytest
# Run with coverage report
pytest --cov=src --cov-report=term-missing
# Run a single test file
pytest tests/test_metrics.py
# Run a single test
pytest tests/test_options_overlay.py::TestBSPutPrice::test_atm_put_known_value
# Skip slow / integration tests
pytest -m "not slow"| Test file | Covers | What it verifies |
|---|---|---|
test_metrics.py |
src/metrics.py |
CAGR / Sharpe / Sortino / Max DD / Calmar / Ulcer / CVaR / Underwater / Recovery — using analytically-known cases |
test_portfolio.py |
src/portfolio.py |
Weights sum to 1.0, equity sleeve sums match, enum integrity, hedging flags, symbol map completeness, benchmark validity |
test_options_overlay.py |
src/options_overlay.py |
Black-Scholes put pricing against Hull-textbook test case (S=K=100, T=1, r=5%, σ=20% → put ≈ 5.57), put-spread math, VIX-skew IV, end-to-end simulator |
test_rebalance.py |
src/rebalance.py |
Target weight construction, portfolio simulation, benchmark simulation, TER deduction |
test_monte_carlo.py |
src/monte_carlo.py |
Block bootstrap shape + determinism + reproducibility, wealth path simulation, MC stats |
test_data_loader.py |
src/data_loader.py |
Synthetic bundle generation, date slicing, data bundle structure |
test_report.py |
src/report.py |
Markdown report creation + section presence + portfolio name inclusion |
test_plots.py |
src/plots.py |
Smoke test for all 11 chart generators (file produced, non-zero size) |
Important invariants enforced by tests:
WEIGHTSdict must sum to 1.00 — any PR that breaks this fails CI- Equity sleeve breakdown must sum to
WEIGHTS["equity"] - Put spread value must always be less than the long put alone (monotonicity)
- Options budget must be in
(0, 1%]NAV/year (safety bound) - Long strike must be strictly greater than short strike
- Block bootstrap must be deterministic given the seed
- All Monte Carlo wealth paths start at 1.0
- Every sleeve in
EQUITYmust have an entry inHEDGED,SYMBOL_MAP,TER_ANNUAL
The .github/workflows/tests.yml workflow:
- Runs the full pytest suite on Ubuntu, macOS, and Windows
- Tests against Python 3.11 and 3.12
- Runs the synthetic-data smoke test end-to-end (
python backtest.py --synthetic) - Verifies that
output/REPORT.md, at least one chart PNG, andsummary_statistics.csvare produced - Reports to GitHub and blocks PRs that fail any check
To see the latest CI run: Actions tab.
When adding a new feature or fixing a bug, include a corresponding test:
- Tests live in
tests/test_<module>.py - Use pytest fixtures in
tests/conftest.pyfor shared test data - Prefer analytically-known cases (e.g., +1%/month for 12 months → CAGR ≈ 12.68%) over random-sampling tests
- Use pytest's
@pytest.mark.slowmarker for tests that take > 5 seconds
✅ Shipped in v2.0 (see What's new in v2.0): Metrics++ · MC scenarios · extra benchmarks · sensitivity sweep · rolling-window · Efficient Frontier · Italian tax · FIRE calculator · AI analysis · Streamlit dashboard · CI/CD.
🛣 Next up — contributions welcome:
-
--taxCLI flag — wire the existingTaxLedger/TaxConfig(already insrc/tax.py, fully tested) tobacktest.pyso tax drag is a one-flag opt-in - Higher-fidelity options pricing — optional CBOE DataShop integration (user provides API key)
- Factor attribution — Fama-French 5 + Momentum attribution per sleeve
- Tax lot optimization — HIFO / specific-lot selection instead of FIFO for CGT minimization
- Monte Carlo regime-switching — Hamilton-style 2-regime MC (calm / crisis) with transition probabilities
- PyPI package — so users can
pip install portfolio-backtest - English AI output toggle — today the LLM writes in Italian; add an
--lang enflag - Live data adapters — polygon.io / EOD Historical Data optional connectors for fresh data without manual downloads
- Notebook gallery — example notebooks showing each analysis interactively
Community contributions on any of these are welcome — see Contributing.
This is an active research project with a growing community. Contributions are very welcome, especially:
- 🐛 Bug fixes — open an issue with a reproduction; PRs welcome
- 📊 Additional benchmarks — add a portfolio to
BENCHMARKSinsrc/portfolio.pywith a PR - 📝 Documentation — typos, clarifications, English or other language translations, additional usage recipes
- 🚀 Performance optimizations — the current code is clarity-optimized, not speed-optimized; vectorized rewrites are welcome
- 🧪 More test coverage — 267 tests today, always room for more — especially property-based tests for the options overlay
- 🎨 Dashboard UX — Streamlit tweaks to improve the flow
- 🤖 New AI providers — Google Gemini, Mistral, Groq would be trivial adds to
src/ai_analyzer.py
# 1. Fork and clone your fork
git clone https://github.com/YOUR_USERNAME/portfolio-backtest.git
cd portfolio-backtest
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt # installs pytest + pytest-cov + runtime deps
# 2. Create a feature branch
git checkout -b feat/short-descriptive-name
# 3. Make your changes. Keep commits small and focused.
# 4. Run the full test suite before pushing
pytest # must pass all tests
pytest --cov=src # optional: check coverage delta
# 5. Verify the synthetic-data pipeline still runs end-to-end
python backtest.py --synthetic --start 2023-01-31 --end 2024-12-31
# 6. (Optional but recommended) Install the local pre-commit hooks
# so commit-time blocks unused imports + non-ASCII print() literals
pip install pre-commit
pre-commit install
# 7. Push and open a PR against main (CI will run the full matrix +
# a `lint` job: ruff F401 + ASCII-only print check)
git push origin feat/short-descriptive-namePRs must pass CI to be merged. The CI runs on Ubuntu / macOS / Windows × Python 3.11 / 3.12 (matrix tests) plus a single Ubuntu / 3.12 lint job that enforces no unused imports (ruff F401) and ASCII-only print() literals on the CLI surface (covers plain string constants AND f-string literal segments; computed expression slots inside f-strings are out of scope). If your PR adds new behavior, please add corresponding unit tests — see Testing for conventions.
This repo accumulated 23 review patterns across 7 PRs of refactor work, distilled into:
CLAUDE.md— standing rules for any AI-assisted contributor (and a useful checklist for human contributors too). Imperative, grouped by moment of code change ("before you commit", "when you change a signature", "when you parse external input", "when you write Streamlit code", etc.). Read it once before your first PR.LESSONS.md— per-incident archive of every Copilot review finding with the PR# and fix. CLAUDE.md is the digest; LESSONS.md is the canonical history..claude/skills/pr-self-review/SKILL.md— Claude Code skill that runs the 23-theme checklist against a staged diff. Useful before pushing.scripts/check_ascii_print.py+ ruff F401 — automated lint enforcement. Optional local hooks via.pre-commit-config.yaml; mandatory CI gate.
Use GitHub Issues for:
- Bugs — include reproduction command, expected vs actual output, environment (
python --version, OS) - Feature requests — open for discussion before implementation
- Data sourcing help — I may be able to point to an alternative source if one provider changes their access model
Please do not use issues for personalized investment advice requests. I can't and won't provide those.
If you use this code or reproduce any result from it in your own writing, please cite:
@misc{padovani_four_umbrellas_2026,
author = {Padovani, Lorenzo},
title = {The Four Umbrellas Portfolio: An Enterprise-Grade, Academically-Grounded Global Allocation with Layered Defensive Convexity — 20-Year Backtest},
year = {2026},
url = {https://github.com/padosoft/portfolio-backtest},
note = {Open-source Python backtest. MIT license.},
}Plain text:
Padovani, L. (2026). The Four Umbrellas Portfolio: A 20-Year Backtest of a Multi-Layer Defensive Global Allocation. GitHub: padosoft/portfolio-backtest.
MIT License — see LICENSE. You are free to use, modify, distribute, and sublicense the Python code for any purpose, including commercial. Attribution appreciated but not required.
Not redistributed. The raw time-series data used as input is subject to the respective providers' licensing terms:
- MSCI indices — licensed; may not be redistributed in raw form
- CBOE indices (PUT, BXM, VIX historical) — licensed
- S&P / FTSE indices — licensed
- SG CTA Index — licensed
- LBMA Gold prices — usable for personal / commentary; redistribution restricted
- Yahoo Finance data — ToS prohibits systematic bulk redistribution
- FRED data — public domain (US Federal Reserve)
Users must download data from original providers. See data/README.md. Output charts and metrics derived from this data are the user's intellectual property, subject to whatever conclusions you draw.
The outputs of this software — charts, tables, numerical results, simulations, including Monte Carlo — are for informational and educational purposes only. They are NOT:
- investment advice
- a recommendation to buy, sell, or hold any security
- an offer or solicitation
- a personalized financial plan
- a guarantee of any future performance
The author is not a registered investment advisor, financial analyst, portfolio manager, broker, fiduciary, or regulated professional in any jurisdiction.
Past performance and hypothetical simulations do not guarantee future results. All backtests are subject to survivorship bias, selection bias, look-ahead bias, and other methodological limitations that bias results upward. Real-world execution will produce different results.
You are solely responsible for your own financial decisions. If you are considering actual investment actions based on this code or its outputs, consult a properly licensed professional in your jurisdiction. The author, contributors, and distributors of this software accept no liability whatsoever for any loss or damage arising from its use.
See the full disclaimer in the accompanying Medium article.
Questions, feedback, corrections welcome. Open an issue or reach out on GitHub.








