liaoch commited on
Commit
1a93ec0
·
0 Parent(s):

Initial commit for Safe Withdrawal Rate Calculator Gradio app

Browse files
Files changed (3) hide show
  1. README.md +29 -0
  2. app.py +222 -0
  3. requirements.txt +3 -0
README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Safe Withdrawal Rate Calculator
2
+
3
+ This Hugging Face Space hosts an interactive application that performs Monte Carlo simulations to determine a safe withdrawal rate from a retirement portfolio. Users can adjust various financial parameters and instantly see the impact on portfolio success rates and projected portfolio paths.
4
+
5
+ ## How to Use
6
+
7
+ 1. **Adjust Parameters**: On the left side of the interface, you will find several sections with sliders and number inputs:
8
+ * **Investment Details**: Set your initial investment, the number of years for the simulation, your target success rate, and the number of Monte Carlo simulations to run.
9
+ * **Market Assumptions**: Define the expected mean returns and standard deviations for stocks and bonds, their correlation, and your portfolio's stock allocation.
10
+ * **Inflation Assumptions**: Input the mean inflation rate and its standard deviation.
11
+ * **SWR Test Range**: Specify the range and step for the Safe Withdrawal Rates you want to test.
12
+ 2. **Run Simulation**: Click the "Run Simulation" button.
13
+ 3. **View Results**: The right side of the interface will display:
14
+ * **Calculated Safe Withdrawal Rate**: Text output showing the highest SWR that meets your target success rate and the corresponding initial annual withdrawal amount.
15
+ * **SWR Success Rates Plot**: A graph showing the probability of portfolio success for various withdrawal rates.
16
+ * **Sample Portfolio Paths Plot**: A visualization of how a sample of portfolios might perform over the simulation period.
17
+
18
+ ## Technical Details
19
+
20
+ This application is built using:
21
+
22
+ * **Python**: For the core simulation logic.
23
+ * **NumPy**: For numerical operations and statistical calculations.
24
+ * **Matplotlib**: For generating the simulation plots.
25
+ * **Gradio**: For creating the interactive web interface, allowing easy deployment to Hugging Face Spaces.
26
+
27
+ ## Deployment
28
+
29
+ This application is designed to be deployed on Hugging Face Spaces. The `app.py` file contains the Gradio application, and `requirements.txt` lists all necessary Python dependencies.
app.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import gradio as gr
4
+
5
+ def run_simulation(
6
+ initial_investment: float,
7
+ num_years: int,
8
+ num_simulations: int,
9
+ target_success_rate: float,
10
+ stock_mean_return: float,
11
+ stock_std_dev: float,
12
+ bond_mean_return: float,
13
+ bond_std_dev: float,
14
+ stock_allocation: float,
15
+ correlation_stock_bond: float,
16
+ mean_inflation: float,
17
+ std_dev_inflation: float,
18
+ min_swr_test: float,
19
+ max_swr_test: float,
20
+ swr_test_step: float
21
+ ):
22
+ # --- Core Parameters ---
23
+ initial_investment = float(initial_investment)
24
+ num_years = int(num_years)
25
+ num_simulations = int(num_simulations)
26
+ target_success_rate = float(target_success_rate) / 100.0 # Convert percentage to decimal
27
+
28
+ # --- Financial Assumptions (NOMINAL) ---
29
+ stock_mean_return = float(stock_mean_return) / 100.0
30
+ stock_std_dev = float(stock_std_dev) / 100.0
31
+ bond_mean_return = float(bond_mean_return) / 100.0
32
+ bond_std_dev = float(bond_std_dev) / 100.0
33
+ stock_allocation = float(stock_allocation) / 100.0
34
+ bond_allocation = 1.0 - stock_allocation
35
+ correlation_stock_bond = float(correlation_stock_bond)
36
+
37
+ mean_inflation = float(mean_inflation) / 100.0
38
+ std_dev_inflation = float(std_dev_inflation) / 100.0
39
+
40
+ # --- Covariance Matrix for generating correlated asset returns ---
41
+ mean_asset_returns = np.array([stock_mean_return, bond_mean_return])
42
+ cov_matrix = np.array([
43
+ [stock_std_dev**2, correlation_stock_bond * stock_std_dev * bond_std_dev],
44
+ [correlation_stock_bond * stock_std_dev * bond_std_dev, bond_std_dev**2]
45
+ ])
46
+
47
+ # --- SWRs to Test ---
48
+ withdrawal_rates_to_test = np.arange(min_swr_test / 100.0, (max_swr_test + swr_test_step) / 100.0, swr_test_step / 100.0)
49
+ all_results = []
50
+ portfolio_paths_for_plotting = {}
51
+
52
+ for swr in withdrawal_rates_to_test:
53
+ success_count = 0
54
+ current_swr_paths = []
55
+
56
+ for i in range(num_simulations):
57
+ portfolio_value = float(initial_investment)
58
+ current_annual_withdrawal_nominal = initial_investment * swr
59
+
60
+ simulation_failed_this_run = False
61
+ path = [portfolio_value]
62
+
63
+ for year in range(num_years):
64
+ if year > 0:
65
+ yearly_inflation = np.random.normal(mean_inflation, std_dev_inflation)
66
+ yearly_inflation = max(yearly_inflation, -0.05)
67
+ current_annual_withdrawal_nominal *= (1 + yearly_inflation)
68
+
69
+ portfolio_value -= current_annual_withdrawal_nominal
70
+
71
+ if portfolio_value <= 0:
72
+ simulation_failed_this_run = True
73
+ portfolio_value = 0
74
+ path.append(portfolio_value)
75
+ break
76
+
77
+ asset_returns_this_year = np.random.multivariate_normal(mean_asset_returns, cov_matrix)
78
+ portfolio_return_this_year = (stock_allocation * asset_returns_this_year[0] +
79
+ bond_allocation * asset_returns_this_year[1])
80
+ portfolio_return_this_year = max(portfolio_return_this_year, -0.99)
81
+
82
+ portfolio_value *= (1 + portfolio_return_this_year)
83
+ path.append(portfolio_value)
84
+
85
+ if not simulation_failed_this_run:
86
+ success_count += 1
87
+
88
+ if swr == 0.035 and i < 100:
89
+ current_swr_paths.append(path)
90
+
91
+ success_probability = success_count / num_simulations
92
+ all_results.append({'swr': swr, 'success_rate': success_probability})
93
+
94
+ if swr == 0.035:
95
+ portfolio_paths_for_plotting[swr] = current_swr_paths
96
+
97
+ final_swr = 0.0
98
+ initial_annual_withdrawal_amount = 0.0
99
+
100
+ eligible_rates = [r for r in all_results if r['success_rate'] >= target_success_rate]
101
+ if eligible_rates:
102
+ final_swr = max(r['swr'] for r in eligible_rates)
103
+ initial_annual_withdrawal_amount = initial_investment * final_swr
104
+
105
+ results_text = ""
106
+ if final_swr > 0:
107
+ 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"
108
+ results_text += f"This corresponds to an initial annual withdrawal of: ${initial_annual_withdrawal_amount:,.2f}\n"
109
+ else:
110
+ results_text += f"No tested SWR achieved the {target_success_rate*100:.0f}% success rate with the given assumptions.\n"
111
+ lowest_tested_swr = min(r['swr'] for r in all_results)
112
+ highest_success_at_lowest_swr = [r['success_rate'] for r in all_results if r['swr'] == lowest_tested_swr][0]
113
+ 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"
114
+ results_text += "Consider revising assumptions (e.g., higher returns, lower volatility/inflation, shorter horizon) or target success rate.\n"
115
+
116
+ # --- Plotting Results ---
117
+ swrs_plot = [r['swr'] * 100 for r in all_results]
118
+ success_rates_plot = [r['success_rate'] * 100 for r in all_results]
119
+
120
+ fig1, ax1 = plt.subplots(figsize=(10, 6))
121
+ ax1.plot(swrs_plot, success_rates_plot, marker='o', linestyle='-')
122
+ ax1.axhline(y=target_success_rate * 100, color='r', linestyle='--', label=f'{target_success_rate*100:.0f}% Target Success')
123
+ if final_swr > 0:
124
+ ax1.axvline(x=final_swr * 100, color='g', linestyle=':', label=f'Calculated SWR: {final_swr*100:.2f}%')
125
+ ax1.set_title(f'Monte Carlo SWR Success Rates ({num_simulations:,} simulations)')
126
+ ax1.set_xlabel('Initial Withdrawal Rate (%)')
127
+ ax1.set_ylabel('Probability of Portfolio Lasting 30 Years (%)')
128
+ ax1.grid(True)
129
+ ax1.legend()
130
+ ax1.set_ylim(0, 105)
131
+
132
+ fig2, ax2 = plt.subplots(figsize=(12, 7))
133
+ chosen_swr_for_path_plot = final_swr if final_swr > 0 else 0.035
134
+ if chosen_swr_for_path_plot in portfolio_paths_for_plotting and portfolio_paths_for_plotting[chosen_swr_for_path_plot]:
135
+ paths_to_plot_sample = portfolio_paths_for_plotting[chosen_swr_for_path_plot]
136
+ for i, path_data in enumerate(paths_to_plot_sample):
137
+ if len(path_data) == num_years + 1:
138
+ ax2.plot(range(num_years + 1), path_data, alpha=0.1, color='blue' if path_data[-1] > 0 else 'red')
139
+
140
+ ax2.set_title(f'Sample Portfolio Paths for {chosen_swr_for_path_plot*100:.2f}% SWR (100 simulations shown)')
141
+ ax2.set_xlabel('Year')
142
+ ax2.set_ylabel('Portfolio Value ($)')
143
+ ax2.set_yscale('log')
144
+ ax2.grid(True, which="both", ls="-", alpha=0.5)
145
+ ax2.axhline(y=initial_investment, color='k', linestyle='--', label=f'Initial: ${initial_investment:,.0f}')
146
+ ax2.axhline(y=1, color='grey', linestyle=':', label='$1 (for log scale visibility near zero)')
147
+ ax2.legend()
148
+ else:
149
+ 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.",
150
+ horizontalalignment='center', verticalalignment='center', transform=ax2.transAxes, fontsize=12)
151
+ ax2.set_title("Sample Portfolio Paths")
152
+ ax2.set_xlabel("Year")
153
+ ax2.set_ylabel("Portfolio Value ($)")
154
+
155
+ return results_text, fig1, fig2
156
+
157
+ # Gradio Interface
158
+ with gr.Blocks() as demo:
159
+ gr.Markdown(
160
+ """
161
+ # Safe Withdrawal Rate Calculator
162
+ This application performs Monte Carlo simulations to determine a safe withdrawal rate from a retirement portfolio.
163
+ Adjust the parameters below and click "Run Simulation" to see the results and portfolio projections.
164
+ """
165
+ )
166
+
167
+ with gr.Row():
168
+ with gr.Column():
169
+ gr.Markdown("### Investment Details")
170
+ initial_investment = gr.Number(label="Initial Investment ($)", value=2_500_000.0, interactive=True)
171
+ num_years = gr.Slider(minimum=10, maximum=60, value=30, step=1, label="Number of Years", interactive=True)
172
+ target_success_rate = gr.Slider(minimum=70, maximum=100, value=95, step=1, label="Target Success Rate (%)", interactive=True)
173
+ num_simulations = gr.Slider(minimum=1000, maximum=50000, value=10000, step=1000, label="Number of Simulations", interactive=True)
174
+
175
+ gr.Markdown("### Market Assumptions (Annualized Nominal Returns)")
176
+ stock_mean_return = gr.Slider(minimum=0, maximum=20, value=9.0, step=0.1, label="Stock Mean Return (%)", interactive=True)
177
+ stock_std_dev = gr.Slider(minimum=5, maximum=30, value=15.0, step=0.1, label="Stock Standard Deviation (%)", interactive=True)
178
+ bond_mean_return = gr.Slider(minimum=0, maximum=10, value=4.0, step=0.1, label="Bond Mean Return (%)", interactive=True)
179
+ bond_std_dev = gr.Slider(minimum=1, maximum=15, value=5.0, step=0.1, label="Bond Standard Deviation (%)", interactive=True)
180
+ correlation_stock_bond = gr.Slider(minimum=-1.0, maximum=1.0, value=-0.2, step=0.01, label="Correlation (Stocks vs. Bonds)", interactive=True)
181
+ stock_allocation = gr.Slider(minimum=0, maximum=100, value=60, step=1, label="Stock Allocation (%)", interactive=True)
182
+
183
+ gr.Markdown("### Inflation Assumptions (Annualized)")
184
+ mean_inflation = gr.Slider(minimum=0, maximum=10, value=2.5, step=0.1, label="Mean Inflation (%)", interactive=True)
185
+ std_dev_inflation = gr.Slider(minimum=0, maximum=5, value=1.5, step=0.1, label="Inflation Standard Deviation (%)", interactive=True)
186
+
187
+ gr.Markdown("### SWR Test Range")
188
+ min_swr_test = gr.Slider(minimum=0.5, maximum=10.0, value=2.5, step=0.1, label="Min SWR to Test (%)", interactive=True)
189
+ max_swr_test = gr.Slider(minimum=0.5, maximum=10.0, value=5.0, step=0.1, label="Max SWR to Test (%)", interactive=True)
190
+ swr_test_step = gr.Slider(minimum=0.01, maximum=0.5, value=0.1, step=0.01, label="SWR Test Step (%)", interactive=True)
191
+
192
+ run_button = gr.Button("Run Simulation")
193
+
194
+ with gr.Column():
195
+ gr.Markdown("### Simulation Results")
196
+ results_output = gr.Textbox(label="Calculated Safe Withdrawal Rate", lines=5)
197
+ swr_plot_output = gr.Plot(label="SWR Success Rates")
198
+ paths_plot_output = gr.Plot(label="Sample Portfolio Paths")
199
+
200
+ run_button.click(
201
+ fn=run_simulation,
202
+ inputs=[
203
+ initial_investment,
204
+ num_years,
205
+ num_simulations,
206
+ target_success_rate,
207
+ stock_mean_return,
208
+ stock_std_dev,
209
+ bond_mean_return,
210
+ bond_std_dev,
211
+ stock_allocation,
212
+ correlation_stock_bond,
213
+ mean_inflation,
214
+ std_dev_inflation,
215
+ min_swr_test,
216
+ max_swr_test,
217
+ swr_test_step
218
+ ],
219
+ outputs=[results_output, swr_plot_output, paths_plot_output]
220
+ )
221
+
222
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ numpy
2
+ matplotlib
3
+ gradio