Ongoing · Low Priority

Portfolio Simulation

A set of Python backtesting scripts that answer “would DCA or lump-sum investing actually have beaten a fixed monthly buy, using real S&P 500 and Nasdaq history since the 1920s?”

The problem

Most investing advice about dollar-cost averaging versus lump-sum, or about “buying the dip harder,” is argued from intuition or from a handful of cherry-picked years. I wanted to know how those strategies actually performed across every rolling period in market history, not just the one everyone quotes (2008, or the last bull run). That’s the same gap a finance team or an investment content business runs into: strategy claims get pitched with one convenient backtest window, and nobody has re-run them across every overlapping period to see if the edge survives.

There’s also a data problem underneath it: getting clean, long-running daily price history for an index isn’t free or trivial to assemble, and once you have it, aggregating it into monthly and yearly series correctly (first trading day per month, rolling 52-week highs, etc.) is easy to get subtly wrong.

The approach

This is a CLI toolkit, not an app with a menu — each script is a standalone simulation you run with a flag. `download_EOD_prices.py` pulls full-history daily S&P 500 data via yfinance and plots normalized comparisons across different historical windows (e.g., 1927-1950 vs. 2003-2009 vs. 2024-2025, rebased to start at the same point) to see how the current market’s shape compares to past crashes and recoveries. `findyear_lumpsum.py` downloads S&P and Nasdaq history back to 1985 and, for 3-, 4-, and 5-year holding windows, compares a once-a-year lump-sum contribution against a monthly DCA contribution, then builds a “master table” flagging any end-year where all three window lengths would have lost money — a quick way to see how rare (or common) a genuinely bad multi-year entry point has been.

The core piece, `portfoliosim_compare_variable_dca.py`, runs `python portfoliosim_compare_variable_dca.py –years N` and simulates every possible N-year monthly-investment period in the loaded price history, twice: once with a fixed $200/month contribution, once with a variable contribution that scales up when price drops below the trailing 52-week high (invest more the deeper the drawdown). For every period it computes total invested, final value, CAGR, max drawdown, annualized volatility, and Sharpe ratio, prints both strategies side by side, and averages performance across all periods so you can see whether “buy more when it’s down” actually beats “buy the same amount every month” once you’re not just looking at one lucky window. The other scripts in the folder (monthly, monthly-compound, yearly-compound variants, plus an `old/` folder of earlier iterations) are variations on the same simulate-and-compare pattern for different compounding and cadence assumptions.

What I learned

The big lesson was methodological: a strategy that looks smart in one backtest window (buy more on dips) can look mediocre or even worse once you average it across every rolling N-year period instead of the one everyone likes to cite — variance between periods matters more than the average people usually quote. I also relearned the practical annoyance of historical data: yfinance’s index tickers, adjusted vs. unadjusted closes, and “first trading day of the month” edge cases all quietly shift results if you’re not careful, which is why the scripts do their own monthly aggregation from raw daily data rather than trusting a pre-aggregated source.

Where this could go

The obvious next step is turning the one-off scripts into a small reusable backtesting library — parameterized strategies (fixed, variable-DCA, lump-sum, rebalancing) run against pluggable data sources, with the results exported instead of just printed to console. That’s the same shape as the backtesting infrastructure a fintech content site, a robo-advisor, or an internal investment-research team needs before it publishes any performance claim: a shared, versioned simulation engine everyone runs the same claim through, rather than each analyst hand-rolling a one-off script and reporting whatever window they happened to test. At team scale that also means storing simulation runs (not just printing tables) so claims are reproducible and auditable months later.

Takeaway

It’s a small toolkit, but it does the thing most investing content skips: it re-runs the strategy across every historical period instead of just the one that makes the best story.

Text summarized and optimized using Anthropic’s models and reviewed by a human.