5 """This defines an account base class which inherits from object, the
6 default base class in python. This is not a 100% abstract class, it
7 includes some default fields (like balance and name) and methods that
8 define behaviors (withdraw, appreciate, etc...). Several subclasses
11 def __init__(self, name, owner):
12 """Constructor for account object takes a name and owner"""
14 assert constants.is_valid_owner(owner), "Bad account owner"
23 def belongs_to_scott(self):
24 return self.get_owner() == constants.SCOTT
26 def belongs_to_lynn(self):
27 return self.get_owner() == constants.LYNN
29 def get_balance(self):
30 """Return the current account balance."""
33 def appreciate(self, multiplier):
34 """Grow the balance using rate."""
37 def withdraw(self, amount, taxes):
38 """Withdraw money from the account if possible. Throws otherwise.
39 Money should be registered with taxes so that we can approximate
43 def deposit(self, amount):
44 """Deposit money into the account."""
47 def is_age_restricted(self):
48 """Is this account age restricted. Subclasses should implement."""
52 """Does this account have a required minimum distribution? Sub-
53 classes should implement."""
56 def do_rmd_withdrawal(self, owner_age, taxes):
57 """Handle RMD withdrawals for account."""
61 """Does this account have a Roth part?"""
64 def dump_final_report(self):
65 """Output account-specific info for the final simulation report."""
66 print " " + self.name + ":"
68 class age_restricted_tax_deferred_account(account):
69 """Account object used to represent an age-restricted tax deferred
70 account such as a 401(k), IRA or annuity."""
72 def __init__(self, total_balance, roth_amount_subset, name, owner):
73 """C'tor for this class takes a roth_amount, the number of dollars
74 in the account that are in-plan Roth and can be taken out tax
75 free. It keeps this pile separate from the pretax pile and
76 tries to estimate taxes."""
77 assert total_balance >= 0, "Initial balance must be >= 0"
78 assert roth_amount_subset <= total_balance, "Roth subset too high!"
79 self.roth = roth_amount_subset
80 self.pretax = total_balance - roth_amount_subset
81 self.total_roth_withdrawals = 0
82 self.total_pretax_withdrawals = 0
83 self.total_investment_gains = 0
84 self.total_roth_conversions = 0
85 self.initial_balance = self.get_balance()
86 assert self.initial_balance >= 0, "Bad initial balance"
88 # This calls the super class' c'tor.
89 account.__init__(self, name, owner)
91 # These accounts are all age-restricted
92 def is_age_restricted(self):
95 def withdraw(self, amount, taxes):
96 """Assume that money withdrawn from this account will be a mixture
97 of pretax funds (which count as ordinary income) and Roth funds
98 (which are available tax-free). Assume that the ratio of pretax
99 to Roth in this overall account determines the amount from each
100 partition in this withdrawal."""
101 if amount <= 0: return 0
102 balance = self.get_balance()
103 if balance < amount: raise Exception("Insufficient funds")
105 ratio = float(self.roth) / float(balance)
106 roth_part = amount * ratio
107 pretax_part = amount - roth_part
109 self.roth -= roth_part
110 self.total_roth_withdrawals += roth_part
111 print "## Satisfying %s from %s with Roth money." % (utils.format_money(roth_part), self.name)
112 taxes.record_roth_income(roth_part)
114 self.pretax -= pretax_part
115 self.total_pretax_withdrawals += pretax_part
116 print "## Satisfying %s from %s with pre-tax money." % (utils.format_money(pretax_part), self.name)
117 taxes.record_ordinary_income(pretax_part)
119 def deposit(self, amount):
120 self.pretax_part += amount
122 def appreciate(self, multiplier):
123 old_pretax = self.pretax
124 self.pretax *= multiplier
125 self.total_investment_gains += self.pretax - old_pretax
127 self.roth *= multiplier
128 self.total_investment_gains += self.roth - old_roth
130 def get_balance(self):
131 """In this class we keep the balance in two parts."""
132 return self.pretax + self.roth
137 # Used to compute RMDs. Source:
138 # https://www.irs.gov/publications/p590b#en_US_2018_publink1000231258
139 def get_actuary_number_years_to_live(self, age):
235 def do_rmd_withdrawal(self, owner_age, taxes):
236 balance = self.get_balance()
237 if balance > 0 and owner_age >= 72:
238 rmd_factor = self.get_actuary_number_years_to_live(owner_age)
239 amount = balance / rmd_factor
240 self.withdraw(amount, taxes)
247 def do_roth_conversion(self, amount, taxes):
248 if amount <= 0: return 0
249 amount = min(amount, self.pretax)
251 self.pretax -= amount
252 assert self.pretax >= 0, "Somehow exhausted more than all pretax money"
253 self.total_roth_conversions += amount
254 taxes.record_ordinary_income(amount)
255 print "## Executed pre-tax --> Roth conversion of %s in %s" % (
256 utils.format_money(amount),
260 def dump_final_report(self):
261 super(age_restricted_tax_deferred_account, self).dump_final_report()
262 print " {:<50}: {:>14}".format("Initial balance",
263 utils.format_money(self.initial_balance))
264 print " {:<50}: {:>14}".format("Ending balance",
265 utils.format_money(self.get_balance()))
266 print " {:<50}: {:>14}".format("Total investment gains",
267 utils.format_money(self.total_investment_gains))
268 print " {:<50}: {:>14}".format("Total Roth withdrawals",
269 utils.format_money(self.total_roth_withdrawals))
270 print " {:<50}: {:>14}".format("Total pre-tax withdrawals",
271 utils.format_money(self.total_pretax_withdrawals))
272 print " {:<50}: {:>14}".format("Total pre-tax converted to Roth",
273 utils.format_money(self.total_roth_conversions))
275 class age_restricted_roth_account(age_restricted_tax_deferred_account):
276 """This is an object to represent a Roth account like a Roth IRA. All
277 money in here is tax free. Most of the base account class works here
278 including the implementation of withdraw() which says that none of the
279 money withdrawn was taxable."""
281 def __init__(self, total_balance, name, owner):
282 """C'tor for this class takes a roth_amount, the number of dollars
283 in the account that are in-plan Roth and can be taken out tax
284 free. It keeps this pile separate from the pretax pile and
285 tries to estimate taxes."""
286 assert total_balance >= 0, "Initial balance must be >= 0"
287 age_restricted_tax_deferred_account.__init__(
288 self, total_balance, total_balance, name, owner)
295 def do_rmd_withdrawal(self, owner_age, taxes):
296 raise Exception("This account has no RMDs")
299 def do_roth_conversion(self, amount, taxes):
302 class brokerage_account(account):
303 """A class to represent money in a taxable brokerage account."""
305 def __init__(self, total_balance, cost_basis, name, owner):
306 """The c'tor of this class partitions balance into three pieces:
307 the cost_basis (i.e. how much money was invested in the account),
308 the short_term_gain (i.e. appreciation that has been around for
309 less than a year) and long_term_gain (i.e. appreciation that has
310 been around for more than a year). We separate these because
311 taxes on long_term_gain (and qualified dividends, which are not
312 modeled) are usually lower than short_term_gain. Today those
313 are taxed at 15% and as ordinary income, respectively."""
314 assert total_balance >= 0, "Initial balance must be >= 0"
315 assert cost_basis <= total_balance, "Bad initial cost basis"
316 self.cost_basis = float(cost_basis)
317 self.short_term_gain = 0.0
318 self.long_term_gain = float(total_balance - cost_basis)
319 self.total_cost_basis_withdrawals = 0
320 self.total_long_term_gain_withdrawals = 0
321 self.total_short_term_gain_withdrawals = 0
322 self.total_investment_gains = 0
323 self.initial_balance = self.get_balance()
325 # Call the base class' c'tor as well to set up the name.
326 account.__init__(self, name, owner)
328 def withdraw(self, amount, taxes):
329 """Override the base class' withdraw implementation since we're
330 dealing with three piles of money instead of one. When you sell
331 securities to get money out of this account the gains are taxed
332 (and the cost_basis part isn't). Assume that the ratio of
333 cost_basis to overall balance can be used to determine how much
334 of the withdrawal will be taxed (and how)."""
335 if amount <= 0: return 0
336 balance = self.get_balance()
337 if balance < amount: raise Exception("Insufficient funds")
339 ratio = self.cost_basis / balance
340 invested_capital_part = amount * ratio
341 gains_part = amount - invested_capital_part
343 if self.cost_basis >= invested_capital_part:
344 self.cost_basis -= invested_capital_part
345 self.total_cost_basis_withdrawals += invested_capital_part
346 print "## Satisfying %s from %s as cost basis." % (utils.format_money(invested_capital_part), self.name)
347 self.withdraw_from_gains(gains_part, taxes)
349 self.withdraw_from_gains(amount, taxes)
351 def withdraw_from_gains(self, amount, taxes):
352 """Withdraw some money from gains. Prefer the long term ones if
354 if amount <= 0: return 0
355 if amount > (self.long_term_gain + self.short_term_gain):
356 raise Exception("Insufficient funds")
358 if self.long_term_gain >= amount:
359 self.long_term_gain -= amount
360 self.total_long_term_gain_withdrawals += amount
361 print "## Satisfying %s from %s as long term gains." % (
362 utils.format_money(amount),
364 taxes.record_dividend_or_long_term_gain(amount)
368 print "## Satisfying %s from %s as long term gains (exhausting all current long term gains in account)." % (
369 utils.format_money(self.long_term_gain),
371 amount -= self.long_term_gain
372 self.total_long_term_gain_withdrawals += self.long_term_gain
373 taxes.record_dividend_or_long_term_gain(self.long_term_gain)
374 self.long_term_gain = 0
375 # Get the rest of amount from short term gains...
377 self.short_term_gain -= amount
378 self.total_short_term_gain_withdrawals += amount
379 print "## Satisfying %s from %s as short term gains." % (
380 utils.format_money(amount),
382 taxes.record_short_term_gain(amount)
384 def deposit(self, amount):
385 assert amount >= 0, "Can't deposit negative amounts"
386 self.cost_basis += amount
388 def get_balance(self):
389 """We're ignoring the base class' balance field in favor of tracking
390 it as three separate partitions of money."""
391 return self.cost_basis + self.long_term_gain + self.short_term_gain
393 def appreciate(self, multiplier):
394 """Appreciate... another year has passed so short_term_gains turn into
395 long_term_gains and the appreciation is our new short_term_gains."""
396 balance = self.get_balance()
397 balance *= multiplier
398 gain_or_loss = balance - self.get_balance()
399 self.total_investment_gains += gain_or_loss
400 self.long_term_gain += self.short_term_gain
401 self.short_term_gain = gain_or_loss
403 def is_age_restricted(self):
412 def do_roth_conversion(self, amount, taxes):
415 def dump_final_report(self):
416 super(brokerage_account, self).dump_final_report()
417 print " {:<50}: {:>14}".format("Initial balance",
418 utils.format_money(self.initial_balance))
419 print " {:<50}: {:>14}".format("Ending balance",
420 utils.format_money(self.get_balance()))
421 print " {:<50}: {:>14}".format("Total investment gains",
422 utils.format_money(self.total_investment_gains))
423 print " {:<50}: {:>14}".format("Total cost basis withdrawals",
424 utils.format_money(self.total_cost_basis_withdrawals))
425 print " {:<50}: {:>14}".format("Total long term gain withdrawals",
426 utils.format_money(self.total_long_term_gain_withdrawals))
427 print " {:<50}: {:>14}".format("Total short term gain withdrawals",
428 utils.format_money(self.total_short_term_gain_withdrawals))