Question

Compare the returns of the following over the 10 year period ending Dec. 31, 2025: Vanguard 2025 TDF Vanguard 2030 TDF 80/20 stock/bond portfolio (Vanguard total stock market fund and total bond market fund)
Executive Summary

Scope

Conclusion

Confidence: High

Sensitivities

Validated Analysis

Opus 4.6 Thinking Click to collapse
Final Verified Code (75 lines)

Show code

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

years = list(range(2016, 2026))
vttvx = np.array([7.48, 15.94, -5.15, 19.63, 13.30, 9.80, -15.55, 14.55, 9.44, 14.60]) / 100
vthrx = np.array([7.85, 17.52, -5.86, 21.07, 14.10, 11.38, -16.27, 16.03, 10.64, 16.24]) / 100
vtsax = np.array([12.66, 21.17, -5.17, 30.80, 20.99, 25.71, -19.53, 26.01, 23.74, 17.12]) / 100
vbtlx = np.array([2.61, 3.56, -0.23, 8.71, 7.72, -1.67, -13.16, 5.70, 1.24, 7.15]) / 100
port_8020 = 0.80 * vtsax + 0.20 * vbtlx

vttvx_cum = np.cumprod(1 + vttvx)
vthrx_cum = np.cumprod(1 + vthrx)
port_cum  = np.cumprod(1 + port_8020)

x_years = [2015] + years
v_vals = np.concatenate([[10000], 10000 * vttvx_cum])
h_vals = np.concatenate([[10000], 10000 * vthrx_cum])
p_vals = np.concatenate([[10000], 10000 * port_cum])

fig, axes = plt.subplots(2, 1, figsize=(12, 11))
fig.suptitle("Vanguard Portfolio Comparison: 10 Years (Jan 1, 2016 – Dec 31, 2025)\n"
             "Total Return | Lump Sum | Annual Rebalancing for 80/20 DIY",
             fontsize=13, fontweight='bold', y=0.99)

# ── Panel 1: Growth of $10,000 ──────────────────────────────────
ax1 = axes[0]
ax1.plot(x_years, p_vals, '^-', color='#2E8B57', lw=2.5, markersize=6,
         label=f'80/20 DIY (VTSAX/VBTLX)  —  $30,693  |  r_a = 11.87%')
ax1.plot(x_years, h_vals, 's-', color='#CC2200', lw=2.2, markersize=6,
         label=f'VTHRX (TDF 2030)         —  $22,938  |  r_a =  8.66%')
ax1.plot(x_years, v_vals, 'o-', color='#003087', lw=2.2, markersize=6,
         label=f'VTTVX (TDF 2025)         —  $21,342  |  r_a =  7.88%')

# Shade 2022 drawdown period
ax1.axvspan(2021.5, 2022.5, alpha=0.08, color='red', label='2022 Rate Shock')
ax1.axvspan(2019.5, 2020.5, alpha=0.08, color='orange', label='COVID-19 Shock (2020)')

ax1.set_ylabel("Portfolio Value (USD)", fontsize=11)
ax1.set_title("Growth of $10,000 Lump Sum Investment", fontsize=11)
ax1.legend(fontsize=9.5, loc='upper left', framealpha=0.9)
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'${x:,.0f}'))
ax1.grid(True, alpha=0.25)
ax1.set_xticks(x_years)
ax1.set_xticklabels([str(y) for y in x_years], fontsize=9)

# ── Panel 2: Annual Returns Bar Chart ───────────────────────────
ax2 = axes[1]
x = np.arange(len(years))
width = 0.28

ax2.bar(x - width, vttvx*100, width, color='#003087', alpha=0.88, label='VTTVX (TDF 2025)')
ax2.bar(x,         vthrx*100, width, color='#CC2200', alpha=0.88, label='VTHRX (TDF 2030)')
ax2.bar(x + width, port_8020*100, width, color='#2E8B57', alpha=0.88, label='80/20 DIY (VTSAX/VBTLX)')

ax2.axhline(0, color='black', linewidth=0.8)
ax2.set_xticks(x)
ax2.set_xticklabels(years, fontsize=9)
ax2.set_ylabel("Annual Total Return (%)", fontsize=11)
ax2.set_title("Annual Total Returns by Calendar Year (Source: investor.vanguard.com)", fontsize=11)
ax2.legend(fontsize=9.5)
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'{x:.0f}%'))
ax2.grid(True, axis='y', alpha=0.25)

# Annotate notable years
for ax2_x, lbl in [(years.index(2022), '2022\nRate Shock'), (years.index(2020), 'COVID\n2020')]:
    ax2.annotate(lbl, xy=(ax2_x, -16), ha='center', fontsize=7.5,
                 color='darkred', style='italic')

plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.savefig("vanguard_10yr_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
print("Final chart rendered.")
Expert Summaries (3)

Model A

Approach:
Conclusion:
Strengths:
Weaknesses:
Unique:

Model B

Approach:
Conclusion:
Strengths:
Weaknesses:
Unique:

Model C

Approach:
Conclusion:
Strengths:
Weaknesses:
Unique:
Assumptions and Limitations
Assumptions
Limitations
Figures (4)
Model B (4 figures)
Figure 1
Figure 2
Figure 3
Figure 4
Nomenclature (14 symbols)
SymbolDefinitionUnits
\(r_c\)
\(r_a\)
\(w_e\)
\(w_b\)
\(ER\)
\(\Delta ER\)
\(\sigma\)
\(MDD\)
\(T\)
\(vttvx\)
\(vthrx\)
\(vtsax\)
\(vbtlx\)
\(port_{8020}\)
🔍 Scope Definition Q&A 0/8 answered
#1 CRITICAL Should this comparison use actual historical return data (requiring real/current data through Dec. 31, 2025), or are you looking for a conceptual/illustrative analysis using available data up to my knowledge cutoff?
Not answered
This determines whether web search is needed to retrieve verified 10-year return figures ending Dec. 31, 2025. The answer fundamentally changes the data sourcing approach.
#2 CRITICAL Which specific share classes should be used for each fund? Options: (1) Investor Shares, (2) Admiral Shares, (3) ETF shares (where applicable). Admiral/ETF shares have lower expense ratios and will show meaningfully different net returns.
Not answered
Expense ratios differ across share classes and compound significantly over 10 years, materially affecting the return comparison.
#3 CRITICAL Should the 80/20 portfolio assume: (1) Static allocation held constant at 80/20 throughout the full 10 years, (2) Annual rebalancing back to 80/20, or (3) No rebalancing (drift allowed)? Rebalancing frequency significantly impacts returns and risk profile.
Not answered
The rebalancing assumption is a core methodological choice that changes the 80/20 portfolio's return calculation and its comparability to the glide-path TDFs.
#4 STRATEGIC What return metrics should be included in the comparison? Options: (1) Total cumulative return only, (2) Annualized return (CAGR) only, (3) Both, plus risk-adjusted metrics like Sharpe ratio or standard deviation.
Not answered
The TDFs have different risk profiles (especially the 2025 fund which is near/at target date), so a pure return comparison without risk context may be misleading.
#5 STRATEGIC Should the analysis account for the glide path changes within the TDFs over the 10-year period? The Vanguard 2025 TDF, for example, significantly shifted its equity/bond allocation between 2015 and 2025, making a direct comparison to a static 80/20 portfolio nuanced.
Not answered
Ignoring the glide path evolution could lead to an apples-to-oranges comparison; acknowledging it adds important interpretive context.
#6 STRATEGIC What is the primary purpose of this comparison? Options: (1) Evaluating which fund performed best historically, (2) Deciding which fund to invest in going forward, (3) Academic or educational analysis of TDF vs. DIY portfolio construction.
Not answered
The purpose shapes which insights to emphasize — e.g., backward-looking performance vs. forward-looking suitability vs. structural design differences.
#7 OPERATIONAL Should the comparison assume a lump-sum investment made on Jan. 1, 2016, or dollar-cost averaging (e.g., monthly contributions) over the 10-year period?
Not answered
Lump-sum vs. DCA produces different effective returns, especially in volatile periods, and changes the practical relevance of the comparison for different investor scenarios.
#8 OPERATIONAL Should dividends and capital gains distributions be treated as: (1) Reinvested (total return basis), or (2) Taken as cash (price return basis)?
Not answered
Total return vs. price return can differ substantially over a 10-year period for income-generating funds like bond-heavy TDFs.
🧠 Independent Analysis 3 models

Bottom Line

Confidence: High

Assumptions

Limitations

Detailed Analysis

Expert Code (2 executions)

import math
# verified values
rc_8020 = 2.0715313667431787
ra_8020 = 0.11875630391568426
rc_vttvx = (1+0.0862)**10 - 1
rc_vthrx = (1+0.0930)**10 - 1
# corrected DIY ER
ER_diy = 0.0004
ER_tdf = 0.0008
fee_only_cum_gap = ((1-ER_tdf)**10 / (1-ER_diy)**10) - 1
print('80/20 cumulative', rc_8020)
print('80/20 CAGR', ra_8020)
print('VTTVX cumulative from CAGR', rc_vttvx)
print('VTHRX cumulative from CAGR', rc_vthrx)
print('Fee-only cumulative relative gap over 10y', fee_only_cum_gap)
print('Fee-only approximate annual gap', ER_tdf-ER_diy)
print('80/20 minus VTTVX cumulative points', rc_8020-rc_vttvx)
print('80/20 minus VTHRX cumulative points', rc_8020-rc_vthrx)
print('80/20 minus VTTVX CAGR points', ra_8020-0.0862)
print('80/20 minus VTHRX CAGR points', ra_8020-0.0930)
Output:
80/20 cumulative 2.0715313667431787
80/20 CAGR 0.11875630391568426
VTTVX cumulative from CAGR 1.2861145333286523
VTHRX cumulative from CAGR 1.4333334123139303
Fee-only cumulative relative gap over 10y -0.00399440256064032
Fee-only approximate annual gap 0.0004
80/20 minus VTTVX cumulative points 0.7854168334145264
80/20 minus VTHRX cumulative points 0.6381979544292484
80/20 minus VTTVX CAGR points 0.03255630391568426
80/20 minus VTHRX CAGR points 0.02575630391568426
Execution History (2 executions)
Execution 2 of 2: success (selected)
import math
# verified values
rc_8020 = 2.0715313667431787
ra_8020 = 0.11875630391568426
rc_vttvx = (1+0.0862)**10 - 1
rc_vthrx = (1+0.0930)**10 - 1
# corrected DIY ER
ER_diy = 0.0004
ER_tdf = 0.0008
fee_only_cum_gap = ((1-ER_tdf)**10 / (1-ER_diy)**10) - 1
print('80/20 cumulative', rc_8020)
print('80/20 CAGR', ra_8020)
print('VTTVX cumulative from CAGR', rc_vttvx)
print('VTHRX cumulative from CAGR', rc_vthrx)
print('Fee-only cumulative relative gap over 10y', fee_only_cum_gap)
print('Fee-only approximate annual gap', ER_tdf-ER_diy)
print('80/20 minus VTTVX cumulative points', rc_8020-rc_vttvx)
print('80/20 minus VTHRX cumulative points', rc_8020-rc_vthrx)
print('80/20 minus VTTVX CAGR points', ra_8020-0.0862)
print('80/20 minus VTHRX CAGR points', ra_8020-0.0930)
80/20 cumulative 2.0715313667431787
80/20 CAGR 0.11875630391568426
VTTVX cumulative from CAGR 1.2861145333286523
VTHRX cumulative from CAGR 1.4333334123139303
Fee-only cumulative relative gap over 10y -0.00399440256064032
Fee-only approximate annual gap 0.0004
80/20 minus VTTVX cumulative points 0.7854168334145264
80/20 minus VTHRX cumulative points 0.6381979544292484
80/20 minus VTTVX CAGR points 0.03255630391568426
80/20 minus VTHRX CAGR points 0.02575630391568426
Execution 1 of 2: success
vtsax = {
2016: 0.1266,
2017: 0.2117,
2018: -0.0517,
2019: 0.3080,
2020: 0.2099,
2021: 0.2571,
2022: -0.1953,
2023: 0.2601,
2024: 0.2374,
2025: 0.1712,
}
# provisional VBTLX annual total returns from third-party total return series, to be cross-checked by search
vbtlx = {
2016: 0.0261,
2017: 0.0356,
2018: 0.0,
2019: 0.0873,
2020: 0.0773,
2021: -0.0165,
2022: -0.1316,
2023: 0.0575,
2024: 0.0127,
2025: 0.0717,
}
# annual rebalanced 80/20
value = 1.0
vals = []
for y in sorted(vtsax):
    r = 0.8*vtsax[y] + 0.2*vbtlx[y]
    value *= (1+r)
    vals.append((y, r, value))
print('Annual rebalanced using weighted yearly returns:')
for row in vals:
    print(row)
rc = value - 1
ra = value**(1/10)-1
print('Cumulative return', rc)
print('CAGR', ra)
# fee differential rough estimate
er_vtsax=0.0004
er_vbtlx=0.0005
er_diy=0.8*er_vtsax+0.2*er_vbtlx
print('DIY ER', er_diy)
# TDF cumulative from Vanguard snippets
vttvx_rc = 1.2862
vthrx_rc = 1.4324
import math
print('VTTVX CAGR from cumulative', (1+vttvx_rc)**(1/10)-1)
print('VTHRX CAGR from cumulative', (1+vthrx_rc)**(1/10)-1)
print('VTTVX rc recon', (1+0.0862)**10-1)
print('VTHRX rc recon', (1+0.0930)**10-1)
# fee deltas in bps
for name, er in [('VTTVX',0.0008),('VTHRX',0.0008)]:
    print(name, 'delta bps vs DIY', (er-er_diy)*10000)
Annual rebalanced using weighted yearly returns:
(2016, 0.1065, 1.1065)
(2017, 0.17648, 1.30177512)
(2018, -0.04136000000000001, 1.2479337010368)
(2019, 0.26386, 1.57721348739237)
(2020, 0.18338000000000002, 1.866442896710383)
(2021, 0.20238, 2.2441736101466305)
(2022, -0.18256000000000003, 1.8344772758782615)
(2023, 0.21958000000000003, 2.2372917961156102)
(2024, 0.19246, 2.6678809751960206)
(2025, 0.1513, 3.0715313667431787)
Cumulative return 2.0715313667431787
CAGR 0.11875630391568426
DIY ER 0.00042
VTTVX CAGR from cumulative 0.08620406070366693
VTHRX CAGR from cumulative 0.09295806592699707
VTTVX rc recon 1.2861145333286523
VTHRX rc recon 1.4333334123139303
VTTVX delta bps vs DIY 3.8000000000000003
VTHRX delta bps vs DIY 3.8000000000000003
Nomenclature (7 symbols)
SymbolDefinitionUnits
\(r_c\)
\(r_a\)
\(w_e\)
\(w_b\)
\(ER\)
\(ΔER\)
\(T\)

Bottom Line

Confidence: High

Assumptions

Limitations

Detailed Analysis

Expert Code (5 executions)


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

years = list(range(2016, 2026))
vttvx = np.array([7.48, 15.94, -5.15, 19.63, 13.30, 9.80, -15.55, 14.55, 9.44, 14.60]) / 100
vthrx = np.array([7.85, 17.52, -5.86, 21.07, 14.10, 11.38, -16.27, 16.03, 10.64, 16.24]) / 100
vtsax = np.array([12.66, 21.17, -5.17, 30.80, 20.99, 25.71, -19.53, 26.01, 23.74, 17.12]) / 100
vbtlx = np.array([2.61, 3.56, -0.23, 8.71, 7.72, -1.67, -13.16, 5.70, 1.24, 7.15]) / 100
port_8020 = 0.80 * vtsax + 0.20 * vbtlx

vttvx_cum = np.cumprod(1 + vttvx)
vthrx_cum = np.cumprod(1 + vthrx)
port_cum  = np.cumprod(1 + port_8020)

x_years = [2015] + years
v_vals = np.concatenate([[10000], 10000 * vttvx_cum])
h_vals = np.concatenate([[10000], 10000 * vthrx_cum])
p_vals = np.concatenate([[10000], 10000 * port_cum])

fig, axes = plt.subplots(2, 1, figsize=(12, 11))
fig.suptitle("Vanguard Portfolio Comparison: 10 Years (Jan 1, 2016 – Dec 31, 2025)\n"
             "Total Return | Lump Sum | Annual Rebalancing for 80/20 DIY",
             fontsize=13, fontweight='bold', y=0.99)

# ── Panel 1: Growth of $10,000 ──────────────────────────────────
ax1 = axes[0]
ax1.plot(x_years, p_vals, '^-', color='#2E8B57', lw=2.5, markersize=6,
         label=f'80/20 DIY (VTSAX/VBTLX)  —  $30,693  |  r_a = 11.87%')
ax1.plot(x_years, h_vals, 's-', color='#CC2200', lw=2.2, markersize=6,
         label=f'VTHRX (TDF 2030)         —  $22,938  |  r_a =  8.66%')
ax1.plot(x_years, v_vals, 'o-', color='#003087', lw=2.2, markersize=6,
         label=f'VTTVX (TDF 2025)         —  $21,342  |  r_a =  7.88%')

# Shade 2022 drawdown period
ax1.axvspan(2021.5, 2022.5, alpha=0.08, color='red', label='2022 Rate Shock')
ax1.axvspan(2019.5, 2020.5, alpha=0.08, color='orange', label='COVID-19 Shock (2020)')

ax1.set_ylabel("Portfolio Value (USD)", fontsize=11)
ax1.set_title("Growth of $10,000 Lump Sum Investment", fontsize=11)
ax1.legend(fontsize=9.5, loc='upper left', framealpha=0.9)
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'${x:,.0f}'))
ax1.grid(True, alpha=0.25)
ax1.set_xticks(x_years)
ax1.set_xticklabels([str(y) for y in x_years], fontsize=9)

# ── Panel 2: Annual Returns Bar Chart ───────────────────────────
ax2 = axes[1]
x = np.arange(len(years))
width = 0.28

ax2.bar(x - width, vttvx*100, width, color='#003087', alpha=0.88, label='VTTVX (TDF 2025)')
ax2.bar(x,         vthrx*100, width, color='#CC2200', alpha=0.88, label='VTHRX (TDF 2030)')
ax2.bar(x + width, port_8020*100, width, color='#2E8B57', alpha=0.88, label='80/20 DIY (VTSAX/VBTLX)')

ax2.axhline(0, color='black', linewidth=0.8)
ax2.set_xticks(x)
ax2.set_xticklabels(years, fontsize=9)
ax2.set_ylabel("Annual Total Return (%)", fontsize=11)
ax2.set_title("Annual Total Returns by Calendar Year (Source: investor.vanguard.com)", fontsize=11)
ax2.legend(fontsize=9.5)
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'{x:.0f}%'))
ax2.grid(True, axis='y', alpha=0.25)

# Annotate notable years
for ax2_x, lbl in [(years.index(2022), '2022\nRate Shock'), (years.index(2020), 'COVID\n2020')]:
    ax2.annotate(lbl, xy=(ax2_x, -16), ha='center', fontsize=7.5,
                 color='darkred', style='italic')

plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.savefig("vanguard_10yr_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
print("Final chart rendered.")
Output:
<PIL.Image.Image image mode=RGBA size=1785x1640>
<Figure size 1200x1100 with 2 Axes>
Final chart rendered.

[stderr] /usr/local/lib/python3.12/site-packages/pydantic/main.py:464: UserWarning: Pydantic serializer warnings:
  PydanticSerializationUnexpectedValue(Expected `LineChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `ScatterChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `BarChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `PieChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `BoxAndWhiskerChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  return self.__pydantic_serializer__.to_python(
Execution History (5 executions)
Execution 5 of 5: success (selected)

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

years = list(range(2016, 2026))
vttvx = np.array([7.48, 15.94, -5.15, 19.63, 13.30, 9.80, -15.55, 14.55, 9.44, 14.60]) / 100
vthrx = np.array([7.85, 17.52, -5.86, 21.07, 14.10, 11.38, -16.27, 16.03, 10.64, 16.24]) / 100
vtsax = np.array([12.66, 21.17, -5.17, 30.80, 20.99, 25.71, -19.53, 26.01, 23.74, 17.12]) / 100
vbtlx = np.array([2.61, 3.56, -0.23, 8.71, 7.72, -1.67, -13.16, 5.70, 1.24, 7.15]) / 100
port_8020 = 0.80 * vtsax + 0.20 * vbtlx

vttvx_cum = np.cumprod(1 + vttvx)
vthrx_cum = np.cumprod(1 + vthrx)
port_cum  = np.cumprod(1 + port_8020)

x_years = [2015] + years
v_vals = np.concatenate([[10000], 10000 * vttvx_cum])
h_vals = np.concatenate([[10000], 10000 * vthrx_cum])
p_vals = np.concatenate([[10000], 10000 * port_cum])

fig, axes = plt.subplots(2, 1, figsize=(12, 11))
fig.suptitle("Vanguard Portfolio Comparison: 10 Years (Jan 1, 2016 – Dec 31, 2025)\n"
             "Total Return | Lump Sum | Annual Rebalancing for 80/20 DIY",
             fontsize=13, fontweight='bold', y=0.99)

# ── Panel 1: Growth of $10,000 ──────────────────────────────────
ax1 = axes[0]
ax1.plot(x_years, p_vals, '^-', color='#2E8B57', lw=2.5, markersize=6,
         label=f'80/20 DIY (VTSAX/VBTLX)  —  $30,693  |  r_a = 11.87%')
ax1.plot(x_years, h_vals, 's-', color='#CC2200', lw=2.2, markersize=6,
         label=f'VTHRX (TDF 2030)         —  $22,938  |  r_a =  8.66%')
ax1.plot(x_years, v_vals, 'o-', color='#003087', lw=2.2, markersize=6,
         label=f'VTTVX (TDF 2025)         —  $21,342  |  r_a =  7.88%')

# Shade 2022 drawdown period
ax1.axvspan(2021.5, 2022.5, alpha=0.08, color='red', label='2022 Rate Shock')
ax1.axvspan(2019.5, 2020.5, alpha=0.08, color='orange', label='COVID-19 Shock (2020)')

ax1.set_ylabel("Portfolio Value (USD)", fontsize=11)
ax1.set_title("Growth of $10,000 Lump Sum Investment", fontsize=11)
ax1.legend(fontsize=9.5, loc='upper left', framealpha=0.9)
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'${x:,.0f}'))
ax1.grid(True, alpha=0.25)
ax1.set_xticks(x_years)
ax1.set_xticklabels([str(y) for y in x_years], fontsize=9)

# ── Panel 2: Annual Returns Bar Chart ───────────────────────────
ax2 = axes[1]
x = np.arange(len(years))
width = 0.28

ax2.bar(x - width, vttvx*100, width, color='#003087', alpha=0.88, label='VTTVX (TDF 2025)')
ax2.bar(x,         vthrx*100, width, color='#CC2200', alpha=0.88, label='VTHRX (TDF 2030)')
ax2.bar(x + width, port_8020*100, width, color='#2E8B57', alpha=0.88, label='80/20 DIY (VTSAX/VBTLX)')

ax2.axhline(0, color='black', linewidth=0.8)
ax2.set_xticks(x)
ax2.set_xticklabels(years, fontsize=9)
ax2.set_ylabel("Annual Total Return (%)", fontsize=11)
ax2.set_title("Annual Total Returns by Calendar Year (Source: investor.vanguard.com)", fontsize=11)
ax2.legend(fontsize=9.5)
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'{x:.0f}%'))
ax2.grid(True, axis='y', alpha=0.25)

# Annotate notable years
for ax2_x, lbl in [(years.index(2022), '2022\nRate Shock'), (years.index(2020), 'COVID\n2020')]:
    ax2.annotate(lbl, xy=(ax2_x, -16), ha='center', fontsize=7.5,
                 color='darkred', style='italic')

plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.savefig("vanguard_10yr_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
print("Final chart rendered.")
<PIL.Image.Image image mode=RGBA size=1785x1640>
<Figure size 1200x1100 with 2 Axes>
Final chart rendered.

[stderr] /usr/local/lib/python3.12/site-packages/pydantic/main.py:464: UserWarning: Pydantic serializer warnings:
  PydanticSerializationUnexpectedValue(Expected `LineChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `ScatterChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `BarChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `PieChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  PydanticSerializationUnexpectedValue(Expected `BoxAndWhiskerChart` - serialized value may not be as expected [field_name='elements', input_value=Chart(type=<ChartType.UNK...nvestment', elements=[]), input_type=Chart])
  return self.__pydantic_serializer__.to_python(
Execution 4 of 5: success

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import matplotlib.patches as mpatches

# ─────────────────────────────────────────────────────────────────
# FINAL VERIFIED ANNUAL TOTAL RETURNS
# Source: investor.vanguard.com, Yahoo Finance / Morningstar cross-check
# All data as of 12/31/2025 (calendar year returns)
# ─────────────────────────────────────────────────────────────────

years = list(range(2016, 2026))

# VTTVX – confirmed from investor.vanguard.com
vttvx = np.array([7.48, 15.94, -5.15, 19.63, 13.30, 9.80, -15.55, 14.55, 9.44, 14.60]) / 100

# VTHRX – confirmed from investor.vanguard.com
vthrx = np.array([7.85, 17.52, -5.86, 21.07, 14.10, 11.38, -16.27, 16.03, 10.64, 16.24]) / 100

# VTSAX – confirmed from investor.vanguard.com
vtsax = np.array([12.66, 21.17, -5.17, 30.80, 20.99, 25.71, -19.53, 26.01, 23.74, 17.12]) / 100

# VBTLX – confirmed; 2018 = -0.23% per quarterly compound check (Q1-Q4 from Yahoo Finance)
# Note: minor sources show -0.03% vs -0.23% for 2018; using -0.23% (quarterly verified)
vbtlx = np.array([2.61, 3.56, -0.23, 8.71, 7.72, -1.67, -13.16, 5.70, 1.24, 7.15]) / 100

# 80/20 annual returns (annual rebalancing = blended each year)
port_8020 = 0.80 * vtsax + 0.20 * vbtlx

T = 10

def rc(rets):
    return np.prod(1 + rets) - 1

def cagr(rets, T=10):
    return np.prod(1 + rets)**(1/T) - 1

def annual_sigma(rets):
    return np.std(rets, ddof=1) * 100

def max_drawdown_annual(rets):
    cum = np.concatenate([[1.0], np.cumprod(1 + rets)])
    peaks = np.maximum.accumulate(cum)
    dd = (cum - peaks) / peaks
    return dd.min() * 100

# ─────────── SUMMARY TABLE ───────────
print("=" * 68)
print(f"  10-Year Performance: Jan 1, 2016 – Dec 31, 2025")
print("=" * 68)
print(f"{'Portfolio':<26} {'r_c (%)':>8} {'r_a/CAGR':>10} {'ER':>6} {'σ':>8} {'MDD':>8}")
print(f"{'':26} {'':>8} {'(%/yr)':>10} {'(%/yr)':>6} {'(%/yr)':>8} {'(%)':>8}")
print("-" * 68)

rows = [
    ("VTTVX (TDF 2025)", vttvx, 0.08),
    ("VTHRX (TDF 2030)", vthrx, 0.08),
    ("80/20 VTSAX/VBTLX", port_8020, 0.04),
]

results = {}
for name, rets, er in rows:
    rc_v = rc(rets) * 100
    ca_v = cagr(rets) * 100
    sig  = annual_sigma(rets)
    mdd  = max_drawdown_annual(rets)
    results[name] = {'rc': rc_v, 'cagr': ca_v, 'er': er, 'sigma': sig, 'mdd': mdd, 'rets': rets}
    print(f"{name:<26} {rc_v:>8.1f} {ca_v:>10.2f} {er:>6.2f} {sig:>8.1f} {mdd:>8.1f}")

print("=" * 68)
print()

# ─────────── ATTRIBUTION: FEE vs ALLOCATION ───────────
# Δ ER = 4 bps/yr
# At matching allocation, fee drag over 10 yrs: (1.0004)^10 - 1 ≈ 0.40%
# Remaining gap is pure allocation effect
tdf25_cagr = results["VTTVX (TDF 2025)"]['cagr']
tdf30_cagr = results["VTHRX (TDF 2030)"]['cagr']
diy_cagr   = results["80/20 VTSAX/VBTLX"]['cagr']

fee_drag_cumulative = ((1.0004)**10 - 1) * 100  # ~0.40%
fee_drag_annual_bps = 4

print("─── Return Gap Attribution ───")
print(f"VTTVX vs 80/20 gap:  {tdf25_cagr - diy_cagr:+.2f}% / yr")
print(f"VTHRX vs 80/20 gap:  {tdf30_cagr - diy_cagr:+.2f}% / yr")
print(f"Fee drag (TDF vs DIY): {fee_drag_annual_bps} bps/yr  (~{fee_drag_cumulative:.2f}% cumulatively)")
print()
print("─── Annual Return Table ───")
print(f"{'Year':<6} {'VTTVX':>8} {'VTHRX':>8} {'80/20':>8} {'VTSAX':>8} {'VBTLX':>8}")
for i, y in enumerate(years):
    print(f"{y:<6} {vttvx[i]*100:>8.2f} {vthrx[i]*100:>8.2f} {port_8020[i]*100:>8.2f} {vtsax[i]*100:>8.2f} {vbtlx[i]*100:>8.2f}")

print()
# Growth of $10k
print("─── Growth of $10,000 ───")
for name, rets, er in rows:
    final = 10000 * np.prod(1 + rets)
    print(f"{name:<26}  ${final:>10,.0f}")
====================================================================
  10-Year Performance: Jan 1, 2016 – Dec 31, 2025
====================================================================
Portfolio                   r_c (%)   r_a/CAGR     ER        σ      MDD
                                        (%/yr) (%/yr)   (%/yr)      (%)
--------------------------------------------------------------------
VTTVX (TDF 2025)              113.4       7.88   0.08     10.8    -15.5
VTHRX (TDF 2030)              129.4       8.66   0.08     11.6    -16.3
80/20 VTSAX/VBTLX             206.9      11.87   0.04     13.7    -18.3
====================================================================

─── Return Gap Attribution ───
VTTVX vs 80/20 gap:  -3.99% / yr
VTHRX vs 80/20 gap:  -3.21% / yr
Fee drag (TDF vs DIY): 4 bps/yr  (~0.40% cumulatively)

─── Annual Return Table ───
Year      VTTVX    VTHRX    80/20    VTSAX    VBTLX
2016       7.48     7.85    10.65    12.66     2.61
2017      15.94    17.52    17.65    21.17     3.56
2018      -5.15    -5.86    -4.18    -5.17    -0.23
2019      19.63    21.07    26.38    30.80     8.71
2020      13.30    14.10    18.34    20.99     7.72
2021       9.80    11.38    20.23    25.71    -1.67
2022     -15.55   -16.27   -18.26   -19.53   -13.16
2023      14.55    16.03    21.95    26.01     5.70
2024       9.44    10.64    19.24    23.74     1.24
2025      14.60    16.24    15.13    17.12     7.15

─── Growth of $10,000 ───
VTTVX (TDF 2025)            $    21,342
VTHRX (TDF 2030)            $    22,938
80/20 VTSAX/VBTLX           $    30,693
Execution 3 of 5: success

# ─────────────────────────────────────────────────────────────────
# FIGURE 1: Growth of $10,000 — all three portfolios
# ─────────────────────────────────────────────────────────────────

fig, axes = plt.subplots(2, 1, figsize=(11, 12))
fig.suptitle("10-Year Vanguard Portfolio Comparison (Jan 1, 2016 – Dec 31, 2025)", 
             fontsize=14, fontweight='bold', y=0.98)

# Plot years on x-axis (end-of-year values)
x_years = [2015] + years  # start from Jan 1, 2016 = end of 2015

vttvx_vals = np.concatenate([[10000], 10000 * vttvx_cum])
vthrx_vals = np.concatenate([[10000], 10000 * vthrx_cum])
port_vals  = np.concatenate([[10000], 10000 * port_cum])

ax1 = axes[0]
ax1.plot(x_years, vttvx_vals, 'o-', color='#003087', lw=2.2, label=f'VTTVX (TDF 2025) — ${vttvx_vals[-1]:,.0f}', markersize=5)
ax1.plot(x_years, vthrx_vals, 's-', color='#CC0000', lw=2.2, label=f'VTHRX (TDF 2030) — ${vthrx_vals[-1]:,.0f}', markersize=5)
ax1.plot(x_years, port_vals,  '^-', color='#228B22', lw=2.2, label=f'80/20 DIY (VTSAX/VBTLX) — ${port_vals[-1]:,.0f}', markersize=5)
ax1.set_ylabel("Portfolio Value (USD)", fontsize=11)
ax1.set_title("Growth of $10,000 Invested (Lump Sum, Total Return, Annual Rebalancing for 80/20)", fontsize=11)
ax1.legend(fontsize=10, loc='upper left')
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'${x:,.0f}'))
ax1.grid(True, alpha=0.3)
ax1.set_xticks(x_years)

# ─────────────────────────────────────────────────────────────────
# FIGURE 2: Annual returns bar chart
# ─────────────────────────────────────────────────────────────────

ax2 = axes[1]
x = np.arange(len(years))
width = 0.28

bars1 = ax2.bar(x - width, vttvx*100, width, color='#003087', alpha=0.85, label='VTTVX (TDF 2025)')
bars2 = ax2.bar(x,         vthrx*100, width, color='#CC0000', alpha=0.85, label='VTHRX (TDF 2030)')
bars3 = ax2.bar(x + width, port_8020*100, width, color='#228B22', alpha=0.85, label='80/20 DIY')

ax2.axhline(0, color='black', linewidth=0.8)
ax2.set_xticks(x)
ax2.set_xticklabels(years)
ax2.set_ylabel("Annual Total Return (%)", fontsize=11)
ax2.set_title("Annual Total Returns by Year", fontsize=11)
ax2.legend(fontsize=10)
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'{x:.0f}%'))
ax2.grid(True, axis='y', alpha=0.3)

plt.tight_layout()
plt.savefig("vanguard_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
print("Figure 1 rendered.")
<PIL.Image.Image image mode=RGBA size=1635x1770>
<Figure size 1100x1200 with 2 Axes>
Figure 1 rendered.

[stderr] /usr/local/lib/python3.12/site-packages/pydantic/main.py:464: UserWarning: Pydantic serializer warnings:
  PydanticSerializationUnexpectedValue(PydanticSerializationUnexpectedValue: Expected `str` - serialized value may not be as expected [field_name='x_ticks', input_value=2015, input_type=int64]
PydanticSerializationUnexpectedValue: Expected `float` - serialized value may not be as expected [field_name='x_ticks', input_value=2015, input_type=int64])
  PydanticSerializationUnexpectedValue(Expected `ScatterChart` - serialized value may not be as expected [field_name='elements', input_value=LineChart(type=<ChartType...000'], y_scale='linear'), input_type=LineChart])
  PydanticSerializationUnexpectedValue(Expected `BarChart` - serialized value may not be as expected [field_name='elements', input_value=LineChart(type=<ChartType...000'], y_scale='linear'), input_type=LineChart])
  PydanticSerializationUnexpectedValue(Expected `PieChart` - serialized value may not be as expected [field_name='elements', input_value=LineChart(type=<ChartType...000'], y_scale='linear'), input_type=LineChart])
  PydanticSerializationUnexpectedValue(Expected `BoxAndWhiskerChart` - serialized value may not be as expected [field_name='elements', input_value=LineChart(type=<ChartType...000'], y_scale='linear'), input_type=LineChart])
  return self.__pydantic_serializer__.to_python(
Execution 2 of 5: success

# ─────────────────────────────────────────────────────────────────
# EXPENSE RATIO DATA (from verified sources)
# VTTVX ER: 0.08% (Vanguard fact sheet 12/31/2025)
# VTHRX ER: confirmed from Vanguard
# VTSAX ER: 0.04%; VBTLX ER: 0.04%
# 80/20 blended ER: 0.80*0.04 + 0.20*0.04 = 0.04%
# ─────────────────────────────────────────────────────────────────

er_vttvx = 0.08
er_vthrx = 0.08  # Vanguard target dates are all 0.08% 
er_8020  = 0.80 * 0.04 + 0.20 * 0.04  # = 0.04%

delta_er_vttvx_vs_8020 = (er_vttvx - er_8020) * 100  # in bps
delta_er_vthrx_vs_8020 = (er_vthrx - er_8020) * 100  # in bps

print("Expense Ratios:")
print(f"  VTTVX:      {er_vttvx:.2f}%")
print(f"  VTHRX:      {er_vthrx:.2f}%")
print(f"  80/20 DIY:  {er_8020:.2f}%")
print(f"  ΔER (VTTVX vs 80/20): {delta_er_vttvx_vs_8020:.0f} bps")
print(f"  ΔER (VTHRX vs 80/20): {delta_er_vthrx_vs_8020:.0f} bps")

# Cost of 4 bps over 10 years on a $10,000 lump sum
# If ER were 0.04% lower: ~ 0.04%/yr compounded for 10 years
# Using approximation: 4bps * 10 = ~40bps total drag, or use compound
# More precisely: (1.0004)^10 / 1 - 1 ≈ 0.40% extra return
extra_return_from_lower_er = (1.0004)**10 - 1
print(f"\n  Extra compounded return if 80/20 ER vs TDF ER over 10yrs: {extra_return_from_lower_er*100:.2f}%")

# ─────────────────────────────────────────────────────────────────
# RISK METRICS: Annual std deviation and max drawdown (calendar-year)
# Note: Using annual returns, so σ is based on 10 annual observations
# This is an approximation; daily/monthly would be more precise
# ─────────────────────────────────────────────────────────────────

def annual_sigma(returns):
    return np.std(returns, ddof=1) * 100

def max_drawdown_annual(returns):
    """Approximate MDD using annual returns as simplified measure"""
    cum = np.cumprod(1 + returns)
    cum_with_start = np.concatenate([[1.0], cum])
    peaks = np.maximum.accumulate(cum_with_start)
    dd = (cum_with_start - peaks) / peaks
    return dd.min() * 100

print("\n--- Risk Metrics (based on annual return series, 10 obs) ---")
print(f"{'Fund':<25} {'σ_annual (%)':>14} {'MDD approx (%)':>16}")
for name, rets in [("VTTVX (TDF 2025)", vttvx), ("VTHRX (TDF 2030)", vthrx), ("80/20 VTSAX/VBTLX", port_8020)]:
    sig = annual_sigma(rets)
    mdd = max_drawdown_annual(rets)
    print(f"{name:<25} {sig:>14.2f} {mdd:>16.2f}")

print("\nNote: σ based on 10 annual observations is a rough estimate.")
print("MDD figures are approximations based on calendar-year data (not peak-to-trough intra-year).")
Expense Ratios:
  VTTVX:      0.08%
  VTHRX:      0.08%
  80/20 DIY:  0.04%
  ΔER (VTTVX vs 80/20): 4 bps
  ΔER (VTHRX vs 80/20): 4 bps

  Extra compounded return if 80/20 ER vs TDF ER over 10yrs: 0.40%

--- Risk Metrics (based on annual return series, 10 obs) ---
Fund                        σ_annual (%)   MDD approx (%)
VTTVX (TDF 2025)                   10.77           -15.55
VTHRX (TDF 2030)                   11.62           -16.27
80/20 VTSAX/VBTLX                  13.66           -18.26

Note: σ based on 10 annual observations is a rough estimate.
MDD figures are approximations based on calendar-year data (not peak-to-trough intra-year).
Execution 1 of 5: success

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

# ─────────────────────────────────────────────────────────────────
# VERIFIED ANNUAL TOTAL RETURNS (Source: Vanguard investor.vanguard.com,
# Vanguard fact sheets, as of 12/31/2025)
# ─────────────────────────────────────────────────────────────────

years = list(range(2016, 2026))

# VTTVX – Vanguard Target Retirement 2025 Fund
vttvx = np.array([7.48, 15.94, -5.15, 19.63, 13.30, 9.80, -15.55, 14.55, 9.44, 14.60]) / 100

# VTHRX – Vanguard Target Retirement 2030 Fund
vthrx = np.array([7.85, 17.52, -5.86, 21.07, 14.10, 11.38, -16.27, 16.03, 10.64, 16.24]) / 100

# VTSAX – Vanguard Total Stock Market (Admiral)
vtsax = np.array([12.66, 21.17, -5.17, 30.80, 20.99, 25.71, -19.53, 26.01, 23.74, 17.12]) / 100

# VBTLX – Vanguard Total Bond Market (Admiral)
vbtlx = np.array([2.60, 3.56, -0.03, 8.71, 7.72, -1.67, -13.16, 5.70, 1.24, 7.15]) / 100

# ─────────────────────────────────────────────────────────────────
# 80/20 PORTFOLIO: Annual rebalancing back to 80/20 (VTSAX/VBTLX)
# With annual rebalancing, the blended return each year is just:
# r_80_20 = 0.80 * r_vtsax + 0.20 * r_vbtlx
# This is exact for annual rebalancing (rebalance at start of each year)
# ─────────────────────────────────────────────────────────────────

port_8020 = 0.80 * vtsax + 0.20 * vbtlx

# ─────────────────────────────────────────────────────────────────
# CUMULATIVE GROWTH (growth of $1)
# ─────────────────────────────────────────────────────────────────

def cumulative_growth(returns):
    return np.cumprod(1 + returns)

vttvx_cum = cumulative_growth(vttvx)
vthrx_cum = cumulative_growth(vthrx)
port_cum  = cumulative_growth(port_8020)

# ─────────────────────────────────────────────────────────────────
# TOTAL CUMULATIVE RETURN and CAGR
# ─────────────────────────────────────────────────────────────────

T = 10

def cagr(returns, T):
    total = np.prod(1 + returns)
    return total**(1/T) - 1

def rc(returns):
    return np.prod(1 + returns) - 1

print("=" * 60)
print(f"{'Fund':<25} {'r_c (%)':>10} {'r_a / CAGR (%)':>15}")
print("=" * 60)
for name, rets in [("VTTVX (TDF 2025)", vttvx), ("VTHRX (TDF 2030)", vthrx), ("80/20 VTSAX/VBTLX", port_8020)]:
    rc_val = rc(rets) * 100
    ra_val = cagr(rets, T) * 100
    print(f"{name:<25} {rc_val:>10.2f} {ra_val:>15.2f}")

print()
print("Year-by-year 80/20 blended returns:")
for y, r in zip(years, port_8020):
    print(f"  {y}: {r*100:.2f}%")

print()
print("Annual return table:")
print(f"{'Year':<6} {'VTTVX':>8} {'VTHRX':>8} {'80/20':>8}")
for i, y in enumerate(years):
    print(f"{y:<6} {vttvx[i]*100:>8.2f} {vthrx[i]*100:>8.2f} {port_8020[i]*100:>8.2f}")
============================================================
Fund                         r_c (%)  r_a / CAGR (%)
============================================================
VTTVX (TDF 2025)              113.42            7.88
VTHRX (TDF 2030)              129.38            8.66
80/20 VTSAX/VBTLX             207.05           11.87

Year-by-year 80/20 blended returns:
  2016: 10.65%
  2017: 17.65%
  2018: -4.14%
  2019: 26.38%
  2020: 18.34%
  2021: 20.23%
  2022: -18.26%
  2023: 21.95%
  2024: 19.24%
  2025: 15.13%

Annual return table:
Year      VTTVX    VTHRX    80/20
2016       7.48     7.85    10.65
2017      15.94    17.52    17.65
2018      -5.15    -5.86    -4.14
2019      19.63    21.07    26.38
2020      13.30    14.10    18.34
2021       9.80    11.38    20.23
2022     -15.55   -16.27   -18.26
2023      14.55    16.03    21.95
2024       9.44    10.64    19.24
2025      14.60    16.24    15.13
Nomenclature (10 symbols)
SymbolDefinitionUnits
\(r_c\)
\(r_a\)
\(w_e\)
\(w_b\)
\(ER\)
\(delta_{ER}\)
\(sigma\)
\(MDD\)
\(T\)
\(NAV_t\)
Figures (4)
Figure 1
Figure 2
Figure 3
Figure 4

Bottom Line

Confidence: High

Assumptions

Limitations

Detailed Analysis

Expert Code (3 executions)

import numpy as np

vttvx_ret = [0.0748, 0.1594, -0.0515, 0.1963, 0.1330, 0.0980, -0.1555, 0.1455, 0.0944, 0.1460]
vthrx_ret = [0.0785, 0.1752, -0.0586, 0.2107, 0.1410, 0.1138, -0.1627, 0.1603, 0.1064, 0.1624]
vtsax_ret = [0.1266, 0.2117, -0.0517, 0.3080, 0.2099, 0.2571, -0.1953, 0.2601, 0.2374, 0.1712]
vbtlx_ret = [0.0261, 0.0361, -0.0023, 0.0871, 0.0772, -0.0167, -0.1316, 0.0570, 0.0124, 0.0715]

port_80_20_ret = [0.8 * s + 0.2 * b for s, b in zip(vtsax_ret, vbtlx_ret)]

def calc_metrics(returns):
    cum_ret = np.prod([1 + r for r in returns]) - 1
    cagr = (1 + cum_ret) ** (1/10) - 1
    return cum_ret, cagr

print("VTTVX:", calc_metrics(vttvx_ret))
print("VTHRX:", calc_metrics(vthrx_ret))
print("VTSAX:", calc_metrics(vtsax_ret))
print("VBTLX:", calc_metrics(vbtlx_ret))
print("80/20:", calc_metrics(port_80_20_ret))

# Expense ratios
# VTTVX: 0.08%
# VTHRX: 0.08%
# VTSAX: 0.04%
# VBTLX: 0.05% (or 0.03% for ETF BND)
# 80/20 ER = 0.8 * 0.04 + 0.2 * 0.05 = 0.042%
Output:
VTTVX: (1.1341556529778924, 0.07875447203259167)
VTHRX: (1.2937954050371707, 0.08656439013534833)
VTSAX: (2.7845813139455147, 0.14235683380944453)
VBTLX: (0.21613651104607623, 0.019760610295048053)
80/20: (2.0695544148718636, 0.11868427573289275)
Execution History (3 executions)
Execution 3 of 3: success (selected)
import numpy as np

vttvx_ret = [0.0748, 0.1594, -0.0515, 0.1963, 0.1330, 0.0980, -0.1555, 0.1455, 0.0944, 0.1460]
vthrx_ret = [0.0785, 0.1752, -0.0586, 0.2107, 0.1410, 0.1138, -0.1627, 0.1603, 0.1064, 0.1624]
vtsax_ret = [0.1266, 0.2117, -0.0517, 0.3080, 0.2099, 0.2571, -0.1953, 0.2601, 0.2374, 0.1712]
vbtlx_ret = [0.0261, 0.0361, -0.0023, 0.0871, 0.0772, -0.0167, -0.1316, 0.0570, 0.0124, 0.0715]

port_80_20_ret = [0.8 * s + 0.2 * b for s, b in zip(vtsax_ret, vbtlx_ret)]

def calc_metrics(returns):
    cum_ret = np.prod([1 + r for r in returns]) - 1
    cagr = (1 + cum_ret) ** (1/10) - 1
    return cum_ret, cagr

print("VTTVX:", calc_metrics(vttvx_ret))
print("VTHRX:", calc_metrics(vthrx_ret))
print("VTSAX:", calc_metrics(vtsax_ret))
print("VBTLX:", calc_metrics(vbtlx_ret))
print("80/20:", calc_metrics(port_80_20_ret))

# Expense ratios
# VTTVX: 0.08%
# VTHRX: 0.08%
# VTSAX: 0.04%
# VBTLX: 0.05% (or 0.03% for ETF BND)
# 80/20 ER = 0.8 * 0.04 + 0.2 * 0.05 = 0.042%
VTTVX: (1.1341556529778924, 0.07875447203259167)
VTHRX: (1.2937954050371707, 0.08656439013534833)
VTSAX: (2.7845813139455147, 0.14235683380944453)
VBTLX: (0.21613651104607623, 0.019760610295048053)
80/20: (2.0695544148718636, 0.11868427573289275)
Execution 2 of 3: success
import numpy as np

# Data we have so far
vttvx_ret = [0.0748, 0.1594, -0.0515, 0.1963, 0.1330, 0.0980, -0.1555, 0.1455, 0.0944, 0.1460]
vthrx_ret = [0.0785, 0.1752, -0.0586, 0.2107, 0.1410, 0.1138, -0.1627, 0.1603, 0.1064, 0.1624]
vtsax_ret = [0.1266, 0.2117, -0.0517, 0.3080, 0.2099, 0.2571, -0.1953, 0.2601, 0.2374, 0.1712]

def calc_metrics(returns):
    cum_ret = np.prod([1 + r for r in returns]) - 1
    cagr = (1 + cum_ret) ** (1/10) - 1
    return cum_ret, cagr

print("VTTVX:", calc_metrics(vttvx_ret))
print("VTHRX:", calc_metrics(vthrx_ret))
print("VTSAX:", calc_metrics(vtsax_ret))
VTTVX: (1.1341556529778924, 0.07875447203259167)
VTHRX: (1.2937954050371707, 0.08656439013534833)
VTSAX: (2.7845813139455147, 0.14235683380944453)
Execution 1 of 3: success
import datetime
print("Current date:", datetime.date.today())
Current date: 2026-03-09
Nomenclature (8 symbols)
SymbolDefinitionUnits
\(r_c\)
\(r_a\)
\(ER\)
\(\Delta ER\)
\(w_e\)
\(w_b\)
\(\sigma\)
\(MDD\)