<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Python &#8211; Be on the Right Side of Change</title>
	<atom:link href="https://blog.finxter.com/category/python/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.finxter.com</link>
	<description></description>
	<lastBuildDate>Tue, 10 Nov 2026 16:58:43 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://blog.finxter.com/wp-content/uploads/2020/08/cropped-cropped-finxter_nobackground-32x32.png</url>
	<title>Python &#8211; Be on the Right Side of Change</title>
	<link>https://blog.finxter.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Stock Bond Correlation Over Time: 33 Years of SPY vs Treasuries, and Why It&#8217;s +0.42 Now (Python)</title>
		<link>https://blog.finxter.com/stock-bond-correlation-over-time/</link>
					<comments>https://blog.finxter.com/stock-bond-correlation-over-time/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 16:58:41 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/stock-bond-correlation-over-time/</guid>

					<description><![CDATA[Stock bond correlation over time, 1993-2026: SPY vs Treasuries was -0.29 in 2000-2021, +0.42 now. Inflation link, 60/40 impact + Python.]]></description>
										<content:encoded><![CDATA[<p><strong>Stock bond correlation over time has flipped twice since 1993.</strong> Using monthly total returns for SPY (stocks) and Vanguard&#8217;s long-term Treasury fund VUSTX (bonds), the 36-month rolling correlation was positive in the late 1990s, negative for nearly all of 2000–2021 (as low as −0.78 in January 2013), and positive again since 2022. It peaked at +0.68 in December 2024 and sits at <strong>+0.42</strong> as of October 2026.</p>
<p>That one number decides whether bonds cushion a stock crash or fall right alongside it. Below are the 405 months of data, the inflation pattern behind the flips, what it did to a 60/40 portfolio&#8217;s volatility, and a short Python script that rebuilds every number.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0"><p><strong>TL;DR</strong></p><ul><li>The 36-month stock-bond correlation (SPY vs long Treasuries) is <strong>+0.42</strong> as of October 2026, after peaking at +0.68 in December 2024.</li><li>It was negative in 69% of windows since 1996, bottoming at −0.78 in January 2013. The 2000–2021 average was −0.29.</li><li>In 2008, SPY lost 36.8% and long Treasuries gained 22.6%. In 2022, SPY lost 18.2% and long Treasuries lost 29.6%.</li><li>Inflation drives it: windows with average CPI under 2.5% averaged −0.27, windows at 4%+ averaged <strong>+0.52</strong>.</li><li>A 60/40 portfolio&#8217;s volatility rose from 8.8% to 13.3%. Short Python script included.</li></ul></div>
<p>If you&#8217;re putting this kind of research into a guide for readers or clients, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=stock-bond-correlation-over-time&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns your notes or a prompt into a full text draft with Word and EPUB export. It&#8217;s a quick way to turn a finance analysis like this one into an ebook or lead magnet.</p>
<h2>The answer in one chart</h2>
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-timeline.gif" alt="Animated line chart drawing the 36-month stock-bond correlation year by year from 1996 to October 2026, pausing at -0.78 in January 2013 and +0.42 in October 2026" class="wp-image-1672603"/><figcaption class="wp-element-caption">Year by year: teal when bonds hedged stocks (negative), coral when they moved together (sbcorr.py).</figcaption></figure>
<p>Correlation runs from −1 (bonds always move opposite to stocks) to +1 (always together). Over the whole 1993–2026 sample, the monthly correlation between SPY and long Treasuries is just −0.05, which sounds like &#8220;no relationship.&#8221; That average hides three very different regimes:</p>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>Period</th><th>Stock-bond correlation (monthly)</th><th>Months both fell</th><th>60/40 volatility</th><th>Stocks-alone volatility</th></tr></thead><tbody><tr><td>Feb 1993 – Dec 1999</td><td>+0.25</td><td>17%</td><td>9.0%</td><td>12.8%</td></tr><tr><td>Jan 2000 – Dec 2021</td><td><strong>−0.29</strong></td><td>11%</td><td><strong>8.8%</strong></td><td>15.0%</td></tr><tr><td>Jan 2022 – Oct 2026</td><td><strong>+0.56</strong></td><td>31%</td><td><strong>13.3%</strong></td><td>15.6%</td></tr></tbody></table><figcaption class="wp-element-caption">SPY vs VUSTX monthly total returns (Yahoo Finance). 60/40 = 60% SPY, 40% VUSTX, rebalanced monthly. sbcorr.py</figcaption></figure>
<p>Out of 370 rolling 36-month windows since January 1996, 69% had a negative correlation. That&#8217;s why a whole generation of investors learned that &#8220;bonds go up when stocks go down.&#8221; The other 31% are concentrated at the two ends of the sample, and we&#8217;re living in one of them.</p>
<p>Total-bond funds tell the same story. VBMFX, Vanguard&#8217;s Total Bond Market Index fund, shows a 36-month correlation with SPY of +0.44 today, nearly the same as long Treasuries.</p>
<h2>What actually happened in losing years</h2>
<p>A correlation coefficient is abstract. Calendar-year returns aren&#8217;t. Here is every year since 1994 in which SPY lost money, next to what long Treasuries did the same year:</p>
<figure class="wp-block-image size-large"><img decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-losing-years.png" alt="Bar chart of every losing SPY year since 1994 with long Treasury returns: 2000, 2001, 2002 and 2008 bonds gained 4.4% to 22.6%; 2018 and 2022 bonds also fell, 2022 by 29.6%" class="wp-image-1672604" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-losing-years.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-losing-years-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-losing-years-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-losing-years-768x422.png 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">In 2008 long Treasuries gained 22.6%. In 2022 they lost 29.6%, more than stocks.</figcaption></figure>
<p>In the four big stock losses of the negative-correlation era (2000, 2001, 2002, 2008), long Treasuries gained between 4.4% and 22.6%. In 2008, SPY lost 36.8% while VUSTX gained 22.6%. That&#8217;s the hedge working exactly as advertised.</p>
<p>Then 2022 happened. SPY fell 18.2% and VUSTX fell 29.6%. Bonds lost <em>more</em> than stocks. Only one other year in the sample, 2018, saw both down, and that was a mild −4.6% and −1.9%.</p>
<p>The monthly data show the same shift. From 2000 to 2021, only 11% of months had both stocks and bonds negative. From January 2022 to October 2026 it was 31%, almost one month in three.</p>
<h2>Why it flips: inflation</h2>
<figure class="wp-block-image size-large"><img decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-vs-inflation.png" alt="Line chart of the 36-month stock-bond correlation alongside 36-month average CPI inflation, 1996 to 2026; the correlation rises when inflation rises, with a +0.60 relationship" class="wp-image-1672605" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-vs-inflation.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-vs-inflation-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-vs-inflation-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-correlation-vs-inflation-768x422.png 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The correlation and average inflation move together (+0.60 across all windows).</figcaption></figure>
<p>The dashed line is the 36-month average of CPI inflation from FRED. When inflation was low and stable, the correlation sat below zero. When inflation surged in 2021–2023, the correlation jumped. Across all windows, the correlation between the two lines is +0.60.</p>
<p>Grouping the windows by their average inflation makes the pattern blunt:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-inflation-buckets.png" alt="Bar chart of mean stock-bond correlation by average inflation: -0.27 when CPI is under 2.5%, +0.04 from 2.5% to 4%, +0.52 at 4% and above" class="wp-image-1672606" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-inflation-buckets.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-inflation-buckets-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-inflation-buckets-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-inflation-buckets-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Group the 36-month windows by inflation and the pattern is hard to miss.</figcaption></figure>
<ul>
<li>Average CPI under 2.5%: mean stock-bond correlation <strong>−0.27</strong> (208 windows)</li>
<li>CPI 2.5% to 4%: <strong>+0.04</strong> (126 windows)</li>
<li>CPI 4% and up: <strong>+0.52</strong> (35 windows)</li>
</ul>
<p>This matches the research. <a href="https://www.aqr.com/Insights/Research/Journal-Article/A-Changing-Stock-Bond-Correlation">AQR&#8217;s analysis</a> finds that growth news pushes stocks and bonds in opposite directions while inflation news pushes them the same way, and that a simple growth-versus-inflation model explains about 70% of the long-term variation in the US stock-bond correlation. AQR also points out that the correlation was more often positive than negative from 1900 to 2000, so the 2000–2021 era, not today, is the historical outlier (<a href="https://www.aqr.com/Insights/Perspectives/A-Positive-Stock-Bond-Correlation-Is-a-Terrible-Reason-to-Add-More-Equity-Risk-to-Your-Portfolio">AQR perspective</a>).</p>
<p>Where are we now? CPI inflation was 3.7% year over year in September 2026, and the 36-month average is 3.0%. That&#8217;s the middle bucket, where history says the correlation is a coin flip. The current +0.42 is still higher than that bucket&#8217;s average, so bonds haven&#8217;t won back their hedge yet.</p>
<blockquote>
<p>A data note: FRED&#8217;s CPIAUCSL series has no value for October 2025, so the 36-month averages simply skip that month.</p>
</blockquote>
<h2>What it costs a 60/40 portfolio</h2>
<p>The point of holding 40% bonds is to make the ride smoother. Here&#8217;s the annualized volatility of monthly returns for stocks alone and for a 60/40 mix (60% SPY, 40% VUSTX, rebalanced monthly):</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-sixty-forty-vol.png" alt="Bar chart of annualized volatility for stocks alone and a 60/40 portfolio: 12.8% vs 9.0% in 1993-1999, 15.0% vs 8.8% in 2000-2021, 15.6% vs 13.3% since 2022, with 9.4% at the old correlation" class="wp-image-1672607" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-sixty-forty-vol.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-sixty-forty-vol-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-sixty-forty-vol-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/stock-bond-correlation-over-time-sixty-forty-vol-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The bonds got jumpier, but most of the 60/40 volatility rise came from the correlation flip.</figcaption></figure>
<p>From 2000 to 2021, the 60/40 portfolio had 8.8% volatility against 15.0% for stocks alone. It removed about 41% of the swings while keeping 60% of the stock exposure. Since 2022, the same mix has run at 13.3% against 15.6% for stocks. It removed only about 15%.</p>
<p>How much of that is the correlation flip and how much is just jumpier bonds (14.0% volatility since 2022, up from 10.9%)? Plug the 2022–2026 volatilities into the 60/40 formula but keep the old −0.29 correlation, and you get 9.4%. So of the 4.5-point rise in 60/40 volatility, about 3.9 points come from the correlation turning positive. The bonds themselves got riskier, but losing the hedge did most of the damage.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0"><p><strong>The rule to remember:</strong> bonds hedge stocks when growth fears dominate, and fall with stocks when inflation fears dominate. Since 2022, a 60/40 portfolio has carried about 85% of the volatility of stocks alone, up from 59% in 2000–2021.</p></div>
<p>If you want to see how this plays out in drawdowns, our <a href="https://blog.finxter.com/60-40-portfolio-max-drawdown/">60/40 portfolio max drawdown</a> post has the peak-to-trough numbers, and <a href="https://blog.finxter.com/how-often-to-rebalance-portfolio/">how often to rebalance</a> covers the rebalancing side. And if you&#8217;re retiring into a positive-correlation regime, the <a href="https://blog.finxter.com/sequence-of-returns-risk-example/">sequence of returns risk example</a> shows why the first few years matter most.</p>
<h2>The Python script</h2>
<p>The script below downloads nothing by itself. Fetch daily adjusted closes once with <code>yfinance</code> (for example <code>yf.download("SPY", start="1990-01-01", auto_adjust=True).to_pickle("src/SPY.pkl")</code>, and the same for <code>VUSTX</code> and <code>VBMFX</code>), and save FRED&#8217;s CPIAUCSL CSV to <code>src/</code>. Then run it:</p>
<pre><code class="language-python">"""Stock bond correlation over time: SPY vs long-term Treasuries (VUSTX), monthly total returns.
Data: Yahoo Finance adjusted closes via yfinance (dividends reinvested), Feb 1993 - Oct 2026."""
import pandas as pd

def monthly(t, pkl="src/{}.pkl"):
    px = pd.read_pickle(pkl.format(t))["Close"].squeeze()
    return px.resample("ME").last().pct_change().dropna()

def data():
    df = pd.concat({"stocks": monthly("SPY"), "bonds": monthly("VUSTX"), "total": monthly("VBMFX")}, axis=1, sort=True).dropna()
    return df.loc["1993-02":"2026-10"]

def rolling_corr(df, n=36, b="bonds"):
    return df["stocks"].rolling(n).corr(df[b]).dropna()

def cpi_yoy(csv="src/CPIAUCSL.csv"):
    c = pd.read_csv(csv, index_col=0, parse_dates=True).squeeze()
    c.index = c.index + pd.offsets.MonthEnd(0)
    return c.pct_change(12).dropna()

def vol_6040(vs, vb, rho, w=0.6):
    return (w*w*vs*vs + (1-w)**2*vb*vb + 2*w*(1-w)*rho*vs*vb) ** 0.5

def regime_stats(df, start, end, w=0.6):
    d = df.loc[start:end]; p = w * d["stocks"] + (1 - w) * d["bonds"]
    vol = lambda s: s.std() * 12 ** 0.5
    return dict(corr=d["stocks"].corr(d["bonds"]), corr_agg=d["stocks"].corr(d["total"]), vol_s=vol(d["stocks"]), vol_b=vol(d["bonds"]), vol_p=vol(p),
                months=len(d), both_down=((d["stocks"] &lt; 0) &amp; (d["bonds"] &lt; 0)).mean())

if __name__ == "__main__":
    df = data(); rc = rolling_corr(df); ra = rolling_corr(df, b="total")
    print(f"months {len(df)}: {df.index[0]:%Y-%m} to {df.index[-1]:%Y-%m}")
    print(f"full-period corr stocks/long Treasuries {df.stocks.corr(df.bonds):+.2f}, stocks/total bond {df.stocks.corr(df.total):+.2f}")
    print(f"36m rolling: latest {rc.iloc[-1]:+.2f} ({rc.index[-1]:%Y-%m}); min {rc.min():+.2f} ({rc.idxmin():%Y-%m}); max {rc.max():+.2f} ({rc.idxmax():%Y-%m})")
    print(f"share of 36m windows below zero: {(rc &lt; 0).mean():.0%}; latest total-bond 36m {ra.iloc[-1]:+.2f}")
    for a, b in (("1993-02", "1999-12"), ("2000-01", "2021-12"), ("2022-01", "2026-10")):
        s = regime_stats(df, a, b)
        print(f"{a}..{b}: corr {s['corr']:+.2f} (agg {s['corr_agg']:+.2f}), vol stocks {s['vol_s']:.1%} bonds {s['vol_b']:.1%} 60/40 {s['vol_p']:.1%}, both-down months {s['both_down']:.0%}")
    y = (1 + df).groupby(df.index.year).prod() - 1; y = y.loc[1994:2025]
    both = y[(y.stocks &lt; 0) &amp; (y.bonds &lt; 0)]
    print("calendar years both down (1994-2025):", ", ".join(f"{i}: stocks {r.stocks:+.1%} bonds {r.bonds:+.1%}" for i, r in both.iterrows()))
    neg = y[y.stocks &lt; 0]
    print("stock down years:", ", ".join(f"{i}: {r.stocks:+.1%}/{r.bonds:+.1%}" for i, r in neg.iterrows()))
    s22 = regime_stats(df, "2022-01", "2026-10"); s00 = regime_stats(df, "2000-01", "2021-12")
    print(f"2022-26 60/40 vol if corr had stayed {s00['corr']:+.2f}: {vol_6040(s22['vol_s'], s22['vol_b'], s00['corr']):.1%} (actual {s22['vol_p']:.1%})")
    inf = cpi_yoy().reindex(rc.index); inf36 = cpi_yoy().resample("ME").last().rolling(36, min_periods=33).mean().reindex(rc.index)
    print(f"corr of 36m stock-bond correlation with 36m avg CPI inflation: {rc.corr(inf36):+.2f}")
    for lo, hi in ((0, 0.025), (0.025, 0.04), (0.04, 0.2)):
        m = (inf36 &gt;= lo) &amp; (inf36 &lt; hi); print(f"  avg CPI {lo:.1%}-{hi:.1%}: {m.sum()} windows, mean corr {rc[m].mean():+.2f}")
    print(f"latest CPI YoY {cpi_yoy().iloc[-1]:.1%} ({cpi_yoy().index[-1]:%Y-%m}), latest 36m avg {inf36.dropna().iloc[-1]:.1%} ({inf36.dropna().index[-1]:%Y-%m})")
    for d in ("2008-12", "2013-01", "2019-12", "2021-12", "2024-12"):
        print(f"  36m corr at {d}: {rc.loc[d].iloc[0]:+.2f}")
</code></pre>
<p>Output (data through October 2026):</p>
<pre><code class="language-text">months 405: 1993-02 to 2026-10
full-period corr stocks/long Treasuries -0.05, stocks/total bond +0.16
36m rolling: latest +0.42 (2026-10); min -0.78 (2013-01); max +0.68 (2024-12)
share of 36m windows below zero: 69%; latest total-bond 36m +0.44
1993-02..1999-12: corr +0.25 (agg +0.32), vol stocks 12.8% bonds 7.9% 60/40 9.0%, both-down months 17%
2000-01..2021-12: corr -0.29 (agg -0.07), vol stocks 15.0% bonds 10.9% 60/40 8.8%, both-down months 11%
2022-01..2026-10: corr +0.56 (agg +0.61), vol stocks 15.6% bonds 14.0% 60/40 13.3%, both-down months 31%
calendar years both down (1994-2025): 2018: stocks -4.6% bonds -1.9%, 2022: stocks -18.2% bonds -29.6%
stock down years: 2000: -9.7%/+19.8%, 2001: -11.8%/+4.4%, 2002: -21.6%/+16.7%, 2008: -36.8%/+22.6%, 2018: -4.6%/-1.9%, 2022: -18.2%/-29.6%
2022-26 60/40 vol if corr had stayed -0.29: 9.4% (actual 13.3%)
corr of 36m stock-bond correlation with 36m avg CPI inflation: +0.60
  avg CPI 0.0%-2.5%: 208 windows, mean corr -0.27
  avg CPI 2.5%-4.0%: 126 windows, mean corr +0.04
  avg CPI 4.0%-20.0%: 35 windows, mean corr +0.52
latest CPI YoY 3.7% (2026-09), latest 36m avg 3.0% (2026-09)
  36m corr at 2008-12: -0.14
  36m corr at 2013-01: -0.78
  36m corr at 2019-12: -0.34
  36m corr at 2021-12: -0.32
  36m corr at 2024-12: +0.68
</code></pre>
<p>A few choices worth knowing about. Monthly returns smooth out daily noise. Thirty-six months is a common window, and a 12-month window swings much harder, while AQR uses 10-year windows in its research. Adjusted closes include dividends and bond interest, so these are total returns. And VUSTX is a long-duration fund, so it reacts more to rate moves than a short-term bond fund would.</p>
<p>To track your own mix of stock and bond ETFs against the S&amp;P 500 without handing your data to anyone, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free tracker that runs in your browser, with no account, no broker connection, and Excel backups.</p>
<h2>What to do with this</h2>
<ol>
<li><strong>Check the regime before trusting the hedge.</strong> Re-run the script every few months. A 36-month correlation above zero means your bonds are not a crash hedge right now.</li>
<li><strong>Watch inflation, not the headlines.</strong> The windows with average CPI under 2.5% averaged a −0.27 correlation. Inflation stabilizing below that level is the clearest historical sign that the old hedge returns.</li>
<li><strong>Don&#8217;t assume more stocks is the fix.</strong> AQR argues that a positive correlation is a bad reason to take more equity risk, because you&#8217;re adding exposure to the same shocks that already hit both assets.</li>
<li><strong>Hold cash for spending needs.</strong> In 2022 both legs fell together. A cash or short-term bucket for near-term withdrawals doesn&#8217;t depend on any correlation.</li>
</ol>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>Finding</th><th>2000–2021</th><th>Since 2022</th><th>What it means</th></tr></thead><tbody><tr><td>Stock-bond correlation</td><td>−0.29</td><td>+0.56 (+0.42 latest 36 mo)</td><td>Bonds no longer offset stock losses reliably</td></tr><tr><td>Months with both down</td><td>11%</td><td>31%</td><td>Nearly one month in three hits both legs</td></tr><tr><td>Worst stock year</td><td>2008: SPY −36.8%, VUSTX +22.6%</td><td>2022: SPY −18.2%, VUSTX −29.6%</td><td>The hedge flipped in the crash that mattered</td></tr><tr><td>60/40 volatility</td><td>8.8%</td><td>13.3% (9.4% at the old correlation)</td><td>About 3.9 points of the rise came from the correlation</td></tr><tr><td>Inflation link</td><td>CPI avg under 2.5%: −0.27</td><td>CPI avg 4%+: +0.52</td><td>Watch inflation to judge the hedge</td></tr></tbody></table><figcaption class="wp-element-caption">All numbers from sbcorr.py on Yahoo Finance and FRED data, Feb 1993 – Oct 2026.</figcaption></figure>
<h2>FAQ</h2>
<h3>What is the stock bond correlation right now?</h3><p>Using monthly SPY and long-term Treasury (VUSTX) returns, the 36-month correlation is +0.42 as of October 2026. It peaked at +0.68 in December 2024 and has been positive since 2022.</p><h3>Has the stock bond correlation always been negative?</h3><p>No. It was +0.25 from 1993 to 1999 and −0.29 from 2000 to 2021. AQR notes it was more often positive than negative from 1900 to 2000, so the negative era was the exception.</p><h3>Why did stocks and bonds fall together in 2022?</h3><p>Inflation. Rising inflation and interest rates hurt both. SPY lost 18.2% and long Treasuries lost 29.6%. Windows with average CPI of 4% or more had a mean correlation of +0.52.</p><h3>Do bonds still diversify a stock portfolio?</h3><p>Less than before. A 60/40 mix had 8.8% volatility versus 15.0% for stocks in 2000–2021, but 13.3% versus 15.6% since 2022. About 3.9 points of that rise came from the correlation turning positive.</p><h3>How do I calculate stock bond correlation in Python?</h3><p>Resample daily adjusted closes to month-end, take pct_change() for monthly returns, then use stocks.rolling(36).corr(bonds) in pandas. The script in this article does that for SPY and VUSTX.</p>

<script type="application/ld+json">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What is the stock bond correlation right now?", "acceptedAnswer": {"@type": "Answer", "text": "Using monthly SPY and long-term Treasury (VUSTX) returns, the 36-month correlation is +0.42 as of October 2026. It peaked at +0.68 in December 2024 and has been positive since 2022."}}, {"@type": "Question", "name": "Has the stock bond correlation always been negative?", "acceptedAnswer": {"@type": "Answer", "text": "No. It was +0.25 from 1993 to 1999 and −0.29 from 2000 to 2021. AQR notes it was more often positive than negative from 1900 to 2000, so the negative era was the exception."}}, {"@type": "Question", "name": "Why did stocks and bonds fall together in 2022?", "acceptedAnswer": {"@type": "Answer", "text": "Inflation. Rising inflation and interest rates hurt both. SPY lost 18.2% and long Treasuries lost 29.6%. Windows with average CPI of 4% or more had a mean correlation of +0.52."}}, {"@type": "Question", "name": "Do bonds still diversify a stock portfolio?", "acceptedAnswer": {"@type": "Answer", "text": "Less than before. A 60/40 mix had 8.8% volatility versus 15.0% for stocks in 2000–2021, but 13.3% versus 15.6% since 2022. About 3.9 points of that rise came from the correlation turning positive."}}, {"@type": "Question", "name": "How do I calculate stock bond correlation in Python?", "acceptedAnswer": {"@type": "Answer", "text": "Resample daily adjusted closes to month-end, take pct_change() for monthly returns, then use stocks.rolling(36).corr(bonds) in pandas. The script in this article does that for SPY and VUSTX."}}]}</script>


<h2>The takeaway</h2>
<p>Stock bond correlation over time isn&#8217;t a constant you can set and forget. It was +0.25 in the 1990s, −0.29 from 2000 to 2021, and +0.56 since 2022, and it tracks inflation closely. Today&#8217;s +0.42 means a 60/40 portfolio is behaving more like 85% of a stock portfolio than the 59% many investors got used to. The script above lets you check where the regime stands in seconds.</p>
<p>If you&#8217;ve built up investing knowledge like this and want to share it as a guide, course companion, or newsletter lead magnet, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=stock-bond-correlation-over-time&amp;utm_content=end">ImagineYourBook</a> can turn your notes into a full book draft. Plans start with a 7-day free trial, and the Author plan is $24.99 a month after that.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked November 10, 2026):</strong> <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance: SPY history</a> · <a href="https://finance.yahoo.com/quote/VUSTX/history/">Yahoo Finance: VUSTX history</a> · <a href="https://finance.yahoo.com/quote/VBMFX/history/">Yahoo Finance: VBMFX history</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://fred.stlouisfed.org/series/CPIAUCSL">FRED: CPIAUCSL consumer price index</a> · <a href="https://www.aqr.com/Insights/Research/Journal-Article/A-Changing-Stock-Bond-Correlation">AQR: A Changing Stock-Bond Correlation</a> · <a href="https://www.aqr.com/Insights/Perspectives/A-Positive-Stock-Bond-Correlation-Is-a-Terrible-Reason-to-Add-More-Equity-Risk-to-Your-Portfolio">AQR: A positive stock-bond correlation is a terrible reason to add equity risk</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: sbcorr.py (shown above). Past correlations don&#8217;t predict future ones. Not investment advice.</p>]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/stock-bond-correlation-over-time/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Sequence of Returns Risk Example: Same 25 SPY Years, Reversed, and $1M Ends at $119,235 vs $3.77M (Python)</title>
		<link>https://blog.finxter.com/sequence-of-returns-risk-example/</link>
					<comments>https://blog.finxter.com/sequence-of-returns-risk-example/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 12:55:53 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/sequence-of-returns-risk-example/</guid>

					<description><![CDATA[Sequence of returns risk example: same 25 SPY years, $1M, 4% rule. Real order ends at $119,235, reversed at $3.77M. Charts, fix + Python.]]></description>
										<content:encoded><![CDATA[<p>Here is a sequence of returns risk example with real numbers: take SPY&#8217;s 25 calendar-year total returns from 2000 to 2024, retire with $1,000,000, and withdraw 4% a year with inflation raises. Retire into those years in their real order and you end with <strong>$119,235</strong>. Run the exact same 25 returns backwards and you end with <strong>$3,773,727</strong>.</p>
<p>Same average. Same compound return (7.61% a year). Same $1,342,287 withdrawn. A $3.65 million difference that comes only from the <em>order</em> of the years.</p>
<p>That&#8217;s sequence of returns risk: when money is flowing out, losses that hit early do far more damage than the same losses late. Below you&#8217;ll see the data, three charts, an animation, a test across 13 different retirement start years, one cheap fix, and the short Python script that produced every number.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0"><p><strong>TL;DR</strong></p><ul><li>Same 25 SPY years (2000–2024, 7.61%/yr), $1M, 4% rule: real order ends at <strong>$119,235</strong>, reversed order at <strong>$3,773,727</strong>.</li><li>Both withdrew the same $1,342,287. With no withdrawals, both end at $6,260,930: order only matters when money flows out.</li><li>Retiring in 1999 (5.51% CAGR) beat retiring in 2000 (5.97% CAGR): $647,015 vs $318,300 after 20 years.</li><li>Skipping the inflation raise after each of six losing years lifted the 2000 retiree&#8217;s ending balance to <strong>$677,335</strong>.</li><li>Short Python script included, built on Yahoo Finance and FRED data.</li></ul></div>
<p>If you build tools or write about money, this kind of worked example is also great raw material for a guide. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=sequence-of-returns-risk-example&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns your notes into a complete, editable draft (a 10,000-word book on the free trial) with Word and EPUB export, useful if you want to turn your retirement-planning knowledge into a client guide or lead-magnet ebook.</p>
<h2>The setup: one data set, two orders</h2>
<p>The data is SPY&#8217;s adjusted close from Yahoo Finance (dividends reinvested), turned into calendar-year returns, plus December-to-December CPI inflation from FRED. The assumptions are deliberately simple, and they&#8217;re assumptions, not advice:</p>
<ul>
<li>$1,000,000 starting balance, 100% in SPY.</li>
<li>4% rule: withdraw $40,000 at the start of year one, then raise the withdrawal by last year&#8217;s CPI inflation every year.</li>
<li>No fees, no taxes, no rebalancing.</li>
</ul>
<p>The forward retiree retires in January 2000. Their first three years are −9.7%, −11.8% and −21.6%, the dot-com bust. The reversed retiree gets 2024 first, so their first three years are +24.9%, +26.2% and −18.2%.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="700" src="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-return-order-bars.png" alt="Two bar charts of SPY&#x27;s calendar-year returns 2000-2024: real order starting -9.7%, -11.8%, -21.6%, and reversed order starting +24.9%, +26.2%, -18.2%; both compound at 7.61% a year" class="wp-image-1672575" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-return-order-bars.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-return-order-bars-300x175.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-return-order-bars-1024x597.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-return-order-bars-768x448.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Identical returns, two orders. The shaded first three years decide most of the outcome (seqrisk.py, Yahoo Finance data).</figcaption></figure>
<p>Without withdrawals, the order doesn&#8217;t matter at all. $1,000,000 grows to exactly $6,260,930 either way, because multiplication doesn&#8217;t care about order. The withdrawals are what break the symmetry.</p>
<h2>The result: $119,235 vs $3,773,727</h2>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-sequence-race.gif" alt="Animated chart drawing both retirees&#x27; balances year by year: after 3 years $1.16M vs $0.54M, after 25 years $3.77M vs $0.12M" class="wp-image-1672576"/><figcaption class="wp-element-caption">Year by year: best years first (teal) vs the real 2000 order (coral). Same returns, same withdrawals.</figcaption></figure>
<p>After just three years, the forward retiree is down to $537,830 while the reversed retiree has $1,160,626. From there, the forward retiree is pulling a rising, inflation-adjusted check (the 25th withdrawal is $72,709) out of a balance that never gets back to $1 million. The reversed retiree&#8217;s portfolio peaks above $6.2 million before the bad years finally show up, and by then there&#8217;s plenty of cushion to absorb them.</p>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th></th><th>Retire Jan 2000 (real order)</th><th>Same returns reversed</th></tr></thead><tbody><tr><td>First three years</td><td>−9.7%, −11.8%, −21.6%</td><td>+24.9%, +26.2%, −18.2%</td></tr><tr><td>Balance after 3 years</td><td>$537,830</td><td>$1,160,626</td></tr><tr><td>Total withdrawn (25 yrs)</td><td>$1,342,287</td><td>$1,342,287</td></tr><tr><td>Balance after 25 years</td><td><strong>$119,235</strong></td><td><strong>$3,773,727</strong></td></tr><tr><td>Without withdrawals</td><td>$6,260,930</td><td>$6,260,930</td></tr></tbody></table><figcaption class="wp-element-caption">$1,000,000 in SPY, $40,000 first withdrawal raised by CPI. Yahoo Finance SPY total return 2000–2024, FRED CPIAUCSL. seqrisk.py</figcaption></figure>
<p>Both retirees took out exactly the same $1,342,287. Neither ran out of money within 25 years. But one ends with a balance that covers less than two years of spending and the other ends richer than they started.</p>
<p>Morningstar&#8217;s retirement researchers describe the same effect in their State of Retirement Income work: in their simulations, nearly 70% of the retirement &#8220;failures&#8221; were trials where the portfolio had lost value by the end of year five. Our 2000 retiree lost 46% of their starting balance in the first three years.</p>
<h2>It isn&#8217;t the average return. It&#8217;s the first few years</h2>
<p>The cleanest way to prove that sequence risk isn&#8217;t just &#8220;bad returns&#8221; is to compare real retirement start years. I ran the same 4% rule over every 20-year window the SPY data allows, from a 1994 start to a 2006 start (each ending by 2025).</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-start-year-balances.png" alt="Bar chart of balances after 20 years for retirement start years 1994 to 2006 with $1M in SPY and the 4% rule; 1999 ends at $0.65M and 2000 at $0.32M despite 2000&#x27;s higher 20-year CAGR of 5.97% vs 5.51%" class="wp-image-1672577" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-start-year-balances.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-start-year-balances-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-start-year-balances-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-start-year-balances-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Lower average, more money: 1999 beat 2000 because its first three years were −4.1% combined, not −37.5%.</figcaption></figure>
<p>Look at 1999 and 2000. A retiree starting in 1999 earned a <em>lower</em> 20-year compound return (5.51%) than one starting in 2000 (5.97%). Yet the 1999 retiree ended with $647,015 and the 2000 retiree with $318,300, about half as much.</p>
<p>Why? The 1999 retiree banked SPY&#8217;s strong 1999 before the crash, so their first three years combined were only −4.1%. The 2000 retiree&#8217;s first three years combined were −37.5%. Higher average return, worse order, half the money.</p>
<p>The rest of the chart tells the same story. The 2006 retiree hit the 2008 crash in year three (first three years −23.0%), but the 20-year CAGR of 10.91% was strong enough to end at $3,643,183. Bad years early hurt; very bad years early, with only a middling recovery, is where plans break.</p>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>Start year</th><th>First 3 years combined</th><th>20-year CAGR</th><th>Balance after 20 years</th></tr></thead><tbody><tr><td>1994</td><td>+69.8%</td><td>9.10%</td><td>$3,357,508</td></tr><tr><td>1995</td><td>+125.7%</td><td>9.77%</td><td>$4,037,107</td></tr><tr><td>1996</td><td>+110.4%</td><td>8.08%</td><td>$2,540,089</td></tr><tr><td>1997</td><td>+106.8%</td><td>7.60%</td><td>$2,070,276</td></tr><tr><td>1998</td><td>+39.8%</td><td>7.10%</td><td>$1,420,040</td></tr><tr><td><strong>1999</strong></td><td>-4.1%</td><td>5.51%</td><td><strong>$647,015</strong></td></tr><tr><td><strong>2000</strong></td><td>-37.5%</td><td>5.97%</td><td><strong>$318,300</strong></td></tr><tr><td>2001</td><td>-11.3%</td><td>7.41%</td><td>$948,356</td></tr><tr><td>2002</td><td>+11.3%</td><td>9.46%</td><td>$2,207,638</td></tr><tr><td>2003</td><td>+48.7%</td><td>9.69%</td><td>$3,377,857</td></tr><tr><td>2004</td><td>+34.4%</td><td>9.61%</td><td>$2,823,689</td></tr><tr><td>2005</td><td>+27.7%</td><td>10.27%</td><td>$3,082,371</td></tr><tr><td>2006</td><td>-23.0%</td><td>10.91%</td><td>$3,643,183</td></tr></tbody></table><figcaption class="wp-element-caption">$1M in SPY, 4% rule with CPI raises, 20-year windows. Yahoo Finance SPY, FRED CPI. seqrisk.py</figcaption></figure>
<h2>A cheap fix: skip the raise after losing years</h2>
<p>Charles Schwab&#8217;s guidance on sequence risk lists two practical defenses: keep a cash and short-term bond reserve for the next few years of spending (they suggest about one year in cash and two to four years in short-term bonds), and, if you didn&#8217;t prepare, spend less after bad years, for example by giving up inflation adjustments.</p>
<p>I tested the second idea on the unlucky 2000 retiree. The only change: in the year after a losing year, don&#8217;t raise the withdrawal for inflation. In 2000 to 2024 that happened six times (after 2000, 2001, 2002, 2008, 2018 and 2022).</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-freeze-fix.png" alt="Line chart for a January 2000 retiree: the standard 4% rule ends at $119,235 while skipping the inflation raise after each losing year ends at $677,335" class="wp-image-1672578" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-freeze-fix.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-freeze-fix-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-freeze-fix-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/sequence-of-returns-risk-example-freeze-fix-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Six skipped raises, $142,773 less spending, $558,100 more left at the end.</figcaption></figure>
<p>That small rule cut total withdrawals from $1,342,287 to $1,199,514 (about $142,773 less over 25 years) and lifted the ending balance from $119,235 to <strong>$677,335</strong>, a $558,100 improvement. Each skipped raise stays skipped, so spending power drops for good; the trade is a slightly smaller check for a much bigger safety margin.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0"><p><strong>The rule to remember:</strong> sequence risk only exists when money moves. With no withdrawals, both orders end at the same $6,260,930. It&#8217;s the first few years of <em>withdrawals</em> during a crash that sell shares at low prices, so those years deserve the most protection.</p></div>
<h2>The Python script</h2>
<p>This is the full script behind every number above. It needs <code>pandas</code>, a SPY price file from <code>yfinance</code> (<code>yf.download("SPY", start="1993-01-01", auto_adjust=True)</code> saved as a pickle) and FRED&#8217;s <code>CPIAUCSL</code> CSV.</p>
<pre><code class="language-python">"""Sequence of returns risk example: same 25 SPY years, two orders.
Data: Yahoo Finance SPY adjusted close (yfinance), FRED CPIAUCSL. Assumptions: 100% SPY,
$1,000,000 start, 4% rule ($40,000 in year 1, raised by Dec-to-Dec CPI), withdrawal at start of year, no fees/taxes."""
import pandas as pd

def annual_returns(pkl="src/spy.pkl"):
    px = pd.read_pickle(pkl)["Close"].squeeze()
    ye = px.groupby(px.index.year).last()          # year-end adjusted close
    return ye.pct_change().dropna()                 # calendar-year total return

def cpi_inflation(csv="src/CPIAUCSL.csv"):
    c = pd.read_csv(csv, index_col=0, parse_dates=True).squeeze()
    dec = c[c.index.month == 12]; dec.index = dec.index.year
    return dec.pct_change().dropna()

def simulate(rets, infl, start=1_000_000, rate=0.04, freeze=False):
    """freeze=True: skip the inflation raise in the year after a losing year (a Schwab-style spending fix)."""
    bal, w, path, total = start, start * rate, [start], 0.0
    for r, i in zip(rets, infl):
        take = min(w, bal); bal = (bal - take) * (1 + r); total += take
        path.append(bal); w *= 1 if (freeze and r &lt; 0) else 1 + i
    return path, total

if __name__ == "__main__":
    R, I = annual_returns(), cpi_inflation()
    yrs = list(range(2000, 2025)); r = R.loc[yrs].values; inf = I.loc[[y - 1 for y in yrs]].values  # year-1 raise uses prior Dec-Dec CPI
    cagr = ((1 + R.loc[yrs]).prod()) ** (1 / 25) - 1
    print(f"SPY 2000-2024: CAGR {cagr:.2%}, first 3 years {r[0]:+.1%} {r[1]:+.1%} {r[2]:+.1%}, last 3 {r[-3]:+.1%} {r[-2]:+.1%} {r[-1]:+.1%}")
    nw_f, _ = simulate(r, inf, rate=0); nw_b, _ = simulate(r[::-1], inf, rate=0)
    print(f"no withdrawals: forward ${nw_f[-1]:,.0f}  reversed ${nw_b[-1]:,.0f}")
    f, tf = simulate(r, inf); b, tb = simulate(r[::-1], inf)
    print(f"4% rule forward (retire Jan 2000): end ${f[-1]:,.0f}, withdrawn ${tf:,.0f}, low ${min(f):,.0f}")
    print(f"4% rule reversed (best years first): end ${b[-1]:,.0f}, withdrawn ${tb:,.0f}")
    fz, tz = simulate(r, inf, freeze=True)
    print(f"forward + freeze raise after down years: end ${fz[-1]:,.0f}, withdrawn ${tz:,.0f}")
    print(f"after 3 years: forward ${f[3]:,.0f}  reversed ${b[3]:,.0f}")
    print(f"year-25 withdrawal ${40000*(1+inf[:-1]).prod():,.0f}")
    print("start-year table (20-year windows, ends 2025):")
    for s in range(1994, 2007):
        ys = list(range(s, s + 20)); p, t = simulate(R.loc[ys].values, I.loc[[y - 1 for y in ys]].values)
        c = (1 + R.loc[ys]).prod() ** (1 / 20) - 1
        print(f"  {s}: CAGR {c:.2%}  end ${p[-1]:,.0f}  first3 {(1+R.loc[ys[:3]]).prod()-1:+.1%}")
</code></pre>
<p>Output:</p>
<pre><code class="language-text">SPY 2000-2024: CAGR 7.61%, first 3 years -9.7% -11.8% -21.6%, last 3 -18.2% +26.2% +24.9%
no withdrawals: forward $6,260,930  reversed $6,260,930
4% rule forward (retire Jan 2000): end $119,235, withdrawn $1,342,287, low $119,235
4% rule reversed (best years first): end $3,773,727, withdrawn $1,342,287
forward + freeze raise after down years: end $677,335, withdrawn $1,199,514
after 3 years: forward $537,830  reversed $1,160,626
year-25 withdrawal $72,709
start-year table (20-year windows, ends 2025):
  1994: CAGR 9.10%  end $3,357,508  first3 +69.8%
  1995: CAGR 9.77%  end $4,037,107  first3 +125.7%
  1996: CAGR 8.08%  end $2,540,089  first3 +110.4%
  1997: CAGR 7.60%  end $2,070,276  first3 +106.8%
  1998: CAGR 7.10%  end $1,420,040  first3 +39.8%
  1999: CAGR 5.51%  end $647,015  first3 -4.1%
  2000: CAGR 5.97%  end $318,300  first3 -37.5%
  2001: CAGR 7.41%  end $948,356  first3 -11.3%
  2002: CAGR 9.46%  end $2,207,638  first3 +11.3%
  2003: CAGR 9.69%  end $3,377,857  first3 +48.7%
  2004: CAGR 9.61%  end $2,823,689  first3 +34.4%
  2005: CAGR 10.27%  end $3,082,371  first3 +27.7%
  2006: CAGR 10.91%  end $3,643,183  first3 -23.0%
</code></pre>
<p>To try your own numbers, change <code>start</code>, <code>rate</code>, or the year list, or pass <code>freeze=True</code>. If you want to see how your actual holdings have done against the S&amp;P 500 without uploading them anywhere, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free tracker that runs in your browser, needs no account or broker connection, and keeps Excel backups.</p>
<h2>What this example doesn&#8217;t show</h2>
<ul>
<li><strong>It&#8217;s one asset.</strong> 100% SPY is the case Morningstar flags as most exposed to sequence risk. A stock-bond mix falls less in a crash; see Finxter&#8217;s <a href="https://blog.finxter.com/60-40-portfolio-max-drawdown/">60/40 portfolio max drawdown</a> analysis for how much less.</li>
<li><strong>History isn&#8217;t a forecast.</strong> The reversed order is a thought experiment, not a scenario anyone lived.</li>
<li><strong>Real life has taxes, fees, and flexible spending.</strong> Most retirees cut back after a crash anyway, which is exactly why the freeze test matters.</li>
</ul>
<p>For the flip side of the same coin, read <a href="https://blog.finxter.com/missing-the-best-days-in-the-market/">missing the best days in the market</a> (the biggest up-days come right after the worst ones) and <a href="https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/">how long it takes the stock market to recover from a crash</a>. If you&#8217;re still in the saving phase, <a href="https://blog.finxter.com/lump-sum-vs-dca-sp-500/">lump sum vs DCA</a> covers the order-of-returns question for money going in.</p>
<h2>FAQ</h2>
<h3>What is a simple example of sequence of returns risk?</h3><p>Take SPY&#x27;s 2000–2024 returns, $1M and a 4% inflation-adjusted withdrawal. In real order (crash first) the portfolio ends at $119,235. Reverse the same 25 returns and it ends at $3,773,727, even though both withdrew $1,342,287.</p><h3>Does sequence of returns risk matter if I&#x27;m not withdrawing?</h3><p>No. With no withdrawals, the same returns in any order give the same result: $1M grows to $6,260,930 either way in this example. Sequence risk appears when money flows in or out.</p><h3>How many years of retirement does sequence risk affect most?</h3><p>The first few. Morningstar found nearly 70% of failed retirement simulations had lost value by the end of year five. In this example the 2000 retiree was down 46% after three years.</p><h3>Is a higher average return always safer?</h3><p>No. Retiring in 1999 earned a lower 20-year CAGR (5.51%) than retiring in 2000 (5.97%), yet ended with $647,015 versus $318,300, because its first three years were −4.1% combined instead of −37.5%.</p><h3>How can I reduce sequence of returns risk?</h3><p>Schwab suggests holding about a year of spending in cash and two to four years in short-term bonds, and spending less after bad years. Skipping the inflation raise after losing years lifted our 2000 retiree&#x27;s ending balance from $119,235 to $677,335.</p>

<script type="application/ld+json">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What is a simple example of sequence of returns risk?", "acceptedAnswer": {"@type": "Answer", "text": "Take SPY's 2000–2024 returns, $1M and a 4% inflation-adjusted withdrawal. In real order (crash first) the portfolio ends at $119,235. Reverse the same 25 returns and it ends at $3,773,727, even though both withdrew $1,342,287."}}, {"@type": "Question", "name": "Does sequence of returns risk matter if I'm not withdrawing?", "acceptedAnswer": {"@type": "Answer", "text": "No. With no withdrawals, the same returns in any order give the same result: $1M grows to $6,260,930 either way in this example. Sequence risk appears when money flows in or out."}}, {"@type": "Question", "name": "How many years of retirement does sequence risk affect most?", "acceptedAnswer": {"@type": "Answer", "text": "The first few. Morningstar found nearly 70% of failed retirement simulations had lost value by the end of year five. In this example the 2000 retiree was down 46% after three years."}}, {"@type": "Question", "name": "Is a higher average return always safer?", "acceptedAnswer": {"@type": "Answer", "text": "No. Retiring in 1999 earned a lower 20-year CAGR (5.51%) than retiring in 2000 (5.97%), yet ended with $647,015 versus $318,300, because its first three years were −4.1% combined instead of −37.5%."}}, {"@type": "Question", "name": "How can I reduce sequence of returns risk?", "acceptedAnswer": {"@type": "Answer", "text": "Schwab suggests holding about a year of spending in cash and two to four years in short-term bonds, and spending less after bad years. Skipping the inflation raise after losing years lifted our 2000 retiree's ending balance from $119,235 to $677,335."}}]}</script>


<h2>The takeaway</h2>
<p>Sequence of returns risk turns the same 7.61% a year into either $119,235 or $3.77 million, depending only on which years come first. You can&#8217;t choose your first five years of retirement, but you can plan for them: hold a few years of spending outside stocks, and be ready to skip a raise after a losing year.</p>
<p>And if you&#8217;ve built a model like this, consider writing it up. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=sequence-of-returns-risk-example&amp;utm_content=end">ImagineYourBook</a> can turn your notes into a full draft of a retirement-planning guide or lead-magnet ebook that you then edit and publish. The free trial includes one 10,000-word book; a card is required, and the $24.99 Author plan starts after 7 days unless you cancel before day 8.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked November 9, 2026):</strong> <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance: SPY history</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://fred.stlouisfed.org/series/CPIAUCSL">FRED: CPIAUCSL consumer price index</a> · <a href="https://www.morningstar.com/retirement/biggest-risk-new-retirees">Morningstar: The biggest risk for new retirees</a> · <a href="https://www.schwab.com/learn/story/timing-matters-understanding-sequence-returns-risk">Charles Schwab: What is sequence-of-returns risk?</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: seqrisk.py (shown above). Not investment advice.</p>]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/sequence-of-returns-risk-example/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Missing the Best Days in the Market: Skip SPY&#8217;s 10 Best Days Since 1993 and $313,613 Becomes $136,082 (Python)</title>
		<link>https://blog.finxter.com/missing-the-best-days-in-the-market/</link>
					<comments>https://blog.finxter.com/missing-the-best-days-in-the-market/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 08:58:51 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/missing-the-best-days-in-the-market/</guid>

					<description><![CDATA[Missing the best days in the market, tested: skip SPY's 10 best days since 1993 and $10k ends at $136,082, not $313,613 (−57%). Charts + Python.]]></description>
										<content:encoded><![CDATA[<p><strong>Short answer:</strong> missing the best days in the market is brutally expensive. $10,000 put into SPY at its first close on January 29, 1993 grew to <strong>$313,613</strong> by November 3, 2026 with dividends reinvested. Sit in cash on just the <strong>10 best days</strong> out of 8,498 trading days, and the same money ends at <strong>$136,082</strong>. That is <strong>57% less</strong>, from 0.12% of the days.</p>
<p>The catch most charts leave out: those 10 best days were not random. Eight of them came within 10 trading days of one of the 20 worst days, and nine came while SPY was more than 20% below its peak. You can&#8217;t skip the crashes and keep the rebounds, because they happen in the same weeks. I ran the numbers on real Yahoo Finance data with a short Python script you can copy below.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0"><p><strong>TL;DR</strong></p><ul><li>$10,000 in SPY from Jan 29, 1993: <strong>$313,613</strong> invested every day vs <strong>$136,082</strong> after missing the 10 best days (−57%).</li><li>Missing 5 days cost 40%, 20 days 75%, 50 days 93%. In the last 20 years, missing 50 days turned $10,000 into $6,917.</li><li><strong>8 of the 10 best days</strong> came within 10 trading days of a top-20 worst day; 9 of 10 came in a 20%+ bear market.</li><li>J.P. Morgan&#8217;s 2006–2025 S&amp;P 500 version agrees: $80,619 vs $35,866.</li><li>Skipping both the 10 best and 10 worst days gave $326,292, barely more than holding. Python script included.</li></ul></div>
<p>If you&#8217;re the friend who always ends up explaining &#8220;time in the market&#8221; to your family or your investing club, this kind of worked example is exactly what makes a good beginner&#8217;s investing guide or newsletter lead magnet. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=missing-the-best-days-in-the-market&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns a prompt or your own notes into a complete ~10,000-word draft with a designed cover, exported to Word and EPUB, so your charts and rules of thumb can become a short finance ebook you then edit and fact-check.</p>
<h2>What &#8220;missing the best days&#8221; actually means</h2>
<p>The test is simple. Take every trading day of an index fund. Rank the daily returns. Then replay history twice: once fully invested, and once sitting in cash on the N biggest up-days. Everything else stays identical.</p>
<p>My setup, so you can check it:</p>
<ul>
<li><strong>Data:</strong> SPY daily adjusted closes from <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance</a> via the open-source <a href="https://github.com/ranaroussi/yfinance">yfinance</a> library, January 29, 1993 (SPY&#8217;s first trading day) to November 3, 2026. The adjusted close includes reinvested dividends, so this is total return.</li>
<li><strong>Cash on missed days earns 0%.</strong> This is an assumption, and it&#8217;s the same one J.P. Morgan uses. It slightly overstates the damage when T-bills pay 4–5%, but a day of T-bill interest is about 0.02%, nothing next to a +10% day.</li>
<li><strong>No fees, taxes or trading costs.</strong> Real market timers would also pay spreads and, in taxable accounts, capital gains tax on every exit.</li>
</ul>
<h2>The cost of missing the best days, in dollars</h2>
<p>Here is the full table. The &#8220;last 20 years&#8221; rows start on November 3, 2006, for comparison with the famous J.P. Morgan chart.</p>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>Scenario ($10,000 in SPY)</th><th>Since Jan 1993</th><th>CAGR</th><th>vs every day</th><th>Last 20 years</th><th>CAGR</th><th>vs every day</th></tr></thead><tbody><tr><td>Invested every day</td><td>$313,613</td><td>10.74%</td><td>—</td><td>$79,634</td><td>10.93%</td><td>—</td></tr><tr><td>Miss the 5 best days</td><td>$187,437</td><td>9.07%</td><td>-40%</td><td>$47,595</td><td>8.11%</td><td>-40%</td></tr><tr><td>Miss the 10 best days</td><td><strong>$136,082</strong></td><td>8.04%</td><td>-57%</td><td>$34,554</td><td>6.40%</td><td>-57%</td></tr><tr><td>Miss the 20 best days</td><td>$78,634</td><td>6.30%</td><td>-75%</td><td>$20,784</td><td>3.73%</td><td>-74%</td></tr><tr><td>Miss the 30 best days</td><td>$49,189</td><td>4.83%</td><td>-84%</td><td>$13,803</td><td>1.62%</td><td>-83%</td></tr><tr><td>Miss the 50 best days</td><td>$22,024</td><td>2.37%</td><td>-93%</td><td>$6,917</td><td>-1.83%</td><td>-91%</td></tr></tbody></table><figcaption class="wp-element-caption">Yahoo Finance SPY adjusted close (total return), Jan 29, 1993 / Nov 3, 2006 to Nov 3, 2026. Cash earns 0% on missed days; no fees or taxes. bestdays.py</figcaption></figure>
<p>Missing five days costs 40%. Missing 10 costs 57%. Miss 50 of the best days in the last 20 years and $10,000 turns into $6,917: you would have <em>lost money</em> in a period when the market compounded at 10.93% a year.</p>
<p>The 10-day line is the same −57% in both windows because all 10 of the best days since 1993 fell after 2006: six in the 2008–2009 crash, three in the 2020 COVID crash, one in April 2025.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-growth-paths.png" alt="Log-scale line chart of $10,000 in SPY since 1993: invested every day $313,613, missing the 10 best days $136,082, missing 20 $78,634, missing 30 $49,189; the lines split in October 2008" class="wp-image-1672536" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-growth-paths.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-growth-paths-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-growth-paths-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-growth-paths-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The lines move together for 15 years, then split in the 2008 crash: the 10 best days were worth $177,531 (bestdays.py, Yahoo Finance data).</figcaption></figure>
<p>Note how all four lines move together for 15 years. Then October 2008 hits, and the gap opens in a few weeks and never closes. That is the whole story of missing the best days: years of nothing, then a handful of sessions that decide the result.</p>
<p>My numbers line up with the most-cited version of this chart. <a href="https://cdn.jpmorganfunds.com/content/dam/jpm-am-aem/global/en/insights/retirement-insights/guide-to-retirement-us.pdf">J.P. Morgan&#8217;s Guide to Retirement 2026</a> uses the S&amp;P 500 Total Return Index from January 2, 2006 to December 31, 2025: $10,000 became $80,619 (11.0% a year) fully invested, and $35,866 (6.6%) after missing the 10 best days. My SPY window, November 2006 to November 2026, gives $79,634 (10.93%) and $34,554 (6.40%). SPY&#8217;s ~0.09% expense ratio and the slightly different dates explain the small gap.</p>
<p>The animation below removes the best days one at a time. Watch how quickly the ending value collapses:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-missing-best-days.gif" alt="Animated chart removing SPY&#x27;s best days one at a time: $10,000 since 1993 falls from $313,613 invested every day to $136,082 missing 10, $78,634 missing 20, $49,189 missing 30 and $22,024 missing 50 best days" class="wp-image-1672537"/><figcaption class="wp-element-caption">Each frame removes one more best day. The first 10 cost $177,531.</figcaption></figure>
<h2>Why you can&#8217;t dodge the bad days and keep the good ones</h2>
<p>The &#8220;missing the best days&#8221; chart is often used to say &#8220;never sell.&#8221; The data supports that, but for a more interesting reason than the chart shows. Look at <em>when</em> the best days happened:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-best-worst-clusters.png" alt="Scatter of every SPY daily return since 1993 with the 10 best days in teal and 20 worst days in coral; the best days cluster with the worst in late 2008, March-April 2020 and April 2025" class="wp-image-1672538" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-best-worst-clusters.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-best-worst-clusters-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-best-worst-clusters-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/missing-the-best-days-in-the-market-best-worst-clusters-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Six of the 10 best days came in the 2008-09 crash, three in the 2020 crash and one in April 2025, right beside the worst days.</figcaption></figure>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>SPY&#8217;s 10 best days</th><th>Return</th><th>SPY vs prior peak</th><th>Nearest top-20 worst day</th><th>Worst day came</th></tr></thead><tbody><tr><td>Oct 13, 2008</td><td><strong>+14.5%</strong></td><td>-34%</td><td>Oct 09, 2008 (-7.0%)</td><td>2 trading days before</td></tr><tr><td>Oct 28, 2008</td><td><strong>+11.7%</strong></td><td>-39%</td><td>Oct 22, 2008 (-5.4%)</td><td>4 trading days before</td></tr><tr><td>Apr 09, 2025</td><td><strong>+10.5%</strong></td><td>-10%</td><td>Apr 04, 2025 (-5.9%)</td><td>3 trading days before</td></tr><tr><td>Mar 24, 2020</td><td><strong>+9.1%</strong></td><td>-28%</td><td>Mar 16, 2020 (-10.9%)</td><td>6 trading days before</td></tr><tr><td>Mar 13, 2020</td><td><strong>+8.5%</strong></td><td>-20%</td><td>Mar 12, 2020 (-9.6%)</td><td>1 trading days before</td></tr><tr><td>Mar 23, 2009</td><td><strong>+7.2%</strong></td><td>-46%</td><td>Jan 20, 2009 (-5.3%)</td><td>43 trading days before</td></tr><tr><td>Nov 24, 2008</td><td><strong>+6.9%</strong></td><td>-45%</td><td>Nov 20, 2008 (-7.4%)</td><td>2 trading days before</td></tr><tr><td>Apr 06, 2020</td><td><strong>+6.7%</strong></td><td>-21%</td><td>Mar 16, 2020 (-10.9%)</td><td>15 trading days before</td></tr><tr><td>Nov 13, 2008</td><td><strong>+6.2%</strong></td><td>-41%</td><td>Nov 19, 2008 (-6.4%)</td><td>4 trading days after</td></tr><tr><td>Oct 20, 2008</td><td><strong>+6.0%</strong></td><td>-36%</td><td>Oct 22, 2008 (-5.4%)</td><td>2 trading days after</td></tr></tbody></table><figcaption class="wp-element-caption">bestdays.py on Yahoo Finance SPY adjusted close, Jan 1993 to Nov 3, 2026. Ties go to the earlier worst day.</figcaption></figure>
<p>Three numbers from the script tell the story:</p>
<ul>
<li><strong>8 of the 10 best days</strong> came within 10 trading days of one of the 20 worst days. October 13, 2008 (+14.5%) came two sessions after the October 9 drop of −7.0%. March 13, 2020 (+8.5%) came one session after March 12 (−9.6%).</li>
<li><strong>9 of the 10 best days</strong> came while SPY was more than 20% below its previous high. Only 17% of all trading days since 1993 were spent that deep in a drawdown.</li>
<li>The one exception, <strong>April 9, 2025 (+10.5%)</strong>, came three days after SPY&#8217;s −5.9% day on April 4, 2025, while SPY was about 10% below its peak.</li>
</ul>
<p>So the best days are bear-market days. The person who sold after a −7% day, &#8220;to wait until things calm down&#8221;, is precisely the person who misses the +14.5% day two sessions later.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0"><p><strong>The one-sided chart problem:</strong> &#8220;missing the best days&#8221; charts never show that missing the worst days would have <em>helped</em> even more ($751,970 here). The real lesson isn&#8217;t that the best days are magic. It&#8217;s that nobody gets to skip the worst days without also skipping the best ones that follow.</p></div>
<p>The fair version of the experiment cuts both ways. Missing the 10 <em>worst</em> days would have turned $10,000 into $751,970. Missing both the 10 best and the 10 worst ends at $326,292, almost exactly the $313,613 of simply staying invested. In other words, someone who somehow dodged both tails would end up only about 4% ahead after 33.8 years. A real one, who sells after the crash and buys back after the rebound, is in the $136,082 row.</p>
<p>If you&#8217;ve already read <a href="https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/">how long the stock market takes to recover from a crash</a>, this is the other half of that answer: the recovery starts with these violent up-days, often while the headlines are still bad.</p>
<h2>The Python script (copy, run, change the ticker)</h2>
<p>The whole analysis fits in about 40 lines of pandas. Install <code>yfinance</code> and <code>pandas</code>, then run it. If you&#8217;re new to Python, Finxter&#8217;s <a href="https://blog.finxter.com/python-crash-course/">Python crash course</a> covers everything you need for this script.</p>
<pre><code class="language-python"># bestdays.py - what does missing the best days in the market cost? (pip install yfinance pandas)
import pandas as pd, yfinance as yf

START, END, CASH = "1993-01-29", "2026-11-04", 10_000   # SPY's first close -&gt; last close (Nov 3, 2026)

def load(ticker="SPY", start=START):
    px = yf.download(ticker, start=start, end=END, auto_adjust=True, progress=False)["Close"].squeeze()
    return px.pct_change().dropna()                     # daily total returns (dividends included)

def grow(r, skip=()):
    """$10k path; on skipped days you sit in cash earning 0% (assumption)."""
    path = CASH * (1 + r.where(~r.index.isin(skip), 0.0)).cumprod()
    start = r.index[0] - pd.tseries.offsets.BDay(1)     # day 0 = $10k at the first close
    return pd.concat([pd.Series({start: CASH}), path])

def cagr(path):
    yrs = (path.index[-1] - path.index[0]).days / 365.25
    return (path.iloc[-1] / CASH) ** (1 / yrs) - 1

def report(r, label):
    best, worst = r.nlargest(50).index, r.nsmallest(50).index
    print(f"--- {label}: {len(r):,} trading days to {r.index[-1]:%Y-%m-%d}")
    for n in (0, 5, 10, 20, 30, 50):
        p = grow(r, best[:n])
        print(f"miss best {n:&gt;2}: ${p.iloc[-1]:&gt;9,.0f}  CAGR {cagr(p):.2%}")
    for n in (10, 20):
        p, q = grow(r, worst[:n]), grow(r, best[:n].union(worst[:n]))
        print(f"miss worst {n}: ${p.iloc[-1]:,.0f} ({cagr(p):.2%}) | miss best+worst {n}: ${q.iloc[-1]:,.0f} ({cagr(q):.2%})")
    return best, worst

if __name__ == "__main__":
    r = load()
    best, worst = report(r, "SPY since Jan 29, 1993")
    report(r[r.index &gt; "2006-11-03"], "SPY last 20 years")
    wealth = (1 + r).cumprod(); dd = wealth / wealth.cummax() - 1
    near = [min(abs(r.index.get_loc(b) - r.index.get_loc(w)) for w in worst[:20]) for b in best[:10]]
    print("10 best days:", ", ".join(f"{d:%Y-%m-%d} {r[d]:+.1%}" for d in best[:10]))
    print(f"trading days to nearest top-20 worst day: {near} -&gt; {sum(g &lt;= 10 for g in near)}/10 within 10")
    print(f"best 10 days in a 20%+ drawdown: {(dd[best[:10]] &lt; -0.2).sum()}/10 (all days: {(dd &lt; -0.2).mean():.0%})")
</code></pre>
<p>Output on November 4, 2026 (Yahoo data through the November 3 close):</p>
<pre><code class="language-text">--- SPY since Jan 29, 1993: 8,498 trading days to 2026-11-03
miss best  0: $  313,613  CAGR 10.74%
miss best  5: $  187,437  CAGR 9.07%
miss best 10: $  136,082  CAGR 8.04%
miss best 20: $   78,634  CAGR 6.30%
miss best 30: $   49,189  CAGR 4.83%
miss best 50: $   22,024  CAGR 2.37%
miss worst 10: $751,970 (13.65%) | miss best+worst 10: $326,292 (10.88%)
miss worst 20: $1,350,227 (15.64%) | miss best+worst 20: $338,549 (11.00%)
--- SPY last 20 years: 5,029 trading days to 2026-11-03
miss best  0: $   79,634  CAGR 10.93%
miss best  5: $   47,595  CAGR 8.11%
miss best 10: $   34,554  CAGR 6.40%
miss best 20: $   20,784  CAGR 3.73%
miss best 30: $   13,803  CAGR 1.62%
miss best 50: $    6,917  CAGR -1.83%
miss worst 10: $187,972 (15.80%) | miss best+worst 10: $81,564 (11.06%)
miss worst 20: $324,161 (19.00%) | miss best+worst 20: $84,604 (11.27%)
10 best days: 2008-10-13 +14.5%, 2008-10-28 +11.7%, 2025-04-09 +10.5%, 2020-03-24 +9.1%, 2020-03-13 +8.5%, 2009-03-23 +7.2%, 2008-11-24 +6.9%, 2020-04-06 +6.7%, 2008-11-13 +6.2%, 2008-10-20 +6.0%
trading days to nearest top-20 worst day: [2, 4, 3, 6, 1, 43, 2, 15, 4, 2] -&gt; 8/10 within 10
best 10 days in a 20%+ drawdown: 9/10 (all days: 17%)
</code></pre>
<p>Three easy modifications:</p>
<ol>
<li><strong>Another fund:</strong> <code>load("QQQ")</code> or <code>load("VT")</code>, and check whether its best days sit in the same crash windows.</li>
<li><strong>A cash yield:</strong> replace the <code>0.0</code> in <code>grow()</code> with a daily T-bill rate, e.g. <code>0.04 / 252</code>, to test the 0% assumption.</li>
<li><strong>Your own exits:</strong> pass the dates you were actually out of the market as <code>skip</code> and see what they cost.</li>
</ol>
<p>That last one is the honest test. To see what your real trades did against simply buying the index on the same dates, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free browser tracker (no account, no broker connection, data stays in your browser with Excel backups) that compares your dated purchases and sales to the same cash flows in the S&amp;P 500 and Bitcoin. For the math behind that comparison, see <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">compare my portfolio to the S&amp;P 500</a>, and for the closely related &#8220;wait for a dip&#8221; question, <a href="https://blog.finxter.com/lump-sum-vs-dca-sp-500/">lump sum vs DCA in the S&amp;P 500</a>.</p>
<h2>What to do with this</h2>
<figure class="wp-block-table is-style-stripes"><table><thead><tr><th>Question</th><th>What the SPY data says</th><th>What to do</th></tr></thead><tbody><tr><td>What does missing the best days cost?</td><td>10 days: $313,613 → $136,082 (−57%); 50 days: −93%</td><td>Stay invested through volatile weeks</td></tr><tr><td>When do the best days happen?</td><td>9 of 10 in a 20%+ drawdown; 8 of 10 within 10 days of a top-20 worst day</td><td>Treat a crash week as the worst time to sell</td></tr><tr><td>Can&#8217;t I just skip the worst days too?</td><td>Skipping both best and worst 10: $326,292, only ~4% above holding</td><td>Not worth the risk of mistiming</td></tr><tr><td>Is it a real effect or a 2008 artifact?</td><td>J.P. Morgan 2006–2025: $80,619 vs $35,866 (11.0% vs 6.6%)</td><td>Assume it applies to the next crash too</td></tr><tr><td>What if the swings are too much?</td><td>60/40 fell −35.8% in 2008 vs −55.2% for SPY</td><td>Lower your stock share, don&#8217;t time exits</td></tr></tbody></table><figcaption class="wp-element-caption">Sources: bestdays.py on Yahoo Finance data; J.P. Morgan Guide to Retirement 2026; Finxter 60/40 max drawdown analysis. History, not a forecast.</figcaption></figure>
<p>If the swings themselves are what make you want to sell, the fix is the allocation, not the timing. A 60/40 mix fell −35.8% at its 2008 low (<a href="https://blog.finxter.com/60-40-portfolio-max-drawdown/">see the 60/40 portfolio max drawdown analysis</a>), versus −55.2% for SPY in the same data, and is much easier to hold through the weeks where the best days happen.</p>
<p><strong>Limitations.</strong> This is one fund, one country, and 33.8 years of history, not a forecast. The cash-at-0% and no-tax assumptions are simplifications. And no investor misses <em>exactly</em> the 10 best days; the point is that the days you&#8217;d be most tempted to be out are the days that carry the result.</p>
<h2>FAQ</h2>
<h3>What happens if you miss the 10 best days in the stock market?</h3><p>In SPY from January 1993 to November 3, 2026, $10,000 grew to $313,613 fully invested but only $136,082 after missing the 10 best days. That is 57% less money, and the annual return falls from 10.74% to 8.04%.</p><h3>Why do the best days happen near the worst days?</h3><p>Big up-days come in high-volatility crashes. Eight of SPY&#x27;s 10 best days since 1993 fell within 10 trading days of one of its 20 worst days, and nine came while SPY was more than 20% below its peak.</p><h3>Is missing the best days in the market a myth?</h3><p>No, but the usual chart is one-sided. Missing SPY&#x27;s 10 worst days would have produced $751,970, and missing both the 10 best and 10 worst gave $326,292. Since nobody can pick either set in advance, staying invested wins in practice.</p><h3>What did J.P. Morgan find about missing the best days?</h3><p>J.P. Morgan&#x27;s Guide to Retirement 2026 shows $10,000 in the S&amp;P 500 Total Return Index from 2006 to 2025 growing to $80,619 fully invested and $35,866 after missing the 10 best days: 11.0% vs 6.6% a year.</p><h3>Should I sell stocks before a market crash?</h3><p>History says selling after big drops usually means missing the rebound. October 13, 2008 (+14.5%) came two trading days after a −7.0% day. If volatility worries you, lower your stock allocation instead of trying to time exits.</p>

<script type="application/ld+json">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What happens if you miss the 10 best days in the stock market?", "acceptedAnswer": {"@type": "Answer", "text": "In SPY from January 1993 to November 3, 2026, $10,000 grew to $313,613 fully invested but only $136,082 after missing the 10 best days. That is 57% less money, and the annual return falls from 10.74% to 8.04%."}}, {"@type": "Question", "name": "Why do the best days happen near the worst days?", "acceptedAnswer": {"@type": "Answer", "text": "Big up-days come in high-volatility crashes. Eight of SPY's 10 best days since 1993 fell within 10 trading days of one of its 20 worst days, and nine came while SPY was more than 20% below its peak."}}, {"@type": "Question", "name": "Is missing the best days in the market a myth?", "acceptedAnswer": {"@type": "Answer", "text": "No, but the usual chart is one-sided. Missing SPY's 10 worst days would have produced $751,970, and missing both the 10 best and 10 worst gave $326,292. Since nobody can pick either set in advance, staying invested wins in practice."}}, {"@type": "Question", "name": "What did J.P. Morgan find about missing the best days?", "acceptedAnswer": {"@type": "Answer", "text": "J.P. Morgan's Guide to Retirement 2026 shows $10,000 in the S&P 500 Total Return Index from 2006 to 2025 growing to $80,619 fully invested and $35,866 after missing the 10 best days: 11.0% vs 6.6% a year."}}, {"@type": "Question", "name": "Should I sell stocks before a market crash?", "acceptedAnswer": {"@type": "Answer", "text": "History says selling after big drops usually means missing the rebound. October 13, 2008 (+14.5%) came two trading days after a −7.0% day. If volatility worries you, lower your stock allocation instead of trying to time exits."}}]}</script>


<h2>The takeaway</h2>
<p>Missing the best days in the market cost 57% of the ending value since 1993, and those days sit right next to the worst days. The practical rule is boring: pick an allocation you can hold through a −10% session, automate your contributions, and don&#8217;t sell in the panic week.</p>
<p>And if you&#8217;ve built up your own investing rules, scripts and charts over the years, they&#8217;re worth packaging. A short guide like &#8220;Stay Invested: What 33 Years of SPY Data Say&#8221; makes a solid newsletter lead magnet or Kindle ebook. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=missing-the-best-days-in-the-market&amp;utm_content=end">ImagineYourBook</a> can turn your notes into a ~10,000-word draft with Word and EPUB export. The 7-day trial is $0 today with a card required, includes one 10k-word Small Book, and becomes $24.99/month unless you cancel before day 8. It writes text books with a cover, not picture books, and you still own the job of checking every number.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked November 4, 2026):</strong> <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance: SPY history</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://cdn.jpmorganfunds.com/content/dam/jpm-am-aem/global/en/insights/retirement-insights/guide-to-retirement-us.pdf">J.P. Morgan Guide to Retirement 2026 (PDF)</a> · <a href="https://blog.finxter.com/60-40-portfolio-max-drawdown/">Finxter: 60/40 max drawdown</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: bestdays.py (shown above). Not investment advice.</p>]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/missing-the-best-days-in-the-market/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Dividend Reinvestment Calculator: $10,000 in SPY Became $83,195 With DRIP vs $59,123 Without (Python)</title>
		<link>https://blog.finxter.com/dividend-reinvestment-calculator/</link>
					<comments>https://blog.finxter.com/dividend-reinvestment-calculator/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 17:05:21 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/dividend-reinvestment-calculator/</guid>

					<description><![CDATA[Dividend reinvestment calculator on real data: $10k in SPY became $83,195 with DRIP vs $59,123 as cash since 2000. VYM, SCHD, QQQ, taxes, Python.]]></description>
										<content:encoded><![CDATA[<p><strong>Short answer:</strong> $10,000 put into SPY on January 3, 2000 grew to <strong>$83,195</strong> by October 30, 2026 with every dividend reinvested. Taking the dividends as cash left you with <strong>$59,123</strong>: $51,962 in shares plus $7,161 in collected dividends. That makes reinvesting worth <strong>$24,072</strong>, or 41% more money, on a fund that yields only about 1% a year.</p>
<p>This dividend reinvestment calculator doesn&#8217;t assume a smooth 7% return. It replays every real SPY, VYM, SCHD and QQQ dividend from <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance</a>, day by day, with and without reinvestment. The Python script is below, so you can run it on your own tickers.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li>$10,000 in SPY from Jan 2000: <strong>$83,195</strong> with dividends reinvested vs $59,123 taking cash, a <strong>+$24,072 (41%)</strong> edge.</li>
<li>Since SPY&#8217;s 1993 launch: $314,411 vs $197,803, <strong>+59%</strong>. Edge scales with yield × years (QQQ +14%, VYM +43%).</li>
<li>The DRIP account was <strong>behind</strong> in 2000–2002 and 2008, and ahead only 3.2% by 2011. The payoff came in the second decade.</li>
<li>With $500/month added: $1,049,823 vs $846,717, a <strong>$203,106</strong> gap.</li>
<li>Reinvested dividends are still taxed in taxable accounts; a 15% rate cut the result to $77,535. Python script included.</li>
</ul>
</div>
<h2>How the calculator works (assumptions labeled)</h2>
<p>The script, <code>drip.py</code>, does what a broker&#8217;s DRIP does, and labels its simplifications:</p>
<ul>
<li><strong>Data:</strong> daily closes plus per-share dividends from Yahoo Finance via <a href="https://github.com/ranaroussi/yfinance">yfinance</a>, through October 30, 2026.</li>
<li><strong>Assumption 1:</strong> each dividend is reinvested at the close on its ex-date. Real brokers buy on the payment date, a few days later. That&#8217;s a small difference over 27 years.</li>
<li><strong>Assumption 2:</strong> in the &#8220;no DRIP&#8221; case the cash just sits there at 0%. This is the honest version of &#8220;I&#8217;ll spend it or leave it in the account&#8221;.</li>
<li><strong>Assumption 3:</strong> no taxes or fees unless I say so. Taxes get their own section below.</li>
</ul>
<p>As a sanity check, Yahoo&#8217;s dividend-adjusted price series turns the same $10,000 into $83,131. My day-by-day replay lands at $83,195, a 0.1% difference. So the replay matches total return.</p>
<h2>The results: DRIP vs cash, five real histories</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>ETF</th>
<th>Start</th>
<th>Years</th>
<th>$10,000 with DRIP</th>
<th>$10,000, dividends as cash</th>
<th>DRIP edge</th>
</tr>
</thead>
<tbody>
<tr>
<td>SPY</td>
<td>Jan 03, 2000</td>
<td>26.8</td>
<td><strong>$83,195</strong> (8.22%/yr)</td>
<td>$59,123 (6.85%/yr)</td>
<td>+$24,072 (+41%)</td>
</tr>
<tr>
<td>SPY</td>
<td>Jan 29, 1993</td>
<td>33.7</td>
<td><strong>$314,411</strong> (10.76%/yr)</td>
<td>$197,803 (9.25%/yr)</td>
<td>+$116,608 (+59%)</td>
</tr>
<tr>
<td>VYM</td>
<td>Nov 16, 2006</td>
<td>20.0</td>
<td><strong>$59,485</strong> (9.35%/yr)</td>
<td>$41,668 (7.41%/yr)</td>
<td>+$17,818 (+43%)</td>
</tr>
<tr>
<td>SCHD</td>
<td>Oct 20, 2011</td>
<td>15.0</td>
<td><strong>$69,132</strong> (13.73%/yr)</td>
<td>$54,081 (11.89%/yr)</td>
<td>+$15,050 (+28%)</td>
</tr>
<tr>
<td>QQQ</td>
<td>Jan 03, 2000</td>
<td>26.8</td>
<td><strong>$86,575</strong> (8.38%/yr)</td>
<td>$75,786 (7.84%/yr)</td>
<td>+$10,789 (+14%)</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Yahoo Finance daily closes + dividends to Oct 30, 2026; cash case = shares + uninvested dividends at 0%; no taxes or fees. drip.py</figcaption></figure>
<p>The SPY row is the headline. Without reinvestment you still owned your original 68.8 shares. With it, quarterly dividends quietly bought another 41 shares, so you finish with <strong>110.1 shares</strong>. Each of those shares is now worth about $756 and pays its own dividends.</p>
<p>Go back further and the gap widens. From SPY&#8217;s first trading day in January 1993, $10,000 grew to <strong>$314,411</strong> with DRIP versus $197,803 without. That&#8217;s <strong>$116,608 (59%)</strong> more, and the only difference is what you did with a 1–2% yield.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-edge-by-etf.png" alt="Bar chart of how much more $10,000 ended with dividends reinvested vs taken as cash: QQQ 2000-26 +14% ($10,789), SCHD 2011-26 +28% ($15,050), SPY 2000-26 +41% ($24,072), VYM 2006-26 +43% ($17,818), SPY 1993-26 +59% ($116,608)" class="wp-image-1672509" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-edge-by-etf.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-edge-by-etf-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-edge-by-etf-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-edge-by-etf-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The DRIP edge scales with yield and years: from +14% for low-yield QQQ to +59% for SPY since 1993 (drip.py, Yahoo Finance data).</figcaption></figure>
<p>Two dials decide how much reinvesting adds:</p>
<ol>
<li><strong>Yield.</strong> QQQ paid only 0.45% over the last 12 months, so 27 years of reinvesting added just 14%. VYM, at 2.25%, added 43% in only 20 years.</li>
<li><strong>Time.</strong> SCHD yields the most (2.95%), but its history only starts in October 2011. Fifteen years of reinvesting added 28% ($15,050). It simply hasn&#8217;t had SPY&#8217;s 33 years yet.</li>
</ol>
<p>Napkin rule: the DRIP edge grows roughly with <strong>yield × years</strong>. A 1% yield over 27 years (SPY) and a 2.25% yield over 20 years (VYM) both landed near +41–43%.</p>
<h2>The surprise: reinvesting barely mattered for 12 years</h2>
<p>Here&#8217;s the part no compound-interest calculator shows you. Reinvesting doesn&#8217;t pull ahead in a straight line. In the SPY run that started in 2000, the DRIP account was behind the cash account at the end of 2000, 2001, 2002 and 2008.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-drip-gap-by-year.gif" alt="Animated bar chart of the DRIP account&#x27;s lead over the cash account for $10,000 in SPY, year by year from 2000: slightly negative in 2000-2002 and -3.2% in 2008, 3.2% in 2011, 10.3% in 2013, 26.2% in 2020 and 40.7% by Oct 2026" class="wp-image-1672510"/><figcaption class="wp-element-caption">Reinvesting barely mattered for 12 years (−3.2% in 2008), then compounding took the lead to 40.7%.</figcaption></figure>
<p>The reason is simple. Every dividend reinvested in 2000–2002 bought shares that then fell during the dot-com crash. The cash version held those dollars at full value. At the end of 2008 the DRIP account was <strong>3.2% behind</strong>. Its lead was still only 3.2% at the end of 2011.</p>
<p>After that, compounding took over. The lead hit 10% in 2013, 26% in 2020, and <strong>40.7%</strong> by October 30, 2026. If you check your DRIP after a few years and see nothing, that&#8217;s normal. The payoff comes in the second decade.</p>
<p>It also explains why the <a href="https://blog.finxter.com/60-40-portfolio-max-drawdown/">60/40 portfolio&#8217;s max drawdown</a> and the <a href="https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/">time to recover from a crash</a> both matter here. A DRIP buys more shares when prices are low, but it only pays off if you keep holding until the recovery.</p>
<h2>The income you end up with</h2>
<p>People rarely reinvest for the final balance alone. They want a larger dividend check later. So the calculator multiplies the last 12 months of per-share payouts by the shares you hold now:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-dividend-income.png" alt="Grouped bar chart of yearly dividend income today from one $10,000 start, cash vs reinvested: QQQ $326 vs $388, SPY $521 vs $835, VYM $728 vs $1,335, SCHD $1,267 vs $2,038" class="wp-image-1672511" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-dividend-income.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-dividend-income-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-dividend-income-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/dividend-reinvestment-calculator-dividend-income-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Reinvested shares pay their own dividends: SPY income is 60% higher ($835 vs $521 a year).</figcaption></figure>
<p>From one $10,000 start, SPY now pays <strong>$835 a year</strong> to the DRIP investor versus $521 to the cash investor, 60% more income from the same original money. SCHD pays $2,038 versus $1,267. With reinvestment, each new share pays dividends too, so the income grows on its own.</p>
<h2>With monthly contributions the gap reaches six figures</h2>
<p>Most people don&#8217;t invest once and walk away. So I ran the calculator again with $10,000 up front plus <strong>$500 on the first trading day of every month</strong> into SPY from January 2000. That&#8217;s $170,500 contributed in total (the same approach as the monthly plan in my <a href="https://blog.finxter.com/lump-sum-vs-dca-sp-500/">lump sum vs DCA on the S&amp;P 500</a> backtest).</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>SPY scenario (Jan 2000–Oct 2026)</th>
<th>Contributed</th>
<th>With DRIP</th>
<th>Dividends as cash</th>
<th>Gap</th>
</tr>
</thead>
<tbody>
<tr>
<td>$10,000 once</td>
<td>$10,000</td>
<td>$83,195</td>
<td>$59,123</td>
<td>+$24,072 (+41%)</td>
</tr>
<tr>
<td>$10,000 + $500/month</td>
<td>$170,500</td>
<td><strong>$1,049,823</strong></td>
<td>$846,717</td>
<td>+$203,106 (+24%)</td>
</tr>
<tr>
<td>$10,000 once, 15% tax on dividends (assumption)</td>
<td>$10,000</td>
<td>$77,535</td>
<td>n/a</td>
<td>−$5,660 vs untaxed DRIP</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">drip.py on Yahoo Finance data. Tax row is a labeled assumption, not advice.</figcaption></figure>
<p>With reinvestment the account crosses <strong>$1,049,823</strong>. Without it, it reaches $846,717. The difference is <strong>$203,106</strong>, more than the total amount contributed. Monthly money shrinks the gap in percentage terms (24% vs 41%), because fresh cash hasn&#8217;t had time to compound. In dollars, though, the gap grows a lot.</p>
<h2>Taxes: reinvesting doesn&#8217;t dodge them</h2>
<p>In a taxable US account, the IRS treats reinvested dividends exactly like cash dividends. <a href="https://www.irs.gov/publications/p550">Publication 550</a> says that if you use your dividends to buy more stock, &#8220;you must still report the dividends as income&#8221;. So DRIP isn&#8217;t a tax strategy. It&#8217;s a compounding strategy.</p>
<p>Qualified dividends are taxed at long-term capital gain rates (<a href="https://www.irs.gov/taxtopics/tc404">IRS Topic 404</a>). For 2025 those rates are 0%, 15% or 20% depending on taxable income, and &#8220;no higher than 15% for most individuals&#8221; (<a href="https://www.irs.gov/taxtopics/tc409">IRS Topic 409</a>).</p>
<p><strong>Assumption:</strong> a flat 15% on every SPY dividend, with tax paid out of the dividend before reinvesting. The DRIP result falls from $83,195 to <strong>$77,535</strong>, a $5,660 drag over 27 years. In a tax-advantaged account like an IRA or 401(k), there&#8217;s no yearly drag and the untaxed numbers above apply.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0">
<p><strong>Don&#8217;t double count:</strong> if you test DRIP yourself, replay dividends on the raw close, or use the adjusted close alone. Doing both counts every dividend twice and inflates the result.</p>
</div>
<h2>The Python dividend reinvestment calculator</h2>
<p>Install <code>yfinance</code> and <code>pandas</code>, then save the prices once (so reruns don&#8217;t hit Yahoo every time) and run the script:</p>
<pre><code class="language-python">import yfinance as yf, pandas as pd
d = {t: yf.Ticker(t).history(start=&quot;1993-01-01&quot;, end=&quot;2026-11-01&quot;, auto_adjust=False, actions=True)
     for t in [&quot;SPY&quot;, &quot;SCHD&quot;, &quot;VYM&quot;, &quot;QQQ&quot;]}
pd.to_pickle(d, &quot;src/prices.pkl&quot;)
</code></pre>
<p><code>auto_adjust=False</code> keeps the raw (split-adjusted) close and the separate <code>Dividends</code> column. If you replayed dividends on top of the dividend-adjusted close, you&#8217;d count them twice.</p>
<pre><code class="language-python">"""Dividend reinvestment calculator: replay real ETF history (Yahoo Finance closes + dividends),
with vs without reinvesting dividends, optional monthly contributions and dividend tax."""
import pandas as pd

def load(path="src/prices.pkl"):
    d = pd.read_pickle(path)
    return {t: h[["Close", "Dividends"]].tz_localize(None).loc[:"2026-10-30"] for t, h in d.items()}

def drip(h, start, initial=10_000, monthly=0, reinvest=True, tax=0.0):
    """Daily replay. Dividends paid on the ex-date at that day's close (a common approximation)."""
    h = h.loc[start:]
    shares, cash, paid, invested, month, rows = 0.0, 0.0, 0.0, 0.0, None, []
    for day, (px, div) in h.iterrows():
        if month is None:
            shares, invested = initial / px, initial
        elif day.month != month and monthly:
            shares += monthly / px; invested += monthly
        month = day.month
        if div &gt; 0:
            net = shares * div * (1 - tax); paid += net
            if reinvest: shares += net / px
            else: cash += net
        rows.append((day, shares * px + cash, shares * px, cash, invested, paid))
    return pd.DataFrame(rows, columns=["date", "total", "stock", "cash", "invested", "divs"]).set_index("date")

def cagr(s):
    yrs = (s.index[-1] - s.index[0]).days / 365.25
    return (s.iloc[-1] / s.iloc[0]) ** (1 / yrs) - 1

if __name__ == "__main__":
    P = load()
    for t, start in (("SPY", "2000-01-03"), ("SPY", "1993-01-29"), ("SCHD", "2011-10-20"), ("VYM", "2006-11-16"), ("QQQ", "2000-01-03")):
        a, b = drip(P[t], start), drip(P[t], start, reinvest=False)
        yrs = (a.index[-1] - a.index[0]).days / 365.25
        print(f"{t} {a.index[0]:%b %Y}-{a.index[-1]:%b %Y} ({yrs:.1f} yrs) $10k: DRIP ${a.total.iloc[-1]:,.0f} "
              f"(CAGR {cagr(a.total):.2%}) | no DRIP ${b.total.iloc[-1]:,.0f} = ${b.stock.iloc[-1]:,.0f} shares + ${b.cash.iloc[-1]:,.0f} cash "
              f"(CAGR {cagr(b.total):.2%}) | DRIP edge ${a.total.iloc[-1]-b.total.iloc[-1]:,.0f} ({a.total.iloc[-1]/b.total.iloc[-1]-1:.0%}) | "
              f"shares {a.stock.iloc[-1]/P[t].Close.iloc[-1]:.1f} vs {b.stock.iloc[-1]/P[t].Close.iloc[-1]:.1f}")
    a = drip(P["SPY"], "2000-01-03", monthly=500); b = drip(P["SPY"], "2000-01-03", monthly=500, reinvest=False)
    print(f"SPY 2000 $10k + $500/mo (invested ${a.invested.iloc[-1]:,.0f}): DRIP ${a.total.iloc[-1]:,.0f} | no DRIP ${b.total.iloc[-1]:,.0f}")
    t15 = drip(P["SPY"], "2000-01-03", tax=0.15)
    print(f"SPY 2000 $10k DRIP with 15% dividend tax: ${t15.total.iloc[-1]:,.0f}")
    # annual dividend income at the end (last 12 months of per-share payouts x shares held)
    for t, start in (("SPY", "2000-01-03"), ("VYM", "2006-11-16"), ("SCHD", "2011-10-20"), ("QQQ", "2000-01-03")):
        ttm = P[t].Dividends.loc["2025-10-31":].sum(); px = P[t].Close.iloc[-1]
        a, b = drip(P[t], start), drip(P[t], start, reinvest=False)
        print(f"{t} trailing-12m payout ${ttm:.3f}/share = {ttm/px:.2%} yield; yearly income DRIP ${ttm*a.stock.iloc[-1]/px:,.0f} vs no DRIP ${ttm*b.stock.iloc[-1]/px:,.0f}")
</code></pre>
<p>Output on October 30, 2026 data:</p>
<pre><code class="language-text">SPY Jan 2000-Oct 2026 (26.8 yrs) $10k: DRIP $83,195 (CAGR 8.22%) | no DRIP $59,123 = $51,962 shares + $7,161 cash (CAGR 6.85%) | DRIP edge $24,072 (41%) | shares 110.1 vs 68.8
SPY Jan 1993-Oct 2026 (33.7 yrs) $10k: DRIP $314,411 (CAGR 10.76%) | no DRIP $197,803 = $171,999 shares + $25,804 cash (CAGR 9.25%) | DRIP edge $116,608 (59%) | shares 416.0 vs 227.6
SCHD Oct 2011-Oct 2026 (15.0 yrs) $10k: DRIP $69,132 (CAGR 13.73%) | no DRIP $54,081 = $42,970 shares + $11,111 cash (CAGR 11.89%) | DRIP edge $15,050 (28%) | shares 1932.1 vs 1201.0
VYM Nov 2006-Oct 2026 (20.0 yrs) $10k: DRIP $59,485 (CAGR 9.35%) | no DRIP $41,668 = $32,449 shares + $9,218 cash (CAGR 7.41%) | DRIP edge $17,818 (43%) | shares 363.3 vs 198.2
QQQ Jan 2000-Oct 2026 (26.8 yrs) $10k: DRIP $86,575 (CAGR 8.38%) | no DRIP $75,786 = $72,875 shares + $2,911 cash (CAGR 7.84%) | DRIP edge $10,789 (14%) | shares 125.4 vs 105.5
SPY 2000 $10k + $500/mo (invested $170,500): DRIP $1,049,823 | no DRIP $846,717
SPY 2000 $10k DRIP with 15% dividend tax: $77,535
SPY trailing-12m payout $7.583/share = 1.00% yield; yearly income DRIP $835 vs no DRIP $521
VYM trailing-12m payout $3.676/share = 2.25% yield; yearly income DRIP $1,335 vs no DRIP $728
SCHD trailing-12m payout $1.055/share = 2.95% yield; yearly income DRIP $2,038 vs no DRIP $1,267
QQQ trailing-12m payout $3.091/share = 0.45% yield; yearly income DRIP $388 vs no DRIP $326
</code></pre>
<p>To use it for your own holdings, change the ticker, <code>start</code>, <code>initial</code>, <code>monthly</code> and <code>tax</code>. If you also want to see whether your picks beat the index after dividends, my guide to <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">comparing your portfolio to the S&amp;P 500</a> uses the same total-return logic.</p>
<h2>What to do with this</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Question</th>
<th>What the data says</th>
<th>What to do</th>
</tr>
</thead>
<tbody>
<tr>
<td>How much is DRIP worth?</td>
<td>+14% (QQQ) to +59% (SPY since 1993) more ending value</td>
<td>Turn it on unless you need the income</td>
</tr>
<tr>
<td>When does it pay off?</td>
<td>SPY lead ≤3.2% through 2011, 40.7% by 2026</td>
<td>Judge it after a decade, not a year</td>
</tr>
<tr>
<td>Does it help income?</td>
<td>SPY: $835 vs $521 a year from the same $10k</td>
<td>Reinvest during accumulation, take cash later</td>
</tr>
<tr>
<td>Does it avoid tax?</td>
<td>No: reinvested dividends are still taxable (IRS Pub 550)</td>
<td>Prefer IRA/401(k) for high-yield funds</td>
</tr>
<tr>
<td>Is high yield better?</td>
<td>QQQ (0.45% yield) still ended at $86,575 vs SPY $83,195</td>
<td>Pick funds on total return, not yield</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Sources: drip.py on Yahoo Finance data; IRS Publication 550, Topics 404 and 409. History, not a forecast.</figcaption></figure>
<ol>
<li><strong>Turn DRIP on in tax-advantaged accounts</strong> unless you need the income now. Across these five histories, that one setting was worth 14–59% more money.</li>
<li><strong>In taxable accounts, reinvest anyway</strong> if you were going to reinvest by hand. The tax is the same either way. DRIP just stops cash from sitting idle.</li>
<li><strong>Judge it after 10+ years, not 3.</strong> The SPY run was behind or within about 3% of the cash version for its first 12 years.</li>
<li><strong>Don&#8217;t chase yield for the DRIP edge alone.</strong> QQQ&#8217;s tiny 0.45% yield still finished with a higher DRIP balance than SPY ($86,575 vs $83,195), because price growth did the work.</li>
</ol>
<p>Limitations: ex-date reinvestment, 0% on idle cash, no fees, and one 15% tax assumption. SCHD and VYM have shorter histories. This is history, not a forecast, and not investment advice.</p>
<h2>From calculator to finance ebook</h2>
<p>If you&#8217;ve worked through numbers like these, you already have the outline of a short, useful ebook: &#8220;DRIP vs cash, with real data&#8221;. You could use it as a lead magnet for a finance newsletter or sell it on KDP. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=dividend-reinvestment-calculator&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns a prompt or your notes into a complete draft with a designed cover and Word and EPUB downloads. It also has a Manuscript Rewrite feature for updating an existing book, for example when next year&#8217;s dividend data comes in.</p>
<p><strong>Takeaway:</strong> reinvesting SPY&#8217;s roughly 1% yield turned $10,000 into $83,195 instead of $59,123. The edge was invisible for 12 years and worth 41% after 27. Run the script on your own tickers, keep DRIP on, and give it a decade.</p>
<p>If you want to package what you learned into a book, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=dividend-reinvestment-calculator&amp;utm_content=end">ImagineYourBook</a>&#8216;s trial is $0 today with a card required. It includes one 10,000-word Small Book, the first $29.99 charge comes after 7 days, and you can cancel any time before day 8. Treat the result as a draft: check every number before you publish.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked November 2, 2026):</strong> <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance: SPY</a> · <a href="https://finance.yahoo.com/quote/VYM/history/">VYM</a> · <a href="https://finance.yahoo.com/quote/SCHD/history/">SCHD</a> · <a href="https://finance.yahoo.com/quote/QQQ/history/">QQQ</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://www.irs.gov/publications/p550">IRS Publication 550</a> · <a href="https://www.irs.gov/taxtopics/tc404">IRS Topic 404</a> · <a href="https://www.irs.gov/taxtopics/tc409">IRS Topic 409</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: drip.py (shown above). Not investment advice.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/dividend-reinvestment-calculator/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>60/40 Portfolio Max Drawdown: −35.8% in 2008, but Only 3.7 Points of Cushion in 2022 (Python)</title>
		<link>https://blog.finxter.com/60-40-portfolio-max-drawdown/</link>
					<comments>https://blog.finxter.com/60-40-portfolio-max-drawdown/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Sun, 01 Nov 2026 08:58:35 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/60-40-portfolio-max-drawdown/</guid>

					<description><![CDATA[60/40 portfolio max drawdown: −35.8% in 2008 vs −55.3% for stocks, but only a 3.7-point cushion in 2022. Daily data since 1986, tables, Python.]]></description>
										<content:encoded><![CDATA[<p>What is the 60/40 portfolio max drawdown? In a daily backtest of 60% S&amp;P 500 index fund and 40% US total bond index fund, rebalanced monthly from December 1986 to October 30, 2026, the worst peak-to-trough loss was <strong>−35.8%</strong>, hit on March 9, 2009. The same stock fund alone fell <strong>−55.3%</strong> over the same stretch. So the classic 60/40 cut the worst loss by about a third.</p>
<p>That cushion is not a constant, though. In 2008 bonds rose while stocks fell, and the 60/40 lost 19.5 points less than stocks. In 2022 both fell together, and the gap shrank to just <strong>3.7 points</strong> (−20.9% vs −24.6%). The 60/40 even took longer than stocks to get back to a new high that time.</p>
<p>(If you turn your own investing rules into something you can share, an AI book-writing tool like <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=60-40-portfolio-max-drawdown&amp;utm_content=top">ImagineYourBook</a> can turn your notes into a draft finance ebook or lead magnet with Word and EPUB export. More on that at the end.)</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li>The 60/40 portfolio max drawdown was <strong>−35.8%</strong> (Mar 9, 2009) in daily data since Dec 1986, vs −55.3% for 100% stocks.</li>
<li>In four of five crashes the 60/40 fell 54–66% as far as stocks. In 2022 it fell <strong>85%</strong> as far: −20.9% vs −24.6%.</li>
<li>Why: stock-bond daily correlation went from −0.35 in 2008 to <strong>+0.27</strong> in 2022, as bonds lost 13.2%.</li>
<li>Month-end data understates it: −32.5% instead of −35.8%.</li>
<li>The cushion&#8217;s price: 8.89% vs 11.09% a year, so $10k became $297,508 instead of $661,370. Python script included.</li>
</ul>
</div>
<h2>How I measured the 60/40 max drawdown</h2>
<p>&#8220;Max drawdown&#8221; is the largest drop from a running high to a later low, before a new high is set. The setup, kept simple and reproducible:</p>
<ul>
<li><strong>Stocks:</strong> Vanguard 500 Index Fund Investor Shares (VFINX), which tracks the S&amp;P 500.</li>
<li><strong>Bonds:</strong> Vanguard Total Bond Market Index Fund Investor Shares (VBMFX), which tracks the broad US investment-grade bond market.</li>
<li><strong>Data:</strong> daily adjusted closes from Yahoo Finance (dividends and interest reinvested, fund fees included), December 11, 1986 to October 30, 2026. That&#8217;s nearly 40 years and five big stock declines.</li>
<li><strong>Rebalancing:</strong> back to target weights on the first trading day of each month. No taxes, no trading costs.</li>
</ul>
<p>I used index mutual funds instead of ETFs because they go back further. As a cross-check, I reran 2008 and 2022 with SPY and AGG (the ETF versions, AGG starts in 2003). The 60/40 drawdowns came out at −35.8% and −20.8%, within 0.1 points of the mutual-fund results.</p>
<p>One detail matters more than people think: <strong>daily vs month-end data</strong>. Measured on month-end values, the same 60/40 portfolio&#8217;s worst drawdown is only −32.5%. That&#8217;s the number you&#8217;ll see in most monthly backtests, including my <a href="https://blog.finxter.com/how-often-to-rebalance-portfolio/">rebalancing study</a>. The real intraday-close low on March 9, 2009 was 3.3 points deeper. If you want to know how bad it actually felt, use daily data.</p>
<h2>The answer, crash by crash</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Episode</th>
<th>60/40 peak</th>
<th>60/40 trough</th>
<th>100% stocks max DD</th>
<th>60/40 max DD</th>
<th>Cushion</th>
<th>Stocks: yrs to new high</th>
<th>60/40: yrs to new high</th>
</tr>
</thead>
<tbody>
<tr>
<td>Oct 1987 crash</td>
<td>Aug 25, 1987</td>
<td>Oct 19, 1987</td>
<td>−33.1%</td>
<td><strong>−21.7%</strong></td>
<td>11.4 pts</td>
<td>1.7</td>
<td>1.4</td>
</tr>
<tr>
<td>Dot-com 2000–02</td>
<td>Sep 01, 2000</td>
<td>Oct 09, 2002</td>
<td>−47.5%</td>
<td><strong>−25.9%</strong></td>
<td>21.6 pts</td>
<td>6.1</td>
<td>4.2</td>
</tr>
<tr>
<td>2008–09 crisis</td>
<td>Oct 09, 2007</td>
<td>Mar 09, 2009</td>
<td>−55.3%</td>
<td><strong>−35.8%</strong></td>
<td>19.5 pts</td>
<td>4.9</td>
<td>3.2</td>
</tr>
<tr>
<td>COVID 2020</td>
<td>Feb 19, 2020</td>
<td>Mar 23, 2020</td>
<td>−33.8%</td>
<td><strong>−21.6%</strong></td>
<td>12.2 pts</td>
<td>0.5</td>
<td>0.4</td>
</tr>
<tr>
<td>2022 bear market</td>
<td>Dec 27, 2021</td>
<td>Oct 14, 2022</td>
<td>−24.6%</td>
<td><strong>−20.9%</strong></td>
<td>3.7 pts</td>
<td>1.9</td>
<td>2.1</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">VFINX (S&#038;P 500 index fund) and VBMFX (total bond index fund), daily adjusted closes, Yahoo Finance, Dec 1986–Oct 30, 2026; 60/40 rebalanced monthly. dd6040.py</figcaption></figure>
<p>Across all five episodes, the 60/40 portfolio&#8217;s drawdown was between <strong>54% and 85%</strong> of the stock fund&#8217;s drawdown. Four of the five cluster at 54–66%. The outlier is 2022, where the 60/40 still fell 85% as far as stocks did.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-underwater.png" alt="Underwater chart of daily drawdowns from 1987 to 2026 for 100% stocks (max -55.3%), 60/40 (max -35.8% on March 9, 2009) and 40/60 (max -23.8%); in 2022 the 60/40 fell -20.9% vs -24.6% for stocks" class="wp-image-1672483" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-underwater.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-underwater-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-underwater-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-underwater-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The 60/40&#8217;s worst close was −35.8% on March 9, 2009; in 2022 it fell nearly as far as stocks (dd6040.py, Yahoo Finance daily data).</figcaption></figure>
<p>Here&#8217;s the napkin math for a real account. Say you hold <strong>$500,000</strong> (an example, not a recommendation):</p>
<ul>
<li>100% stocks at the 2008–09 low: −55.3%, so about <strong>$276,500</strong> gone on paper.</li>
<li>60/40 at the same low: −35.8%, about <strong>$179,000</strong> gone.</li>
<li>In 2022: −24.6% for stocks ($123,000) vs −20.9% for the 60/40 ($104,500). The &#8220;safe&#8221; mix saved you only $18,500 that year.</li>
</ul>
<h2>Why 2022 broke the cushion</h2>
<p>The 60/40 works when bonds zig while stocks zag. That depends on the correlation between the two, and it isn&#8217;t fixed. From the same daily data:</p>
<ul>
<li><strong>2008:</strong> the daily correlation between the stock and bond funds was <strong>−0.35</strong>. The stock fund lost 37.0% for the calendar year while the bond fund gained 5.1%.</li>
<li><strong>2022:</strong> the correlation flipped to <strong>+0.27</strong>. The stock fund lost 18.2% and the bond fund lost 13.2% in the same year.</li>
</ul>
<p>The reason was inflation and rate hikes. The <a href="https://www.federalreserve.gov/monetarypolicy/openmarket.htm">Federal Reserve</a> raised its target range from near zero in March 2022 to 4.25–4.50% by December 2022, and existing bonds lose value when rates jump. A bond fund that falls 13% can&#8217;t cushion a stock fund that falls 18%.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-crash-2008-vs-2022.gif" alt="Animated chart of 100% stocks vs 60/40 in the 18 months after the peak: in 2008 stocks fell to -55% and the 60/40 to -36%; in 2022 stocks fell to -25% and the 60/40 to -21%" class="wp-image-1672485"/><figcaption class="wp-element-caption">Same portfolio, two crashes: in 2008 bonds rose and halved the pain; in 2022 bonds fell too.</figcaption></figure>
<p>The bond side still hasn&#8217;t fully healed. VBMFX peaked on August 4, 2020, and on October 30, 2026 it was still <strong>4.4%</strong> below that high on a total-return basis. Its max drawdown of −18.8% (October 2022) is the deepest in the fund&#8217;s history in this data.</p>
<p>That&#8217;s also why the 60/40 took <strong>2.1 years</strong> to reach a new high after 2022, while stocks needed 1.9 years. In 2008 it was the other way round: 3.2 years for the 60/40 vs 4.9 years for stocks.</p>
<h2>What the cushion costs you</h2>
<p>Lower drawdowns aren&#8217;t free. Over the full period, every 20 points you move from stocks to bonds cuts the worst drawdown by roughly 9 to 12 points and costs about 1.0 to 1.2 points of annual return:</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Mix (stocks/bonds)</th>
<th>CAGR</th>
<th>Max drawdown (daily)</th>
<th>$10,000 grew to</th>
</tr>
</thead>
<tbody>
<tr>
<td>100% stocks</td>
<td>11.09%</td>
<td>−55.3%</td>
<td>$661,370</td>
</tr>
<tr>
<td>80/20</td>
<td>10.04%</td>
<td>−46.3%</td>
<td>$452,010</td>
</tr>
<tr>
<td>60/40</td>
<td>8.89%</td>
<td>−35.8%</td>
<td>$297,508</td>
</tr>
<tr>
<td>40/60</td>
<td>7.65%</td>
<td>−23.8%</td>
<td>$188,700</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Dec 11, 1986 to Oct 30, 2026, monthly rebalancing, no taxes or costs. dd6040.py</figcaption></figure>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-return-vs-drawdown.png" alt="Scatter of CAGR vs max drawdown Dec 1986 to Oct 2026: 100% stocks 11.09% and -55.3%, 80/20 10.04% and -46.3%, 60/40 8.89% and -35.8%, 40/60 7.65% and -23.8%" class="wp-image-1672484" srcset="https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-return-vs-drawdown.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-return-vs-drawdown-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-return-vs-drawdown-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/11/60-40-portfolio-max-drawdown-return-vs-drawdown-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Each step toward bonds trades roughly 1 point of annual return for 9–12 points less drawdown.</figcaption></figure>
<p>The dollar gap compounds. $10,000 in the stock fund in December 1986 grew to about $661,000. The same $10,000 in the 60/40 grew to about $298,000. That&#8217;s the price of a portfolio whose worst day was −35.8% instead of −55.3%.</p>
<p>Whether that&#8217;s worth it depends on one question: would you have <strong>sold</strong> at −55%? An investor who holds through −55% does better with stocks. An investor who panics at −40% and sells does much worse than a 60/40 holder who stays put. For more on that recovery side, see <a href="https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/">how long the stock market takes to recover from a crash</a>.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0">
<p><strong>Rule:</strong> plan for the 60/40&#8217;s real worst case, about −36% in daily data, and don&#8217;t count on bonds to cushion every crash. In 2022 they fell alongside stocks.</p>
</div>
<h2>The Python script</h2>
<p>This is the whole backtest. It needs <code>pandas</code> and <code>yfinance</code> (<code>pip install pandas yfinance</code>). Download the prices once, save them, and run it:</p>
<pre><code class="language-python">import yfinance as yf
yf.download([&quot;VFINX&quot;, &quot;VBMFX&quot;], start=&quot;1986-01-01&quot;, auto_adjust=True)[&quot;Close&quot;].to_pickle(&quot;src/prices.pkl&quot;)
</code></pre>
<pre><code class="language-python">"""60/40 portfolio max drawdown: VFINX (S&amp;P 500 index fund) + VBMFX (US total bond index fund),
daily adjusted closes from Yahoo Finance, rebalanced to target on the first trading day of each month."""
import pandas as pd

def load(path="src/prices.pkl"):
    px = pd.read_pickle(path)[["VFINX", "VBMFX"]].dropna()
    return px.loc[:"2026-10-30"]

def portfolio(px, w_stock):
    r = px.pct_change().dropna()
    value, vals, w = 1.0, [], None
    month = None
    for day, row in r.iterrows():
        if day.month != month:                    # rebalance at start of each month
            month, w = day.month, {"VFINX": w_stock, "VBMFX": 1 - w_stock}
            hold = {k: value * v for k, v in w.items()}
        hold = {k: hold[k] * (1 + row[k]) for k in hold}
        value = sum(hold.values()); vals.append(value)
    return pd.Series(vals, index=r.index)

def drawdown(v):
    return v / v.cummax() - 1

def episode(v, start, end):
    """Max drawdown inside [start, end], peak date, trough date, recovery date."""
    dd = drawdown(v)
    trough = dd.loc[start:end].idxmin()
    peak = v.loc[:trough].idxmax()
    after = v.loc[trough:]
    rec = after[after &gt;= v.loc[peak]]
    return dict(depth=dd.loc[trough], peak=peak, trough=trough,
                recovered=rec.index[0] if len(rec) else None)

def cagr(v):
    yrs = (v.index[-1] - v.index[0]).days / 365.25
    return (v.iloc[-1] / v.iloc[0]) ** (1 / yrs) - 1

if __name__ == "__main__":
    px = load()
    eps = {"1987 crash": ("1987-08-01", "1987-12-31"), "Dot-com 2000-02": ("2000-01-01", "2003-03-31"),
           "2008 crisis": ("2007-10-01", "2009-06-30"), "COVID 2020": ("2020-02-01", "2020-04-30"),
           "2022 bear": ("2022-01-01", "2022-12-31")}
    for w in (1.0, 0.8, 0.6, 0.4):
        v = portfolio(px, w)
        print(f"\n{int(w*100)}/{int(round((1-w)*100))}: CAGR {cagr(v):.2%}, max DD {drawdown(v).min():.1%}, "
              f"$10k -&gt; ${10000*v.iloc[-1]:,.0f} ({v.index[0]:%b %Y}-{v.index[-1]:%b %Y})")
        for name, (a, b) in eps.items():
            e = episode(v, a, b)
            rec = e["recovered"]
            yrs = f"{(rec - e['peak']).days/365.25:.1f} yrs to new high" if rec is not None else "not recovered"
            print(f"  {name:16s} {e['depth']:7.1%}  peak {e['peak']:%Y-%m-%d} trough {e['trough']:%Y-%m-%d}  {yrs}")
    bonds = px["VBMFX"] / px["VBMFX"].iloc[0]
    e = episode(bonds, "2020-08-01", "2023-12-31")
    print(f"\nBonds alone (VBMFX): max DD {drawdown(bonds).min():.1%}, 2020-23 drawdown {e['depth']:.1%} trough {e['trough']:%Y-%m-%d} recovered {e['recovered']}")
</code></pre>
<p>Output (October 30, 2026 data):</p>
<pre><code class="language-text">100/0: CAGR 11.09%, max DD -55.3%, $10k -&gt; $661,370 (Dec 1986-Oct 2026)
  1987 crash        -33.1%  peak 1987-08-25 trough 1987-10-19  1.7 yrs to new high
  Dot-com 2000-02   -47.5%  peak 2000-09-01 trough 2002-10-09  6.1 yrs to new high
  2008 crisis       -55.3%  peak 2007-10-09 trough 2009-03-09  4.9 yrs to new high
  COVID 2020        -33.8%  peak 2020-02-19 trough 2020-03-23  0.5 yrs to new high
  2022 bear         -24.6%  peak 2022-01-03 trough 2022-10-12  1.9 yrs to new high

80/20: CAGR 10.04%, max DD -46.3%, $10k -&gt; $452,010 (Dec 1986-Oct 2026)
  1987 crash        -27.4%  peak 1987-08-25 trough 1987-10-19  1.7 yrs to new high
  Dot-com 2000-02   -37.5%  peak 2000-09-01 trough 2002-10-09  5.3 yrs to new high
  2008 crisis       -46.3%  peak 2007-10-09 trough 2009-03-09  3.5 yrs to new high
  COVID 2020        -27.9%  peak 2020-02-19 trough 2020-03-23  0.5 yrs to new high
  2022 bear         -22.6%  peak 2021-12-27 trough 2022-10-14  2.0 yrs to new high

60/40: CAGR 8.89%, max DD -35.8%, $10k -&gt; $297,508 (Dec 1986-Oct 2026)
  1987 crash        -21.7%  peak 1987-08-25 trough 1987-10-19  1.4 yrs to new high
  Dot-com 2000-02   -25.9%  peak 2000-09-01 trough 2002-10-09  4.2 yrs to new high
  2008 crisis       -35.8%  peak 2007-10-09 trough 2009-03-09  3.2 yrs to new high
  COVID 2020        -21.6%  peak 2020-02-19 trough 2020-03-23  0.4 yrs to new high
  2022 bear         -20.9%  peak 2021-12-27 trough 2022-10-14  2.1 yrs to new high

40/60: CAGR 7.65%, max DD -23.8%, $10k -&gt; $188,700 (Dec 1986-Oct 2026)
  1987 crash        -16.0%  peak 1987-08-25 trough 1987-10-19  1.1 yrs to new high
  Dot-com 2000-02   -13.0%  peak 2001-02-01 trough 2002-07-23  2.3 yrs to new high
  2008 crisis       -23.8%  peak 2007-12-06 trough 2009-03-09  2.3 yrs to new high
  COVID 2020        -15.1%  peak 2020-02-19 trough 2020-03-23  0.3 yrs to new high
  2022 bear         -19.2%  peak 2021-12-27 trough 2022-10-14  2.4 yrs to new high

Bonds alone (VBMFX): max DD -18.8%, 2020-23 drawdown -18.8% trough 2022-10-24 recovered None
</code></pre>
<p>Two things to try:</p>
<ol>
<li>Change <code>w_stock</code> to your own mix, or swap in your own funds.</li>
<li>Change the rebalance rule. Monthly rebalancing is what this script does. My <a href="https://blog.finxter.com/how-often-to-rebalance-portfolio/">rebalancing study</a> found annual rebalancing gave similar risk-adjusted results with far fewer trades.</li>
</ol>
<p>If you&#8217;d rather see the observed drawdown of your actual holdings without writing code, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free tracker that runs in your browser with no account or broker connection. It shows observed drawdown, Sharpe and Sortino, and compares your returns with the S&amp;P 500 and Bitcoin, with Excel backups (prices are entered manually). For the risk-adjusted side, see <a href="https://blog.finxter.com/sharpe-ratio-vs-sortino-ratio/">Sharpe vs Sortino ratio</a> and <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">how to compare your portfolio to the S&amp;P 500</a>.</p>
<h2>Summary: what to expect from a 60/40</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Question</th>
<th>What 1986–2026 data says</th>
<th>What to do</th>
</tr>
</thead>
<tbody>
<tr>
<td>Worst loss to plan for</td>
<td>−35.8% (daily), −32.5% (month-end)</td>
<td>Size your stock share so you could hold through −36%</td>
</tr>
<tr>
<td>How much bonds help in a crash</td>
<td>60/40 fell 54–66% as far as stocks in four of five crashes</td>
<td>Expect roughly a one-third cut, not immunity</td>
</tr>
<tr>
<td>When the cushion fails</td>
<td>2022: stock-bond correlation +0.27, gap only 3.7 points</td>
<td>Don&#8217;t treat bonds as a guaranteed hedge when inflation and rates jump</td>
</tr>
<tr>
<td>Recovery time</td>
<td>2008: 3.2 vs 4.9 years; 2022: 2.1 vs 1.9 years</td>
<td>60/40 usually recovers faster, but not always</td>
</tr>
<tr>
<td>Cost of the cushion</td>
<td>8.89% vs 11.09% a year; $297,508 vs $661,370 per $10k</td>
<td>Only worth it if it stops you from selling in a crash</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Sources: dd6040.py on Yahoo Finance data; Federal Reserve rate history. History, not a forecast.</figcaption></figure>
<h2>Limitations</h2>
<ul>
<li>This is US-only: one S&amp;P 500 fund and one US bond fund. Global or inflation-linked bonds would behave differently.</li>
<li>40 years is still a small sample: five big stock declines, and only one with stocks and bonds falling hard together.</li>
<li>No taxes or trading costs. Monthly rebalancing in a taxable account would cost something.</li>
<li>Past drawdowns are history, not a forecast. The next −35% could come from a different mix of stock and bond moves.</li>
</ul>
<h2>The takeaway</h2>
<p>The honest answer to &#8220;what is the 60/40 portfolio max drawdown&#8221; is <strong>−35.8%</strong> in daily data since 1987, about two-thirds of the stock market&#8217;s −55.3%. Plan for that number, not the −20% people assume. And remember that the bond cushion depends on bonds and stocks moving in opposite directions. In 2022 they didn&#8217;t, and the 60/40 lost almost as much as stocks.</p>
<p>If this kind of analysis is how you think about money, you could package your own rules into a short investing guide or lead magnet for readers or clients. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=60-40-portfolio-max-drawdown&amp;utm_content=end">ImagineYourBook</a> turns a prompt or notes into a complete draft with a cover and Word/EPUB export. The trial is $0 today with a card and includes one 10,000-word book; the first $29.99 payment starts after 7 days unless you cancel before day 8. Check the numbers in any draft yourself, same as you would with this backtest.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked November 1, 2026):</strong> <a href="https://finance.yahoo.com/quote/VFINX/history/">Yahoo Finance: VFINX history</a> · <a href="https://finance.yahoo.com/quote/VBMFX/history/">Yahoo Finance: VBMFX history</a> · <a href="https://finance.yahoo.com/quote/AGG/history/">Yahoo Finance: AGG (cross-check)</a> · <a href="https://www.federalreserve.gov/monetarypolicy/openmarket.htm">Federal Reserve: FOMC target rate history</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: dd6040.py (shown above). Not investment advice.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/60-40-portfolio-max-drawdown/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How Long Does It Take the Stock Market to Recover From a Crash? Median 2.1 Years (Python)</title>
		<link>https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/</link>
					<comments>https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/#respond</comments>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Fri, 30 Oct 2026 17:04:53 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/</guid>

					<description><![CDATA[How long does it take the stock market to recover from a crash? S&#038;P 500 median: 2.1 years across 12 crashes since 1928. Depth table, dividends, Python.]]></description>
										<content:encoded><![CDATA[<p>How long does it take the stock market to recover from a crash? For the S&amp;P 500, the typical answer is about two years: across all 12 declines of 20% or more since 1928, the median time from the old record high to a new record high was <strong>2.1 years</strong>. The spread around that median is huge, though. The fastest recovery (2020) took <strong>0.5 years</strong>, and the slowest (1929) took <strong>25.0 years</strong> on the price index alone.</p>
<p>I measured every crash from daily closing prices with a short pandas script, which is included below so you can rerun it on any index or fund. The main pattern: how deep a crash goes tells you more about the wait than anything else. The four crashes that took five or more years to recover all fell <strong>48% or more</strong>. Every crash shallower than 40% was back at a record within <strong>3.3 years</strong>.</p>
<p>(If you end up with a sharp investing framework of your own, an AI book-writing tool like <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=how-long-does-it-take-the-stock-market-to-recover-from-a-crash&amp;utm_content=top">ImagineYourBook</a> can turn your notes into a draft finance ebook or lead magnet with Word and EPUB export. More on that at the end.)</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li>Across all 12 S&#038;P 500 crashes of 20%+ since 1928, the median time from record high to a new record was <strong>2.1 years</strong> (2.0 years since 1946).</li>
<li>From the bottom, the median climb back to the old high took <strong>1.5 years</strong>.</li>
<li>Depth decides the wait: every crash shallower than 40% recovered within 3.3 years; the four that fell 48%+ took <strong>5.5 to 25 years</strong>.</li>
<li>Corrections of 10–20% (14 of them) took a median <strong>7 months</strong> to reach a new high.</li>
<li>Dividends cut the infamous 1929 wait from 25.0 to <strong>15.3 years</strong> (7.2 after inflation). Pandas script included.</li>
</ul>
</div>
<h2>How I measured &#8220;recovery&#8221;</h2>
<p>&#8220;Recovered&#8221; can mean three different things, and articles on this question often mix them up. Here is the definition I used, kept strict:</p>
<ul>
<li><strong>Crash:</strong> the S&amp;P 500 closes at least 20% below its all-time closing high.</li>
<li><strong>Peak:</strong> the last record close before the drop.</li>
<li><strong>Trough:</strong> the lowest close before the index makes a new record.</li>
<li><strong>Recovered:</strong> the first close at or above the old record.</li>
</ul>
<p>The data is the S&amp;P 500 price index (<code>^GSPC</code>) from <a href="https://finance.yahoo.com/quote/%5EGSPC/history/">Yahoo Finance</a>, daily closes from December 30, 1927 to October 29, 2026, loaded with <a href="https://github.com/ranaroussi/yfinance">yfinance</a>. The finding itself is a <a href="https://blog.finxter.com/pandas-dataframe-cummax-method/"><code>cummax()</code></a> one-liner: divide the price by its running maximum, and every value below 1.0 is time spent &#8220;under water.&#8221;</p>
<p>One consequence of measuring from all-time highs: a slump that happens while the market is <em>still</em> below an older record counts as part of the bigger episode, not as a separate crash. The 1937–38 slide (−54.5% from its March 1937 high) sits inside the 1929 recovery, and 2011&#8217;s −19.4% dip came while the index was still below its 2007 record. If you count every bear market from its own local peak, you get more episodes and shorter typical waits.</p>
<h2>All 12 S&amp;P 500 crashes since 1928</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Record high</th>
<th>Trough</th>
<th>New record</th>
<th>Depth</th>
<th>Peak → trough (yrs)</th>
<th>Trough → old high (yrs)</th>
<th>Peak → new high (yrs)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sep 16, 1929</td>
<td>Jun 01, 1932</td>
<td>Sep 22, 1954</td>
<td>−86.2%</td>
<td>2.7</td>
<td>22.3</td>
<td><strong>25.0</strong></td>
</tr>
<tr>
<td>Aug 03, 1956</td>
<td>Oct 22, 1957</td>
<td>Sep 24, 1958</td>
<td>−21.5%</td>
<td>1.2</td>
<td>0.9</td>
<td><strong>2.1</strong></td>
</tr>
<tr>
<td>Dec 12, 1961</td>
<td>Jun 26, 1962</td>
<td>Sep 03, 1963</td>
<td>−28.0%</td>
<td>0.5</td>
<td>1.2</td>
<td><strong>1.7</strong></td>
</tr>
<tr>
<td>Feb 09, 1966</td>
<td>Oct 07, 1966</td>
<td>May 04, 1967</td>
<td>−22.2%</td>
<td>0.7</td>
<td>0.6</td>
<td><strong>1.2</strong></td>
</tr>
<tr>
<td>Nov 29, 1968</td>
<td>May 26, 1970</td>
<td>Mar 06, 1972</td>
<td>−36.1%</td>
<td>1.5</td>
<td>1.8</td>
<td><strong>3.3</strong></td>
</tr>
<tr>
<td>Jan 11, 1973</td>
<td>Oct 03, 1974</td>
<td>Jul 17, 1980</td>
<td>−48.2%</td>
<td>1.7</td>
<td>5.8</td>
<td><strong>7.5</strong></td>
</tr>
<tr>
<td>Nov 28, 1980</td>
<td>Aug 12, 1982</td>
<td>Nov 03, 1982</td>
<td>−27.1%</td>
<td>1.7</td>
<td>0.2</td>
<td><strong>1.9</strong></td>
</tr>
<tr>
<td>Aug 25, 1987</td>
<td>Dec 04, 1987</td>
<td>Jul 26, 1989</td>
<td>−33.5%</td>
<td>0.3</td>
<td>1.6</td>
<td><strong>1.9</strong></td>
</tr>
<tr>
<td>Mar 24, 2000</td>
<td>Oct 09, 2002</td>
<td>May 30, 2007</td>
<td>−49.1%</td>
<td>2.5</td>
<td>4.6</td>
<td><strong>7.2</strong></td>
</tr>
<tr>
<td>Oct 09, 2007</td>
<td>Mar 09, 2009</td>
<td>Mar 28, 2013</td>
<td>−56.8%</td>
<td>1.4</td>
<td>4.1</td>
<td><strong>5.5</strong></td>
</tr>
<tr>
<td>Feb 19, 2020</td>
<td>Mar 23, 2020</td>
<td>Aug 18, 2020</td>
<td>−33.9%</td>
<td>0.1</td>
<td>0.4</td>
<td><strong>0.5</strong></td>
</tr>
<tr>
<td>Jan 03, 2022</td>
<td>Oct 12, 2022</td>
<td>Jan 19, 2024</td>
<td>−25.4%</td>
<td>0.8</td>
<td>1.3</td>
<td><strong>2.0</strong></td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">S&#038;P 500 price index (^GSPC), daily closes Dec 1927 to Oct 29, 2026, Yahoo Finance. Declines of 20%+ from an all-time closing high. recovery.py</figcaption></figure>
<p>The median crash fell <strong>33.7%</strong>. Measured from the old peak, the median wait for a new record was 2.1 years. Measured from the bottom, the median climb back took just <strong>1.5 years</strong>. Both views matter. The first is what someone who bought at the top went through. The second is what a buyer at the bottom saw, or anyone who kept investing on the way down.</p>
<p>Excluding the Great Depression barely moves the number: for the 11 crashes that started in 1946 or later, the median peak-to-new-high time was <strong>2.0 years</strong>, and the median trough-to-old-high time was <strong>1.3 years</strong>.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-depth-vs-recovery.png" alt="Scatter chart of S&amp;P 500 crash depth vs years to a new record high on a log scale: crashes of 21% to 36% recovered in 0.5 to 3.3 years, while 1973 (-48%), 2000 (-49%), 2007 (-57%) and 1929 (-86%) took 7.5, 7.2, 5.5 and 25.0 years" class="wp-image-1672459" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-depth-vs-recovery.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-depth-vs-recovery-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-depth-vs-recovery-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-depth-vs-recovery-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Every crash right of the 45% line took 5.5+ years; every one shallower than 40% was back within 3.3 years (recovery.py).</figcaption></figure>
<h2>Why depth matters more than anything else</h2>
<p>A loss and the gain needed to undo it are not symmetric. That is plain arithmetic, not a market forecast:</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Drop from the high</th>
<th>Gain needed to get back</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>−10%</td>
<td>+11%</td>
<td>a typical correction</td>
</tr>
<tr>
<td>−20%</td>
<td>+25%</td>
<td>bear-market threshold</td>
</tr>
<tr>
<td>−34%</td>
<td>+52%</td>
<td>2020 (−33.9%)</td>
</tr>
<tr>
<td>−50%</td>
<td>+100%</td>
<td>2000 (−49.1%)</td>
</tr>
<tr>
<td>−57%</td>
<td>+133%</td>
<td>2007–09 (−56.8%)</td>
</tr>
<tr>
<td>−86%</td>
<td>+614%</td>
<td>1929–32 (−86.2%)</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Required gain = 1 / (1 − loss) − 1. Printed by recovery.py.</figcaption></figure>
<p>A 20% drop needs a 25% gain to get back to even. A 57% drop, like 2007–09, needs <strong>+133%</strong>. The 1929 crash, at −86.2% on daily closes, needed <strong>+614%</strong>. Because the required gain grows faster than the loss, recovery time grows faster than depth, which is why the scatter chart above uses a log scale. Below about −40%, the S&amp;P 500&#8217;s history has been a matter of months to a few years. Past roughly −45%, every recovery took at least 5.5 years.</p>
<p>The speed of the fall matters too. In 2020 the index took 33 calendar days to fall 33.9% (from 3,386.15 on February 19 to 2,237.40 on March 23) and had a new record close of 3,389.78 by August 18. The 2000 crash was shallower than 2007&#8217;s at the bottom (−49.1% vs −56.8%) yet took longer overall (7.2 vs 5.5 years), because the decline itself dragged on for 2.5 years.</p>
<p>The animation lines up six post-war crashes on the same clock, with each record high set to 100:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-crash-paths.gif" alt="Animated line chart of six S&amp;P 500 crashes rebased to their record high = 100, years after the peak on the x-axis: 2020 recovers in 0.5 years, 1987 in 1.9, 2022 in 2.0, 2007 in 5.5, 2000 in 7.2 and 1973 in 7.5 years" class="wp-image-1672461"/><figcaption class="wp-element-caption">Six post-war crashes on one clock: three were back within 2 years, three needed 5.5 to 7.5 years (weekly closes, recovery.py).</figcaption></figure>
<p>Three of the six had recovered before the three-year mark. The other three needed 5.5 to 7.5 years.</p>
<h2>Smaller drops: corrections of 10% to 20%</h2>
<p>Most of the scary headlines are about corrections, not crashes. Using the same rules, the S&amp;P 500 had <strong>14</strong> declines of 10% to just under 20% from a record close that never turned into a full crash. The median time from peak to new high was <strong>7 months</strong>, and the longest took <strong>1.5 years</strong>. That list includes 2025: the index fell 18.9% from its February 19 record to its April 8 low and set a new record on June 27, 2025.</p>
<h2>The famous &#8220;25 years&#8221; is a price-only number</h2>
<p>The price index ignores dividends, and dividends do a lot of the work in a recovery. For crashes since 1988 I reran the same test on the S&amp;P 500 Total Return index (<code>^SP500TR</code>, <a href="https://finance.yahoo.com/quote/%5ESP500TR/history/">Yahoo Finance</a>). For 1929 I used <a href="https://shillerdata.com/">Robert Shiller&#8217;s monthly S&amp;P data</a>, which includes dividends and the CPI (monthly averages, so its dates are a little coarser):</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-dividends-shorten-recovery.png" alt="Horizontal bar chart of years from peak to break-even, price index vs dividends reinvested: 2000 crash 7.2 vs 6.1 years, 2007 crash 5.5 vs 4.5 years, 1929 crash 25.0 years on price, 15.3 years with dividends, 7.2 years with dividends after inflation" class="wp-image-1672460" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-dividends-shorten-recovery.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-dividends-shorten-recovery-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-dividends-shorten-recovery-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/how-long-does-it-take-the-stock-market-to-recover-from-a-crash-dividends-shorten-recovery-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">With dividends reinvested, the 1929 wait drops from 25.0 to 15.3 years, and to 7.2 years after inflation (Shiller monthly data).</figcaption></figure>
<ul>
<li><strong>2000 crash:</strong> 7.2 years on price, <strong>6.1 years</strong> with dividends reinvested.</li>
<li><strong>2007 crash:</strong> 5.5 years on price, <strong>4.5 years</strong> with dividends.</li>
<li><strong>2020 and 2022:</strong> dividends hardly matter over 0.5 to 2 years (0.5 and 1.9 years with dividends).</li>
<li><strong>1929 crash:</strong> 25.0 years on price (September 1929 to September 1954), <strong>15.3 years</strong> with dividends reinvested (January 1945). Adjusted for the 1930s deflation as well, a dividend-reinvesting investor was whole in real terms by November 1936, <strong>7.2 years</strong> after the peak.</li>
</ul>
<p>That doesn&#8217;t make 1929 painless. Even with dividends, the real loss at the bottom was about 77%. But &#8220;it took 25 years to recover&#8221; describes a price chart, not the experience of someone who held the index and reinvested its income.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0">
<p><strong>Rule:</strong> when someone quotes a recovery time, ask two questions: measured from the peak or from the bottom, and price or total return? The same 1929 crash is a 25.0-year, 15.3-year or 7.2-year recovery depending on the answer.</p>
</div>
<h2>The Python script</h2>
<p>This script finds every crash, prints the table above, and compares price-only recovery with total-return recovery since 1988. Change the <code>ticker</code> argument to <code>"^IXIC"</code>, <code>"QQQ"</code>, or any fund you own to run the same test on it. The 1929 dividend check lives in a separate helper (<code>shiller_1929.py</code>) because it reads Shiller&#8217;s spreadsheet instead of Yahoo data.</p>
<pre><code class="language-python">"""How long does it take the stock market to recover from a crash?
Every S&amp;P 500 decline of 20%+ since 1928, measured peak -&gt; trough -&gt; back to the old peak.
Data: Yahoo Finance daily closes (^GSPC price index, ^SP500TR total return) via yfinance.
pip install yfinance pandas"""
import pandas as pd

END = "2026-10-29"   # last full trading day when this was written

def load(ticker="^GSPC"):
    try:
        s = pd.read_pickle(f"src/{ticker.strip('^').lower()}.pkl")["Close"][ticker]
    except FileNotFoundError:
        import yfinance as yf
        s = yf.download(ticker, start="1927-12-01", auto_adjust=False, progress=False)["Close"][ticker]
    return s.loc[:END].dropna()

def crashes(s, threshold=0.20):
    """One row per decline of at least `threshold` from a record close, until a new record."""
    peak = s.cummax(); dd = s / peak - 1
    rows, start = [], None
    for date, d in dd.items():
        if d &lt; 0 and start is None:
            start = date                                  # first close below the old record
        elif d == 0 and start is not None:
            seg = dd.loc[start:date]
            if seg.min() &lt;= -threshold:
                p = s.loc[:start].index[-2]               # the record close itself
                t = seg.idxmin()
                rows.append(dict(peak=p, trough=t, recovered=date, depth=seg.min()))
            start = None
    if start is not None and dd.loc[start:].min() &lt;= -threshold:   # still under water
        seg = dd.loc[start:]
        rows.append(dict(peak=s.loc[:start].index[-2], trough=seg.idxmin(), recovered=pd.NaT, depth=seg.min()))
    df = pd.DataFrame(rows)
    yrs = lambda a, b: (b - a).dt.days / 365.25
    df["down_yrs"] = yrs(df.peak, df.trough)
    df["up_yrs"] = yrs(df.trough, df.recovered)            # trough back to the old peak
    df["total_yrs"] = yrs(df.peak, df.recovered)           # peak to new record
    return df

def recover_date(s, peak, trough):
    """First close at or above the peak level after the trough (None if not yet)."""
    after = s.loc[trough:]
    hit = after[after &gt;= s.loc[peak]]
    return hit.index[0] if len(hit) else pd.NaT

if __name__ == "__main__":
    s = load()
    C = crashes(s)
    print(f"S&amp;P 500 price index, {s.index[0]:%b %d %Y} to {s.index[-1]:%b %d %Y}: {len(C)} declines of 20%+")
    print(f"{'peak':&gt;11} {'trough':&gt;11} {'recovered':&gt;11} {'depth':&gt;7} {'down':&gt;6} {'up':&gt;6} {'total':&gt;6}  (years)")
    for _, r in C.iterrows():
        rec = f"{r.recovered:%Y-%m-%d}" if pd.notna(r.recovered) else "not yet"
        print(f"{r.peak:%Y-%m-%d} {r.trough:%Y-%m-%d} {rec:&gt;11} {r.depth:&gt;7.1%} {r.down_yrs:&gt;6.1f} {r.up_yrs:&gt;6.1f} {r.total_yrs:&gt;6.1f}")
    done = C.dropna(subset=["recovered"])
    post = done[done.peak &gt;= "1946-01-01"]
    print(f"\nMedian peak-to-new-high: all {done.total_yrs.median():.1f} yrs, since 1946 {post.total_yrs.median():.1f} yrs")
    print(f"Median trough-to-old-high: all {done.up_yrs.median():.1f} yrs, since 1946 {post.up_yrs.median():.1f} yrs")
    print(f"Median depth: {done.depth.median():.1%};  current close {s.iloc[-1]:,.2f} ({s.iloc[-1]/s.max()-1:.1%} from record)")
    c = crashes(s, 0.10); c = c[(c.depth &gt; -0.20)].dropna(subset=["recovered"])
    print(f"Corrections of 10-20% from a record: {len(c)}, median {c.total_yrs.median()*12:.0f} months peak to new high, "
          f"longest {c.total_yrs.max():.1f} yrs")
    for d in (0.10, 0.20, 0.34, 0.50, 0.57, 0.86):
        print(f"  a -{d:.0%} fall needs +{1/(1-d)-1:.0%} to get back")
    tr = load("^SP500TR")                                  # dividends reinvested, starts 1988
    print("\nSince 1988, price-only vs dividends reinvested (years from peak to a new high):")
    for _, r in done[done.peak &gt;= "1988-01-04"].iterrows():
        p2 = tr.loc[r.peak - pd.Timedelta(days=10):r.trough].idxmax()
        t2 = tr.loc[p2:r.recovered].idxmin()
        rec2 = recover_date(tr, p2, t2)
        print(f"  {r.peak:%Y} crash: price {r.total_yrs:.1f} yrs, total return {(rec2 - p2).days/365.25:.1f} yrs "
              f"(depth {tr.loc[t2]/tr.loc[p2]-1:.1%})")
</code></pre>
<p>Output (data through October 29, 2026):</p>
<pre><code class="language-text">S&amp;P 500 price index, Dec 30 1927 to Oct 29 2026: 12 declines of 20%+
       peak      trough   recovered   depth   down     up  total  (years)
1929-09-16 1932-06-01  1954-09-22  -86.2%    2.7   22.3   25.0
1956-08-03 1957-10-22  1958-09-24  -21.5%    1.2    0.9    2.1
1961-12-12 1962-06-26  1963-09-03  -28.0%    0.5    1.2    1.7
1966-02-09 1966-10-07  1967-05-04  -22.2%    0.7    0.6    1.2
1968-11-29 1970-05-26  1972-03-06  -36.1%    1.5    1.8    3.3
1973-01-11 1974-10-03  1980-07-17  -48.2%    1.7    5.8    7.5
1980-11-28 1982-08-12  1982-11-03  -27.1%    1.7    0.2    1.9
1987-08-25 1987-12-04  1989-07-26  -33.5%    0.3    1.6    1.9
2000-03-24 2002-10-09  2007-05-30  -49.1%    2.5    4.6    7.2
2007-10-09 2009-03-09  2013-03-28  -56.8%    1.4    4.1    5.5
2020-02-19 2020-03-23  2020-08-18  -33.9%    0.1    0.4    0.5
2022-01-03 2022-10-12  2024-01-19  -25.4%    0.8    1.3    2.0

Median peak-to-new-high: all 2.1 yrs, since 1946 2.0 yrs
Median trough-to-old-high: all 1.5 yrs, since 1946 1.3 yrs
Median depth: -33.7%;  current close 7,720.39 (-1.3% from record)
Corrections of 10-20% from a record: 14, median 7 months peak to new high, longest 1.5 yrs
  a -10% fall needs +11% to get back
  a -20% fall needs +25% to get back
  a -34% fall needs +52% to get back
  a -50% fall needs +100% to get back
  a -57% fall needs +133% to get back
  a -86% fall needs +614% to get back

Since 1988, price-only vs dividends reinvested (years from peak to a new high):
  2000 crash: price 7.2 yrs, total return 6.1 yrs (depth -47.4%)
  2007 crash: price 5.5 yrs, total return 4.5 yrs (depth -55.3%)
  2020 crash: price 0.5 yrs, total return 0.5 yrs (depth -33.8%)
  2022 crash: price 2.0 yrs, total return 1.9 yrs (depth -24.5%)
</code></pre>
<h2>What this means for your own portfolio</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Situation</th>
<th>What 1928–2026 history says</th>
<th>What to do</th>
</tr>
</thead>
<tbody>
<tr>
<td>A 10–20% correction</td>
<td>14 cases, median 7 months to a new high, longest 1.5 years</td>
<td>Usually nothing; rebalance if your bands are hit</td>
</tr>
<tr>
<td>A 20–40% bear market</td>
<td>8 cases, 0.5 to 3.3 years to a new high</td>
<td>Keep contributing; don&#8217;t sell to cash</td>
</tr>
<tr>
<td>A 45%+ collapse</td>
<td>4 cases, 5.5 to 25 years on price</td>
<td>Hold money needed within 5 years outside stocks</td>
</tr>
<tr>
<td>You reinvest dividends</td>
<td>2000: 7.2 → 6.1 yrs; 1929: 25.0 → 15.3 yrs</td>
<td>Measure your recovery on total return, not price</td>
</tr>
<tr>
<td>You hold one stock or sector</td>
<td>Nasdaq Composite: about 15 years after 2000</td>
<td>Index history doesn&#8217;t transfer to concentrated bets</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Sources: recovery.py, shiller_1929.py; Yahoo Finance; Robert Shiller data. History, not a forecast.</figcaption></figure>
<p>Three practical takeaways from the data:</p>
<ol>
<li><strong>Plan for the median, but survive the tail.</strong> Two years is the typical wait. Waits of 5.5 to 7.5 years have happened three times since 1973. Money you need within five years arguably shouldn&#8217;t depend on a quick rebound.</li>
<li><strong>Keep contributing.</strong> The trough-to-old-high leg (median 1.5 years) is shorter than the full episode because shares bought near the bottom recover first. Our <a href="https://blog.finxter.com/lump-sum-vs-dca-sp-500/">lump sum vs DCA test</a> looks at what happens to a cash pile that waits on the sidelines instead.</li>
<li><strong>Rebalancing helps in practice.</strong> Selling bonds to buy stocks during a crash shortens your personal recovery. The <a href="https://blog.finxter.com/how-often-to-rebalance-portfolio/">how often to rebalance</a> backtest shows annual rebalancing was enough. For judging how bumpy a fund&#8217;s ride is beyond its drawdown, see <a href="https://blog.finxter.com/sharpe-ratio-vs-sortino-ratio/">Sharpe ratio vs Sortino ratio</a>.</li>
</ol>
<p>For context on today: the S&amp;P 500 closed at 7,720.39 on October 29, 2026, 1.3% below its record close of 7,818.93 on October 6, 2026. That&#8217;s nowhere near crash territory. This is history, not a forecast, and not investment advice.</p>
<h2>Limitations</h2>
<ul>
<li><strong>Only 12 crashes.</strong> The median moves if you add or drop one episode, and four of the 12 come from very different eras (pre-war, 1970s inflation, dot-com, 2008).</li>
<li><strong>Index, not your portfolio.</strong> A single stock, a sector fund, or a leveraged ETF can take far longer. The Nasdaq Composite took about 15 years to regain its March 2000 closing peak (April 2015, per <a href="https://finance.yahoo.com/quote/%5EIXIC/history/">Yahoo Finance <code>^IXIC</code></a>), and plenty of individual stocks never did.</li>
<li><strong>Nominal for most rows.</strong> Apart from the 1929 check, the numbers ignore inflation, and inflation can flip the story. In Shiller&#8217;s monthly data the 1973 crash took 7.5 years on price, only <strong>3.5 years</strong> with dividends reinvested (July 1976), but <strong>12.0 years</strong> with dividends after inflation (January 1985).</li>
<li><strong>Closing prices.</strong> Intraday lows were slightly deeper, and the 20% line is a convention, not a law (1990 and 2018 both stopped just short at −19.9% and −19.8%).</li>
</ul>
<h2>Turn your crash notes into something people read</h2>
<p>If you&#8217;ve been building this kind of analysis for yourself (drawdown rules, a rebalancing checklist, a crash playbook for a nervous spouse), it is close to being a short finance guide or a lead magnet for a newsletter. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=how-long-does-it-take-the-stock-market-to-recover-from-a-crash&amp;utm_content=end">ImagineYourBook</a> turns a prompt or your notes into a complete text draft with a cover, exported as Word and EPUB. If you already have a long write-up, its Manuscript Rewrite can reshape it into a shorter edition. The trial is $0 today but requires a card. It includes one 10,000-word Small Book, and the Author plan ($29.99/month) starts after 7 days unless you cancel before day 8. Check every number in the draft against your own script before you publish it.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked October 30, 2026):</strong> <a href="https://finance.yahoo.com/quote/%5EGSPC/history/">Yahoo Finance: S&#038;P 500 (^GSPC) history</a> · <a href="https://finance.yahoo.com/quote/%5ESP500TR/history/">Yahoo Finance: S&#038;P 500 Total Return (^SP500TR)</a> · <a href="https://finance.yahoo.com/quote/%5EIXIC/history/">Yahoo Finance: Nasdaq Composite (^IXIC)</a> · <a href="https://shillerdata.com/">Robert Shiller, U.S. stock market data (ie_data.xls)</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Scripts: recovery.py (shown above), shiller_1929.py. Not investment advice.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.finxter.com/how-long-does-it-take-the-stock-market-to-recover-from-a-crash/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Sharpe Ratio vs Sortino Ratio: A Sharpe Tie Became 2.08 vs 1.65 (Python)</title>
		<link>https://blog.finxter.com/sharpe-ratio-vs-sortino-ratio/</link>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 12:57:40 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/sharpe-ratio-vs-sortino-ratio/</guid>

					<description><![CDATA[Sharpe ratio vs Sortino ratio in Python: Bitcoin and QQQ tied on Sharpe (0.98 vs 0.97) over 10 years, but Sortino scored them 2.08 vs 1.65. Here's why.]]></description>
										<content:encoded><![CDATA[<p><strong>Sharpe ratio vs Sortino ratio, in one line:</strong> both divide your return above cash by risk, but the Sharpe ratio counts <em>all</em> volatility as risk, while the Sortino ratio counts only the months that fell short of your target. Use Sharpe to compare diversified, roughly symmetric portfolios; use Sortino when returns are lopsided; and never compare one fund&#8217;s Sharpe with another fund&#8217;s Sortino.</p>
<p>That difference is not academic. On ten years of real monthly data, Bitcoin and the Nasdaq-100 ETF QQQ finished with practically the same Sharpe ratio (0.98 vs 0.97), yet the Sortino ratio rated Bitcoin <strong>2.08</strong> and QQQ <strong>1.65</strong>. Same &#8220;quality&#8221; on one ruler, a 0.43 gap on the other, and Bitcoin still fell <strong>−75.6%</strong> from peak along the way.</p>
<p>Below are the formulas, the numbers for eight assets, a short pandas script you can run on your own portfolio, and the one Sortino mistake that shows up in a lot of copy-paste code. (If your analysis ends up worth sharing, an AI book tool like <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=sharpe-ratio-vs-sortino-ratio&amp;utm_content=top">ImagineYourBook</a> can turn your notes into a draft investing ebook or lead magnet with Word/EPUB export; more on that at the end.)</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li><strong>Sharpe</strong> divides excess return by <em>all</em> volatility; <strong>Sortino</strong> divides it by downside deviation only, so it ignores big up months.</li>
<li>Bitcoin and QQQ tied on Sharpe (0.98 vs 0.97) over Oct 2016 to Sep 2026, but Sortino rated them <strong>2.08 vs 1.65</strong>.</li>
<li>Sortino ran <strong>1.32x to 2.12x</strong> the Sharpe ratio across 8 assets, so never compare one fund&#8217;s Sharpe with another&#8217;s Sortino.</li>
<li>In 85 rolling 3-year windows the two ratios picked different winners <strong>15 times</strong>, always with Sortino favoring Bitcoin, whose max drawdown was −75.6%.</li>
<li>The popular <code>returns[returns &lt; 0].std()</code> shortcut is not Sortino: it moved results by about ±9% here. Correct pandas script included.</li>
</ul>
</div>
<h2>The two formulas, side by side</h2>
<p>Both ratios share the same numerator: the average return minus the risk-free rate (here, the 3-month Treasury bill from <a href="https://fred.stlouisfed.org/series/TB3MS">FRED&#8217;s TB3MS series</a>, which averaged 2.43% a year over the test period).</p>
<ul>
<li><strong>Sharpe ratio</strong> = average excess return ÷ standard deviation of excess returns. William Sharpe&#8217;s own <a href="https://web.stanford.edu/~wfsharpe/art/sr/sr.htm">1994 restatement</a> defines it this way.</li>
<li><strong>Sortino ratio</strong> = average excess return ÷ <strong>downside deviation</strong>. Downside deviation is the root-mean-square of the shortfalls below the target, where every month <em>at or above</em> the target counts as a shortfall of zero but still counts in the average (<a href="https://www.cmegroup.com/education/files/rr-sortino-a-sharper-ratio.pdf">Rollinger and Hoffman, &#8220;Sortino: A &#8216;Sharper&#8217; Ratio&#8221;</a>).</li>
</ul>
<p>I use monthly returns and annualize the usual way: multiply the monthly mean by 12 and the monthly deviation by √12.</p>
<h3>A worked example: QQQ</h3>
<p>For QQQ, October 2016 to September 2026:</p>
<ul>
<li>Average excess return: <strong>18.47%</strong> a year</li>
<li>Standard deviation of excess returns: <strong>18.99%</strong></li>
<li>Downside deviation: <strong>11.21%</strong></li>
</ul>
<p>So Sharpe = 18.47 ÷ 18.99 = <strong>0.97</strong>, and Sortino = 18.47 ÷ 11.21 = <strong>1.65</strong>. Same numerator, smaller denominator. That&#8217;s why Sortino ratios are almost always bigger than Sharpe ratios for assets that made money, and why &#8220;a Sortino of 1.6&#8221; and &#8220;a Sharpe of 1.0&#8221; can describe the same fund.</p>
<h2>The data: 8 assets, 120 months</h2>
<p>I pulled month-end adjusted closes (dividends reinvested) from Yahoo Finance via <a href="https://github.com/ranaroussi/yfinance">yfinance</a> for <a href="https://finance.yahoo.com/quote/SPY/history/">SPY</a>, <a href="https://finance.yahoo.com/quote/QQQ/history/">QQQ</a>, <a href="https://finance.yahoo.com/quote/BTC-USD/history/">BTC-USD</a>, <a href="https://finance.yahoo.com/quote/GLD/history/">GLD</a>, <a href="https://finance.yahoo.com/quote/SPLV/history/">SPLV</a> (low-volatility S&amp;P 500), <a href="https://finance.yahoo.com/quote/AGG/history/">AGG</a> and <a href="https://finance.yahoo.com/quote/TLT/history/">TLT</a>, plus a 60/40 SPY/AGG mix rebalanced monthly. The window covers 120 months, October 2016 to September 2026.</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Asset</th>
<th>CAGR</th>
<th>Volatility</th>
<th>Downside dev.</th>
<th>Sharpe</th>
<th>Sortino</th>
<th>Sortino ÷ Sharpe</th>
<th>Skew</th>
<th>Max drawdown</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin (BTC-USD)</td>
<td>63.6%</td>
<td>74.1%</td>
<td>34.9%</td>
<td>0.98</td>
<td>2.08</td>
<td>2.12x</td>
<td>0.62</td>
<td>−75.6%</td>
</tr>
<tr>
<td>QQQ</td>
<td>20.9%</td>
<td>19.0%</td>
<td>11.2%</td>
<td>0.97</td>
<td>1.65</td>
<td>1.69x</td>
<td>−0.21</td>
<td>−32.6%</td>
</tr>
<tr>
<td>SPY</td>
<td>15.2%</td>
<td>15.3%</td>
<td>9.7%</td>
<td>0.85</td>
<td>1.34</td>
<td>1.58x</td>
<td>−0.44</td>
<td>−23.9%</td>
</tr>
<tr>
<td>60/40 SPY/AGG</td>
<td>9.6%</td>
<td>10.2%</td>
<td>6.7%</td>
<td>0.72</td>
<td>1.10</td>
<td>1.53x</td>
<td>−0.42</td>
<td>−20.0%</td>
</tr>
<tr>
<td>GLD</td>
<td>11.7%</td>
<td>15.5%</td>
<td>8.9%</td>
<td>0.64</td>
<td>1.10</td>
<td>1.72x</td>
<td>0.11</td>
<td>−23.8%</td>
</tr>
<tr>
<td>SPLV</td>
<td>7.7%</td>
<td>12.8%</td>
<td>8.8%</td>
<td>0.45</td>
<td>0.66</td>
<td>1.45x</td>
<td>−0.62</td>
<td>−21.4%</td>
</tr>
<tr>
<td>AGG</td>
<td>1.1%</td>
<td>5.2%</td>
<td>3.9%</td>
<td>−0.23</td>
<td>−0.30</td>
<td>1.32x</td>
<td>−0.03</td>
<td>−17.1%</td>
</tr>
<tr>
<td>TLT</td>
<td>−2.8%</td>
<td>13.6%</td>
<td>10.0%</td>
<td>−0.32</td>
<td>−0.44</td>
<td>1.36x</td>
<td>0.28</td>
<td>−47.6%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Monthly returns Oct 2016 to Sep 2026 (120 months), Yahoo Finance adjusted closes; risk-free and Sortino target = 3-month T-bill (FRED TB3MS). Sorted by Sharpe. sharpe_sortino.py</figcaption></figure>
<p>Three things stand out.</p>
<p><strong>1. The Sortino-to-Sharpe multiple is different for every asset.</strong> It ranges from 1.32x (AGG) to 2.12x (Bitcoin). There is no conversion factor, so a fund page quoting Sortino can&#8217;t be lined up against another quoting Sharpe.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-sharpe-vs-sortino-bars.png" alt="Grouped bar chart of Sharpe and Sortino ratios, monthly returns Oct 2016 to Sep 2026: Bitcoin 0.98 and 2.08 (2.12x), QQQ 0.97 and 1.65, SPY 0.85 and 1.34, 60/40 0.72 and 1.10, GLD 0.64 and 1.10, SPLV 0.45 and 0.66, AGG -0.23 and -0.30, TLT -0.32 and -0.44" class="wp-image-1672435" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-sharpe-vs-sortino-bars.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-sharpe-vs-sortino-bars-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-sharpe-vs-sortino-bars-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-sharpe-vs-sortino-bars-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">The Sortino-to-Sharpe multiple ranged from 1.32x (AGG) to 2.12x (Bitcoin): there is no fixed conversion (sharpe_sortino.py).</figcaption></figure>
<p><strong>2. Skew drives the multiple.</strong> Bitcoin&#8217;s monthly returns had a positive skew of 0.62: its big months were up months. Sortino doesn&#8217;t penalize those, so its denominator shrinks much more than QQQ&#8217;s (skew −0.21) or SPY&#8217;s (skew −0.44). Gold, with a mildly positive skew (0.11), got the second-highest multiple, 1.72x.</p>
<p><strong>3. Rankings can flip.</strong> By Sharpe, the 60/40 portfolio beats gold, 0.718 to 0.641. By Sortino, gold edges ahead, 1.105 to 1.098. Same data, same months, different winner.</p>
<h2>Why Bitcoin and QQQ tie on Sharpe but not on Sortino</h2>
<p>Bitcoin&#8217;s Sharpe beat QQQ&#8217;s by just 0.006. Its Sortino beat QQQ&#8217;s by 0.43. Here&#8217;s what each ratio is looking at:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-monthly-excess-returns.png" alt="Two bar charts of monthly excess returns, Oct 2016 to Sep 2026, up months grey and down months coral: Bitcoin volatility 74.1%, downside deviation 34.9%, max drawdown -75.6%; QQQ volatility 19.0%, downside deviation 11.2%, max drawdown -32.6%" class="wp-image-1672436" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-monthly-excess-returns.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-monthly-excess-returns-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-monthly-excess-returns-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-monthly-excess-returns-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Sharpe counts every bar; Sortino only the coral ones, so Bitcoin&#8217;s big up months stop counting as risk.</figcaption></figure>
<p>Bitcoin&#8217;s volatility (74.1%) is almost four times QQQ&#8217;s (19.0%), and much of it comes from huge up months. Sharpe charges Bitcoin for every one of those grey bars. Sortino charges nothing for them, so Bitcoin&#8217;s risk measure drops from 74.1% to 34.9%, while QQQ&#8217;s drops from 19.0% to 11.2%.</p>
<p>Interesting detail: Bitcoin actually had <strong>more</strong> months below the T-bill rate, 53 of 120 versus 41 for QQQ. Sortino still likes it more, because downside deviation measures how deep the shortfalls are relative to the return, not how often they happen.</p>
<p>And here&#8217;s the catch that neither ratio shows: Bitcoin&#8217;s <strong>maximum drawdown was −75.6%</strong>, QQQ&#8217;s −32.6%. A high Sortino is not a promise of shallow losses. It says the <em>typical</em> monthly downside was small relative to the average return, not that the worst stretch was survivable.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0">
<p><strong>Rule:</strong> a Sortino ratio tells you the bad months were small <em>on average</em>. It says nothing about how many bad months came in a row. Always print max drawdown next to it.</p>
</div>
<h2>Does the disagreement last? 85 rolling 3-year windows</h2>
<p>One 10-year number can be a fluke, so I recomputed both ratios for Bitcoin and QQQ in every rolling 36-month window (85 windows, ending September 2019 to September 2026).</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/10/sharpe-ratio-vs-sortino-ratio-rolling-gap.gif" alt="Animated line chart of Bitcoin minus QQQ in rolling 36-month windows ending Sep 2019 to Sep 2026: the Sharpe gap and Sortino gap move together but Sortino swings wider; sand bands mark the 15 windows where the two ratios pick different winners" class="wp-image-1672437"/><figcaption class="wp-element-caption">85 rolling 3-year windows: the ratios disagreed 15 times, and Sortino picked Bitcoin every time.</figcaption></figure>
<ul>
<li>Bitcoin beat QQQ on Sharpe in <strong>41</strong> of 85 windows.</li>
<li>Bitcoin beat QQQ on Sortino in <strong>56</strong> of 85 windows.</li>
<li>The two ratios disagreed on the winner in <strong>15</strong> windows, and in all 15, Sortino picked Bitcoin.</li>
</ul>
<p>That&#8217;s the pattern you&#8217;d expect from a positively skewed asset. Sortino isn&#8217;t wrong here; it&#8217;s answering a different question. If upside surprises are fine with you, Sortino is the more honest score. If you&#8217;d have sold in a 75% crash, look at drawdown, not either ratio.</p>
<h2>The Sortino mistake in a lot of Python code</h2>
<p>A common shortcut is to take <code>returns[returns &lt; 0].std()</code> as the downside risk. That&#8217;s not Sortino&#8217;s formula. It drops the good months from the average entirely (instead of counting them as zero shortfall) and measures spread around the mean of the bad months instead of distance below the target. The Red Rock paper linked above walks through this exact error.</p>
<p>How much does it matter on real data? With the shortcut, Bitcoin&#8217;s Sortino rises from 2.08 to <strong>2.27</strong> (+9.2%), while SPY&#8217;s falls from 1.34 to <strong>1.21</strong> (−9.1%). So the shortcut flatters one asset and penalizes another, in the same table.</p>
<p>If you build returns with pandas, Finxter&#8217;s guide to <a href="https://blog.finxter.com/pandas-dataframe-pct_change-method/">pandas <code>pct_change()</code></a> covers the first step; the script below does the rest correctly.</p>
<h2>The Python script</h2>
<p>It downloads the prices (or uses a cached copy), builds the 60/40 mix, and prints the full table, both rankings and the rolling comparison. Requirements: <code>pip install yfinance pandas</code>.</p>
<pre><code class="language-python">"""Sharpe ratio vs Sortino ratio on 10 years of real monthly returns (Oct 2016 to Sep 2026).
Prices: Yahoo Finance adjusted closes (dividends reinvested) via yfinance. Cash rate: FRED TB3MS.
pip install yfinance pandas"""
import numpy as np, pandas as pd

TICKERS = ["SPY", "QQQ", "BTC-USD", "GLD", "SPLV", "AGG", "TLT"]
START, END = "2016-09-30", "2026-09-30"

def load():
    try:  # cached copies used for the article
        adj = pd.read_pickle("src/raw_yf.pkl")["Adj Close"][TICKERS]
        tb = pd.read_csv("src/tb3ms.csv", parse_dates=[0], index_col=0).iloc[:, 0]
    except FileNotFoundError:
        import yfinance as yf
        adj = yf.download(TICKERS, start="2016-01-01", end="2026-10-01", auto_adjust=False, progress=False)["Adj Close"]
        tb = pd.read_csv("https://fred.stlouisfed.org/graph/fredgraph.csv?id=TB3MS", parse_dates=[0], index_col=0).iloc[:, 0]
    r = adj.resample("ME").last().loc[START:END].pct_change().iloc[1:]
    r["60/40"] = 0.6 * r["SPY"] + 0.4 * r["AGG"]          # rebalanced monthly
    tb.index = tb.index + pd.offsets.MonthEnd(0)
    rf = (tb / 100 / 12).reindex(r.index).ffill()           # monthly T-bill return
    return r, rf

def sharpe(r, rf):
    ex = r.sub(rf, axis=0)
    return ex.mean() / ex.std() * np.sqrt(12)

def downside_dev(r, rf):
    """Sortino's target downside deviation: RMS of the shortfalls below the target, over ALL months."""
    ex = r.sub(rf, axis=0)
    return np.sqrt((ex.clip(upper=0) ** 2).mean()) * np.sqrt(12)

def sortino(r, rf):
    return r.sub(rf, axis=0).mean() * 12 / downside_dev(r, rf)

def sortino_wrong(r, rf):
    """Common shortcut: std() of only the negative months. Not Sortino's formula."""
    ex = r.sub(rf, axis=0)
    return ex.mean() * 12 / (ex[ex &lt; 0].std() * np.sqrt(12))

def rolling(r, rf, a="BTC-USD", b="QQQ", months=36):
    """Sharpe and Sortino of two assets in every rolling window (end month as index)."""
    rows = []
    for i in range(months, len(r) + 1):
        w, f = r.iloc[i - months:i][[a, b]], rf.iloc[i - months:i]
        sh, so = sharpe(w, f), sortino(w, f)
        rows.append({"end": w.index[-1], "sh_a": sh[a], "sh_b": sh[b], "so_a": so[a], "so_b": so[b]})
    return pd.DataFrame(rows).set_index("end")

def table(r, rf):
    wealth = (1 + r).cumprod()
    return pd.DataFrame({
        "CAGR": wealth.iloc[-1] ** (12 / len(r)) - 1,
        "Vol": r.std() * np.sqrt(12),
        "DownDev": downside_dev(r, rf),
        "Sharpe": sharpe(r, rf),
        "Sortino": sortino(r, rf),
        "Sortino/Sharpe": sortino(r, rf) / sharpe(r, rf),
        "Skew": r.skew(),
        "MaxDD": (wealth / wealth.cummax() - 1).min(),
        "Shortcut": sortino_wrong(r, rf),
    }).sort_values("Sharpe", ascending=False)

if __name__ == "__main__":
    r, rf = load()
    T = table(r, rf)
    print(f"{len(r)} months, {r.index[0]:%b %Y} to {r.index[-1]:%b %Y}; risk-free = 3-month T-bill (avg {rf.mean()*12:.2%}/yr)")
    print(f"{'':8}{'CAGR':&gt;7}{'Vol':&gt;7}{'DownDev':&gt;8}{'Sharpe':&gt;7}{'Sortino':&gt;8}{'So/Sh':&gt;6}{'Skew':&gt;6}{'MaxDD':&gt;7}{'Shortcut':&gt;9}")
    for k, s in T.iterrows():
        print(f"{k:8}{s.CAGR:&gt;7.1%}{s.Vol:&gt;7.1%}{s.DownDev:&gt;8.1%}{s.Sharpe:&gt;7.2f}{s.Sortino:&gt;8.2f}"
              f"{s['Sortino/Sharpe']:&gt;6.2f}{s.Skew:&gt;6.2f}{s.MaxDD:&gt;7.1%}{s.Shortcut:&gt;9.2f}")
    print("\nRank by Sharpe :", " &gt; ".join(T.index))
    print("Rank by Sortino:", " &gt; ".join(T.sort_values("Sortino", ascending=False).index))
    ex = r["QQQ"] - rf
    print(f"QQQ worked: mean excess {ex.mean()*12:.2%}/yr, std of excess {ex.std()*np.sqrt(12):.2%}, "
          f"downside dev {T.loc['QQQ', 'DownDev']:.2%} -&gt; {ex.mean()*12/(ex.std()*np.sqrt(12)):.2f} and {T.loc['QQQ', 'Sortino']:.2f}")
    g, s = T.loc["GLD"], T.loc["60/40"]
    print(f"GLD vs 60/40: Sharpe {g.Sharpe:.3f} vs {s.Sharpe:.3f}, Sortino {g.Sortino:.3f} vs {s.Sortino:.3f}")
    b, q = T.loc["BTC-USD"], T.loc["QQQ"]
    print(f"BTC vs QQQ: Sharpe gap {b.Sharpe - q.Sharpe:.3f}, Sortino gap {b.Sortino - q.Sortino:.2f}, "
          f"down months {(r['BTC-USD'] &lt; rf).sum()} vs {(r['QQQ'] &lt; rf).sum()} of {len(r)}")
    R = rolling(r, rf)
    print(f"\n{len(R)} rolling 36-month windows (ending {R.index[0]:%b %Y} to {R.index[-1]:%b %Y}):")
    print(f"  BTC beats QQQ on Sharpe in {(R.sh_a &gt; R.sh_b).sum()}, on Sortino in {(R.so_a &gt; R.so_b).sum()}")
    dis = (R.sh_a &gt; R.sh_b) != (R.so_a &gt; R.so_b)
    print(f"  Sharpe and Sortino disagree on the winner in {dis.sum()} windows; Sortino picks BTC in {(dis &amp; (R.so_a &gt; R.so_b)).sum()} of them")
</code></pre>
<p>Output, as run for this article:</p>
<pre><code class="language-text">120 months, Oct 2016 to Sep 2026; risk-free = 3-month T-bill (avg 2.43%/yr)
           CAGR    Vol DownDev Sharpe Sortino So/Sh  Skew  MaxDD Shortcut
BTC-USD   63.6%  74.1%   34.9%   0.98    2.08  2.12  0.62 -75.6%     2.27
QQQ       20.9%  19.0%   11.2%   0.97    1.65  1.69 -0.21 -32.6%     1.55
SPY       15.2%  15.3%    9.7%   0.85    1.34  1.58 -0.44 -23.9%     1.21
60/40      9.6%  10.2%    6.7%   0.72    1.10  1.53 -0.42 -20.0%     1.02
GLD       11.7%  15.5%    8.9%   0.64    1.10  1.72  0.11 -23.8%     1.12
SPLV       7.7%  12.8%    8.8%   0.45    0.66  1.45 -0.62 -21.4%     0.62
AGG        1.1%   5.2%    3.9%  -0.23   -0.30  1.32 -0.03 -17.1%    -0.32
TLT       -2.8%  13.6%   10.0%  -0.32   -0.44  1.36  0.28 -47.6%    -0.54

Rank by Sharpe : BTC-USD &gt; QQQ &gt; SPY &gt; 60/40 &gt; GLD &gt; SPLV &gt; AGG &gt; TLT
Rank by Sortino: BTC-USD &gt; QQQ &gt; SPY &gt; GLD &gt; 60/40 &gt; SPLV &gt; AGG &gt; TLT
QQQ worked: mean excess 18.47%/yr, std of excess 18.99%, downside dev 11.21% -&gt; 0.97 and 1.65
GLD vs 60/40: Sharpe 0.641 vs 0.718, Sortino 1.105 vs 1.098
BTC vs QQQ: Sharpe gap 0.006, Sortino gap 0.43, down months 53 vs 41 of 120

85 rolling 36-month windows (ending Sep 2019 to Sep 2026):
  BTC beats QQQ on Sharpe in 41, on Sortino in 56
  Sharpe and Sortino disagree on the winner in 15 windows; Sortino picks BTC in 15 of them
</code></pre>
<p>To use it on your own holdings, replace <code>TICKERS</code> with your funds, or swap <code>r</code> for your portfolio&#8217;s own monthly returns. If you want the benchmark side too, the earlier post on how to <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">compare your portfolio to the S&amp;P 500</a> builds a cash-flow-adjusted S&amp;P 500 twin in Python.</p>
<p>Prefer not to maintain a script? <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free browser-based tracker that reports observed Sharpe and Sortino ratios and drawdown for your own trades, plus an S&amp;P 500 and Bitcoin comparison. It needs no account or broker connection, keeps data in your browser and backs it up to Excel. Prices are entered manually, and it states that Sharpe and Sortino need a regular return series to be meaningful.</p>
<h2>Which one should you use?</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Situation</th>
<th>Use</th>
<th>Why (from this data)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Comparing index funds or diversified portfolios</td>
<td>Sharpe (Sortino as a check)</td>
<td>SPY, 60/40 and QQQ kept the same order on both ratios</td>
</tr>
<tr>
<td>Skewed assets: crypto, trend-following, options</td>
<td>Both, side by side</td>
<td>Bitcoin vs QQQ: Sharpe gap 0.006, Sortino gap 0.43</td>
</tr>
<tr>
<td>Judging how bad a crash could feel</td>
<td>Max drawdown</td>
<td>Neither ratio flagged Bitcoin&#8217;s −75.6% vs QQQ&#8217;s −32.6%</td>
</tr>
<tr>
<td>Comparing numbers from two websites</td>
<td>Only the same ratio, same window and frequency</td>
<td>Sortino ÷ Sharpe ranged 1.32x to 2.12x</td>
</tr>
<tr>
<td>Writing your own code</td>
<td>Target downside deviation over all periods</td>
<td>The negative-months std() shortcut moved results ±9%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Sources: sharpe_sortino.py results; Rollinger &#038; Hoffman, Red Rock Capital; W. F. Sharpe (1994).</figcaption></figure>
<p>My rule of thumb after running these numbers:</p>
<ol>
<li><strong>Comparing diversified portfolios or index funds?</strong> Use Sharpe. Their returns are close enough to symmetric that the extra complexity of Sortino rarely changes the ranking. If you&#8217;re tuning a portfolio&#8217;s mix, rebalancing matters more; the <a href="https://blog.finxter.com/how-often-to-rebalance-portfolio/">how often to rebalance</a> backtest found once a year enough on a 40-year 60/40.</li>
<li><strong>Comparing skewed assets</strong> (crypto, trend-following, option-selling, single stocks)? Report both. A big Sortino-over-Sharpe gap tells you the return distribution is lopsided, which is information in itself.</li>
<li><strong>Always add max drawdown.</strong> Neither ratio warned that Bitcoin&#8217;s worst fall was 2.3 times deeper than QQQ&#8217;s.</li>
<li><strong>Use one convention throughout:</strong> same frequency, same risk-free rate, same window. A monthly Sharpe and a daily Sharpe of the same fund won&#8217;t match.</li>
<li><strong>Be careful with negative ratios.</strong> AGG and TLT lost to cash over this decade, and their Sortino ratios (−0.30, −0.44) look &#8220;worse&#8221; than their Sharpe ratios. With a negative numerator, a smaller denominator makes the number more negative, so don&#8217;t read much into the ranking below zero.</li>
</ol>
<h2>Limitations</h2>
<p>This is a 10-year sample that includes Bitcoin&#8217;s 2017 and 2020-21 booms and a sharp bond sell-off; another decade could rank things differently. Yahoo&#8217;s adjusted closes include fund fees and dividends, but not taxes or trading costs. The risk-free rate is the monthly T-bill, used both as the Sharpe baseline and the Sortino target; using 0% as the target would change the Sortino values. Past ratios don&#8217;t predict future returns. Nothing here is investment advice.</p>
<h2>Takeaway</h2>
<p>The Sharpe ratio vs Sortino ratio question has a short answer. Sharpe treats every swing as risk, while Sortino only counts the bad months. That&#8217;s why they agree on boring, symmetric portfolios and drift apart on lopsided ones, by 0.43 on the same Sharpe for Bitcoin and QQQ in this test. Calculate both with the right formula, quote them with max drawdown, and you&#8217;ll catch what a single number hides.</p>
<p>If you&#8217;ve got a spreadsheet full of analysis like this and want to turn it into something shareable, like a short investing guide, a newsletter lead magnet or a family-finance handbook, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=sharpe-ratio-vs-sortino-ratio&amp;utm_content=end">ImagineYourBook</a> turns a prompt or notes into a complete text draft with a cover, which you then edit and export to Word or EPUB. The trial is $0 for 7 days and includes one 10,000-word Small Book; a card is required, and the Author plan is $29.99 a month after day 7 unless you cancel before day 8. Check the numbers in your draft against your own data before you publish.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked October 29, 2026):</strong> <a href="https://finance.yahoo.com/quote/QQQ/history/">Yahoo Finance price histories (SPY, QQQ, BTC-USD, GLD, SPLV, AGG, TLT) via yfinance</a> · <a href="https://fred.stlouisfed.org/series/TB3MS">FRED: 3-Month Treasury Bill rate (TB3MS)</a> · <a href="https://web.stanford.edu/~wfsharpe/art/sr/sr.htm">William F. Sharpe, The Sharpe Ratio (1994)</a> · <a href="https://www.cmegroup.com/education/files/rr-sortino-a-sharper-ratio.pdf">Rollinger &#038; Hoffman, Sortino: A &#8216;Sharper&#8217; Ratio (Red Rock Capital, PDF)</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://portfoliotracker24.com/">PortfolioTracker24 (features as described on its site)</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: sharpe_sortino.py (shown above). Not investment advice.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How Often to Rebalance Portfolio? Once a Year (39 Trades Matched 477 in 40 Years)</title>
		<link>https://blog.finxter.com/how-often-to-rebalance-portfolio/</link>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Tue, 27 Oct 2026 16:54:43 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/how-often-to-rebalance-portfolio/</guid>

					<description><![CDATA[How often to rebalance portfolio? Once a year: in a 40-year 60/40 Python backtest, 39 trades matched monthly's 477 on Sharpe with a smaller drawdown.]]></description>
										<content:encoded><![CDATA[<p><strong>How often to rebalance a portfolio? Once a year is enough for most people.</strong> I backtested a 60/40 portfolio on real Vanguard fund data from December 1986 to September 2026. Rebalancing every December took <strong>39 trades</strong>. Monthly rebalancing took <strong>477</strong>. Both landed on the same Sharpe ratio (0.64 vs 0.63), and the yearly version actually had the smaller worst drawdown (−30.8% vs −32.5%).</p>
<p>Never rebalancing is the one choice that really changes the result. Leave a &#8220;60/40&#8221; alone for 40 years and it ends up <strong>93.9% stocks</strong>. You get a higher return, but you&#8217;re also holding a much riskier portfolio than the one you picked.</p>
<p>Below you&#8217;ll find the numbers, the charts, and the full Python script, so you can rerun it on your own funds.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li><strong>Once a year is enough:</strong> 39 yearly rebalances matched 477 monthly ones on Sharpe ratio (0.64 vs 0.63) in a 60/40 VFINX/VBMFX backtest, Dec 1986 to Sep 2026.</li>
<li>Yearly rebalancing even had the smaller worst drawdown: <strong>−30.8%</strong> vs −32.5% for monthly.</li>
<li>A 5-point band (trade only when stocks leave 55-65%) needed just <strong>26 trades</strong> for the best Sharpe (0.65).</li>
<li><strong>Never rebalancing</strong> earned more ($442,046 vs $312,520 for yearly, from $10,000) but drifted to <strong>93.9% stocks</strong> and fell −38.7% in 2008-09.</li>
<li>Across 358 rolling 10-year windows, monthly vs yearly differed by a median of only −0.11 points a year. Python script included.</li>
</ul>
</div>
<h2>The setup: two real funds, five rules</h2>
<p>I wanted to test funds you could actually have owned, not idealized indexes:</p>
<ul>
<li><strong>Stocks:</strong> VFINX (Vanguard 500 Index Fund, tracks the S&amp;P 500)</li>
<li><strong>Bonds:</strong> VBMFX (Vanguard Total Bond Market Index Fund)</li>
</ul>
<p>Both series are Yahoo Finance <em>adjusted</em> closes, so dividends and interest are reinvested and fund fees are already included. Each test starts with $10,000 at 60% stocks and 40% bonds on December 31, 1986, and runs on month-end data for 477 months through September 30, 2026. The 3-month T-bill rate from FRED is the cash rate for the Sharpe ratio.</p>
<p>The only thing that changes between tests is the rebalancing rule:</p>
<ol>
<li><strong>Never</strong> (buy and hold)</li>
<li><strong>Monthly</strong>, back to 60/40 at every month-end</li>
<li><strong>Quarterly</strong>, every March, June, September and December</li>
<li><strong>Once a year</strong>, every December</li>
<li><strong>5-point band</strong>: check every month, but only trade when stocks drift more than 5 percentage points from 60% (below 55% or above 65%)</li>
</ol>
<p>Here is the script. It needs <code>pip install yfinance pandas</code>. If you&#8217;re new to time-series resampling, Finxter&#8217;s <a href="https://blog.finxter.com/pandas-dataframe-resample-method/">pandas DataFrame resample() guide</a> explains the <code>resample("ME").last()</code> line that turns daily prices into month-end prices.</p>
<pre><code class="language-python">"""How often to rebalance a 60/40 portfolio? Backtest on real fund data, Dec 1986 to Sep 2026.
Stocks: VFINX (Vanguard 500 Index, S&amp;P 500). Bonds: VBMFX (Vanguard Total Bond Market Index).
Yahoo Finance adjusted closes (dividends reinvested, fund fees included), month-ends.
Cash rate for Sharpe: FRED TB3MS (3-month T-bill).  pip install yfinance pandas"""
import pandas as pd

START, END, W = "1986-12-31", "2026-09-30", 0.60

def load():
    try:  # cached copies used for the article
        adj = pd.read_pickle("src/raw_yf.pkl")["Adj Close"][["VFINX", "VBMFX"]]
        tb = pd.read_csv("src/tb3ms.csv", parse_dates=[0], index_col=0).iloc[:, 0]
    except FileNotFoundError:
        import yfinance as yf
        adj = yf.download(["VFINX", "VBMFX"], start="1986-12-01", auto_adjust=False, progress=False)["Adj Close"]
        tb = pd.read_csv("https://fred.stlouisfed.org/graph/fredgraph.csv?id=TB3MS", parse_dates=[0], index_col=0).iloc[:, 0]
    px = adj.resample("ME").last().loc[START:END]
    tb.index = tb.index + pd.offsets.MonthEnd(0)
    rf = (tb / 100 / 12).reindex(px.index).ffill()
    return px.pct_change().dropna(), rf.iloc[1:]

def backtest(r, rule, w=W, band=0.05, start_value=10_000):
    """rule: 'never' | 'monthly' | 'quarterly' | 'annual' | 'band'. Rebalance at month-end after returns."""
    s, b = start_value * w, start_value * (1 - w)
    vals, wts, n_reb, turnover = [], [], 0, 0.0
    for d, (rs, rb) in r.iterrows():
        s *= 1 + rs; b *= 1 + rb; v = s + b; eq = s / v
        due = {"never": False, "monthly": True, "quarterly": d.month in (3, 6, 9, 12),
               "annual": d.month == 12, "band": abs(eq - w) &gt; band}[rule]
        if due and abs(eq - w) &gt; 1e-12:
            turnover += abs(eq - w) * v; n_reb += 1
            s, b = v * w, v * (1 - w); eq = w
        vals.append(v); wts.append(eq)
    return pd.DataFrame({"value": vals, "equity_w": wts}, index=r.index), n_reb, turnover

def stats(df, rf, n_reb, turnover):
    v = df.value; m = v.pct_change().fillna(v.iloc[0] / 10_000 - 1); yrs = len(v) / 12
    ex = m - rf.values
    return dict(end=v.iloc[-1], cagr=(v.iloc[-1] / 10_000) ** (1 / yrs) - 1, vol=m.std() * 12 ** 0.5,
                max_dd=(v / v.cummax() - 1).min(), dd_at=(v / v.cummax() - 1).idxmin(),
                sharpe=ex.mean() / m.std() * 12 ** 0.5, rebalances=n_reb,
                turnover_yr=turnover / v.mean() / yrs, max_eq=df.equity_w.max(), end_eq=df.equity_w.iloc[-1])

RULES = {"never": "Never (buy &amp; hold)", "monthly": "Monthly", "quarterly": "Quarterly",
         "annual": "Once a year (Dec)", "band": "When off by 5 points"}

def run():
    r, rf = load()
    res = {k: backtest(r, k) for k in RULES}
    return r, rf, res, {k: stats(df, rf, n, t) for k, (df, n, t) in res.items()}

def rolling(r, rf, years=10):
    """CAGR and max drawdown of each rule in every rolling `years` window (monthly starts)."""
    n, rows = years * 12, []
    for i in range(0, len(r) - n + 1):
        sub = r.iloc[i:i + n]; row = {"start": sub.index[0]}
        for k in ("never", "monthly", "annual", "band"):
            v = backtest(sub, k)[0].value
            row[k] = (v.iloc[-1] / 10_000) ** (1 / years) - 1; row[k + "_dd"] = (v / v.cummax() - 1).min()
        rows.append(row)
    return pd.DataFrame(rows).set_index("start")

def napkin_cost(turnover_yr, cost=0.002, value=100_000):
    """ASSUMPTION: each dollar traded costs `cost` (spread, fees or tax drag). Annual drag in $ on `value`."""
    return turnover_yr * cost * value

if __name__ == "__main__":
    r, rf, res, S = run()
    print(f"60/40 VFINX/VBMFX, $10,000 on {START}, month-end data to {END} ({len(r)} months)")
    print(f"{'Rule':&lt;22}{'End value':&gt;11}{'CAGR':&gt;7}{'Vol':&gt;7}{'MaxDD':&gt;8}{'Sharpe':&gt;7}{'Trades':&gt;7}{'Turn/yr':&gt;8}{'Max stock%':&gt;11}")
    for k, s in S.items():
        print(f"{RULES[k]:&lt;22}{s['end']:&gt;11,.0f}{s['cagr']:&gt;7.2%}{s['vol']:&gt;7.2%}{s['max_dd']:&gt;8.1%}{s['sharpe']:&gt;7.2f}"
              f"{s['rebalances']:&gt;7}{s['turnover_yr']:&gt;8.1%}{s['max_eq']:&gt;11.1%}")
    w, a = res["never"][0].equity_w, res["annual"][0].equity_w
    print(f"\nBuy &amp; hold ends at {S['never']['end_eq']:.1%} stocks; it entered the Oct 2007 peak at {w.loc['2007-10-31']:.1%} stocks.")
    print(f"Buy &amp; hold first passed 70% stocks in {w[w &gt; .7].index[0]:%b %Y} and 80% in {w[w &gt; .8].index[0]:%b %Y}.")
    print(f"Annual rebalancing still drifted between {a.min():.1%} ({a.idxmin():%b %Y}) and {a.max():.1%} stocks between Decembers.")
    print(f"All five rules hit their worst drawdown in {S['never']['dd_at']:%b %Y}.")
    R = rolling(r, rf)
    print(f"\n{len(R)} rolling 10-year windows (starts {R.index[0]:%b %Y} to {R.index[-1]:%b %Y}):")
    for k in ("monthly", "band"):
        g = R[k] - R["annual"]
        print(f"  {RULES[k]} minus annual CAGR: median {g.median()*100:+.2f} pts, range {g.min()*100:+.2f} to {g.max()*100:+.2f}")
    g = R["never"] - R["annual"]; dd = R["never_dd"] - R["annual_dd"]
    print(f"  Never minus annual CAGR: median {g.median()*100:+.2f} pts; never had the deeper drawdown in {(dd &lt; 0).mean():.0%} of windows")
    print(f"\nNapkin (assumption: 0.2% cost per dollar traded, $100,000 portfolio):")
    for k in ("monthly", "quarterly", "annual", "band"):
        print(f"  {RULES[k]:&lt;22} {napkin_cost(S[k]['turnover_yr']):&gt;6,.0f} $/yr")
</code></pre>
<h2>The result: how often matters less than whether</h2>
<p>Output of <code>python3 rebalance.py</code>:</p>
<pre><code class="language-text">60/40 VFINX/VBMFX, $10,000 on 1986-12-31, month-end data to 2026-09-30 (477 months)
Rule                    End value   CAGR    Vol   MaxDD Sharpe Trades Turn/yr Max stock%
Never (buy &amp; hold)        442,046 10.00% 11.75%  -38.7%   0.61      0    0.0%      93.9%
Monthly                   304,242  8.97%  9.54%  -32.5%   0.63    477    9.2%      60.0%
Quarterly                 313,794  9.06%  9.50%  -32.0%   0.64    159    6.1%      63.6%
Once a year (Dec)         312,520  9.05%  9.53%  -30.8%   0.64     39    3.3%      67.9%
When off by 5 points      325,566  9.16%  9.62%  -31.9%   0.65     26    3.6%      65.0%

Buy &amp; hold ends at 93.9% stocks; it entered the Oct 2007 peak at 78.6% stocks.
Buy &amp; hold first passed 70% stocks in Nov 1995 and 80% in Dec 1998.
Annual rebalancing still drifted between 47.9% (Nov 2008) and 67.9% stocks between Decembers.
All five rules hit their worst drawdown in Feb 2009.

358 rolling 10-year windows (starts Jan 1987 to Oct 2016):
  Monthly minus annual CAGR: median -0.11 pts, range -0.38 to +0.12
  When off by 5 points minus annual CAGR: median +0.03 pts, range -0.35 to +0.31
  Never minus annual CAGR: median -0.09 pts; never had the deeper drawdown in 73% of windows

Napkin (assumption: 0.2% cost per dollar traded, $100,000 portfolio):
  Monthly                    18 $/yr
  Quarterly                  12 $/yr
  Once a year (Dec)           7 $/yr
  When off by 5 points        7 $/yr
</code></pre>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Rule</th>
<th>$10,000 became</th>
<th>CAGR</th>
<th>Volatility</th>
<th>Max drawdown</th>
<th>Sharpe</th>
<th>Trades</th>
<th>Max stock share</th>
</tr>
</thead>
<tbody>
<tr>
<td>Never (buy &#038; hold)</td>
<td>$442,046</td>
<td>10.00%</td>
<td>11.75%</td>
<td>−38.7%</td>
<td>0.61</td>
<td>0</td>
<td>93.9%</td>
</tr>
<tr>
<td>Monthly</td>
<td>$304,242</td>
<td>8.97%</td>
<td>9.54%</td>
<td>−32.5%</td>
<td>0.63</td>
<td>477</td>
<td>60.0%</td>
</tr>
<tr>
<td>Quarterly</td>
<td>$313,794</td>
<td>9.06%</td>
<td>9.50%</td>
<td>−32.0%</td>
<td>0.64</td>
<td>159</td>
<td>63.6%</td>
</tr>
<tr>
<td>Once a year (Dec)</td>
<td>$312,520</td>
<td>9.05%</td>
<td>9.53%</td>
<td>−30.8%</td>
<td>0.64</td>
<td>39</td>
<td>67.9%</td>
</tr>
<tr>
<td>When off by 5 points</td>
<td>$325,566</td>
<td>9.16%</td>
<td>9.62%</td>
<td>−31.9%</td>
<td>0.65</td>
<td>26</td>
<td>65.0%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">60% VFINX / 40% VBMFX, month-end Yahoo Finance adjusted closes, Dec 31, 1986 to Sep 30, 2026. Sharpe uses FRED TB3MS. No taxes or trading costs. rebalance.py</figcaption></figure>
<p>The three calendar rules are almost identical: 8.97% to 9.06% a year, about 9.5% volatility, and Sharpe ratios of 0.63 to 0.64. <strong>Rebalancing 12 times more often bought you nothing.</strong> Monthly came out a hair behind because it held exactly 60% stocks all the time. The yearly portfolio averaged 60.6% stocks because it drifted a little between Decembers, and in a market where stocks win, that small extra share earns a bit more.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-trades-vs-results.png" alt="Bar chart of rebalancing events for a 60/40 portfolio, Dec 1986 to Sep 2026: monthly 477 trades, Sharpe 0.63, max drawdown -32.5%; quarterly 159, 0.64, -32.0%; once a year 39, 0.64, -30.8%; 5-point band 26, 0.65, -31.9%; never 0, 0.61, -38.7%" class="wp-image-1672412" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-trades-vs-results.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-trades-vs-results-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-trades-vs-results-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-trades-vs-results-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">39 yearly trades matched 477 monthly ones on Sharpe and drawdown (Yahoo Finance fund data; rebalance.py).</figcaption></figure>
<p>The 5-point band came out slightly ahead, with the highest end value ($325,566), the best Sharpe (0.65) and the fewest trades (26). That edge is small and depends on the period. The steadier way to say it: <strong>a band rule gets you yearly-level risk control with even fewer trades.</strong></p>
<p>This matches Vanguard&#8217;s much longer study (1926 to 2009). Its monthly-rebalanced 60/40 needed 1,008 rebalancing events and its annual one 83, with average returns of 8.5% and 8.6%. Vanguard&#8217;s conclusion was that annual or semiannual monitoring, with rebalancing at 5% thresholds, is a reasonable balance for most broadly diversified stock and bond portfolios.</p>
<h2>The real choice: rebalance or let it drift</h2>
<p>The never-rebalanced portfolio ended with $442,046, about $116,500 more than the best rebalanced one. So why rebalance at all?</p>
<p>Because by the end it wasn&#8217;t a 60/40 portfolio anymore. Stocks grow faster than bonds, so with no rebalancing the stock share keeps climbing:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-stock-drift.gif" alt="Animated bar chart, December 1987 to September 2026, of the stock share of a 60/40 portfolio: never rebalanced climbs from about 60% to 94%, the 5-point band and the yearly rule stay near 60%; it pauses at December 2007 with buy and hold at 77% stocks" class="wp-image-1672414"/><figcaption class="wp-element-caption">Each December: buy and hold drifts toward 94% stocks while the band and yearly rules stay near 60%.</figcaption></figure>
<p>It passed 70% stocks in November 1995 and 80% in December 1998. It went into the October 2007 market peak at <strong>78.6% stocks</strong>, so the 2008 crash hit far harder than a 60/40 should. The worst drawdown was −38.7%, against −30.8% for the yearly rebalancer. Volatility was 11.75% vs 9.53%. On a risk-adjusted basis buy and hold came last (Sharpe 0.61).</p>
<p>Vanguard found the same pattern over 1926 to 2009. Its never-rebalanced 60/40 drifted to a maximum of about 99% stocks and earned a higher average return (9.1% vs 8.5% for monthly), with more risk (14.4% vs 12.1% standard deviation). The extra return is mostly payment for holding more stock than you signed up for.</p>
<div class="wp-block-group callout" style="background:#fdf3ef;border-left:5px solid #e76f51;padding:14px 20px;border-radius:6px;margin:22px 0">
<p><strong>Rule:</strong> rebalancing isn&#8217;t about squeezing out return; it keeps the portfolio&#8217;s risk at the level you chose. If you never rebalance, your 60/40 quietly turns into a stock portfolio, right before a crash shows you the difference.</p>
</div>
<h2>Rolling 10-year windows: no lucky period</h2>
<p>One 40-year run could just be luck, so the script also tests every rolling 10-year window: 358 of them, starting each month from January 1987 to October 2016.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-rolling-10y-gap.png" alt="Line chart of the annual return gap against yearly rebalancing over 358 rolling 10-year windows from 1987 to 2016 starts: monthly stays within -0.38 to +0.12 points, the 5-point band within -0.35 to +0.31, never rebalancing swings from about -0.6 to +1.6 points" class="wp-image-1672413" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-rolling-10y-gap.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-rolling-10y-gap-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-rolling-10y-gap-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/how-often-to-rebalance-portfolio-rolling-10y-gap-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Every rolling 10-year window: monthly vs yearly is a rounding error; never rebalancing is a swing (rebalance.py).</figcaption></figure>
<ul>
<li><strong>Monthly minus yearly:</strong> median <strong>−0.11</strong> percentage points a year, never more than +0.12 better. Monthly almost never wins, and when it does, it wins by a rounding error.</li>
<li><strong>5-point band minus yearly:</strong> median <strong>+0.03</strong> points, range −0.35 to +0.31. That&#8217;s a coin flip.</li>
<li><strong>Never minus yearly:</strong> median <strong>−0.09</strong> points, with swings from about −0.6 to +1.6. Buy and hold also had the deeper drawdown in <strong>73%</strong> of the windows.</li>
</ul>
<p>So over a typical decade, never rebalancing didn&#8217;t even reliably earn more. It took more risk for an uncertain reward. The 40-year advantage comes from the drift compounding over a very long bull run in stocks.</p>
<h2>What about trading costs?</h2>
<p>At these frequencies, the cost of trades is small. Annual rebalancing traded about 3.3% of the portfolio per year, and monthly about 9.2%. The napkin math below rests on a <strong>labeled assumption</strong> that each traded dollar costs 0.2% (spread, commissions or tax drag). Then a $100,000 portfolio pays about <strong>$18 a year</strong> for monthly rebalancing and <strong>$7</strong> for yearly.</p>
<p>Taxes are the cost that actually matters. In a taxable account, every rebalance sells winners and can trigger capital gains, which is why Vanguard says annual rebalancing is likely preferred when taxes or substantial costs are involved. Three practical fixes:</p>
<ol>
<li><strong>Rebalance with cash flows.</strong> Put new contributions, dividends and interest into whichever side is underweight. Vanguard specifically recommends this to cut costs.</li>
<li><strong>Rebalance inside tax-advantaged accounts</strong> (401(k), IRA or your country&#8217;s equivalent) where selling doesn&#8217;t create a tax bill.</li>
<li><strong>Use the 5-point band</strong> so you only trade when the drift is big enough to matter.</li>
</ol>
<h2>A simple rebalancing routine</h2>
<ol>
<li><strong>Pick a date</strong> you&#8217;ll remember, for example your birthday or the first weekend of January.</li>
<li><strong>Check your current weights.</strong> If you track holdings by hand, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free browser tracker that keeps your data on your machine (no account, no broker connection), shows allocation by value, and exports Excel backups.</li>
<li><strong>If stocks are more than 5 points off target, rebalance</strong>, starting with new cash and then tax-advantaged accounts.</li>
<li><strong>If they&#8217;re inside the band, do nothing.</strong> That&#8217;s the rule working, not you being lazy.</li>
</ol>
<p>To check whether your mix is really beating the index, see <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">how to compare your portfolio to the S&amp;P 500</a> (with Python). If you&#8217;re about to put a lump sum to work before your first rebalance, the backtest in <a href="https://blog.finxter.com/lump-sum-vs-dca-sp-500/">lump sum vs DCA on the S&amp;P 500</a> is the companion read.</p>
<h2>Turn your backtest into something you can share</h2>
<p>Once you&#8217;ve run this on your own funds, you have original analysis: real numbers, charts and a clear rule. That&#8217;s good material for a short guide, a newsletter lead magnet, or a small ebook. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=how-often-to-rebalance-portfolio&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns a prompt or your notes into a complete ~10,000-word draft with its own cover, with Word and EPUB export on paid plans. Paste in your results table and rules, then edit the draft so every number matches your script. If you plan to sell it on Kindle, run the price through Finxter&#8217;s <a href="https://blog.finxter.com/kdp-royalty-calculator-python/">KDP royalty calculator in Python</a> first.</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Question</th>
<th>What 40 years of data says</th>
<th>Do this</th>
</tr>
</thead>
<tbody>
<tr>
<td>Monthly or quarterly?</td>
<td>Same Sharpe as yearly (0.63-0.64), 4-12x the trades</td>
<td>Skip it</td>
</tr>
<tr>
<td>Once a year?</td>
<td>39 trades, Sharpe 0.64, smallest drawdown (−30.8%)</td>
<td>Default choice: one fixed date</td>
</tr>
<tr>
<td>Threshold band?</td>
<td>26 trades, Sharpe 0.65, but needs monitoring</td>
<td>Check monthly or yearly, trade only past ±5 points</td>
</tr>
<tr>
<td>Never?</td>
<td>Higher return, but 93.9% stocks and −38.7% drawdown</td>
<td>Only if you really want a stock-heavy portfolio</td>
</tr>
<tr>
<td>Taxable account?</td>
<td>Selling winners can trigger capital gains</td>
<td>Rebalance with new cash and inside tax-advantaged accounts</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Sources: rebalance.py results; Vanguard, Best practices for portfolio rebalancing.</figcaption></figure>
<h2>Limitations</h2>
<ul>
<li><strong>One stock fund, one bond fund, US only.</strong> International stocks, small caps or other bond types will drift at different speeds.</li>
<li><strong>Month-end data.</strong> A band rule checked daily would trade at slightly different points. Vanguard notes that threshold-only rules need daily monitoring.</li>
<li><strong>No taxes or trading costs</strong> in the backtest itself; the cost section above is assumption-based napkin math.</li>
<li><strong>The past isn&#8217;t a forecast.</strong> Stocks returned much more than bonds over 1987 to 2026. In a period where bonds keep up, the drift (and the gap from never rebalancing) would be smaller. Not investment advice.</li>
</ul>
<h2>The takeaway</h2>
<p>How often to rebalance a portfolio: <strong>once a year, or whenever an asset class drifts more than 5 points off target.</strong> Monthly and quarterly rebalancing only added trades in 40 years of real fund data. Never rebalancing turned a 60/40 into a 94/6 portfolio and gave it the deepest crash. Set one date a year and use new cash first. And if your backtest turns into a guide worth sharing, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=how-often-to-rebalance-portfolio&amp;utm_content=end">ImagineYourBook</a> can turn your notes into a first draft: the $0 trial includes one 10,000-word book, requires a card, and becomes $29.99 a month unless you cancel before day 8.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked October 27, 2026):</strong> <a href="https://finance.yahoo.com/quote/VFINX/history/">Yahoo Finance: VFINX historical data (via yfinance)</a> · <a href="https://finance.yahoo.com/quote/VBMFX/history/">Yahoo Finance: VBMFX historical data (via yfinance)</a> · <a href="https://help.yahoo.com/kb/SLN28256.html">Yahoo Help: adjusted close</a> · <a href="https://fred.stlouisfed.org/series/TB3MS">FRED: 3-Month Treasury Bill rate (TB3MS)</a> · <a href="https://cdn.indexacapital.com/bundles/unaiadvisor/docs/papers/2010-Vanguard-Best-practices-for-portfolio-rebalancing.pdf">Vanguard: Best practices for portfolio rebalancing (PDF)</a> · <a href="https://www.aaii.com/journal/article/best-practices-for-portfolio-rebalancing">AAII Journal: Best practices for portfolio rebalancing (Vanguard summary)</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://portfoliotracker24.com/">PortfolioTracker24 (features checked on its site)</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: rebalance.py (shown above). Napkin math is labeled as assumptions. Not investment advice.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Lump Sum vs DCA S&#038;P 500: A 3-Month DCA Lost Money More Often Than Investing at Once</title>
		<link>https://blog.finxter.com/lump-sum-vs-dca-sp-500/</link>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Sun, 25 Oct 2026 08:55:05 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/lump-sum-vs-dca-sp-500/</guid>

					<description><![CDATA[Lump sum vs DCA S&#38;P 500 since 1988: lump sum won 65-78% of 453 start months, and DCA barely changed the odds of a losing year. Python script included.]]></description>
										<content:encoded><![CDATA[<p>Dollar-cost averaging is sold as protection against a bad first year. On the S&amp;P 500 since 1988, the popular 3-month DCA was still down after one year in <strong>16.6%</strong> of start months, slightly <em>more</em> often than putting the whole lump sum in on day one (<strong>16.3%</strong>).</p>
<p>That is the non-obvious part of the <strong>lump sum vs DCA S&amp;P 500</strong> question, and it changes what DCA is actually for. The short answer first: across 453 rolling start months (January 1988 to September 2025, dividends reinvested, idle cash earning T-bill interest), investing the lump sum at once beat DCA <strong>64.7%</strong> of the time against a 3-month plan and <strong>77.7%</strong> of the time against a 12-month plan. DCA did not lower your odds of a losing year in any meaningful way. What a <em>long</em> DCA did buy was a thinner tail: a 12-month plan cut the share of years that lost more than 20% from 4.4% to 1.5%. The median price of that was $4,677 per $100,000.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li>Lump sum beat DCA on the S&amp;P 500 in <strong>64.7%</strong> (3-month plan) to <strong>77.7%</strong> (12-month plan) of 453 start months since 1988.</li>
<li>DCA did <strong>not</strong> cut the odds of a losing year: 16.3% for lump sum vs 16.6%, 16.3% and 15.9% for 3-, 6- and 12-month DCA.</li>
<li>A 12-month DCA did shrink the tail: years losing more than 20% fell from <strong>4.4% to 1.5%</strong>, worst year −33.3% instead of −43.3%.</li>
<li>The price: a median <strong>$4,677 per $100,000</strong> after one year, about $16,915 by year 20 at an assumed 7%.</li>
<li>Python script included: swap the ticker or the DCA length and rerun.</li>
</ul>
</div>
<p>The numbers below come from a short Python script you can run yourself (full code further down). It builds on the shadow-benchmark idea from <a href="https://blog.finxter.com/compare-my-portfolio-to-sp-500/">how to compare your portfolio to the S&amp;P 500 the right way</a>: same index, same dollars, only the timing changes.</p>
<p>A side note for readers who turn analyses like this into something publishable: a clean backtest plus your own rules is the skeleton of a short investing ebook or a newsletter lead magnet. If that&#8217;s your next step, <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=lump-sum-vs-dca-sp-500&amp;utm_content=top">ImagineYourBook</a> is the AI book-writing tool I&#8217;d start with: it turns a prompt or your notes into a complete 10,000-word draft with its own cover, and paid plans export Word and EPUB for editing. You still check every number yourself, which you already do.</p>
<h2>The setup: what &#8220;lump sum&#8221; and &#8220;DCA&#8221; mean here</h2>
<p>Imagine $100,000 lands in your account at a month-end: a bonus, an inheritance, the sale of a business.</p>
<ul>
<li><strong>Lump sum:</strong> buy the S&amp;P 500 total-return index with all of it that day.</li>
<li><strong>DCA (dollar-cost averaging):</strong> invest 1/3, 1/6 or 1/12 of it at each month-end, starting that day. The cash still waiting earns the 3-month T-bill rate (FRED series TB3MS), so DCA is not handicapped by idle cash earning zero.</li>
<li><strong>Scorecard:</strong> compare both after exactly 12 months, the same horizon <a href="https://corporate.vanguard.com/content/dam/corp/research/pdf/cost_averaging_invest_now_or_temporarily_hold_your_cash.pdf">Vanguard&#8217;s 2023 study</a> uses.</li>
</ul>
<p>Data: Yahoo Finance&#8217;s <code>^SP500TR</code>, the S&amp;P 500 with dividends reinvested, month-end closes from January 1988 to September 2026. That gives 453 start months, each with a full year to measure. One useful detail: once the DCA plan is fully invested, both portfolios hold the same index, so whatever percentage gap exists at month 12 stays locked in for good. Measuring at one year is not a short-term trick. It is the permanent result.</p>
<h2>Result 1: lump sum wins most of the time, and the gap grows with DCA length</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Lump sum beat it</th>
<th>Median lump-sum edge</th>
<th>Down after 1 year</th>
<th>Down &gt;20%</th>
<th>Worst year</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lump sum (all on day 1)</td>
<td>n/a</td>
<td>n/a</td>
<td>16.3%</td>
<td>4.4%</td>
<td>−43.3%</td>
</tr>
<tr>
<td>3-month DCA</td>
<td>64.7%</td>
<td>1.08%</td>
<td>16.6%</td>
<td>4.0%</td>
<td>−43.9%</td>
</tr>
<tr>
<td>6-month DCA</td>
<td>68.9%</td>
<td>2.20%</td>
<td>16.3%</td>
<td>2.2%</td>
<td>−43.2%</td>
</tr>
<tr>
<td>12-month DCA</td>
<td>77.7%</td>
<td>4.24%</td>
<td>15.9%</td>
<td>1.5%</td>
<td>−33.3%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">453 monthly starts, Jan 1988 to Sep 2025, scored after 12 months. S&amp;P 500 total return (^SP500TR); DCA cash earns 3-month T-bills (TB3MS). lump_sum_vs_dca.py.</figcaption></figure>
<p>The 3-month row is the one Vanguard uses as its base case. Vanguard found that lump sum beat cost averaging 68% of the time on the MSCI World index (1976–2022), with a range of 61.6% to 73.7% across markets and split lengths. My 64.7% on the S&amp;P 500 sits inside that range, which is a good sanity check for the script.</p>
<p>The pattern is simple: every extra month in cash is a month without the equity risk premium. Stretch the DCA from 3 to 12 months and lump sum&#8217;s win rate climbs from 64.7% to 77.7%, while the median edge grows from 1.08% to 4.24%.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-edge-by-start-month.png" alt="Bar chart of lump sum minus 12-month DCA after one year per $100,000 for each start month from January 1988 to September 2025: lump sum ahead in 77.7% of months, DCA ahead by $31,082 for an August 2008 start, lump sum ahead by $36,295 for a February 2009 start" class="wp-image-1672381" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-edge-by-start-month.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-edge-by-start-month-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-edge-by-start-month-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-edge-by-start-month-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Lump sum minus a 12-month DCA after one year, per $100,000 (S&amp;P 500 total return, lump_sum_vs_dca.py).</figcaption></figure>
<p>The chart also shows that DCA&#8217;s wins are not rare flukes. They cluster in exactly the years you remember: 2000–2002 and 2007–2008. Start a 12-month DCA in August 2008 and your $100,000 was worth $112,829 a year later, while the lump sum sat at $81,747, a $31,082 difference. Start in February 2009, near the bottom, and the lump sum was ahead by $36,295.</p>
<p>On average the two sides win by similar amounts: $7,895 per $100,000 when lump sum wins, $7,190 when DCA wins. So the decision is about <em>how often</em>, and lump sum wins about three times out of four over 12 months.</p>
<h2>Result 2: DCA does not lower the odds of a losing year</h2>
<p>This is the convention-breaking part. The usual pitch for DCA is &#8220;you won&#8217;t lose money if the market drops right after you invest.&#8221; Count the losing years:</p>
<ul>
<li>Lump sum: down after one year in <strong>74</strong> of 453 windows (16.3%).</li>
<li>3-month DCA: down in <strong>75</strong> (16.6%).</li>
<li>6-month DCA: down in <strong>74</strong> (16.3%).</li>
<li>12-month DCA: down in <strong>72</strong> (15.9%).</li>
</ul>
<p>That&#8217;s a difference of one or two windows out of 453, which is noise. The reason is simple once you look at it: a bear market that lasts a year (2001, 2008) pulls down money invested in month 3 almost as much as money invested in month 1. And in flat years, DCA can buy after a small rise and end slightly below zero while the lump sum ends slightly above it.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-running-win-rate.gif" alt="Animation building the lump sum minus 12-month DCA bars start month by start month from 1988 to 2025, with the running lump-sum win rate ending at 77.7%" class="wp-image-1672382" /><figcaption class="wp-element-caption">Running win rate: about 85% by 1999, about 71% by 2002, 77.7% by the September 2025 start.</figcaption></figure>
<p>The animation builds the 12-month comparison start month by start month. Watch the running win rate: it climbs to about 85% by the end of the 1990s, slides to about 71% by 2002 as the dot-com and 2008 starts pile up (lump sum won only 57.5% of the 2000s starts), then recovers to 77.7%.</p>
<h2>Result 3: what DCA actually buys is a thinner tail</h2>
<p>If DCA doesn&#8217;t prevent losing years, what does it do? It makes the <em>bad</em> losing years less bad, but only if you spread it out long enough.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-insurance-tradeoff.png" alt="Scatter of median cost after one year per $100,000 versus share of windows that lost more than 20%: lump sum $0 and 4.4%, 3-month DCA $1,194 and 4.0%, 6-month DCA $2,442 and 2.2%, 12-month DCA $4,677 and 1.5%" class="wp-image-1672383" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-insurance-tradeoff.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-insurance-tradeoff-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-insurance-tradeoff-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/lump-sum-vs-dca-sp-500-insurance-tradeoff-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">DCA as insurance: a longer plan costs more and is the only one that clearly thins the tail.</figcaption></figure>
<ul>
<li>The worst lump-sum year (start February 2008) lost <strong>43.3%</strong>. The worst 12-month DCA year lost <strong>33.3%</strong>.</li>
<li>Lump sum lost more than 20% in 4.4% of windows (20 of 453). The 12-month DCA did so in 1.5% (7 windows).</li>
<li>A 3-month DCA barely moved the tail: 4.0%, and its worst year (−43.9%) was no better than the lump sum&#8217;s.</li>
</ul>
<p>So think of DCA as an insurance policy against the one-in-twenty disaster year, with a premium you pay in the other nineteen. The premium is not small.</p>
<h2>The compounding napkin math: what the premium costs over 20 years</h2>
<p>The median 12-month DCA trailed the lump sum by <strong>$4,677 per $100,000</strong> after one year. Because both portfolios are fully invested from then on, that gap compounds along with everything else.</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Plan</th>
<th>Median cost after 1 year</th>
<th>Cost by year 20 (assumed 7%/yr)</th>
<th>Years down &gt;20%</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lump sum</td>
<td>$0</td>
<td>$0</td>
<td>4.4%</td>
</tr>
<tr>
<td>3-month DCA</td>
<td>$1,194</td>
<td>$4,317</td>
<td>4.0%</td>
</tr>
<tr>
<td>6-month DCA</td>
<td>$2,442</td>
<td>$8,831</td>
<td>2.2%</td>
</tr>
<tr>
<td>12-month DCA</td>
<td>$4,677</td>
<td>$16,915</td>
<td>1.5%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Per $100,000. Year-1 cost is data (median start month). Year-20 column is napkin math with an assumed 7% a year, no taxes, fees or deposits. napkin() in lump_sum_vs_dca.py.</figcaption></figure>
<p>The two right-hand columns rest on an <strong>assumption</strong>, not data: both portfolios then grow at 7% a year with no taxes, fees or new deposits. Under that assumption the median 12-month DCA costs about $16,915 by year 20. The 3-month version costs about $4,317. Neither is a forecast. The point is the ratio: you pay roughly four times more for the 12-month plan, and it&#8217;s the only one that meaningfully cut the tail.</p>
<p>If you&#8217;re doing similar planning for a fixed goal, the same compounding logic is in <a href="https://blog.finxter.com/project-using-python-to-plan-how-much-i-should-invest-for-my-kids-so-they-have-70k-at-age-21/">this Python plan for having $70k for a kid at 21</a>.</p>
<h2>The Python script</h2>
<p>It uses only pandas and yfinance. Month-end resampling is one line (see Finxter&#8217;s <a href="https://blog.finxter.com/pandas-dataframe-resample-method/">pandas <code>resample()</code> guide</a> if that&#8217;s new). Swap <code>^SP500TR</code> for any ticker, or change <code>n</code> to test a 9-month plan.</p>
<pre><code class="language-python">"""Lump sum vs DCA on the S&amp;P 500 (total return), every rolling start month since 1988.
Data: Yahoo Finance ^SP500TR (dividends reinvested) month-end closes; FRED TB3MS (3-month T-bill) for idle DCA cash.
pip install yfinance pandas"""
import pandas as pd

def load(end="2026-09-30"):
    try:  # cached copies used for the article
        px = pd.read_pickle("src/SP500TR.pkl")["Close"].squeeze()
        tb = pd.read_csv("src/tb3ms.csv", parse_dates=[0], index_col=0)["TB3MS"]
    except FileNotFoundError:
        import yfinance as yf
        px = yf.download("^SP500TR", start="1988-01-01", auto_adjust=False, progress=False)["Close"].squeeze()
        tb = pd.read_csv("https://fred.stlouisfed.org/graph/fredgraph.csv?id=TB3MS", parse_dates=[0], index_col=0)["TB3MS"]
    m = px.resample("ME").last().loc[:end]
    tb.index = tb.index + pd.offsets.MonthEnd(0)
    rf = (tb / 100 / 12).reindex(m.index).ffill()      # monthly T-bill yield on cash waiting to be invested
    return m, rf

def simulate(m, rf, n=12, horizon=12):
    """$1 at month-end t. Lump sum: all in at t. DCA: 1/n at t, t+1, ..., t+n-1; the rest earns T-bills.
    Both are valued at t+horizon (horizon &gt;= n, so the DCA is fully invested by then)."""
    P, R, rows = m.values, rf.values, []
    for t in range(len(P) - horizon):
        cash, units = 1.0, 0.0
        for k in range(n):
            units += (1 / n) / P[t + k]; cash -= 1 / n
            cash *= 1 + R[t + k]
        rows.append((m.index[t], P[t + horizon] / P[t], units * P[t + horizon] + cash))
    df = pd.DataFrame(rows, columns=["start", "lump", "dca"]).set_index("start")
    df["gap"] = df.lump / df.dca - 1          # &gt; 0: lump sum ended ahead
    return df

def summary(df):
    return dict(windows=len(df), lump_wins=(df.gap &gt; 0).mean(), median_gap=df.gap.median(), mean_gap=df.gap.mean(),
                lump_down=(df.lump &lt; 1).mean(), dca_down=(df.dca &lt; 1).mean(),
                lump_down20=(df.lump &lt; 0.8).mean(), dca_down20=(df.dca &lt; 0.8).mean(),
                lump_worst=df.lump.min() - 1, dca_worst=df.dca.min() - 1, lump_worst_at=df.lump.idxmin(),
                best_dca_win=-df.gap.min(), best_dca_win_at=df.gap.idxmin(), best_lump_win=df.gap.max(), best_lump_win_at=df.gap.idxmax(),
                avg_lump_win=df.gap[df.gap &gt; 0].mean(), avg_dca_win=-df.gap[df.gap &lt; 0].mean())

def napkin(df, amount=100_000, growth=0.07, years=20):
    """ASSUMPTION-BASED: median dollar gap after year 1, then both portfolios grow at an assumed `growth` a year.
    After the DCA is fully invested both hold the same index, so the percentage gap never closes."""
    gap1 = ((df.lump - df.dca) * amount).median()
    return gap1, gap1 * (1 + growth) ** (years - 1)

def run():
    m, rf = load()
    out = {n: simulate(m, rf, n=n, horizon=12) for n in (3, 6, 12)}
    return m, rf, out, {n: summary(d) for n, d in out.items()}

if __name__ == "__main__":
    m, rf, out, S = run()
    print(f"S&amp;P 500 total return, month-ends {m.index[0]:%b %Y} to {m.index[-1]:%b %Y}; idle cash earns 3-month T-bills")
    print(f"{'DCA over':&lt;10}{'windows':&gt;8}{'LS wins':&gt;9}{'LS edge':&gt;8}{'LS down':&gt;9}{'DCA down':&gt;9}{'LS&lt;-20%':&gt;9}{'DCA&lt;-20%':&gt;9}{'LS worst':&gt;10}{'DCA worst':&gt;10}")
    for n, s in S.items():
        print(f"{n:&gt;2} months {s['windows']:&gt;8}{s['lump_wins']:&gt;9.1%}{s['median_gap']:&gt;8.2%}{s['lump_down']:&gt;9.1%}{s['dca_down']:&gt;9.1%}"
              f"{s['lump_down20']:&gt;9.1%}{s['dca_down20']:&gt;9.1%}{s['lump_worst']:&gt;10.1%}{s['dca_worst']:&gt;10.1%}")
    d = out[12]
    print("\n12-month DCA, lump-sum win rate by start decade:")
    for dec, g in d.groupby(d.index.year // 10 * 10).gap:
        print(f"  {dec}s: {(g &gt; 0).mean():.1%} of {len(g)} starts")
    s = S[12]
    diff = (d.lump - d.dca) * 100_000
    for label, t in (("Best DCA start", diff.idxmin()), ("Best lump-sum start", diff.idxmax())):
        r = d.loc[t]
        print(f"{label} ({t:%b %Y}), $100,000: lump sum ${r.lump*1e5:,.0f} vs DCA ${r.dca*1e5:,.0f}")
    g1, g20 = napkin(d)
    print(f"Napkin ($100,000, median start): 12-month DCA trails by ${g1:,.0f} after year 1; at an assumed 7%/yr that is ${g20:,.0f} by year 20")
    print(f"Average edge per $100,000: ${diff[diff &gt; 0].mean():,.0f} when lump sum wins, ${-diff[diff &lt; 0].mean():,.0f} when DCA wins")
</code></pre>
<p>Output on data through September 30, 2026:</p>
<pre><code class="language-text">S&amp;P 500 total return, month-ends Jan 1988 to Sep 2026; idle cash earns 3-month T-bills
DCA over   windows  LS wins LS edge  LS down DCA down  LS&lt;-20% DCA&lt;-20%  LS worst DCA worst
 3 months      453    64.7%   1.08%    16.3%    16.6%     4.4%     4.0%    -43.3%    -43.9%
 6 months      453    68.9%   2.20%    16.3%    16.3%     4.4%     2.2%    -43.3%    -43.2%
12 months      453    77.7%   4.24%    16.3%    15.9%     4.4%     1.5%    -43.3%    -33.3%

12-month DCA, lump-sum win rate by start decade:
  1980s: 75.0% of 24 starts
  1990s: 87.5% of 120 starts
  2000s: 57.5% of 120 starts
  2010s: 85.0% of 120 starts
  2020s: 84.1% of 69 starts
Best DCA start (Aug 2008), $100,000: lump sum $81,747 vs DCA $112,829
Best lump-sum start (Feb 2009), $100,000: lump sum $153,618 vs DCA $117,323
Napkin ($100,000, median start): 12-month DCA trails by $4,677 after year 1; at an assumed 7%/yr that is $16,915 by year 20
Average edge per $100,000: $7,895 when lump sum wins, $7,190 when DCA wins
</code></pre>
<p>Three choices in the script matter. If you change them, your numbers will move:</p>
<ol>
<li><strong>Total return, not price.</strong> <code>^GSPC</code> ignores dividends: over this period it compounded at 9.2% a year versus 11.5% with dividends, and a lower stock return flatters DCA.</li>
<li><strong>Cash earns T-bills.</strong> Leaving DCA cash at 0% makes it look worse than it is.</li>
<li><strong>Same 12-month scorecard.</strong> Comparing DCA after 12 months with the lump sum after 3 is the most common mistake in DCA backtests.</li>
</ol>
<h2>So which should you do?</h2>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Your situation</th>
<th>What the data supports</th>
<th>Number behind it</th>
</tr>
</thead>
<tbody>
<tr>
<td>You&#8217;d hold through a 40% drop</td>
<td>Invest the lump sum now</td>
<td>Won 64.7–77.7% of start months</td>
</tr>
<tr>
<td>You&#8217;d likely sell in a crash</td>
<td>6–12-month DCA, on autopilot</td>
<td>Years down &gt;20%: 4.4% → 1.5% (12 months)</td>
</tr>
<tr>
<td>You want &#8216;a bit of protection&#8217;</td>
<td>Skip the 3-month DCA</td>
<td>Losing years 16.6% vs 16.3%, worst −43.9%</td>
</tr>
<tr>
<td>You&#8217;re keeping DCA cash at 0%</td>
<td>Park it in T-bills or a money market fund</td>
<td>The script credits idle cash with TB3MS</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">All figures from lump_sum_vs_dca.py on S&amp;P 500 total-return data. Not investment advice.</figcaption></figure>
<p>My reading of the data:</p>
<ul>
<li><strong>If you can stomach a 40% drop in year one without selling, invest the lump sum.</strong> It won 65–78% of the time, and the median edge compounds.</li>
<li><strong>If the honest answer is &#8220;I would panic-sell,&#8221; a 6- to 12-month DCA is a rational premium.</strong> A plan you stick with beats a better plan you abandon in month four. Vanguard makes the same point: cost averaging beat staying in cash 69% of the time.</li>
<li><strong>Skip the 3-month compromise if your goal is protection.</strong> It cost $1,194 per $100,000 at the median and did not cut losing years or the worst case. It mostly buys a feeling.</li>
</ul>
<p>Whatever you pick, track it against a same-dates S&amp;P 500 benchmark so you see the real cost of your timing. The free browser-based <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> does this with an &#8220;S&amp;P Equivalent&#8221; built from your own purchase dates. It needs no account or broker connection, your trades stay in your browser, and it keeps Excel backups.</p>
<p><strong>Limits:</strong> one index, one country, 38 years of starts that overlap heavily (453 windows are not 453 independent trials), and no taxes or fees. Past returns don&#8217;t guarantee future ones, and this is not investment advice.</p>
<h2>The takeaway</h2>
<p>Lump sum beat DCA on the S&amp;P 500 in roughly three of four start months, and DCA did not reduce the chance of a losing year at all. Its only real job is shrinking the worst case, and that takes 6–12 months of spreading, at a median cost of $2,442–$4,677 per $100,000. Pick it deliberately, as insurance, or not at all.</p>
<p>And if your notes on this are turning into a guide of your own, say an investing primer for your newsletter, you can draft it with <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=lump-sum-vs-dca-sp-500&amp;utm_content=end">ImagineYourBook</a>. The trial is $0 today with a card on file and includes one 10,000-word book; the first $29.99 charge comes after 7 days unless you cancel before day 8. Then check every figure against your own script before anyone reads it.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked October 25, 2026):</strong> <a href="https://finance.yahoo.com/quote/%5ESP500TR/history/">Yahoo Finance: S&amp;P 500 Total Return (^SP500TR) history</a> · <a href="https://finance.yahoo.com/quote/%5EGSPC/history/">Yahoo Finance: S&amp;P 500 price index (^GSPC)</a> · <a href="https://fred.stlouisfed.org/series/TB3MS">FRED: 3-Month Treasury Bill secondary market rate (TB3MS)</a> · <a href="https://corporate.vanguard.com/content/dam/corp/research/pdf/cost_averaging_invest_now_or_temporarily_hold_your_cash.pdf">Vanguard Research (Feb 2023): Cost averaging: invest now or temporarily hold your cash?</a> · <a href="https://www.vanguard.co.uk/professional/vanguard-365/financial-planning/financial-well-being/cost-averaging">Vanguard UK: The truth about cost averaging (61.6%–73.7% range)</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://portfoliotracker24.com/">PortfolioTracker24 (features checked on its site)</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and trial terms</a>. Script: lump_sum_vs_dca.py (shown above). Not investment advice.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Compare My Portfolio to the S&#038;P 500: Why a 3.7-Point Loss Can Look Like a Win (Python)</title>
		<link>https://blog.finxter.com/compare-my-portfolio-to-sp-500/</link>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Fri, 23 Oct 2026 16:00:18 +0000</pubDate>
				<category><![CDATA[Finance]]></category>
		<category><![CDATA[Investment]]></category>
		<category><![CDATA[Pandas Library]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Pandas]]></category>
		<guid isPermaLink="false">https://blog.finxter.com/compare-my-portfolio-to-sp-500/</guid>

					<description><![CDATA[How to compare my portfolio to the S&#38;P 500 the right way: a Python shadow benchmark on 5 years of real data turns a fake +1.5-point win into a 3.7-point loss.]]></description>
										<content:encoded><![CDATA[<p>You type &#8220;compare my portfolio to S&amp;P 500&#8221; into Google because you want one honest answer: <strong>am I beating the index, or would I have been better off just buying it?</strong></p>
<p>The catch: the two most common ways people run that comparison give the wrong answer. I tested both on five years of real price data, from October 22, 2021 to October 22, 2026, using Yahoo Finance closes and a short pandas script. In the worked example below, a portfolio that <em>looks</em> 1.5 points ahead of the S&amp;P 500 is really <strong>3.7 points a year behind</strong> it.</p>
<p>Below you&#8217;ll find the three traps, the fix (a &#8220;shadow S&amp;P 500&#8221; that buys the index with your exact deposits on your exact dates), and a Python script you can point at your own trades.</p>
<p>One side note for readers who write about money: once you&#8217;ve run this analysis, you already have the outline of a useful investing guide or newsletter lead magnet. <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=compare-my-portfolio-to-sp-500&amp;utm_content=top">ImagineYourBook</a> is an AI book-writing tool that turns a prompt or your notes into a ~10,000-word draft with Word and EPUB export, so your findings can become a short ebook you then fact-check and edit.</p>
<div class="wp-block-group tldr-box" style="background:#f3f7f6;border-left:5px solid #2a9d8f;padding:18px 22px;border-radius:6px;margin:24px 0">
<p><strong>TL;DR</strong></p>
<ul>
<li>The price-only S&amp;P 500 rose <strong>68.6%</strong> in five years; with dividends (SPY) it rose <strong>80.3%</strong>. Compare against total return or you gift yourself 11.6 points.</li>
<li>$500 a month into a 60/20/20 portfolio earned a <strong>14.0% IRR</strong>, which looks 1.5 points ahead of the S&amp;P 500&#8217;s 12.5% CAGR.</li>
<li>A <strong>shadow S&amp;P 500</strong> (same dollars, same dates) earned <strong>17.7%</strong>: the real gap is −3.7 points a year, or $4,139.</li>
<li>Risk didn&#8217;t rescue it: max drawdown −24.0% vs −24.5%, Sharpe 0.40 vs 0.55.</li>
<li>Copy the Python script below to run the shadow benchmark on your own trades.</li>
</ul>
</div>
<h2>The setup: one example portfolio, five real years</h2>
<p>To keep the numbers concrete, I used an example three-fund portfolio. <strong>These weights are an assumption for illustration, not a recommendation:</strong></p>
<ul>
<li>60% VTI (US total stock market)</li>
<li>20% VXUS (international stocks)</li>
<li>20% BND (US bonds)</li>
</ul>
<p>The benchmark is SPY, an S&amp;P 500 ETF, using Yahoo&#8217;s <em>Adjusted Close</em>. Yahoo <a href="https://help.yahoo.com/kb/SLN28256.html">documents</a> that adjusted close is &#8220;the closing price after adjustments for all applicable splits and dividend distributions&#8221;, so it behaves like a dividends-reinvested total-return series. For the risk-free rate in the Sharpe ratio, I used the daily 3-month Treasury bill yield (FRED series <a href="https://fred.stlouisfed.org/series/DTB3">DTB3</a>), which averaged about 3.8% over the window.</p>
<p>Here is what the script printed:</p>
<pre><code class="language-text">Period: 2021-10-22 to 2026-10-22 (1255 trading days)
                          Total    CAGR     Vol  Sharpe  Sortino   Max DD
60/20/20 portfolio        52.2%    8.8%   13.9%    0.40     0.58   -24.0%
S&amp;P 500 (SPY, TR)         80.3%   12.5%   17.2%    0.55     0.80   -24.5%
S&amp;P 500 price only        68.6%   11.0%   17.0%    0.48     0.68   -25.4%
Bitcoin                   34.0%    6.0%   51.7%    0.30     0.44   -76.6%
Dividend gap (SPY TR - price index): +11.6% points
Beta vs SPY: 0.79, correlation: 0.98; portfolio max DD on 2022-10-14, SPY on 2022-10-12
$500/month x 61 = $30,500 deposited
  portfolio $43,385 (gain/deposits 42.2%, IRR 14.0%)
  shadow SPY $47,523 (gain/deposits 55.8%, IRR 17.7%)
</code></pre>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Lump sum, Oct 22 2021 to Oct 22 2026</th>
<th>Total return</th>
<th>CAGR</th>
<th>Volatility</th>
<th>Sharpe</th>
<th>Sortino</th>
<th>Max drawdown</th>
</tr>
</thead>
<tbody>
<tr>
<td>60/20/20 VTI/VXUS/BND</td>
<td>+52.2%</td>
<td>8.8%</td>
<td>13.9%</td>
<td>0.40</td>
<td>0.58</td>
<td>−24.0%</td>
</tr>
<tr>
<td>S&amp;P 500 total return (SPY)</td>
<td>+80.3%</td>
<td>12.5%</td>
<td>17.2%</td>
<td>0.55</td>
<td>0.80</td>
<td>−24.5%</td>
</tr>
<tr>
<td>S&amp;P 500 price only (^GSPC)</td>
<td>+68.6%</td>
<td>11.0%</td>
<td>17.0%</td>
<td>0.48</td>
<td>0.68</td>
<td>−25.4%</td>
</tr>
<tr>
<td>Bitcoin (BTC-USD)</td>
<td>+34.0%</td>
<td>6.0%</td>
<td>51.7%</td>
<td>0.30</td>
<td>0.44</td>
<td>−76.6%</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">compare_to_sp500.py on Yahoo Finance daily data (Adj Close; ^GSPC uses Close). Risk-free: FRED DTB3. Portfolio weights are an assumption; bought once, not rebalanced.</figcaption></figure>
<h2>Trap 1: comparing against the price-only S&amp;P 500</h2>
<p>The number most people Google is the S&amp;P 500 <em>index level</em> (ticker ^GSPC). It ignores dividends.</p>
<p>Over these five years, the price index rose <strong>68.6%</strong>. SPY with dividends reinvested rose <strong>80.3%</strong>. That is an <strong>11.6-point gap</strong>, and it&#8217;s almost entirely dividends (SPY&#8217;s small fund fee is already netted out of its adjusted close, so the true index-with-dividends gap is slightly larger).</p>
<p>If your brokerage statement counts your dividends but you compare it with the bare index level, you hand yourself 11.6 free points. Always benchmark against a total-return series.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-growth-of-1.png" alt="Line chart of growth of $1 from October 22, 2021 to October 22, 2026: S&amp;P 500 total return via SPY up 80.3%, price-only S&amp;P 500 index up 68.6%, a gap of 11.6 points, and the 60/20/20 VTI/VXUS/BND portfolio up 52.2%" class="wp-image-1672364" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-growth-of-1.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-growth-of-1-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-growth-of-1-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-growth-of-1-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Growth of $1, Oct 2021 to Oct 2026: dividends add 11.6 points to the S&amp;P 500 (Yahoo Finance data, compare_to_sp500.py).</figcaption></figure>
<h2>Trap 2: comparing your money-weighted return to the index&#8217;s lump-sum return</h2>
<p>Most real investors don&#8217;t drop one lump sum on day one. They add money every month. So let&#8217;s assume <strong>$500 on the first trading day of every month</strong>, split 60/20/20, for 61 months. That&#8217;s <strong>$30,500 deposited</strong>.</p>
<p>By October 22, 2026, the portfolio was worth <strong>$43,385</strong>. Its internal rate of return (IRR, a money-weighted return that accounts for when each dollar went in) was <strong>14.0% a year</strong>.</p>
<p>Now the common mistake: you look up &#8220;S&amp;P 500 five-year annualized return&#8221;, find something like SPY&#8217;s <strong>12.5% CAGR</strong> from our table, and conclude you won by 1.5 points.</p>
<p>But that 12.5% belongs to a single lump sum invested in October 2021, right before the 2022 bear market. Your monthly deposits bought a lot of cheap shares <em>during</em> that drop, which is why your money-weighted figure looks so good. The comparison mixes two different cash-flow patterns. Wikipedia&#8217;s <a href="https://en.wikipedia.org/wiki/Time-weighted_return">time-weighted return article</a> walks through exactly this &#8220;problem of external flows&#8221;: deposits and withdrawals distort a naive return.</p>
<h3>The fix: a shadow S&amp;P 500</h3>
<p>Give the S&amp;P 500 the same deal you got. Every time you bought, pretend you bought the same dollar amount of SPY on the same day. Then compare ending values and IRRs.</p>
<p>The shadow S&amp;P 500 ended at <strong>$47,523</strong>, an IRR of <strong>17.7% a year</strong>. Your real gap is <strong>−3.7 points a year, or $4,139</strong> on $30,500 of deposits. The fake +1.5-point win was really a 3.7-point loss.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="340" src="https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-shadow-sp500.gif" alt="Animation of $500 monthly deposits from October 2021 to October 2026: deposits reach $30,500, the portfolio reaches $43,385 and the shadow S&amp;P 500 with the same deposits reaches $47,523, a $4,139 gap" class="wp-image-1672365" /><figcaption class="wp-element-caption">Same 61 deposits, same dates: the shadow S&amp;P 500 ends $4,139 ahead.</figcaption></figure>
<p>The animation shows the gap opening up month by month: both lines got the same 61 deposits, so the only difference is what the money bought.</p>
<h2>Trap 3: ignoring risk (and why &#8220;lower risk&#8221; didn&#8217;t help here)</h2>
<p>Return alone isn&#8217;t the whole story. A portfolio with 20% bonds <em>should</em> be steadier than 100% stocks, so a fair comparison also looks at risk.</p>
<ul>
<li><strong>Volatility:</strong> 13.9% for the portfolio vs 17.2% for SPY.</li>
<li><strong>Beta vs SPY:</strong> 0.79, correlation 0.98. The portfolio moved with the market, just a bit less.</li>
<li><strong>Max drawdown:</strong> −24.0% vs −24.5% for SPY, both bottoming in mid-October 2022.</li>
<li><strong>Sharpe ratio:</strong> 0.40 vs 0.55. <strong>Sortino:</strong> 0.58 vs 0.80.</li>
</ul>
<p>That drawdown result surprised me. The bonds barely cushioned the crash because BND itself fell <strong>13.1% in calendar 2022</strong> in this data, and VXUS fell 16.1%. So in this particular window the portfolio had nearly the same worst drop <em>and</em> a lower return. Even risk-adjusted, SPY won.</p>
<p>Treat that as a statement about this five-year window, not a law. William Sharpe&#8217;s own <a href="https://web.stanford.edu/~wfsharpe/art/sr/sr.htm">1994 paper on the Sharpe ratio</a> separates the <em>ex post</em> (historic) ratio from the <em>ex ante</em> (expected) one. What we computed is ex post: a description of this sample, not a promise about the next five years.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1200" height="660" src="https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-drawdowns.png" alt="Underwater chart of drawdowns from previous peaks, October 2021 to October 2026: SPY max drawdown -24.5% on October 12, 2022 and the 60/20/20 portfolio -24.0%, despite a beta of 0.79" class="wp-image-1672366" srcset="https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-drawdowns.png 1200w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-drawdowns-300x165.png 300w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-drawdowns-1024x563.png 1024w, https://blog.finxter.com/wp-content/uploads/2026/10/compare-my-portfolio-to-sp-500-drawdowns-768x422.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /><figcaption class="wp-element-caption">Drawdowns: the bond sleeve barely cushioned 2022 (max drawdown -24.0% vs -24.5%).</figcaption></figure>
<p>For completeness, the script also runs Bitcoin over the same trading days: <strong>+34.0%</strong> total with a <strong>−76.6%</strong> max drawdown. The October 2021 start date sits close to a peak, which shows how much your start date shapes any &#8220;vs. benchmark&#8221; claim.</p>
<h2>The napkin math: why a 3.7-point gap matters more than it sounds</h2>
<p>Five years in, the gap is &#8220;only&#8221; $4,139. Compounding turns that into real money. <strong>Assumptions (not forecasts):</strong> $500 a month for 20 years, a constant 7% baseline return, and a gap that stays the same every year.</p>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Annual return (assumed)</th>
<th>Deposits</th>
<th>Value after 20 years</th>
<th>Behind the 10.7% investor</th>
</tr>
</thead>
<tbody>
<tr>
<td>7.0% (baseline)</td>
<td>$120,000</td>
<td>$255,203</td>
<td>$138,231</td>
</tr>
<tr>
<td>8.0% (+1.0 pts)</td>
<td>$120,000</td>
<td>$286,330</td>
<td>$107,104</td>
</tr>
<tr>
<td>9.0% (+2.0 pts)</td>
<td>$120,000</td>
<td>$321,728</td>
<td>$71,706</td>
</tr>
<tr>
<td>10.7% (+3.7 pts)</td>
<td>$120,000</td>
<td>$393,434</td>
<td>$0</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">Napkin math with assumptions only: $500 at the start of each month, constant returns, no taxes or fees. Not a forecast. napkin_fv() in compare_to_sp500.py.</figcaption></figure>
<p>A persistent 3.7-point shortfall on a 7% baseline leaves you about <strong>$138,000 (54%) behind</strong> after 20 years on the same $120,000 of deposits. That&#8217;s why the shadow benchmark is worth the 10 minutes.</p>
<h2>The Python script</h2>
<p>This is the exact script that produced every number and chart in this post. It needs <code>pandas</code>, <code>numpy</code> and <code>yfinance</code>; save the FRED DTB3 CSV next to it in <code>src/dtb3.csv</code>. If you&#8217;re new to time-series tricks in pandas, the Finxter guide on <a href="https://blog.finxter.com/how-to-use-pandas-rolling/">pandas rolling windows</a> pairs well with the drawdown code (<code>value / value.cummax() - 1</code>).</p>
<pre><code class="language-python">"""Compare a portfolio to the S&amp;P 500 the right way (Finxter, 2026-10-23).
Data: Yahoo Finance daily closes via yfinance (Adj Close = dividends reinvested), FRED DTB3 3-month T-bill."""
import os, numpy as np, pandas as pd
HERE = os.path.dirname(os.path.abspath(__file__))
START, END = "2021-10-22", "2026-10-22"          # five years, last full close before publishing
WEIGHTS = {"VTI": 0.60, "VXUS": 0.20, "BND": 0.20}  # ASSUMPTION: an example three-fund portfolio
MONTHLY = 500                                       # ASSUMPTION: $500 bought on the first trading day of each month

def load():
    p = os.path.join(HERE, "src", "raw_yf.pkl")
    if os.path.exists(p): raw = pd.read_pickle(p)
    else:
        import yfinance as yf
        raw = yf.download(["SPY", "VTI", "VXUS", "BND", "^GSPC", "BTC-USD"], start="2021-10-01", auto_adjust=False, progress=False)
    adj, close = raw["Adj Close"], raw["Close"]
    days = adj["SPY"].dropna().index                      # stock-market trading days only
    adj, close = adj.loc[days].loc[START:END], close.loc[days].loc[START:END]
    adj["BTC-USD"] = raw["Adj Close"]["BTC-USD"].reindex(adj.index)
    rf = pd.read_csv(os.path.join(HERE, "src", "dtb3.csv"), index_col=0, parse_dates=True, na_values=".")["DTB3"]
    rf = rf.reindex(adj.index).ffill() / 100              # annual T-bill yield
    return adj, close, rf

def portfolio_index(adj, weights=WEIGHTS):
    """Buy-and-hold: invest weights once at START, never rebalance. Value of $1."""
    norm = adj[list(weights)] / adj[list(weights)].iloc[0]
    return sum(w * norm[t] for t, w in weights.items())

def stats(value, rf):
    r = value.pct_change().dropna()
    years = (value.index[-1] - value.index[0]).days / 365.25
    total = value.iloc[-1] / value.iloc[0] - 1
    cagr = (1 + total) ** (1 / years) - 1
    excess = r - rf.loc[r.index] / 252
    sharpe = excess.mean() / r.std() * np.sqrt(252)
    downside = np.sqrt((np.minimum(excess, 0) ** 2).mean()) * np.sqrt(252)
    sortino = excess.mean() * 252 / downside
    dd = value / value.cummax() - 1
    return dict(total=total, cagr=cagr, vol=r.std() * np.sqrt(252), sharpe=sharpe, sortino=sortino,
                max_dd=dd.min(), dd_date=dd.idxmin(), drawdown=dd)

def beta(value, bench):
    a, b = value.pct_change().dropna(), bench.pct_change().dropna()
    return a.cov(b) / b.var(), a.corr(b)

def contributions(adj, weights=WEIGHTS, monthly=MONTHLY):
    """Buy $monthly on the first trading day of each month; same dollars, same dates into SPY (the 'shadow S&amp;P 500')."""
    firsts = adj.groupby(adj.index.to_period("M")).head(1).index
    port_sh = {t: 0.0 for t in weights}; spy_sh = 0.0; flows = []
    for d in firsts:
        for t, w in weights.items(): port_sh[t] += monthly * w / adj.at[d, t]
        spy_sh += monthly / adj.at[d, "SPY"]; flows.append((d, -monthly))
    last = adj.index[-1]
    port_val = sum(s * adj.at[last, t] for t, s in port_sh.items()); spy_val = spy_sh * adj.at[last, "SPY"]
    return dict(deposits=monthly * len(firsts), n=len(firsts), port_val=port_val, spy_val=spy_val, flows=flows, last=last)

def xirr(flows, final_date, final_value):
    cf = flows + [(final_date, final_value)]
    t0 = cf[0][0]; ts = np.array([(d - t0).days / 365.25 for d, _ in cf]); v = np.array([x for _, x in cf])
    lo, hi = -0.99, 1.0
    for _ in range(200):
        mid = (lo + hi) / 2
        if (v / (1 + mid) ** ts).sum() &gt; 0: lo = mid
        else: hi = mid
    return mid

def run():
    adj, close, rf = load()
    port = portfolio_index(adj); spy = adj["SPY"] / adj["SPY"].iloc[0]
    gspc = close["^GSPC"] / close["^GSPC"].iloc[0]; btc = adj["BTC-USD"] / adj["BTC-USD"].iloc[0]
    S = {k: stats(v, rf) for k, v in dict(portfolio=port, spy=spy, gspc=gspc, btc=btc).items()}
    b, c = beta(port, spy)
    C = contributions(adj)
    C["port_irr"] = xirr(C["flows"], C["last"], C["port_val"]); C["spy_irr"] = xirr(C["flows"], C["last"], C["spy_val"])
    return dict(adj=adj, close=close, rf=rf, port=port, spy=spy, gspc=gspc, btc=btc, S=S, beta=b, corr=c, C=C)

if __name__ == "__main__":
    R = run(); S, C = R["S"], R["C"]
    print(f"Period: {R['adj'].index[0].date()} to {R['adj'].index[-1].date()} ({len(R['adj'])} trading days)")
    print(f"{'':22}{'Total':&gt;9}{'CAGR':&gt;8}{'Vol':&gt;8}{'Sharpe':&gt;8}{'Sortino':&gt;9}{'Max DD':&gt;9}")
    for k, lab in (("portfolio", "60/20/20 portfolio"), ("spy", "S&amp;P 500 (SPY, TR)"), ("gspc", "S&amp;P 500 price only"), ("btc", "Bitcoin")):
        s = S[k]; print(f"{lab:22}{s['total']:&gt;9.1%}{s['cagr']:&gt;8.1%}{s['vol']:&gt;8.1%}{s['sharpe']:&gt;8.2f}{s['sortino']:&gt;9.2f}{s['max_dd']:&gt;9.1%}")
    print(f"Dividend gap (SPY TR - price index): {S['spy']['total']-S['gspc']['total']:+.1%} points")
    print(f"Beta vs SPY: {R['beta']:.2f}, correlation: {R['corr']:.2f}; portfolio max DD on {S['portfolio']['dd_date'].date()}, SPY on {S['spy']['dd_date'].date()}")
    print(f"${MONTHLY}/month x {C['n']} = ${C['deposits']:,} deposited")
    print(f"  portfolio ${C['port_val']:,.0f} (gain/deposits {C['port_val']/C['deposits']-1:.1%}, IRR {C['port_irr']:.1%})")
    print(f"  shadow SPY ${C['spy_val']:,.0f} (gain/deposits {C['spy_val']/C['deposits']-1:.1%}, IRR {C['spy_irr']:.1%})")

def napkin_fv(annual_rate, years, monthly=MONTHLY):
    """ASSUMPTION-only napkin math: $monthly at the start of each month, constant annual_rate. Not a forecast."""
    m = (1 + annual_rate) ** (1 / 12) - 1
    return sum(monthly * (1 + m) ** (years * 12 - k) for k in range(years * 12))
</code></pre>
<p>To use your own trades, replace <code>WEIGHTS</code> and <code>contributions()</code> with your real buy dates and dollar amounts. The shadow logic stays the same: same dollars, same days, into SPY.</p>
<h2>The no-code option</h2>
<p>If you&#8217;d rather not maintain a script, <a href="https://portfoliotracker24.com/?utm_source=finxter-blog&amp;utm_medium=article">PortfolioTracker24</a> is a free, private, browser-based stock and ETF tracker that uses the same idea: its &#8220;S&amp;P Equivalent&#8221; and &#8220;BTC Equivalent&#8221; figures replay your dated purchases and sale proceeds into the benchmark. Trades stay in your browser (no account, no broker connection), backups are Excel files, and it reports observed Sharpe, Sortino and drawdown. Be aware of its stated limits: you update prices manually, and cash, dividends, tax and FX aren&#8217;t modeled. A $39 one-time Windows edition exists for offline use. For a broader comparison, see Finxter&#8217;s roundup of <a href="https://blog.finxter.com/best-completely-free-investment-portfolio-trackers-2025-edition/">free investment portfolio trackers</a>.</p>
<h2>Your 5-step checklist</h2>
<ol>
<li><strong>Use a total-return benchmark</strong> (SPY or VOO adjusted close), never the bare ^GSPC level.</li>
<li><strong>List every deposit and withdrawal</strong> with its date and amount.</li>
<li><strong>Build the shadow S&amp;P 500</strong>: buy the benchmark with the same dollars on the same dates.</li>
<li><strong>Compare like with like</strong>: your IRR vs the shadow&#8217;s IRR, your ending value vs the shadow&#8217;s ending value.</li>
<li><strong>Add risk</strong>: volatility, max drawdown, Sharpe and beta, and say which window you measured.</li>
</ol>
<figure class="wp-block-table is-style-stripes">
<table>
<thead>
<tr>
<th>Mistake</th>
<th>What it does to the result (this test)</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Benchmark = price-only ^GSPC</td>
<td>Understates the S&amp;P 500 by 11.6 points over 5 years</td>
<td>Use SPY/VOO adjusted close (total return)</td>
</tr>
<tr>
<td>Your IRR vs the index&#8217;s lump-sum CAGR</td>
<td>Shows a fake +1.5-point win (14.0% vs 12.5%)</td>
<td>Shadow S&amp;P 500: same dollars, same dates (17.7%)</td>
</tr>
<tr>
<td>Gain divided by deposits vs 5-year index return</td>
<td>Shows a fake 38-point loss (42.2% vs 80.3%)</td>
<td>Compare with the shadow&#8217;s 55.8% gain on deposits</td>
</tr>
<tr>
<td>Return only, no risk</td>
<td>Hides that the bonds barely cut the 2022 drop</td>
<td>Add volatility, max drawdown, Sharpe, beta</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption">All figures from compare_to_sp500.py, Oct 22, 2021 to Oct 22, 2026.</figcaption></figure>
<h2>Limitations</h2>
<ul>
<li>One example portfolio and one five-year window. A different start date can flip the result.</li>
<li>Taxes, trading costs and FX aren&#8217;t modeled. Bitcoin is sampled on stock-market trading days only.</li>
<li>Yahoo adjusted closes assume dividends are reinvested on the ex-date at that day&#8217;s price.</li>
<li>Nothing here is investment advice.</li>
</ul>
<p>Want more Python-for-money projects? The <a href="https://blog.finxter.com/kdp-royalty-calculator-python/">KDP royalty calculator in Python</a> applies the same &#8220;script the real rules&#8221; approach to book income, and <a href="https://blog.finxter.com/buying-and-holding-tech-stocks-500k-in-10-years/">buying and holding tech stocks</a> runs a longer buy-and-hold thought experiment.</p>
<h2>The takeaway</h2>
<p>To compare your portfolio to the S&amp;P 500 honestly, give the index the same deal you got: same dollars, same dates, dividends included. In this five-year test, that one change turned a &#8220;+1.5-point win&#8221; into a <strong>3.7-point-a-year loss</strong>. Run the shadow benchmark once a year, and you&#8217;ll know whether your stock picking is earning its keep.</p>
<p>And if your analysis turns into a story worth sharing, say a &#8220;What I learned benchmarking my portfolio&#8221; guide for your newsletter, you can draft it with <a href="https://www.imagineyourbook.com/?utm_source=finxter&amp;utm_medium=blog&amp;utm_campaign=compare-my-portfolio-to-sp-500&amp;utm_content=end">ImagineYourBook</a>. The 7-day trial is $0 today with a card required and includes one 10,000-word Small Book; the first $29.99 charge comes after 7 days unless you cancel before day 8. Check the numbers, add your own charts, and publish it as yours.</p>
<p class="sources" style="font-size:0.9em;color:#555"><strong>Sources (checked October 23, 2026):</strong> <a href="https://finance.yahoo.com/quote/SPY/history/">Yahoo Finance: SPY historical data (daily prices via yfinance)</a> · <a href="https://help.yahoo.com/kb/SLN28256.html">Yahoo Help: what is the adjusted close?</a> · <a href="https://fred.stlouisfed.org/series/DTB3">FRED: 3-Month Treasury Bill secondary market rate (DTB3)</a> · <a href="https://github.com/ranaroussi/yfinance">yfinance on GitHub</a> · <a href="https://en.wikipedia.org/wiki/Time-weighted_return">Wikipedia: time-weighted return and the problem of external flows</a> · <a href="https://web.stanford.edu/~wfsharpe/art/sr/sr.htm">William F. Sharpe: The Sharpe Ratio (1994)</a> · <a href="https://portfoliotracker24.com/">PortfolioTracker24 (feature claims checked on its site)</a> · <a href="https://www.imagineyourbook.com/pricing">ImagineYourBook pricing and FAQ</a>. Script: compare_to_sp500.py (shown above). Not investment advice.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Minified using Disk

Served from: blog.finxter.com @ 2026-11-10 22:36:59 by W3 Total Cache
-->