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"""
22 def belongs_to_scott(self):
23 return self.get_owner() == constants.SCOTT
25 def belongs_to_lynn(self):
26 return self.get_owner() == constants.LYNN
28 def get_balance(self):
31 def appreciate(self, multiplier):
32 """Grow the balance using rate."""
35 def withdraw(self, amount, taxes):
36 """Withdraw money from the account if possible. Throws otherwise.
37 Money should be registered with taxes so that we can approximate
41 def deposit(self, amount):
44 def is_age_restricted(self):
45 """Is this account age restricted. Subclasses should implement."""
49 """Does this account have a required minimum distribution? Sub-
50 classes should implement."""
53 def do_rmd_withdrawal(self, owner_age, taxes):
59 def dump_final_report(self):
60 print " " + self.name + ":"
62 class age_restricted_tax_deferred_account(account):
63 """Account object used to represent an age-restricted tax deferred
64 account such as a 401(k), IRA or annuity."""
66 def __init__(self, total_balance, roth_amount_subset, name, owner):
67 """C'tor for this class takes a roth_amount, the number of dollars
68 in the account that are in-plan Roth and can be taken out tax
69 free. It keeps this pile separate from the pretax pile and
70 tries to estimate taxes."""
71 self.roth = roth_amount_subset
72 self.pretax = total_balance - roth_amount_subset
73 self.total_roth_withdrawals = 0
74 self.total_pretax_withdrawals = 0
75 self.total_investment_gains = 0
76 self.total_roth_conversions = 0
77 self.initial_balance = self.get_balance()
79 # This calls the super class' c'tor.
80 account.__init__(self, name, owner)
82 # These accounts are all age-restricted
83 def is_age_restricted(self):
86 def withdraw(self, amount, taxes):
87 """Assume that money withdrawn from this account will be a mixture
88 of pretax funds (which count as ordinary income) and Roth funds
89 (which are available tax-free). Assume that the ratio of pretax
90 to Roth in this overall account determines the amount from each
91 partition in this withdrawal."""
92 balance = self.get_balance()
94 raise Exception("Insufficient funds")
96 ratio = float(self.roth) / float(balance)
97 roth_part = amount * ratio
98 pretax_part = amount - roth_part
100 self.roth -= roth_part
101 self.total_roth_withdrawals += roth_part
102 print "## Satisfying %s from %s with Roth money." % (utils.format_money(roth_part), self.name)
103 taxes.record_roth_income(roth_part)
105 self.pretax -= pretax_part
106 self.total_pretax_withdrawals += pretax_part
107 print "## Satisfying %s from %s with pre-tax money." % (utils.format_money(pretax_part), self.name)
108 taxes.record_ordinary_income(pretax_part)
110 def deposit(self, amount):
111 self.pretax_part += amount
113 def appreciate(self, multiplier):
114 """In this class we basically ignore the balance field in favor of
115 just using pretax and roth so that we can track them separately."""
116 old_pretax = self.pretax
117 self.pretax *= multiplier
118 self.total_investment_gains += self.pretax - old_pretax
120 self.roth *= multiplier
121 self.total_investment_gains += self.roth - old_roth
123 def get_balance(self):
124 """In this class we keep the balance in two parts."""
125 return self.pretax + self.roth
130 # Used to compute RMDs. Source:
131 # https://www.irs.gov/publications/p590b#en_US_2018_publink1000231258
132 def get_actuary_number_years_to_live(self, age):
228 def do_rmd_withdrawal(self, owner_age, taxes):
229 balance = self.get_balance()
230 if balance > 0 and owner_age >= 72:
231 rmd_factor = self.get_actuary_number_years_to_live(owner_age)
232 amount = balance / rmd_factor
233 self.withdraw(amount, taxes)
240 def do_roth_conversion(self, amount):
243 if self.pretax >= amount:
245 self.pretax -= amount
246 self.total_roth_conversions += amount
247 print "## Executed pre-tax --> Roth conversion of %s in %s" % (
248 utils.format_money(amount),
251 elif self.pretax > 0:
252 actual_amount = self.pretax
253 self.roth += actual_amount
255 self.total_roth_conversions += actual_amount
256 print "## Executed pre-tax --> Roth conversion of %s in %s" % (
257 utils.format_money(actual_amount),
262 def dump_final_report(self):
263 super(age_restricted_tax_deferred_account, self).dump_final_report()
264 print " {:<50}: {:>14}".format("Initial balance",
265 utils.format_money(self.initial_balance))
266 print " {:<50}: {:>14}".format("Ending balance",
267 utils.format_money(self.get_balance()))
268 print " {:<50}: {:>14}".format("Total investment gains",
269 utils.format_money(self.total_investment_gains))
270 print " {:<50}: {:>14}".format("Total Roth withdrawals",
271 utils.format_money(self.total_roth_withdrawals))
272 print " {:<50}: {:>14}".format("Total pre-tax withdrawals",
273 utils.format_money(self.total_pretax_withdrawals))
274 print " {:<50}: {:>14}".format("Total pre-tax converted to Roth",
275 utils.format_money(self.total_roth_conversions))
277 class age_restricted_roth_account(age_restricted_tax_deferred_account):
278 """This is an object to represent a Roth account like a Roth IRA. All
279 money in here is tax free. Most of the base account class works here
280 including the implementation of withdraw() which says that none of the
281 money withdrawn was taxable."""
283 def __init__(self, total_balance, name, owner):
284 """C'tor for this class takes a roth_amount, the number of dollars
285 in the account that are in-plan Roth and can be taken out tax
286 free. It keeps this pile separate from the pretax pile and
287 tries to estimate taxes."""
288 age_restricted_tax_deferred_account.__init__(
289 self, total_balance, 0, name, owner)
296 def do_rmd_withdrawal(self, owner_age, taxes):
297 raise Exception("This account has no RMDs")
300 def do_roth_conversion(self, amount):
303 class brokerage_account(account):
304 """A class to represent money in a taxable brokerage account."""
306 def __init__(self, total_balance, cost_basis, name, owner):
307 """The c'tor of this class partitions balance into three pieces:
308 the cost_basis (i.e. how much money was invested in the account),
309 the short_term_gain (i.e. appreciation that has been around for
310 less than a year) and long_term_gain (i.e. appreciation that has
311 been around for more than a year). We separate these because
312 taxes on long_term_gain (and qualified dividends, which are not
313 modeled) are usually lower than short_term_gain. Today those
314 are taxed at 15% and as ordinary income, respectively."""
315 self.cost_basis = float(cost_basis)
316 self.short_term_gain = 0.0
317 self.long_term_gain = float(total_balance - cost_basis)
318 self.total_cost_basis_withdrawals = 0
319 self.total_long_term_gain_withdrawals = 0
320 self.total_short_term_gain_withdrawals = 0
321 self.total_investment_gains = 0
322 self.initial_balance = self.get_balance()
324 # Call the base class' c'tor as well to set up the name.
325 account.__init__(self, name, owner)
327 def withdraw(self, amount, taxes):
328 """Override the base class' withdraw implementation since we're
329 dealing with three piles of money instead of one. When you sell
330 securities to get money out of this account the gains are taxed
331 (and the cost_basis part isn't). Assume that the ratio of
332 cost_basis to overall balance can be used to determine how much
333 of the withdrawal will be taxed (and how)."""
334 balance = self.get_balance()
336 raise Exception("Insufficient funds")
338 ratio = self.cost_basis / balance
339 invested_capital_part = amount * ratio
340 gains_part = amount - invested_capital_part
342 if self.cost_basis >= invested_capital_part:
343 self.cost_basis -= invested_capital_part
344 self.total_cost_basis_withdrawals += invested_capital_part
345 print "## Satisfying %s from %s as cost basis." % (utils.format_money(invested_capital_part), self.name)
346 self.withdraw_from_gains(gains_part, taxes)
348 self.withdraw_from_gains(amount, taxes)
350 def withdraw_from_gains(self, amount, taxes):
351 """Withdraw some money from gains. Prefer the long term ones if
353 if amount > (self.long_term_gain + self.short_term_gain):
354 raise Exception("Insufficient funds")
356 if self.long_term_gain >= amount:
357 self.long_term_gain -= amount
358 self.total_long_term_gain_withdrawals += amount
359 print "## Satisfying %s from %s as long term gains." % (
360 utils.format_money(amount),
362 taxes.record_dividend_or_long_term_gain(amount)
366 print "## Satisfying %s from %s as long term gains (exhausting all current long term gains in account)." % (
367 utils.format_money(self.long_term_gain),
369 amount -= self.long_term_gain
370 self.total_long_term_gain_withdrawals += self.long_term_gain
371 taxes.record_dividend_or_long_term_gain(self.long_term_gain)
372 self.long_term_gain = 0
373 # Get the rest of amount from short term gains...
375 self.short_term_gain -= amount
376 self.total_short_term_gain_withdrawals += amount
377 print "## Satisfying %s from %s as short term gains." % (
378 utils.format_money(amount),
380 taxes.record_short_term_gain(amount)
382 def deposit(self, amount):
383 self.cost_basis += amount
385 def get_balance(self):
386 """We're ignoring the base class' balance field in favor of tracking
387 it as three separate partitions of money."""
388 return self.cost_basis + self.long_term_gain + self.short_term_gain
390 def appreciate(self, multiplier):
391 """Appreciate... another year has passed so short_term_gains turn into
392 long_term_gains and the appreciation is our new short_term_gains."""
393 balance = self.get_balance()
394 balance *= multiplier
395 gain_or_loss = balance - self.get_balance()
396 self.total_investment_gains += gain_or_loss
397 self.long_term_gain += self.short_term_gain
398 self.short_term_gain = gain_or_loss
400 def is_age_restricted(self):
409 def do_roth_conversion(self, amount):
412 def dump_final_report(self):
413 super(brokerage_account, self).dump_final_report()
414 print " {:<50}: {:>14}".format("Initial balance",
415 utils.format_money(self.initial_balance))
416 print " {:<50}: {:>14}".format("Ending balance",
417 utils.format_money(self.get_balance()))
418 print " {:<50}: {:>14}".format("Total investment gains",
419 utils.format_money(self.total_investment_gains))
420 print " {:<50}: {:>14}".format("Total cost basis withdrawals",
421 utils.format_money(self.total_cost_basis_withdrawals))
422 print " {:<50}: {:>14}".format("Total long term gain withdrawals",
423 utils.format_money(self.total_long_term_gain_withdrawals))
424 print " {:<50}: {:>14}".format("Total short term gain withdrawals",
425 utils.format_money(self.total_short_term_gain_withdrawals))