Creates a parameters subclass that chooses random runs of years from
authorScott Gasch <[email protected]>
Sun, 19 Jan 2020 16:47:41 +0000 (08:47 -0800)
committerScott Gasch <[email protected]>
Sun, 19 Jan 2020 16:47:41 +0000 (08:47 -0800)
US market history to replay when it reports inflation, social security
and investment returns back to the main driver program.

parameters.py
retire.py
tax_collector.py
utils.py

index 989d5265468cce99ea9f64589d4ba003cfbbd390..1d6443c40250d5b8ad76c397ce60160a042f873a 100644 (file)
@@ -2,8 +2,43 @@ import constants
 import utils
 from tax_brackets import tax_brackets
 from money import money
+from numpy import random
 
 class parameters(object):
+    def get_initial_annual_expenses(self):
+        pass
+
+    def get_average_inflation_multiplier(self):
+        pass
+
+    def get_average_social_security_multiplier(self):
+        pass
+
+    def get_average_investment_return_multiplier(self):
+        pass
+
+    def get_initial_social_security_age(self, person):
+        pass
+
+    def get_initial_social_security_benefit(self, person):
+        pass
+
+    def get_federal_standard_deduction(self):
+        pass
+
+    def get_federal_ordinary_income_tax_brackets(self):
+        pass
+
+    def get_federal_dividends_and_long_term_gains_income_tax_brackets(self):
+        pass
+
+    def dump(self):
+        pass
+
+    def report_year(self, year):
+        pass
+
+class mutable_default_parameters(parameters):
     """A container to hold the initial states of several simulation
        parameters.  Play with them as you see fit and see what happens."""
 
@@ -61,7 +96,7 @@ class parameters(object):
             constants.CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS)
         return self
 
-    def with_initial_annual_expenses(self, expenses):
+    def set_initial_annual_expenses(self, expenses):
         assert expenses >= 0, "You can't have negative expenses"
         self.initial_annual_expenses = expenses
         return self
@@ -69,31 +104,31 @@ class parameters(object):
     def get_initial_annual_expenses(self):
         return self.initial_annual_expenses
 
-    def with_average_inflation_multiplier(self, multiplier):
+    def set_average_inflation_multiplier(self, multiplier):
         self.inflation_multiplier = multiplier
         return self
 
     def get_average_inflation_multiplier(self):
         return self.inflation_multiplier
 
-    def with_average_social_security_multiplier(self, multiplier):
+    def set_average_social_security_multiplier(self, multiplier):
         self.social_security_multiplier = multiplier
         return self
 
     def get_average_social_security_multiplier(self):
         return self.social_security_multiplier
 
-    def with_average_investment_return_multiplier(self, multiplier):
+    def set_average_investment_return_multiplier(self, multiplier):
         self.investment_multiplier = multiplier
         return self
 
     def get_average_investment_return_multiplier(self):
         return self.investment_multiplier
 
-    def with_initial_social_security_age_and_benefits(self,
-                                                      person,
-                                                      age,
-                                                      amount):
+    def set_initial_social_security_age_and_benefits(self,
+                                                     person,
+                                                     age,
+                                                     amount):
         assert age >= 60 and age <= 70, "age should be between 60 and 70"
         self.social_security_age[person] = age
         assert amount >= 0, "Social security won't pay negative dollars"
@@ -106,7 +141,7 @@ class parameters(object):
     def get_initial_social_security_benefit(self, person):
         return self.initial_social_security_dollars[person]
 
-    def with_federal_standard_deduction(self, deduction):
+    def set_federal_standard_deduction(self, deduction):
         assert deduction >= 0, "Standard deduction should be non-negative"
         self.federal_standard_deduction_dollars = deduction
         return self
@@ -114,15 +149,15 @@ class parameters(object):
     def get_federal_standard_deduction(self):
         return self.federal_standard_deduction_dollars
 
-    def with_federal_ordinary_income_tax_brackets(self, brackets):
+    def set_federal_ordinary_income_tax_brackets(self, brackets):
         self.federal_ordinary_income_tax_brackets = brackets
         return self
 
     def get_federal_ordinary_income_tax_brackets(self):
         return self.federal_ordinary_income_tax_brackets
 
-    def with_federal_dividends_and_long_term_gains_income_tax_brackets(self,
-                                                                       brackets):
+    def set_federal_dividends_and_long_term_gains_income_tax_brackets(self,
+                                                                      brackets):
         self.federal_dividends_and_long_term_gains_brackets = brackets;
         return self
 
@@ -155,3 +190,143 @@ class parameters(object):
         self.federal_dividends_and_long_term_gains_brackets.dump()
         print "  ]"
         print "  We assume Roth money continues to be available tax-free."
+
+class mutable_dynamic_historical_parameters(mutable_default_parameters):
+    def __init__(self):
+        self.selection_index = None
+        self.selection_duration = 0
+        self.historical_tuples = [
+           # year  stock  infl  bond
+            (2020,  3.14,  1.90,  1.54),
+            (2019, 31.49,  1.70,  2.15),
+            (2018, -4.38,  2.40,  2.33),
+            (2017, 21.83,  2.10,  1.19),
+            (2016, 11.96,  1.30,  0.61),
+            (2015,  1.38,  0.10,  0.32),
+            (2014, 13.69,  1.60,  0.12),
+            (2013, 32.39,  1.50,  0.13),
+            (2012, 16.00,  2.10,  0.17),
+            (2011,  2.11,  3.20,  0.18),
+            (2010, 15.06,  1.60,  0.32),
+            (2009, 26.46, -0.40,  0.47),
+            (2008,-37.00,  3.80,  1.83),
+            (2007,  5.49,  2.80,  4.53),
+            (2006, 15.79,  3.20,  4.94),
+            (2005,  4.91,  3.40,  3.62),
+            (2004, 10.88,  2.70,  1.89),
+            (2003, 28.68,  2.30,  1.24),
+            (2002,-22.10,  1.60,  2.00),
+            (2001,-11.89,  2.80,  3.49),
+            (2000, -9.10,  3.40,  6.11),
+            (1999, 21.04,  2.20,  5.08),
+            (1998, 28.58,  1.50,  5.05),
+            (1997, 33.36,  2.30,  5.63),
+            (1996, 22.96,  3.00,  5.52),
+            (1995, 37.58,  2.80,  5.94),
+            (1994,  1.32,  2.60,  5.32),
+            (1993, 10.08,  3.00,  3.43),
+            (1992,  7.62,  3.00,  3.89),
+            (1991, 30.47,  4.20,  5.86),
+            (1990, -3.10,  5.40,  7.89),
+            (1989, 31.69,  4.82,  8.54),
+            (1988, 16.61,  4.14,  7.65),
+            (1987, 15.25,  3.65,  6.77),
+            (1986, 18.67,  0.86,  6.45),
+            (1985, 31.73,  3.56,  8.42),
+            (1984,  6.27,  4.32, 10.91),
+            (1983, 22.56,  3.21,  9.58),
+            (1982, 21.55,  6.16, 12.27),
+            (1981, -4.91, 10.32, 14.80),
+            (1980, 32.42, 13.50, 12.00),
+            (1979, 18.44, 11.35, 10.65),
+            (1978,  6.65,  7.59,  7.00),
+            (1977, -7.18,  6.50,  6.08),
+            (1976, 23.84,  5.76,  5.88),
+            (1975, 37.20,  9.13,  6.78),
+            (1974,-26.47, 11.04,  8.20),
+            (1973,-14.66,  6.22,  7.32),
+            (1972, 18.90,  3.21,  4.95),
+            (1971, 14.31,  4.38,  4.89),
+            (1970, 14.01,  5.72,  6.90),
+            (1969, -8.50,  5.46,  7.12),
+            (1968, 11.06,  4.19,  5.69),
+            (1967, 23.98,  3.09,  4.88),
+            (1966,-10.06,  2.86,  5.20),
+            (1965, 12.45,  1.61,  4.15),
+            (1964, 16.48,  1.31,  3.85),
+            (1963, 22.80,  1.32,  3.36),
+            (1962, -8.73,  1.00,  2.90),
+            (1961, 26.89,  1.01,  2.60),
+            (1960,  0.47,  1.72,  3.24),
+            (1959, 11.96,  0.69,  3.83),
+            (1958, 43.36,  2.85,  3.5),
+            (1957,-10.78,  3.31,  3.6),
+            (1956,  6.56,  1.49,  2.9),
+            (1955, 31.56, -0.37,  2.5),
+            (1954, 52.62,  0.75,  2.37),
+            (1953, -0.99,  0.75,  2.71),
+            (1952, 18.37,  1.92,  2.19),
+            (1951, 24.02,  7.88,  2.00),
+            (1950, 31.71,  1.26,  1.98),
+            (1949, 18.79, -1.24,  2.21),
+            (1948,  5.50,  8.07,  2.40),
+            (1947,  5.71, 14.36,  2.01),
+            (1946, -8.07,  8.33,  1.64),
+            (1945, 36.44,  2.27,  1.67),
+            (1944, 19.75,  1.73,  1.86),
+            (1943, 25.90,  6.13,  2.05),
+            (1942, 20.34, 10.88,  2.36),
+            (1941,-11.59,  5.00,  2.10),
+            (1940, -9.78,  0.72,  2.76),
+            (1939, -0.41, -1.42,  2.76),
+            (1938, 31.12, -2.08,  2.94),
+            (1937,-35.03,  3.60,  2.76),
+            (1936, 33.92,  1.46,  3.39),
+            (1935, 47.67,  2.24,  2.76),
+            (1934, -1.44,  3.08,  2.76),
+            (1933, 53.99, -5.11,  4.71),
+            (1932, -8.19, -9.87,  4.71),
+            (1931,-43.34, -8.90,  3.99),
+            (1930,-24.90, -2.34,  4.71),
+            (1929, -8.42,  0.00,  4.27) ]
+        mutable_default_parameters.__init__(self)
+
+    def report_year(self, year):
+        if self.selection_index is None:
+            self.selection_index = random.randint(0,
+                                                  len(self.historical_tuples) - 1)
+            self.selection_duration = 1
+
+        t = self.historical_tuples[self.selection_index]
+        sim_year = t[0]
+        stock_return = t[1]
+        inflation = t[2]
+        bond_return = t[3]
+        print
+        print "## REPLAY YEAR %d" % sim_year
+        inflation_multiplier = utils.convert_rate_to_multiplier(inflation)
+        self.set_average_inflation_multiplier(inflation_multiplier)
+        print "## INFLATION is %s (%f)" % (
+            utils.format_rate(inflation_multiplier),
+            inflation_multiplier)
+        ss_multiplier = inflation_multiplier
+        if ss_multiplier >= 1.0:
+            ss_multiplier -= 1.0
+            ss_multiplier *= 0.6666
+            ss_multiplier += 1.0
+        self.set_average_social_security_multiplier(ss_multiplier)
+        print "## SS is %s (%f)" % (
+            utils.format_rate(self.get_average_social_security_multiplier()),
+            ss_multiplier)
+
+        # Assumes 50/50 Stocks/Bonds portfolio
+        our_return = (stock_return * 0.5 +
+                      bond_return * 0.5)
+        self.set_average_investment_return_multiplier(utils.convert_rate_to_multiplier(our_return))
+        print "## 50/50 INVESTMENTS RETURN is %s" % utils.format_rate(self.get_average_investment_return_multiplier())
+        self.selection_duration += 1
+        self.selection_index -= 1
+        if self.selection_index < 0 or random.randint(1, 100) < 20:
+            self.selection_index = None
+            self.selection_duration = None
+
index fa86bdaaf2e641f38303960a40069c4fb1e98b8b..578869b402e55b04ccfec4e1e7e1e72b20cf9265 100755 (executable)
--- a/retire.py
+++ b/retire.py
@@ -4,7 +4,7 @@ import sys
 
 from accounts import *
 import constants
-from parameters import parameters
+from parameters import *
 from tax_brackets import tax_brackets
 from tax_collector import tax_collector
 import utils
@@ -152,6 +152,7 @@ class simulation(object):
                 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)
@@ -251,7 +252,8 @@ class simulation(object):
             self.dump_final_report(taxes)
 
 # main
-params = parameters().with_default_values()
+#params = mutable_default_parameters()
+params = mutable_dynamic_historical_parameters()
 accounts = secrets.accounts
 s = simulation(params, accounts)
 s.run()
index 455d8b5eceff893e02c5cd6277a034079ccaa85f..76807644a2b25c1d370cd9bfc062a4089e7a60d4 100644 (file)
@@ -30,22 +30,22 @@ class tax_collector(object):
         self.roth_income = money(0)
 
     def record_ordinary_income(self, amount):
-        assert amount >= 0, "Income should be non-negative"
+        assert amount >= 0, "Income should be non-negative; got %s" % amount
         self.ordinary_income += amount
         self.total_ordinary_income += amount
 
     def record_short_term_gain(self, amount):
-        assert amount >= 0, "Income should be non-negative"
+        assert amount >= 0, "Income should be non-negative; got %s" % amount
         self.short_term_gains += amount
         self.total_short_term_gains += amount
 
     def record_dividend_or_long_term_gain(self, amount):
-        assert amount >= 0, "Income should be non-negative"
+        assert amount >= 0, "Income should be non-negative; got %s" % amount
         self.dividends_and_long_term_gains += amount
         self.total_dividends_and_long_term_gains += amount
 
     def record_roth_income(self, amount):
-        assert amount >= 0, "Income should be non-negative"
+        assert amount >= 0, "Income should be non-negative; got %s" % amount
         self.roth_income += amount
         self.total_roth_income += amount
 
index 97b98199c1ac2786714d8a2f6f32a178e337fed7..eddefdb7d93ab7427ecbbf212fb38037bd9425f2 100644 (file)
--- a/utils.py
+++ b/utils.py
@@ -12,3 +12,6 @@ def format_rate(rate):
         return format_rate(rate - 1.0)
     else:
         return "{:<}%".format(round(rate * 100, 3))
+
+def convert_rate_to_multiplier(rate):
+    return 1.0 + (rate / 100)