A brute-force portfolio grid search over long-run daily asset histories. It tests every weight combination at a fixed increment, backtests each one from 1970 to the present, and writes a ranked results table plus a self-contained interactive chart you can open in any browser.
The point is to see the whole trade-off surface at once rather than a single "optimal" answer. Every blend is a dot, so you can look at what growth actually costs in drawdown, where the diminishing returns start, and how wide the band of near-equivalent portfolios really is.
Above: every 5% blend of four assets over 1970 to 2026. The pinned dot is the shallow-drawdown corner, 20/60/15/5 across stocks, bonds, gold and commodities, which gave up about two points of CAGR against the best blends but fell only 21% at its worst instead of 40%+. The curved upper edge is the frontier, and the width of the cloud beneath it shows how many quite different recipes land in much the same place.
pip install pandas numpy
python portfolio_optimizer.py
Outputs land in output/:
| File | What it is |
|---|---|
portfolio_optimisation_results.csv |
Every portfolio tested, ranked, with all metrics |
portfolio_optimisation_results.html |
Interactive chart and table, no dependencies, no server needed |
A generated example of both is committed to this repo. The screenshot above is a still of that page; download the HTML and open it locally to get the interactive version, since GitHub will not render it in the browser.
Price data comes from the companion repository mwilczynska/financial_datasets. Every series there runs from 1970 to the present. No real fund goes back that far, so only the most recent stretch of each series is a fund's own returns; before that it is reconstructed from published index data and documented models, with the method and its limits written up per asset. Check the repo out as a sibling directory:
your-folder/
├── financial_datasets/
└── portfolio_optimisation/
DATA_ROOT in portfolio_optimizer.py resolves to ../financial_datasets/data/processed. Point
it somewhere else if your layout differs.
Two asset sets are wired up out of the box. The default is un-levered:
| Code | What it is | Series covers |
|---|---|---|
GLSTOCK |
All-world equities in USD, dividends reinvested (VT-like) | 1970 on, VT's own returns from 2008 |
GLBOND |
Unhedged global government bonds in USD (BND/BWX blend) | 1970 on, the fund blend from 2007 |
GOLDPM |
Gold bullion as a total return (GLD-like) | 1970 on, GLD's own returns from 2004 |
COMM |
Broad commodity futures with roll yield and collateral (DBC-like) | 1970 on, DBC's own returns from 2006 |
The alternative, commented out in the config block, is a leveraged set: USLCAP3x, LTT3x,
ITT3x, GOLDPM2x and COMM, modelling daily-reset funds like UPRO, TMF, TYD and UGL with their
financing costs, fees and volatility decay included. Those series also start in 1970 and switch to
the real funds when they launched, around 2008 to 2009. The generated page adapts its explanations
and warnings to whichever set you actually ran.
Everything editable sits at the top of portfolio_optimizer.py:
| Setting | Purpose |
|---|---|
asset_list |
Which assets to blend |
weight_step |
Grid increment. 0.05 for the full 5% run, 0.20 for a quick test |
min_weights / max_weights |
Per-asset floors and ceilings |
max_drawdown_limit |
Drop portfolios worse than this, or None to keep everything |
rank_by / rank_ascending |
Which metric sorts the table |
rebalance_annually |
Annual rebalance, or buy and hold |
starting_value |
Opening pot for every portfolio |
risk_free_rate_annual |
Used by Sharpe and Sortino |
Grid size grows fast: four assets at 5% steps is 1,771 portfolios, five assets is 10,626.
Final value, CAGR, annual return, standard deviation, TWRR, Sharpe, Sortino, Calmar, max drawdown, Ulcer Index, average drawdown, longest drawdown in days, daily win rate, best and worst day, month and year, 95% VaR and CVaR, skew, and excess kurtosis.
Each row also carries an Existing Script Syntax column, a ready-to-paste
tickers, weights = [...],[...] line for use in other backtesting scripts.
Written as plain HTML, SVG and JavaScript with no libraries and no build step, so it works from a local file or any static host.
- Pick what the X and Y axes measure from any numeric column.
- Hover a dot for that portfolio's mix and statistics. Click to pin it, click again to unpin.
- Highlight every dot past a threshold you choose on the current X axis.
- Sort the table by any column, search the weights, or stack numeric filters such as "Max Drawdown at most 60%". Loss-type columns are magnitude-aware, so that filter keeps the shallower drawdowns rather than matching everything.
- Chart and table are linked. Pinning a dot scrolls to and highlights its row, and vice versa.
The page also carries a plain-language explainer of what it shows, how each data series was built,
and what to be sceptical about. ABOUT.md is a standalone copy of that write-up.
This is a historical simulation, not advice, and past results do not predict future returns.
Hindsight is baked into any ranking of backtests: the top row is the blend most fitted to this
particular history. Taxes, commissions, bid/ask spreads and rebalancing friction are not modelled.
The early decades of every series are reconstructions rather than observed history, since no real
fund reaches back to 1970, and ABOUT.md spells out which parts, for which asset.
Code is MIT, see LICENSE.
The datasets are not covered by that licence. Some upstream sources carry their own redistribution
or non-commercial terms, notably the S&P GSCI republication behind the early commodity history and
the Jorda-Schularick-Taylor macrohistory database behind the early bond history. See
DATA_LICENSE.md in
mwilczynska/financial_datasets before reusing
the data itself.
