Overview
The purpose of this example is to demonstrate how to use RetirementPlanners.jl
with a simple, retirement simulation. Our focus on a simple simulation will have the benefit of making the API clear, but will not result in a valid stress test of your retirement plan. For a more realistic example, please read the documentation for the advanced example.
API
In this section, we will provide an overview of the API for configuring a retirement simulation. As detailed below, some parameters require user input, whereas other parameters have default values which can optionally be overwritten with your desired values.
Transaction
The transfer of money between accounts is scheduled through an object called Transaction
, which contains the following fields:
start_age = 0.0
: the age at which a series of transactions beginend_age = Inf
: the age at which a series of transactions endamount
: the amount of each transaction
To schedule multiple transactions across time, you may specify a vector of Transaction
objects, which can be useful when, for example, you have multiple income sources occuring at different times (e.g., social security, pension, rent income etc.). The function transact
will carry out the transaction at allowable times. By default, the RetirementPlanners
supports transaction for real numbers, probability distributions, and a few adaptive rules described below. You can create your own type and method for transact
to implement a custom strategy.
Required Parameters
The model requires numerous parameters to control the timing of events and the initial value of the investment. The required parameters are as follows:
Δt
: the time step in yearsstart_age
: your age in years at the beginning of the simulationduration
: the number of years to simulatestart_amount
: the amount of money you have in investments at the beginning of the simulation
Optional Update Functions
The discrete time simulation is governed by seven update functions, which are executed on each time step:
withdraw!
: a function called on each time step to withdraw from investmentsinvest!
: a function called on each time step to invest money into investmentsupdate_income!
: a function called on each time step to update income sourcesupdate_inflation!
: a function called on each time step to compute inflationupdate_market!
: a function called on each time step to simulate the stock marketupdate_investments!
: a function called on each time step to compute the total value of the investmentslog!
: a function called on each time step to log data
Each function is assigned a default method with default arguments. You can specify alternative parameter values as well as alternative update functions. Note that in advanced applications, you can specify a new model type and update!
to execute a different sequence of update functions. The update functions listed above will suffice for a wide range of use cases.
Optional Update Function Parameters
Each update function described in the previous section has default parameter values which can be overwritten. For example, we could specify a set of parameters kw_income = (X₁ = x₁, X₂ = x₂, ..., Xₙ = xₙ)
to pass the function update_income!
. The keyword for each update function is given below:
kw_income
: optional keyword arguments passed toupdate_income!
kw_withdraw
: optional keyword arguments passed towithdraw!
kw_invest
: optional keyword arguments passed toinvest!
kw_inflation
: optional keyword arguments passed toupdate_inflation!
kw_interest
: optional keyword arguments passed toupdate_market!
kw_net_worth
: optional keyword arguments passed toupdate_investments!
kw_log
: optional keyword arguments passed tolog!
Example
Now that we have explained the API for configuring simulations, we are now in the position to develop a simple retirement simulation based on the following scenario:
Let's assume that you are 27 years old with an initial investment of $
10,000, and you invest $
625 each month until early retirement at age 60. Assume further that the yearly interest rate on investments is .07, and inflation is .035. Upon reaching 60 years old, we will assume you will withdraw $
2,200 per month until reaching age 85.
Load Packages
The first step is to load the required packages. The code block below loads the package RetirementPlanners
to configure and run the retirement simulation, and the package Plots
to visualize the results.
using RetirementPlanners
using Plots
Configure Simulation
In this section, we will configure the simulation based on the scenario described above. As shown below, all of the configuration details will be defined in a data structure named config
.
Required Parameters
Based on the scenario above, we will use the following required parameters:
Δt
: $\frac{1}{12}$start_age
: $27$duration
: $58$start_amount
:$
$10,000$
Optional Update Functions
In this simple simulation, we use several, simple update functions pre-fixed with the word fixed
. As the names suggest, these simplified functions use fixed quantities in the simulation. However, for update_investments!
and log!
, we will use the default update functions. Each update function is described below:
fixed_withdraw
: withdraw a fixed amount from investments on each time step starting at a specified ageinvest!
: invest a specified amount on each time step until a specified age is reached. In this case, we will invest a fixed amountfixed_income
: recieve specified income (e.g., social security, or pension) on each time step starting at a specified agefixed_inflation
: a fixed yearly inflation rate used to adjust interest (i.e., growth) earned on investmentsfixed_interest
: a fixed yearly interest rate earned on intestmentsupdate_investments!
: computes total value of investments on each time step based on inflation, interest, investments, and withdraws.default_log!
: records interest rate, inflation rate, and net worth on each time step
Note: you can view additional documentation for the update functions above via ? function_name
in the REPL, or by referencing the API.
Optional Update Function Parameters
Putting all of this information together, we get the following configuration:
config = (
# time step in years
Δt = 1 / 12,
# starting age of simulation
start_age = 25,
# duration of simulation
duration = 58,
# initial investment amount
start_amount = 10_000,
# function for simulating growth in the market
update_market! = fixed_market,
# interest parameters
kw_market = (interest_rate = 0.07,),
# function for updating inflation
update_inflation! = fixed_inflation,
# inflation parameters
kw_inflation = (inflation_rate = 0.035,),
# invest parameters
kw_invest = (investments = Transaction(; end_age = 60, amount = 625),),
# withdraw parameters
kw_withdraw = (withdraws = Transaction(; start_age = 60, amount = 2200),)
)
(Δt = 0.08333333333333333, start_age = 25, duration = 58, start_amount = 10000, update_market! = RetirementPlanners.fixed_market, kw_market = (interest_rate = 0.07,), update_inflation! = RetirementPlanners.fixed_inflation, kw_inflation = (inflation_rate = 0.035,), kw_invest = (investments = Transaction{Float64, Int64}(0.0, 60.0, 625),), kw_withdraw = (withdraws = Transaction{Float64, Int64}(60.0, Inf, 2200),))
Construct Model
Now that the model settings have been configured, we can create the model. To do so, we will pass the configuration settings to the model constructor.
model = Model(; config...)
Model
┌─────────────────────┬─────────────────────────────────────────────────────────
│ Field │ Value ⋯
├─────────────────────┼─────────────────────────────────────────────────────────
│ Δt │ 0.08 ⋯
│ duration │ 58.00 ⋯
│ start_age │ 25.00 ⋯
│ start_amount │ 10000.00 ⋯
│ log_times │ 25.083333333333332:0.08333333333333333:83.0 ⋯
│ state │ State{Float64}(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1) ⋯
│ withdraw! │ withdraw! ⋯
│ invest! │ invest! ⋯
│ update_income! │ update_income! ⋯
│ update_inflation! │ fixed_inflation ⋯
│ update_market! │ fixed_market ⋯
│ update_investments! │ update_investments! ⋯
│ log! │ default_log! ⋯
│ config │ (kw_market = (interest_rate = 0.07,), kw_inflation = ( ⋯
└─────────────────────┴─────────────────────────────────────────────────────────
1 column omitted
The output above summarizes the configuration of the Model
object. At the bottom, the field config
contains all of the parameters for the update functions. The remaining fields are global parameters, which include the starting amount and update functions. The field called state
stores current values of the system for each time step, including investment amount, and net worth. You can see the details of the State
object by expanding the menu below.
Show State
model.state
State
┌─────────────────┬───────┐
│ Field │ Value │
├─────────────────┼───────┤
│ interest_rate │ 0.00 │
│ inflation_rate │ 0.00 │
│ income_amount │ 0.00 │
│ invest_amount │ 0.00 │
│ withdraw_amount │ 0.00 │
│ net_worth │ 0.00 │
│ log_idx │ 1.00 │
└─────────────────┴───────┘
Setup Logger
The next step is to initialize the data logger. On each time step, the data logger stores the following quantities:
- annualized interest rate
- annualized inflation rate
- net worth
- income summed across all sources
The Logger
object requires two inputs:
n_steps
: the total number of time steps in one simulationn_reps
: the total repetitions of the simulation.
The total number of time steps can be found by getting the length of the time steps. In this simple scenario, we will set n_reps=1
because the simulation is deterministic (i.e., it provides the same result each time).
times = get_times(model)
n_steps = length(times)
n_reps = 1
logger = Logger(; n_reps, n_steps)
Logger
┌──────────────┬────────────────────────────┐
│ Field │ Value │
├──────────────┼────────────────────────────┤
│ net_worth │ [0.0; 0.0; … ; 0.0; 0.0;;] │
│ interest │ [0.0; 0.0; … ; 0.0; 0.0;;] │
│ inflation │ [0.0; 0.0; … ; 0.0; 0.0;;] │
│ total_income │ [0.0; 0.0; … ; 0.0; 0.0;;] │
└──────────────┴────────────────────────────┘
Run Simulation
Now that we have specified the parameters of the simulation, we can use the function simulate!
to generate quantities of interest and save them to the Logger
object. As shown below, simulate!
requires our Model
object, Logger
object, and the number of repetitions, n_reps
.
simulate!(model, logger, n_reps)
Visualize the Results
The code block below plots net worth as a function of age. The time steps are contained in times
and net worth is contained within the Logger
object.
plot(times, logger.net_worth, grid=false, label=false, xlabel="Age", ylabel="Net Worth")
Based on the assumptions we have made, you will have $
219,771 remaining in investments at age 85. Needless to say, this simulation is too simplistic stress test your financial situation. Perhaps the most significant limitation is that is deterministic: investments, withdraws, infation, and interest are fixed throughout. In actuality, these values vary across time, thus introducing uncertainty into the planning process. The advanced example will show you how to introduce random variables into the simulation to account for various sources of uncertainty.
All Code
using RetirementPlanners
using Plots
config = (
# time step in years
Δt = 1 / 12,
# starting age of simulation
start_age = 25,
# duration of simulation
duration = 58,
# initial investment amount
start_amount = 10_000,
# function for simulating growth in the market
update_market! = fixed_market,
# interest parameters
kw_market = (interest_rate = 0.07,),
# function for updating inflation
update_inflation! = fixed_inflation,
# inflation parameters
kw_inflation = (inflation_rate = 0.035,),
# invest parameters
kw_invest = (investments = Transaction(; end_age = 60, amount = 625),),
# withdraw parameters
kw_withdraw = (withdraws = Transaction(; start_age = 60, amount = 2200),)
)
model = Model(; config...)
times = get_times(model)
n_steps = length(times)
n_reps = 1
logger = Logger(; n_reps, n_steps)
simulate!(model, logger, n_reps)
plot(
times,
logger.net_worth,
grid = false,
label = false,
xlabel = "Age",
ylabel = "Net Worth"
)