|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import gradio as gr |
|
|
|
|
|
def run_simulation( |
|
|
initial_investment: float, |
|
|
num_years: int, |
|
|
num_simulations: int, |
|
|
target_success_rate: float, |
|
|
stock_mean_return: float, |
|
|
stock_std_dev: float, |
|
|
bond_mean_return: float, |
|
|
bond_std_dev: float, |
|
|
stock_allocation: float, |
|
|
correlation_stock_bond: float, |
|
|
mean_inflation: float, |
|
|
std_dev_inflation: float, |
|
|
min_swr_test: float, |
|
|
max_swr_test: float, |
|
|
swr_test_step: float |
|
|
): |
|
|
|
|
|
initial_investment = float(initial_investment) |
|
|
num_years = int(num_years) |
|
|
num_simulations = int(num_simulations) |
|
|
target_success_rate = float(target_success_rate) / 100.0 |
|
|
|
|
|
|
|
|
stock_mean_return = float(stock_mean_return) / 100.0 |
|
|
stock_std_dev = float(stock_std_dev) / 100.0 |
|
|
bond_mean_return = float(bond_mean_return) / 100.0 |
|
|
bond_std_dev = float(bond_std_dev) / 100.0 |
|
|
stock_allocation = float(stock_allocation) / 100.0 |
|
|
bond_allocation = 1.0 - stock_allocation |
|
|
correlation_stock_bond = float(correlation_stock_bond) |
|
|
|
|
|
mean_inflation = float(mean_inflation) / 100.0 |
|
|
std_dev_inflation = float(std_dev_inflation) / 100.0 |
|
|
|
|
|
|
|
|
mean_asset_returns = np.array([stock_mean_return, bond_mean_return]) |
|
|
cov_matrix = np.array([ |
|
|
[stock_std_dev**2, correlation_stock_bond * stock_std_dev * bond_std_dev], |
|
|
[correlation_stock_bond * stock_std_dev * bond_std_dev, bond_std_dev**2] |
|
|
]) |
|
|
|
|
|
|
|
|
withdrawal_rates_to_test = np.arange(min_swr_test / 100.0, (max_swr_test + swr_test_step) / 100.0, swr_test_step / 100.0) |
|
|
all_results = [] |
|
|
portfolio_paths_for_plotting = {} |
|
|
|
|
|
for swr in withdrawal_rates_to_test: |
|
|
success_count = 0 |
|
|
current_swr_paths = [] |
|
|
|
|
|
for i in range(num_simulations): |
|
|
portfolio_value = float(initial_investment) |
|
|
current_annual_withdrawal_nominal = initial_investment * swr |
|
|
|
|
|
simulation_failed_this_run = False |
|
|
path = [portfolio_value] |
|
|
|
|
|
for year in range(num_years): |
|
|
if year > 0: |
|
|
yearly_inflation = np.random.normal(mean_inflation, std_dev_inflation) |
|
|
yearly_inflation = max(yearly_inflation, -0.05) |
|
|
current_annual_withdrawal_nominal *= (1 + yearly_inflation) |
|
|
|
|
|
portfolio_value -= current_annual_withdrawal_nominal |
|
|
|
|
|
if portfolio_value <= 0: |
|
|
simulation_failed_this_run = True |
|
|
portfolio_value = 0 |
|
|
path.append(portfolio_value) |
|
|
break |
|
|
|
|
|
asset_returns_this_year = np.random.multivariate_normal(mean_asset_returns, cov_matrix) |
|
|
portfolio_return_this_year = (stock_allocation * asset_returns_this_year[0] + |
|
|
bond_allocation * asset_returns_this_year[1]) |
|
|
portfolio_return_this_year = max(portfolio_return_this_year, -0.99) |
|
|
|
|
|
portfolio_value *= (1 + portfolio_return_this_year) |
|
|
path.append(portfolio_value) |
|
|
|
|
|
if not simulation_failed_this_run: |
|
|
success_count += 1 |
|
|
|
|
|
if swr == 0.035 and i < 100: |
|
|
current_swr_paths.append(path) |
|
|
|
|
|
success_probability = success_count / num_simulations |
|
|
all_results.append({'swr': swr, 'success_rate': success_probability}) |
|
|
|
|
|
if swr == 0.035: |
|
|
portfolio_paths_for_plotting[swr] = current_swr_paths |
|
|
|
|
|
final_swr = 0.0 |
|
|
initial_annual_withdrawal_amount = 0.0 |
|
|
|
|
|
eligible_rates = [r for r in all_results if r['success_rate'] >= target_success_rate] |
|
|
if eligible_rates: |
|
|
final_swr = max(r['swr'] for r in eligible_rates) |
|
|
initial_annual_withdrawal_amount = initial_investment * final_swr |
|
|
|
|
|
results_text = "" |
|
|
if final_swr > 0: |
|
|
results_text += f"The highest Safe Withdrawal Rate for {target_success_rate*100:.0f}% success over {num_years} years is approximately: {final_swr*100:.2f}%\n" |
|
|
results_text += f"This corresponds to an initial annual withdrawal of: ${initial_annual_withdrawal_amount:,.2f}\n" |
|
|
else: |
|
|
results_text += f"No tested SWR achieved the {target_success_rate*100:.0f}% success rate with the given assumptions.\n" |
|
|
lowest_tested_swr = min(r['swr'] for r in all_results) |
|
|
highest_success_at_lowest_swr = [r['success_rate'] for r in all_results if r['swr'] == lowest_tested_swr][0] |
|
|
results_text += f"The lowest tested SWR ({lowest_tested_swr*100:.1f}%) had a success rate of {highest_success_at_lowest_swr*100:.2f}%.\n" |
|
|
results_text += "Consider revising assumptions (e.g., higher returns, lower volatility/inflation, shorter horizon) or target success rate.\n" |
|
|
|
|
|
|
|
|
swrs_plot = [r['swr'] * 100 for r in all_results] |
|
|
success_rates_plot = [r['success_rate'] * 100 for r in all_results] |
|
|
|
|
|
fig1, ax1 = plt.subplots(figsize=(10, 6)) |
|
|
ax1.plot(swrs_plot, success_rates_plot, marker='o', linestyle='-') |
|
|
ax1.axhline(y=target_success_rate * 100, color='r', linestyle='--', label=f'{target_success_rate*100:.0f}% Target Success') |
|
|
if final_swr > 0: |
|
|
ax1.axvline(x=final_swr * 100, color='g', linestyle=':', label=f'Calculated SWR: {final_swr*100:.2f}%') |
|
|
ax1.set_title(f'Monte Carlo SWR Success Rates ({num_simulations:,} simulations)') |
|
|
ax1.set_xlabel('Initial Withdrawal Rate (%)') |
|
|
ax1.set_ylabel('Probability of Portfolio Lasting 30 Years (%)') |
|
|
ax1.grid(True) |
|
|
ax1.legend() |
|
|
ax1.set_ylim(0, 105) |
|
|
|
|
|
fig2, ax2 = plt.subplots(figsize=(12, 7)) |
|
|
chosen_swr_for_path_plot = final_swr if final_swr > 0 else 0.035 |
|
|
if chosen_swr_for_path_plot in portfolio_paths_for_plotting and portfolio_paths_for_plotting[chosen_swr_for_path_plot]: |
|
|
paths_to_plot_sample = portfolio_paths_for_plotting[chosen_swr_for_path_plot] |
|
|
for i, path_data in enumerate(paths_to_plot_sample): |
|
|
if len(path_data) == num_years + 1: |
|
|
ax2.plot(range(num_years + 1), path_data, alpha=0.1, color='blue' if path_data[-1] > 0 else 'red') |
|
|
|
|
|
ax2.set_title(f'Sample Portfolio Paths for {chosen_swr_for_path_plot*100:.2f}% SWR (100 simulations shown)') |
|
|
ax2.set_xlabel('Year') |
|
|
ax2.set_ylabel('Portfolio Value ($)') |
|
|
ax2.set_yscale('log') |
|
|
ax2.grid(True, which="both", ls="-", alpha=0.5) |
|
|
ax2.axhline(y=initial_investment, color='k', linestyle='--', label=f'Initial: ${initial_investment:,.0f}') |
|
|
ax2.axhline(y=1, color='grey', linestyle=':', label='$1 (for log scale visibility near zero)') |
|
|
ax2.legend() |
|
|
else: |
|
|
ax2.text(0.5, 0.5, f"No portfolio paths were stored for SWR {chosen_swr_for_path_plot*100:.2f}% to plot individual simulations.", |
|
|
horizontalalignment='center', verticalalignment='center', transform=ax2.transAxes, fontsize=12) |
|
|
ax2.set_title("Sample Portfolio Paths") |
|
|
ax2.set_xlabel("Year") |
|
|
ax2.set_ylabel("Portfolio Value ($)") |
|
|
|
|
|
return results_text, fig1, fig2 |
|
|
|
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
gr.Markdown( |
|
|
""" |
|
|
# Safe Withdrawal Rate Calculator |
|
|
This application performs Monte Carlo simulations to determine a safe withdrawal rate from a retirement portfolio. |
|
|
Adjust the parameters below and click "Run Simulation" to see the results and portfolio projections. |
|
|
""" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
gr.Markdown("### Investment Details") |
|
|
initial_investment = gr.Number(label="Initial Investment ($)", value=2_500_000.0, interactive=True) |
|
|
num_years = gr.Slider(minimum=10, maximum=60, value=30, step=1, label="Number of Years", interactive=True) |
|
|
target_success_rate = gr.Slider(minimum=70, maximum=100, value=95, step=1, label="Target Success Rate (%)", interactive=True) |
|
|
num_simulations = gr.Slider(minimum=1000, maximum=50000, value=10000, step=1000, label="Number of Simulations", interactive=True) |
|
|
|
|
|
gr.Markdown("### Market Assumptions (Annualized Nominal Returns)") |
|
|
stock_mean_return = gr.Slider(minimum=0, maximum=20, value=9.0, step=0.1, label="Stock Mean Return (%)", interactive=True) |
|
|
stock_std_dev = gr.Slider(minimum=5, maximum=30, value=15.0, step=0.1, label="Stock Standard Deviation (%)", interactive=True) |
|
|
bond_mean_return = gr.Slider(minimum=0, maximum=10, value=4.0, step=0.1, label="Bond Mean Return (%)", interactive=True) |
|
|
bond_std_dev = gr.Slider(minimum=1, maximum=15, value=5.0, step=0.1, label="Bond Standard Deviation (%)", interactive=True) |
|
|
correlation_stock_bond = gr.Slider(minimum=-1.0, maximum=1.0, value=-0.2, step=0.01, label="Correlation (Stocks vs. Bonds)", interactive=True) |
|
|
stock_allocation = gr.Slider(minimum=0, maximum=100, value=60, step=1, label="Stock Allocation (%)", interactive=True) |
|
|
|
|
|
gr.Markdown("### Inflation Assumptions (Annualized)") |
|
|
mean_inflation = gr.Slider(minimum=0, maximum=10, value=2.5, step=0.1, label="Mean Inflation (%)", interactive=True) |
|
|
std_dev_inflation = gr.Slider(minimum=0, maximum=5, value=1.5, step=0.1, label="Inflation Standard Deviation (%)", interactive=True) |
|
|
|
|
|
gr.Markdown("### SWR Test Range") |
|
|
min_swr_test = gr.Slider(minimum=0.5, maximum=10.0, value=2.5, step=0.1, label="Min SWR to Test (%)", interactive=True) |
|
|
max_swr_test = gr.Slider(minimum=0.5, maximum=10.0, value=5.0, step=0.1, label="Max SWR to Test (%)", interactive=True) |
|
|
swr_test_step = gr.Slider(minimum=0.01, maximum=0.5, value=0.1, step=0.01, label="SWR Test Step (%)", interactive=True) |
|
|
|
|
|
run_button = gr.Button("Run Simulation") |
|
|
|
|
|
with gr.Column(): |
|
|
gr.Markdown("### Simulation Results") |
|
|
results_output = gr.Textbox(label="Calculated Safe Withdrawal Rate", lines=5) |
|
|
swr_plot_output = gr.Plot(label="SWR Success Rates") |
|
|
paths_plot_output = gr.Plot(label="Sample Portfolio Paths") |
|
|
|
|
|
run_button.click( |
|
|
fn=run_simulation, |
|
|
inputs=[ |
|
|
initial_investment, |
|
|
num_years, |
|
|
num_simulations, |
|
|
target_success_rate, |
|
|
stock_mean_return, |
|
|
stock_std_dev, |
|
|
bond_mean_return, |
|
|
bond_std_dev, |
|
|
stock_allocation, |
|
|
correlation_stock_bond, |
|
|
mean_inflation, |
|
|
std_dev_inflation, |
|
|
min_swr_test, |
|
|
max_swr_test, |
|
|
swr_test_step |
|
|
], |
|
|
outputs=[results_output, swr_plot_output, paths_plot_output] |
|
|
) |
|
|
|
|
|
demo.launch() |
|
|
|