Updated.
[retire.git] / retire.py
index 578869b402e55b04ccfec4e1e7e1e72b20cf9265..4282da73b4e90e919d927fd6da1da57ce592cb43 100755 (executable)
--- a/retire.py
+++ b/retire.py
-#!/usr/local/bin/python
-
-import sys
-
-from accounts import *
-import constants
-from parameters import *
-from tax_brackets import tax_brackets
-from tax_collector import tax_collector
-import utils
-import secrets
-from money import money
-
-class simulation(object):
-    def __init__(self, parameters, accounts):
-        """Initialize simulation parameters and starting account balances"""
-        self.params = parameters
-        self.accounts = accounts
-        self.lynn_age = 0
-        self.scott_age = 0
-        self.alex_age = 0
-        self.year = 0
-
-    def do_rmd_withdrawals(self, taxes):
-        """Determine if any account that has RMDs will require someone to
-           take money out of it this year and, if so, how much.  Then do
-           the withdrawal."""
-        total_withdrawn = money(0)
-        for x in self.accounts:
-            if x.has_rmd() and x.get_owner() == constants.SCOTT:
-                rmd = x.do_rmd_withdrawal(self.scott_age, taxes)
-                total_withdrawn += rmd
-            elif x.has_rmd() and x.get_owner() == constants.LYNN:
-                rmd = x.do_rmd_withdrawal(self.lynn_age, taxes)
-                total_withdrawn += rmd
-        return total_withdrawn
-
-    def go_find_money(self, amount_needed, taxes):
-        """Look through accounts and try to find amount using some heuristics
-           about where to withdraw money first."""
-
-        # Try brokerage accounts first
-        for x in self.accounts:
-            if not x.is_age_restricted():
-                amount_to_withdraw = min(amount_needed, x.get_balance())
-                if amount_to_withdraw > 0:
-                    print "## Withdrawing %s from %s" % (amount_to_withdraw,
-                                                         x.get_name())
-                    x.withdraw(amount_to_withdraw, taxes)
-                    amount_needed -= amount_to_withdraw
-                    if amount_needed <= 0: return
-
-        # Next try age restircted accounts
-        for x in self.accounts:
-            if (x.is_age_restricted() and
-                x.has_rmd() and
-                ((x.belongs_to_lynn() and self.lynn_age > 60) or
-                 (x.belongs_to_scott() and self.scott_age > 60))):
-
-                amount_to_withdraw = min(amount_needed, x.get_balance())
-                if amount_to_withdraw > 0:
-                    print "## Withdrawing %s from %s" % (amount_to_withdraw,
-                                                         x.get_name())
-                    x.withdraw(amount_to_withdraw, taxes)
-                    amount_needed -= amount_to_withdraw
-                    if amount_needed <= 0: return
-
-        # Last try Roth accounts
-        for x in self.accounts:
-            if (x.is_age_restricted() and
-                x.has_roth() and
-                ((x.belongs_to_lynn() and self.lynn_age > 60) or
-                 (x.belongs_to_scott() and self.scott_age > 60))):
-
-                amount_to_withdraw = min(amount_needed, x.get_balance())
-                if amount_to_withdraw > 0:
-                    print "## Withdrawing %s from %s" % (amount_to_withdraw,
-                                                         x.get_name())
-                    x.withdraw(amount_to_withdraw, taxes)
-                    amount_needed -= amount_to_withdraw
-                    if amount_needed <= 0: return
-        raise Exception("Unable to find enough money this year, still need %s more!" % amount_needed)
-
-    def get_social_security(self,
-                            scott_annual_social_security_dollars,
-                            lynn_annual_social_security_dollars,
-                            taxes):
-        """Figure out if Scott and/or Lynn are taking social security at
-           their present age in the simulation and, if so, how much their
-           annual benefit should be."""
-        total_benefit = money(0)
-        if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
-            total_benefit += scott_annual_social_security_dollars
-            taxes.record_ordinary_income(scott_annual_social_security_dollars)
-        if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
-            total_benefit += lynn_annual_social_security_dollars
-            taxes.record_ordinary_income(lynn_annual_social_security_dollars)
-        return total_benefit
-
-    def do_opportunistic_roth_conversions(self, taxes):
-        """Roll over money from pretax 401(k)s or IRAs into Roth."""
-        desired_conversion_amount = (
-            taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate(
-                self.params.get_federal_ordinary_income_tax_brackets()))
-        if desired_conversion_amount > 0:
-            money_converted = 0
-            for x in self.accounts:
-                if x.has_roth():
-                    amount = x.do_roth_conversion(desired_conversion_amount, taxes)
-                    money_converted += amount
-                    desired_conversion_amount -= amount
-            if money_converted > 0:
-                return True
-        return False
-
-    def dump_report_header(self):
-        self.params.dump()
-
-    def dump_annual_header(self, money_needed):
-        print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
-            self.year,
-            money_needed)
-        print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
-            self.scott_age, self.lynn_age, self.alex_age)
-
-        # Print out how much money is in each account + overall net worth.
-        total = money(0)
-        for x in self.accounts:
-            total += x.get_balance()
-            print "{:<50}: {:>14}".format(x.get_name(), x.get_balance())
-        print "{:<50}: {:>14}\n".format("TOTAL", total)
-
-    def dump_final_report(self, taxes):
-        print "\nAGGREGATE STATS FINAL REPORT:"
-        for x in self.accounts:
-            x.dump_final_report()
-        taxes.dump_final_report()
-
-    def run(self):
-        """Run the simulation!"""
-        self.dump_report_header()
-
-        taxes = tax_collector()
-        adjusted_annual_expenses = self.params.get_initial_annual_expenses()
-        adjusted_scott_annual_social_security_dollars = (
-            self.params.get_initial_social_security_benefit(constants.SCOTT))
-        adjusted_lynn_annual_social_security_dollars = (
-            self.params.get_initial_social_security_benefit(constants.LYNN))
-
-        try:
-            for self.year in xrange(2020, 2080):
-                self.scott_age = self.year - 1974
-                self.lynn_age = self.year - 1964
-                self.alex_age = self.year - 2005
-                self.params.report_year(self.year)
-
-                # Computed money needed this year based on inflated budget.
-                money_needed = money(adjusted_annual_expenses)
-
-                # When Alex is in college, we need $50K more per year.
-                if self.alex_age > 18 and self.alex_age <= 22:
-                    money_needed += 50000
-
-                self.dump_annual_header(money_needed)
-
-                # Now, figure out how to find money to pay for this year
-                # and how much of it is taxable.
-                total_income = money(0)
-
-                # When we reach a certain age we have to take RMDs from
-                # some accounts.  Handle that here.
-                rmds = self.do_rmd_withdrawals(taxes)
-                if rmds > 0:
-                    print "## Satisfied %s of RMDs from age-restricted accounts." % rmds
-                    total_income += rmds
-                    money_needed -= rmds
-
-                # When we reach a certain age we are eligible for SS
-                # payments.
-                ss = self.get_social_security(
-                    adjusted_scott_annual_social_security_dollars,
-                    adjusted_lynn_annual_social_security_dollars,
-                    taxes)
-                if ss > 0:
-                    print "## Social security paid %s" % ss
-                    total_income += ss
-                    money_needed -= ss
-
-                # If we still need money, try to go find it.
-                if money_needed > 0:
-                    self.go_find_money(money_needed, taxes)
-                    total_income += money_needed
-                    money_needed = 0
-
-                look_for_conversions = True
-                tax_limit = money(25000)
-                while True:
-                    # Maybe do some opportunistic Roth conversions.
-                    taxes_due = taxes.approximate_taxes(
-                        self.params.get_federal_standard_deduction(),
-                        self.params.get_federal_ordinary_income_tax_brackets(),
-                        self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
-                    total_income = taxes.get_total_income()
-                    tax_rate = float(taxes_due) / float(total_income)
-
-                    if (look_for_conversions and
-                        tax_rate <= 0.14 and
-                        taxes_due < tax_limit and
-                        self.year <= 2036):
-
-                        look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
-                        # because these conversions affect taxes, spin
-                        # once more in the loop and recompute taxes
-                        # due and tax rate.
-                    else:
-                        break
-
-                # Pay taxes_due by going to find more money.  This is a
-                # bit hacky since withdrawing more money to cover taxes
-                # will, in turn, cause taxable income.  But I think taxes
-                # are low enough and this simulation is rough enough that
-                # we can ignore this.
-                if taxes_due > 0:
-                    print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
-                        taxes_due, utils.format_rate(tax_rate))
-                    self.go_find_money(taxes_due, taxes)
-                else:
-                    print "## No federal taxes due this year!"
-                taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
-
-                # Inflation and appreciation:
-                #   * Cost of living increases
-                #   * Social security benefits increase
-                #   * Tax brackets are adjusted for inflation
-                inflation_multiplier = self.params.get_average_inflation_multiplier()
-                returns_multiplier = self.params.get_average_investment_return_multiplier()
-                ss_multiplier = self.params.get_average_social_security_multiplier()
-                adjusted_annual_expenses *= inflation_multiplier
-                for x in self.accounts:
-                    x.appreciate(returns_multiplier)
-                if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
-                    adjusted_scott_annual_social_security_dollars *= ss_multiplier
-                if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
-                    adjusted_lynn_annual_social_security_dollars *= ss_multiplier
-                self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
-                self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
-        except Exception as e:
-            print "Exception: %s" % e
-            print "Ran out of money!?!"
-
-        finally:
-            self.dump_final_report(taxes)
-
-# main
-#params = mutable_default_parameters()
-params = mutable_dynamic_historical_parameters()
-accounts = secrets.accounts
-s = simulation(params, accounts)
-s.run()
+#!/usr/bin/env python3
+
+from typing import List
+
+import bootstrap
+import config
+
+import account
+from person import Person
+import result_summarizer
+import returns_and_expenses as rai
+import simulation
+import simulation_params
+import trials
+from type.money import Money
+
+
+args = config.add_commandline_args(
+    'Retire!',
+    'Args that drive the retirement simulator',
+)
+args.add_argument(
+    '--num_trials',
+    '-n',
+    type=int,
+    default=1,
+    help='How many simulations to run'
+)
+args.add_argument(
+    '--verbosity',
+    '-v',
+    type=int,
+    choices=range(0, 3),
+    default=1,
+    help='How verbose should I be?',
+)
+
+
+# This defines the set of account and their initial balances.
+accounts: List[account.Account] = [
+
+    # Your accounts here....
+
+]
+
+
+def main() -> None:
+    params = simulation_params.DEFAULT_SIMULATION_PARAMS
+    params.initial_account_states = accounts
+    params.returns_and_expenses = rai.GaussianRae()  #rai.HistoricalRaE()
+
+    if config.config['num_trials'] > 1:
+        with simulation.ReportColorizer():
+            print(params)
+        results = trials.run_multiple_trials(
+            params=params,
+            num_trials=config.config['num_trials']
+        )
+        print
+        result_summarizer.summarize_results(results)
+    else:
+        sim = simulation.Simulation(params)
+        results = sim.simulate(
+            simulation.Verbosity(config.config['verbosity'])
+        )
+        if not results.success:
+            print("Unsuccessful")
+            return 1
+        return 0
+
+
+if __name__ == '__main__':
+    main()