This is a tool used to compare various strategies you could use to determine how much to withdrawal from your accounts during retirement. You can set up the configuration for various strategies in .json files, and then compare how those strategies fare, both against each other, and against themselves with different parameters. A simple example would be comparing the constant dollar and constant percentage withdrawal strategies with a withdrawal of $40,000/year vs $50,000/year vs 4% vs 4.5%. A simulation will be run with many retirement runs of however many years you specify. Results will then be displayed showing what the average withdrawal was using those strategies, as well as what the ending portfolio values were.
This program was largely made as a companion to the great FICalc tool. If you haven't played around with FICalc first, I'd highly recommend doing that. For most people FICalc will be better suited for what they want. It's easier to use and understand. My tool is intending for comparing many scenarios all against each other under a wide variety of circumstances.
This program requires Ruby. If you have no idea what that means, you may be better off just using FICalc. However, it should only require Ruby; I've strived to avoid any external dependencies outside the standard library. I've also tested on a variety of versions of Ruby, and avoided any features that I think might be deprecated in the future. As a result, if you can runy ruby --version and see any 3.x version of Ruby, you should be fine.
You can clone the entire repo, or just download the files. In the future I intend to provide a single file version, but for now you'll need these files:
analyzer.rbclasses.rb
The following are technically optional, but you should also have:
defaults.jsonhistorical_market_data.csvlife_expectancy_data.csvstrategiesfolder with.jsonfiles in it
The program is very configurable, but is also intended to be simple to get started with. There are two types of configuration that can be done: Options and Strategies.
Options are simulation level configurations which will apply to everything in your simulation. You can run ruby analyzer.rb --help to see the options. All those options can be specified on the command line, or put in the defaults.json file. If they are included in both places, the command line argument will be used. A basic defaults.json file would be:
{
"age_at_start": 60,
"simulation_pattern": "normal_with_wrap",
"years_to_simulate": 30,
"strategy_path": "strategies",
"allocation": [80, 15, 5],
"cash_interest_percentage": 0.015,
}
If you ran the program with ruby analyzer.rb --age_at_start 50 then it would use 50, and not 60 as the age at the start of retirement.
Strategies are the details of what one particular retirement withdrawal strategy will follow. If you run ruby analyzer.rb --descriptions you will get a description of the various strategies as well as what parameters are available to be configured for each. You can create many strategy .json files in a folder, and then pass that folder in as an option and the program will simulate all of them against the same data. These strategy .json files can be quite complex, as they allow for things like specifying different parameters or even entire strategies for every year of retirement, as well as support for things like gradual changing a parameter over a number of years, or repeating a set of parameters for a certain number of years and then doing something else.
There is a strategies folder, with many simple strategies as an example and starting point. Just opening those files and changing the parameters inside should get you pretty far. Further details on the strategies configuration is further down.
After a simulation the results will be displayed. You can control how verbose the results are with the -r flag. -r 1 will only show the summary results of each strategy, and will generally be the level of detail you want when simulating multiple strategies. If you specify -r 2 you will get the results for every starting year retirement run. In other words, (if you are simulating a 30 year retirement) you will see how the 30 historical years starting in 1950 compare to the 30 years starting in 1951 and 1952, etc. Level -r 3 will show every simulation year, meaning you would see all 30 years of that 1950 run, and all 30 for the 1951 run, etc for every starting year, and then again for every strategy you are simulating.
You can also pass in the -i flag to run in interactive mode where after the simulation is done you can explore the results in more detail for individual strategies or runs.
Here's an example run, showing the options used.
> ruby analyzer.rb -p ./my_strategies/ --starting_wealth 1000000 --r 1 -s normal_with_wrap -a 60 -y 30 --global_min_withdrawal 35000 --global_max_withdrawal 100000 --no-cents
Version 0.1 (20250930)
Options: {age_at_start: 60, starting_wealth: 1000000, simulation_pattern: "normal_with_wrap", years_to_simulate: 30, strategy_path: "./my_strategies/", allocation: [80, 15, 5], cash_interest_percentage: 0.015, jump_chance: 8, results_detail_level: 1, historical_market_data_filename: "historical_market_data.csv", life_expectancy_data_filename: "life_expectancy_data.csv", seed: 3439, max_runs: 100, global_min_withdrawal: 35000, global_max_withdrawal: 100000, no_cents: true}
Loaded 17 strategies
Simulating...
0 5wxavq7z CAPE - CAPE Based (30) - ./my_strategies/cape_based.json 0.028 s
1 3awj285d 4% Rule Bonds - Constant Percentage (30) - ./my_strategies/constant_percentage_array_gradient.json 0.026 s
2 4t4udo0y 3.5 -> 5% Rule - Constant Percentage (30) - ./my_strategies/constant_percentage_increase_percentage.json 0.024 s
3 31h3f2fz 4% Rule Repeats - Constant Percentage (30) - ./my_strategies/constant_percentage_repeats.json 0.034 s
4 1lzaypml 4% Rule Top Level - Constant Percentage (30) - ./my_strategies/constant_percentage_top_level.json 0.024 s
5 nuefkdv9 Dynamic SWR - Dynamic Safe Withdrawal Rate (30) - ./my_strategies/dynamic_safe_withdrawal_rate.json 0.025 s
6 1zymf9ed Endowment - Endowment (30) - ./my_strategies/endowment.json 0.039 s
7 3opim9m7 HA 1 - Hebeler Autopilot 1 (30) - ./my_strategies/hebeler_autopilot_1.json 0.031 s
8 5s968zh9 HA 1 All Stocks - Hebeler Autopilot 1 (30) - ./my_strategies/hebeler_autopilot_1_all_stocks.json 0.025 s
9 1jnahf1h HA 1 Bonds - Hebeler Autopilot 1 (30) - ./my_strategies/hebeler_autopilot_1_bonds.json 0.026 s
10 1ws7c0x9 HA 1 Stocks - Hebeler Autopilot 1 (30) - ./my_strategies/hebeler_autopilot_1_stocks.json 0.025 s
11 2b0jim26 HA 2 - Hebeler Autopilot 2 (30) - ./my_strategies/hebeler_autopilot_2.json 0.052 s
12 2672ur00 HA 2 50% - Hebeler Autopilot 2 (30) - ./my_strategies/hebeler_autopilot_2_50.json 0.027 s
13 1iw96ke2 HA 2 ficalc - Hebeler Autopilot 2 (30) - ./my_strategies/hebeler_autopilot_2_ficalc.json 0.027 s
14 5xmzsnw7 95% Rule - 95% Rule (30) - ./my_strategies/ninity_five_percent_rule.json 0.03 s
15 4jvdy5pp Vanguard - Vanguard Dynamic Spending (30) - ./my_strategies/vanguard_dynamic_spending.json 0.025 s
16 5utpu4ap VPW - Variable Percentage Withdrawal (VPW) (30) - ./my_strategies/variable_percentage_withdrawal.json 0.027 s
Summary results for strategies:
0 5wxavq7z CAPE - Zero runs: 0/154, Min Final: $212,814, Median Final: $1,677,073, Max Final: $8,588,564, Withdrawals - Min: $35,000, Max: $100,000, Median: $48,464
1 3awj285d 4% Rule Bonds - Zero runs: 0/154, Min Final: $152,993, Median Final: $1,601,700, Max Final: $4,567,296, Withdrawals - Min: $35,000, Max: $100,000, Median: $51,522
2 4t4udo0y 3.5 -> 5% Rule - Zero runs: 0/154, Min Final: $211,047, Median Final: $1,839,108, Max Final: $5,452,413, Withdrawals - Min: $35,000, Max: $100,000, Median: $54,759
3 31h3f2fz 4% Rule Repeats - Zero runs: 0/154, Min Final: $212,814, Median Final: $1,864,702, Max Final: $5,617,469, Withdrawals - Min: $35,000, Max: $100,000, Median: $55,337
4 1lzaypml 4% Rule Top Level - Zero runs: 0/154, Min Final: $181,252, Median Final: $1,809,968, Max Final: $5,277,976, Withdrawals - Min: $35,000, Max: $100,000, Median: $52,853
5 nuefkdv9 Dynamic SWR - Zero runs: 6/154, Min Final: $0, Median Final: $109,997, Max Final: $1,966,976, Withdrawals - Min: $0, Max: $100,000, Median: $74,965
6 1zymf9ed Endowment - Zero runs: 1/154, Min Final: $0, Median Final: $1,375,269, Max Final: $4,353,469, Withdrawals - Min: $0, Max: $100,000, Median: $56,260
7 3opim9m7 HA 1 - Zero runs: 0/154, Min Final: $170,191, Median Final: $862,339, Max Final: $4,302,746, Withdrawals - Min: $35,000, Max: $100,000, Median: $76,101
8 5s968zh9 HA 1 All Stocks - Zero runs: 0/154, Min Final: $193,645, Median Final: $1,293,644, Max Final: $8,048,279, Withdrawals - Min: $35,000, Max: $100,000, Median: $82,290
9 1jnahf1h HA 1 Bonds - Zero runs: 0/154, Min Final: $142,581, Median Final: $664,210, Max Final: $3,538,634, Withdrawals - Min: $35,000, Max: $100,000, Median: $74,045
10 1ws7c0x9 HA 1 Stocks - Zero runs: 0/154, Min Final: $183,472, Median Final: $1,047,260, Max Final: $5,858,567, Withdrawals - Min: $35,000, Max: $100,000, Median: $78,418
11 2b0jim26 HA 2 - Zero runs: 1/154, Min Final: $0, Median Final: $561,914, Max Final: $3,599,118, Withdrawals - Min: $0, Max: $100,000, Median: $75,179
12 2672ur00 HA 2 50% - Zero runs: 1/154, Min Final: $0, Median Final: $492,693, Max Final: $2,988,814, Withdrawals - Min: $0, Max: $100,000, Median: $74,334
13 1iw96ke2 HA 2 ficalc - Zero runs: 0/154, Min Final: $92,081, Median Final: $1,211,872, Max Final: $4,547,428, Withdrawals - Min: $35,000, Max: $100,000, Median: $65,400
14 5xmzsnw7 95% Rule - Zero runs: 0/154, Min Final: $166,544, Median Final: $1,675,777, Max Final: $5,175,352, Withdrawals - Min: $35,000, Max: $100,000, Median: $53,542
15 4jvdy5pp Vanguard - Zero runs: 0/154, Min Final: $151,817, Median Final: $1,930,324, Max Final: $5,745,804, Withdrawals - Min: $35,000, Max: $100,000, Median: $51,544
16 5utpu4ap VPW - Zero runs: 6/154, Min Final: $0, Median Final: $141,926, Max Final: $1,987,560, Withdrawals - Min: $0, Max: $100,000, Median: $75,104
Here you can see that we simulated 154 runs for each strategy, one for each of the years of historical data we have available (wrapping around to the start when we reach the end). The options we passed in are all show at the top, although a few of the options are from a defaults.json file (like the allocation). The next batch of lines shows each strategy as it's being simulated, along with an unique 10 character code representing each, and the path to that strategy file and the simulation time. After that is the results table. It shows that most strategies had no "Zero runs" (ie, retirement runs that ran out of money), but the "Dynamic SWR" strategy had 6. The highest median withdrawal rate was the "HA 1 All Stocks" strategy with $82,290/year.
Here I'll go into greater details for many of the things I glanced over above.
A Simulation is the entire execution of the program. It will consist of many Strategies which are the rules you will follow to determine your withdrawal every year in retirement. A single simulated retirement is called a Run. A run might last 30 years and either end in running out of money or reaching the end of the 30 years with money left over. Lastly, a Simulation Year means a single year within a run. Putting it all together, the program will use historical returns data from 1950 to 1979 to simulate a retirement Run over that period against many Strategies. Within that 30 year period, each individual year would be a Simulation Year. A different run would be starting in 1951 until 1980. The collection of all those runs and strategies would be the overall Simulation.
Options are things set for the entire simulation. Something like age at start is an options that all your strategies will use. Below is the output of ruby analyzer.rb --help showing all the available options.
-a, --age_at_start AGE Age at start of simulation
-y, --years_to_simulate YEARS How many years to simulate your retirement
-w, --starting_wealth WEALTH Wealth at start of retirement
-m, --max_runs MAX Number of retirement runs to simulate per strategy. Not used with historical data patterns
-s, --simulation_pattern PATTERN Source of market data
(normal, normal_with_wrap, reverse, reverse_with_wrap, random_walk, random_walk_only_normal, random_years)
-p, --strategy_path PATH Path to strategy files to simulate
--allocation ALLOCATION Portfolio allocation in the form of stocks,bonds,cash as percentages.
Example: --allocation 80,15,5
--start_years START_YEARS An array of starting years to test.
Example: --start_years 1965,1966,1967
-r, --results_detail_level LEVEL What level of detail to display the results. 0 - none, 1 - strategy summaries, 2 - all runs, 3 - all simulation years
-c, --no_cents Flag to suppress the displaying of cents. Cents are still used for calculations
--cash_interest_percentage PCT
Interest rate on cash, unadjusted for inflation
--historical_market_data_filename FILENAME
File that contains the historical market data
--life_expectancy_data_filename FILENAME
File that contains the life expectancy data
--descriptions Show withdrawal strategy descriptions and exit
-q, --quiet Suppress output. The -r flag is independent of this and must be set to -r 0 to suppress all output.
--jump_chance CHANCE When using the random walk pattern, there is a 1 in x chance of jumping to a random year after every year
--seed SEED The seed used by the random number generator. Using the same seed will produce the same results.
--global_min_withdrawal AMOUNT
Minimum withdrawal limit to be used for all strategies, in addition to any withdrawal limit specified in the strategy.
--global_max_withdrawal AMOUNT
Maximum withdrawal limit to be used for all strategies, in addition to any withdrawal limit specified in the strategy.
-i, --interactive Interactive mode which allows you to examine results after simulation ends
Many of these are self-explanatory, but I'll explain a few further.
First you'll see references to the simulation pattern. This is the source for historical returns data to be used in the simulation. There are few options available, which are specified above. The goal here is to provide more options than just using historical returns from 1871-2024, which is rather limiting. 154 years may sound like a lot, but consider that there's going to be a ton of correlation between a retirement run from 1950-1979 and one from 1951-1980. There are a variety of options available for how to generate returns data, which give a wider range at the cost of being less based on actual historical results.
The normal pattern is just using historical returns data, starting in 1871 and going until 2024. If you specify a retirement of 30 years that means the last run will be 1995-2024, because there is no data to simulate 2025 available. The pattern normal_with_wrap prevents this by wrapping around back to 1871 once you reach 2024. In other words, this would simulate things as if the year 2025 was the same as 1871, and if 2026 were 1872. The world of 2025 is very different from the world of 1871, so this isn't a great solution, but it does help increase the amount of data you have to simulate runs with. The reverse and reverse_with_wrap are the same, but the years are ran in reverse. So, if you start in 1950, the next simulation year would be 1949, then 1948, etc.
Then there are randomly generated patterns. The random_years pattern simply chooses a random year every year. You might get the same terrible year 30 times in a row, or you might get the same amazing year 30 times in a row, but over a large number of simulations the expectation is to get something closer to an average set of years. The problem with this approach is it doesn't take into account the correlation one year has on the next. As an attempt to address that, while still providing a wider range of returns than just actual historical data, the random_walk will choose a random year, then continue normally onto the next historical year. Every year there is a chance though, of another random jump (specified with the jump_chance option). With a jump_chance of 10, that means you would expect every 10 years for the simulation to jump to another random year, then continue on from there. This strategy also chooses a random direction, either forwards or backwards, to travel in. If you want to only move forwards in years, then the random_walk_only_normal pattern is just that.
These are all the strategies currently implemented, along with a brief description and the parameters that can be configured for it.
Accumulation - This is just contributing to your portfolio, with no withdrawals.
contribution_amount: How much to contribute this year, in dollars.
Constant Dollar - Withdrawal the same number of dollars every year, adjusted for inflation.
amount_to_withdrawal: How much to withdrawal this year, in dollars.
Constant Percentage - Withdrawal the same percentage of your total portfolio every year. AKA the 4% rule, although the percentage can be configured.
withdrawal_percentage: Percentage of your total portfolio to withdrawal.
1/N - Withdrawal 1/N of your portfolio, where N is how many years are remaining in your retirement. For long retirements will start as a low percentage and gradually increase to a very high percentage, reaching 100% in the last year. Intentionally will end with a $0 portfolio.
target_end_portfolio_value: Desired portfolio at the end of retirement.
Variable Percentage Withdrawal (VPW) - Looks at your stocks/bonds ratio and years remaining in retirement to calculate a withdrawal percentage using the PMT formula. Developed by the Bogleheads community.
stocks_growth_trend: Expected growth of stocks.
bonds_growth_trend: Expected growth of bonds.
future_value: Roughly the final portfolio value.
Dynamic Safe Withdrawal Rate - Similar to 1/N, but starts with a low withdrawal percentage that gradually increases. The percentage is not based on market conditions or portfolio size, just years remaining. Based on the concept of writing yourself an annuity every year.
roi_assumption: Assumed growth rate of portfolio.
inflation_assumption: Assumed inflation rate.
Endowment - Combines previous year withdrawal with set percentage of portfolio to ensure a gradual progression. Similar to the Constant Percentage strategy, but averaging that and the prior year. Both the percentage of this year, and the weighting between current portfolio and the prior year's withdrawal can be configured.
portfolio_value_percentage: Base percentage of portfolio to take, weighted by portfolio_value_weight.
portfolio_value_weight: How much of the withdrawal_percentage value to combine with the value from the portfolio value percentage.
previous_year_weight: How much of the previous year withdrawal to combine with the result from the portfolio value calculation.
Guyton-Klinger - Uses three decision rules every year, not implemented yet.
95% Rule - Takes the greater of a Constant Percentage of the current portfolio or a set percentage of last year's withdrawal. Similar to the Constant Percentage strategy, but the year to year withdrawal amount will never drop too much, at the trade off of being slower to respond to market conditions.
withdrawal_percentage: Base percentage of portfolio to take.
prior_year_min_percentage: Minimum amount of previous year withdrawal to take.
CAPE Based - Uses the CAPE (AKA Shiller P/E), which is essentially a 10 year average of how the stock market is doing, to determine the withdrawal percentage.
base_withdrawal_percentage: Percentage of portfolio to be added to the amount determined by CAPE multiplier.
cape_multiplier: Multiple this value by 1 over the current year's CAPE.
Sensible Withdrawals - Start with a low base withdrawal percentage and in any year in which your portfolio increases in value (after your withdrawals and adjusting for inflation) you take some percentage of the increase. This means that in 'good' years your potentiallywill withdrawal a lot more, but in 'bad' years you'll be limited to your base withdrawal rate.
base_withdrawal_amount: Starting amount to withdrawal.
extra_withdrawal_rate: In any year where the portfolio increases in value, take this percentage of the increase as an extra bonus to the base withdrawal amount.
Hebeler Autopilot 1 - Uses IRS life expectancy tables (usually used for RMD determinations) combined with last year's withdrawal. The base formula is: 0.5 x (Last year's withdrawal amount x (1 + Inflation %) + Last year's ending balance / RMD)
base_withdrawal_percentage: Percentage to withdrawal in the first year.
prior_year_percent: What percentage of your prior year withdrawal to combine with the amount calculated for this year.
life_expectancy_source: Source for life expectancy data, IRS 590 tables for various years are available.
age_adder: Add this amount to your current age before looking up life expectancy.
life_expectancy_adder: Add this amount to the life expectancy.
Hebeler Autopilot 2 - Similar to Hebeler Autopilot 1, except instead of just taking portfolio / RMD, the RMD (ie, life expectancy) is used in the PMT formula. The result is combined with the previous year as in HA 1.
base_withdrawal_percentage: Percentage to withdrawal in the first year.
prior_year_percent: What percentage of your prior year withdrawal to combine with the amount calculated for this year.
life_expectancy_source: Source for life expectancy data, IRS 590 tables for various years are available.
age_adder: Add this amount to your current age before looking up life expectancy.
life_expectancy_adder: Add this amount to the life expectancy.
assumed_annual_rate: Rate used in the PMT calculation.
Vanguard Dynamic Spending - Developed by Vanguard, this is essentially the Constant Percentage strategy with guardrails to ensure the year to year withdrawal never changes by too much. Similar to the 95% rule.
base_withdrawal_percentage: Base percentage of portfolio to take.
max_withdrawal_decrease_percentage: Maximum decrease in withdrawal amount from previous year.
max_withdrawal_increase_percentage: Maximum increase in withdrawal amount from previous year.
Properties available in all strategies
age: Your age in this year.
year: Which year of retirement this is.
remaining: How many years of retirement are remaining.
allocation: Portfolio allocation as an array of percentages in Stocks, Bonds, Cash order.
additional_income: Total amount of additional income for this year.
additional_spend: Total amount of additional spending for this year.
min_withdrawal: The minimum amount to withdrawal, as set for this year in the strategy.
max_withdrawal: The maximum amount to withdrawal, as set for this year in the strategy.
global_min_withdrawal: The global minimum amount to withdrawal, set in options for the entire simulation.
global_max_withdrawal: The global maximum amount to withdrawal, set in options for the entire simulation.
Strategies are defined in .json files. You can define many strategies in a single folder and then simulate all of them to compare how they perform. You can define many variations using the same base strategy but with different parameters. For example, you could compare 3.5% vs 4% vs 4.5% withdrawal rates.
A simple example of the 4% rule file.
{
"description": "Constant 4% withdrawals",
"short_name": "4% Rule Top Level",
"default_values":
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.04
}
}
The description field isn't used by the program, and you can put whatever you want there. The short_name is what is shown in the results. The default_values section will apply to all years, unless they are overridden elsewhere. So here we will use the constant_percentage strategy with a value of 4%.
This file uses the gradient feature to gradually transition from one value to another. In this case, moving from 3.5% to 5% over 40 years.
{
"description": "Constant 4% withdrawals",
"short_name": "3.5 -> 5% Rule",
"default_values":
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.04,
"allocation": [80,15,5]
},
"gradient_values":
[
{
"param": "withdrawal_percentage",
"start": 0.035,
"end": 0.05,
"number_of_years": 40,
"starting_year": 1
}
]
}
There is still a default_values section, with a withdrawal_percentage of 4% defined there, but that will only be used when there is no value from the gradient_values section. Here, we will start with a withdrawal percentage of 3.5% in year 1, and then move an equal amount each year so that we end up with 5% in year 40. After year 40, the value of 5% will repeat, it will not revert back to the default value.
This allows you to define years individually.
{
"description": "One year of 3% withdrawals, followed by a year of 3.5% and then 4% for the rest of the retirement",
"short_name": "3% -> 4% Rule",
"years": [
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.03,
},
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.035,
},
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.04,
}
]
}
This isn't a very likely strategy, but it highlights that you can define a years array and within it define what to do for each year. If you don't define enough years for your simulation it will repeat the last year forever.
This is a more useful version of the above, using the repeat_for_years keyword
{
"description": "3.5% for 10 years, then 4% for 10 years, then 4.5% withdrawals",
"short_name": "3.5% -> 4.5% Repeats",
"years": [
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.035,
"repeat_for_years": 10
},
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.04,
"repeat_for_years": 10
},
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.045,
"repeat_for_years": 10
}
]
}
This is equivalent to writing this block 10 times in a row, followed by the other 2 variations 10 times each.
{
"strategy_name": "constant_percentage",
"withdrawal_percentage": 0.035,
},