6 """This defines an account base class which inherits from object, the
7 default base class in python. This is not a 100% abstract class, it
8 includes some default fields (like balance and name) and methods that
9 define behaviors (withdraw, appreciate, etc...). Several subclasses
12 def __init__(self, name, owner):
13 """Constructor for account object takes a name and owner"""
15 assert constants.is_valid_owner(owner), "Bad account owner"
24 def belongs_to_scott(self):
25 return self.get_owner() == constants.SCOTT
27 def belongs_to_lynn(self):
28 return self.get_owner() == constants.LYNN
30 def get_balance(self):
31 """Return the current account balance."""
34 def appreciate(self, multiplier):
35 """Grow the balance using rate."""
38 def withdraw(self, amount, taxes):
39 """Withdraw money from the account if possible. Throws otherwise.
40 Money should be registered with taxes so that we can approximate
44 def deposit(self, amount):
45 """Deposit money into the account."""
48 def is_age_restricted(self):
49 """Is this account age restricted. Subclasses should implement."""
53 """Does this account have a required minimum distribution? Sub-
54 classes should implement."""
57 def do_rmd_withdrawal(self, owner_age, taxes):
58 """Handle RMD withdrawals for account."""
62 """Does this account have a Roth part?"""
65 def dump_final_report(self):
66 """Output account-specific info for the final simulation report."""
67 print " " + self.name + ":"
69 class age_restricted_tax_deferred_account(account):
70 """Account object used to represent an age-restricted tax deferred
71 account such as a 401(k), IRA or annuity."""
73 def __init__(self, total_balance, roth_amount_subset, name, owner):
74 """C'tor for this class takes a roth_amount, the number of dollars
75 in the account that are in-plan Roth and can be taken out tax
76 free. It keeps this pile separate from the pretax pile and
77 tries to estimate taxes."""
78 assert total_balance >= 0, "Initial balance must be >= 0"
79 assert roth_amount_subset <= total_balance, "Roth subset too high!"
80 self.roth = money(roth_amount_subset)
81 self.pretax = total_balance - money(roth_amount_subset)
82 self.total_roth_withdrawals = money(0)
83 self.total_pretax_withdrawals = money(0)
84 self.total_investment_gains = money(0)
85 self.total_roth_conversions = money(0)
86 self.initial_balance = self.get_balance()
87 assert self.initial_balance >= 0, "Bad initial balance"
89 # This calls the super class' c'tor.
90 account.__init__(self, name, owner)
92 # These accounts are all age-restricted
93 def is_age_restricted(self):
96 def withdraw(self, amount, taxes):
97 """Assume that money withdrawn from this account will be a mixture
98 of pretax funds (which count as ordinary income) and Roth funds
99 (which are available tax-free). Assume that the ratio of pretax
100 to Roth in this overall account determines the amount from each
101 partition in this withdrawal."""
102 if amount <= 0: return 0
103 balance = self.get_balance()
104 if balance < amount: raise Exception("Insufficient funds")
106 ratio = self.roth / balance
107 roth_part = amount * ratio
108 pretax_part = amount - roth_part
110 self.roth -= roth_part
111 self.total_roth_withdrawals += roth_part
112 print "## Satisfying %s from %s with Roth money." % (roth_part,
114 taxes.record_roth_income(roth_part)
116 self.pretax -= pretax_part
117 self.total_pretax_withdrawals += pretax_part
118 print "## Satisfying %s from %s with pre-tax money." % (pretax_part,
120 taxes.record_ordinary_income(pretax_part)
122 def deposit(self, amount):
123 self.pretax_part += amount
125 def appreciate(self, multiplier):
126 old_pretax = self.pretax
127 self.pretax *= multiplier
128 self.total_investment_gains += self.pretax - old_pretax
130 self.roth *= multiplier
131 self.total_investment_gains += self.roth - old_roth
133 def get_balance(self):
134 """In this class we keep the balance in two parts."""
135 return self.pretax + self.roth
140 # Used to compute RMDs. Source:
141 # https://www.irs.gov/publications/p590b#en_US_2018_publink1000231258
142 def get_actuary_number_years_to_live(self, age):
238 def do_rmd_withdrawal(self, owner_age, taxes):
239 balance = self.get_balance()
240 if balance > 0 and owner_age >= 72:
241 rmd_factor = self.get_actuary_number_years_to_live(owner_age)
242 amount = balance / rmd_factor
243 self.withdraw(amount, taxes)
250 def do_roth_conversion(self, amount, taxes):
251 if amount <= 0: return 0
252 amount = min(amount, self.pretax)
254 self.pretax -= amount
255 assert self.pretax >= 0, "Somehow exhausted more than all pretax money"
256 self.total_roth_conversions += amount
257 taxes.record_ordinary_income(amount)
258 print "## Executed pre-tax --> Roth conversion of %s in %s" % (
259 amount, self.get_name())
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 self.initial_balance)
266 print " {:<50}: {:>14}".format("Ending balance",
268 print " {:<50}: {:>14}".format("Total investment gains",
269 self.total_investment_gains)
270 print " {:<50}: {:>14}".format("Total Roth withdrawals",
271 self.total_roth_withdrawals)
272 print " {:<50}: {:>14}".format("Total pre-tax withdrawals",
273 self.total_pretax_withdrawals)
274 print " {:<50}: {:>14}".format("Total pre-tax converted to Roth",
275 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 assert total_balance >= 0, "Initial balance must be >= 0"
289 age_restricted_tax_deferred_account.__init__(
290 self, total_balance, total_balance, name, owner)
297 def do_rmd_withdrawal(self, owner_age, taxes):
298 raise Exception("This account has no RMDs")
301 def do_roth_conversion(self, amount, taxes):
304 class brokerage_account(account):
305 """A class to represent money in a taxable brokerage account."""
307 def __init__(self, total_balance, cost_basis, name, owner):
308 """The c'tor of this class partitions balance into three pieces:
309 the cost_basis (i.e. how much money was invested in the account),
310 the short_term_gain (i.e. appreciation that has been around for
311 less than a year) and long_term_gain (i.e. appreciation that has
312 been around for more than a year). We separate these because
313 taxes on long_term_gain (and qualified dividends, which are not
314 modeled) are usually lower than short_term_gain. Today those
315 are taxed at 15% and as ordinary income, respectively."""
316 assert total_balance >= 0, "Initial balance must be >= 0"
317 assert cost_basis <= total_balance, "Bad initial cost basis"
318 self.cost_basis = money(cost_basis)
319 self.short_term_gain = money(0)
320 self.long_term_gain = money(total_balance - cost_basis)
321 self.total_cost_basis_withdrawals = money(0)
322 self.total_long_term_gain_withdrawals = money(0)
323 self.total_short_term_gain_withdrawals = money(0)
324 self.total_investment_gains = money(0)
325 self.initial_balance = self.get_balance()
327 # Call the base class' c'tor as well to set up the name.
328 account.__init__(self, name, owner)
330 def withdraw(self, amount, taxes):
331 """Override the base class' withdraw implementation since we're
332 dealing with three piles of money instead of one. When you sell
333 securities to get money out of this account the gains are taxed
334 (and the cost_basis part isn't). Assume that the ratio of
335 cost_basis to overall balance can be used to determine how much
336 of the withdrawal will be taxed (and how)."""
337 if amount <= 0: return 0
338 balance = self.get_balance()
339 if balance < amount: raise Exception("Insufficient funds")
341 ratio = self.cost_basis / balance
342 invested_capital_part = amount * ratio
343 gains_part = amount - invested_capital_part
345 if self.cost_basis >= invested_capital_part:
346 self.cost_basis -= invested_capital_part
347 self.total_cost_basis_withdrawals += invested_capital_part
348 print "## Satisfying %s from %s as cost basis." % (invested_capital_part,
350 self.withdraw_from_gains(gains_part, taxes)
352 self.withdraw_from_gains(amount, taxes)
354 def withdraw_from_gains(self, amount, taxes):
355 """Withdraw some money from gains. Prefer the long term ones if
357 if amount <= 0: return 0
358 if amount > (self.long_term_gain + self.short_term_gain):
359 raise Exception("Insufficient funds")
361 if self.long_term_gain >= amount:
362 self.long_term_gain -= amount
363 self.total_long_term_gain_withdrawals += amount
364 print "## Satisfying %s from %s as long term gains." % (
366 taxes.record_dividend_or_long_term_gain(amount)
370 print "## Satisfying %s from %s as long term gains (exhausting all current long term gains in account)." % (
371 self.long_term_gain, self.name)
372 amount -= self.long_term_gain
373 self.total_long_term_gain_withdrawals += self.long_term_gain
374 taxes.record_dividend_or_long_term_gain(self.long_term_gain)
375 self.long_term_gain = 0
376 # Get the rest of amount from short term gains...
378 self.short_term_gain -= amount
379 self.total_short_term_gain_withdrawals += amount
380 print "## Satisfying %s from %s as short term gains." % (
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 self.initial_balance)
419 print " {:<50}: {:>14}".format("Ending balance",
421 print " {:<50}: {:>14}".format("Total investment gains",
422 self.total_investment_gains)
423 print " {:<50}: {:>14}".format("Total cost basis withdrawals",
424 self.total_cost_basis_withdrawals)
425 print " {:<50}: {:>14}".format("Total long term gain withdrawals",
426 self.total_long_term_gain_withdrawals)
427 print " {:<50}: {:>14}".format("Total short term gain withdrawals",
428 self.total_short_term_gain_withdrawals)