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, taxes):
241 if amount <= 0: return 0
242 amount = min(amount, self.pretax)
244 self.pretax -= amount
245 self.total_roth_conversions += amount
246 taxes.record_ordinary_income(amount)
247 print "## Executed pre-tax --> Roth conversion of %s in %s" % (
248 utils.format_money(amount),
252 def dump_final_report(self):
253 super(age_restricted_tax_deferred_account, self).dump_final_report()
254 print " {:<50}: {:>14}".format("Initial balance",
255 utils.format_money(self.initial_balance))
256 print " {:<50}: {:>14}".format("Ending balance",
257 utils.format_money(self.get_balance()))
258 print " {:<50}: {:>14}".format("Total investment gains",
259 utils.format_money(self.total_investment_gains))
260 print " {:<50}: {:>14}".format("Total Roth withdrawals",
261 utils.format_money(self.total_roth_withdrawals))
262 print " {:<50}: {:>14}".format("Total pre-tax withdrawals",
263 utils.format_money(self.total_pretax_withdrawals))
264 print " {:<50}: {:>14}".format("Total pre-tax converted to Roth",
265 utils.format_money(self.total_roth_conversions))
267 class age_restricted_roth_account(age_restricted_tax_deferred_account):
268 """This is an object to represent a Roth account like a Roth IRA. All
269 money in here is tax free. Most of the base account class works here
270 including the implementation of withdraw() which says that none of the
271 money withdrawn was taxable."""
273 def __init__(self, total_balance, name, owner):
274 """C'tor for this class takes a roth_amount, the number of dollars
275 in the account that are in-plan Roth and can be taken out tax
276 free. It keeps this pile separate from the pretax pile and
277 tries to estimate taxes."""
278 age_restricted_tax_deferred_account.__init__(
279 self, total_balance, total_balance, name, owner)
286 def do_rmd_withdrawal(self, owner_age, taxes):
287 raise Exception("This account has no RMDs")
290 def do_roth_conversion(self, amount, taxes):
293 class brokerage_account(account):
294 """A class to represent money in a taxable brokerage account."""
296 def __init__(self, total_balance, cost_basis, name, owner):
297 """The c'tor of this class partitions balance into three pieces:
298 the cost_basis (i.e. how much money was invested in the account),
299 the short_term_gain (i.e. appreciation that has been around for
300 less than a year) and long_term_gain (i.e. appreciation that has
301 been around for more than a year). We separate these because
302 taxes on long_term_gain (and qualified dividends, which are not
303 modeled) are usually lower than short_term_gain. Today those
304 are taxed at 15% and as ordinary income, respectively."""
305 self.cost_basis = float(cost_basis)
306 self.short_term_gain = 0.0
307 self.long_term_gain = float(total_balance - cost_basis)
308 self.total_cost_basis_withdrawals = 0
309 self.total_long_term_gain_withdrawals = 0
310 self.total_short_term_gain_withdrawals = 0
311 self.total_investment_gains = 0
312 self.initial_balance = self.get_balance()
314 # Call the base class' c'tor as well to set up the name.
315 account.__init__(self, name, owner)
317 def withdraw(self, amount, taxes):
318 """Override the base class' withdraw implementation since we're
319 dealing with three piles of money instead of one. When you sell
320 securities to get money out of this account the gains are taxed
321 (and the cost_basis part isn't). Assume that the ratio of
322 cost_basis to overall balance can be used to determine how much
323 of the withdrawal will be taxed (and how)."""
324 balance = self.get_balance()
326 raise Exception("Insufficient funds")
328 ratio = self.cost_basis / balance
329 invested_capital_part = amount * ratio
330 gains_part = amount - invested_capital_part
332 if self.cost_basis >= invested_capital_part:
333 self.cost_basis -= invested_capital_part
334 self.total_cost_basis_withdrawals += invested_capital_part
335 print "## Satisfying %s from %s as cost basis." % (utils.format_money(invested_capital_part), self.name)
336 self.withdraw_from_gains(gains_part, taxes)
338 self.withdraw_from_gains(amount, taxes)
340 def withdraw_from_gains(self, amount, taxes):
341 """Withdraw some money from gains. Prefer the long term ones if
343 if amount > (self.long_term_gain + self.short_term_gain):
344 raise Exception("Insufficient funds")
346 if self.long_term_gain >= amount:
347 self.long_term_gain -= amount
348 self.total_long_term_gain_withdrawals += amount
349 print "## Satisfying %s from %s as long term gains." % (
350 utils.format_money(amount),
352 taxes.record_dividend_or_long_term_gain(amount)
356 print "## Satisfying %s from %s as long term gains (exhausting all current long term gains in account)." % (
357 utils.format_money(self.long_term_gain),
359 amount -= self.long_term_gain
360 self.total_long_term_gain_withdrawals += self.long_term_gain
361 taxes.record_dividend_or_long_term_gain(self.long_term_gain)
362 self.long_term_gain = 0
363 # Get the rest of amount from short term gains...
365 self.short_term_gain -= amount
366 self.total_short_term_gain_withdrawals += amount
367 print "## Satisfying %s from %s as short term gains." % (
368 utils.format_money(amount),
370 taxes.record_short_term_gain(amount)
372 def deposit(self, amount):
373 self.cost_basis += amount
375 def get_balance(self):
376 """We're ignoring the base class' balance field in favor of tracking
377 it as three separate partitions of money."""
378 return self.cost_basis + self.long_term_gain + self.short_term_gain
380 def appreciate(self, multiplier):
381 """Appreciate... another year has passed so short_term_gains turn into
382 long_term_gains and the appreciation is our new short_term_gains."""
383 balance = self.get_balance()
384 balance *= multiplier
385 gain_or_loss = balance - self.get_balance()
386 self.total_investment_gains += gain_or_loss
387 self.long_term_gain += self.short_term_gain
388 self.short_term_gain = gain_or_loss
390 def is_age_restricted(self):
399 def do_roth_conversion(self, amount, taxes):
402 def dump_final_report(self):
403 super(brokerage_account, self).dump_final_report()
404 print " {:<50}: {:>14}".format("Initial balance",
405 utils.format_money(self.initial_balance))
406 print " {:<50}: {:>14}".format("Ending balance",
407 utils.format_money(self.get_balance()))
408 print " {:<50}: {:>14}".format("Total investment gains",
409 utils.format_money(self.total_investment_gains))
410 print " {:<50}: {:>14}".format("Total cost basis withdrawals",
411 utils.format_money(self.total_cost_basis_withdrawals))
412 print " {:<50}: {:>14}".format("Total long term gain withdrawals",
413 utils.format_money(self.total_long_term_gain_withdrawals))
414 print " {:<50}: {:>14}".format("Total short term gain withdrawals",
415 utils.format_money(self.total_short_term_gain_withdrawals))