| import numpy as np |
| import copy |
|
|
| |
| |
| |
|
|
| INFLATION_RATE = 0.03 |
| INIT_AGE = 31 |
|
|
| |
| |
| |
|
|
| def set_INIT_AGE(age): |
| global INIT_AGE |
| INIT_AGE = age |
| return 0 |
|
|
| def pretty_num(num): |
| neg = num < 0 |
| num = abs(num) |
| if num > 9.9e5: |
| snum = round(num / 1e6, 1) |
| s = "$" + str(snum) + "M" |
| elif num > 9.9e2: |
| snum = round(num / 1e3, 1) |
| s = "$" + str(snum) + "K" |
| else: |
| snum = round(num, -1) |
| s = "$" + str(snum) |
|
|
| if neg: |
| return "-" + s |
| return s |
|
|
|
|
| |
| |
| |
|
|
| FEDERAL_INCOME_TAX_BRACKETS = [ |
| (11000, 0.10), |
| (44725, 0.12), |
| (95375, 0.22), |
| (182_100, 0.24), |
| (231_250, 0.32), |
| (578_125, 0.35), |
| (np.inf, 0.37), |
| ] |
| NEW_YORK_INCOME_TAX_BRACKETS = [ |
| (8500, 0.0400), |
| (11700, 0.0450), |
| (13900, 0.0525), |
| (80650, 0.0585), |
| (215_400, 0.0625), |
| (1_077_550, 0.0685), |
| (5_000_000, 0.0965), |
| (25_000_000, 0.1030), |
| (np.inf, 0.1090), |
| ] |
|
|
| NEW_YORK_CITY_INCOME_TAX_BRACKETS = [ |
| (14400, 0.03078), |
| (30000, 0.03762), |
| (60000, 0.03819), |
| (np.inf, 0.03876), |
| ] |
|
|
| FEDERAL_GAINS_TAX_BRACKETS = [(41675, 0.00), (492_300, 0.15), (np.inf, 0.2)] |
|
|
|
|
| def marginal_tax(pay, brackets): |
|
|
| left_to_pay = pay |
| tax = 0 |
| for ii,bb in enumerate(brackets): |
| bracket, rate = bb |
| if left_to_pay < 1: |
| break |
| if ii > 0: |
| tax += (min(pay, bracket) - brackets[ii-1][0]) * rate |
| else: |
| tax += min(pay, bracket) * rate |
| left_to_pay = pay - bracket |
| return tax |
|
|
|
|
| class IRS_agent(object): |
| def __init__( |
| self, |
| year_offset=0.0, |
| federal_gains_brackets=FEDERAL_GAINS_TAX_BRACKETS, |
| federal_income_brackets=FEDERAL_INCOME_TAX_BRACKETS, |
| state_income_brackets=NEW_YORK_INCOME_TAX_BRACKETS, |
| city_income_brackets=NEW_YORK_CITY_INCOME_TAX_BRACKETS, |
| ): |
|
|
| self.year_offset = year_offset |
| self.federal_gains_brackets = copy.deepcopy(federal_gains_brackets) |
| self.federal_income_brackets = copy.deepcopy(federal_income_brackets) |
| self.state_income_brackets = copy.deepcopy(state_income_brackets) |
| self.city_income_brackets = copy.deepcopy(city_income_brackets) |
|
|
| def inflate_tax_brackets(self): |
| for i, (bracket, rate) in enumerate(self.federal_gains_brackets): |
| self.federal_gains_brackets[i] = (bracket * (1.0 + INFLATION_RATE), rate) |
| for i, (bracket, rate) in enumerate(self.federal_income_brackets): |
| self.federal_income_brackets[i] = (bracket * (1.0 + INFLATION_RATE), rate) |
| for i, (bracket, rate) in enumerate(self.state_income_brackets): |
| self.state_income_brackets[i] = (bracket * (1.0 + INFLATION_RATE), rate) |
| for i, (bracket, rate) in enumerate(self.city_income_brackets): |
| self.city_income_brackets[i] = (bracket * (1.0 + INFLATION_RATE), rate) |
|
|
| def combined_tax(self, pay, gain=0.0): |
| |
| tax = marginal_tax(gain, self.federal_gains_brackets) |
| tax += marginal_tax(pay, self.federal_income_brackets) |
| |
| tax += marginal_tax(pay + gain, self.state_income_brackets) |
| tax += marginal_tax(pay + gain, self.city_income_brackets) |
|
|
| return tax |
|
|
|
|
| |
| |
| |
|
|
|
|
| class checkings_accnt(object): |
| def __init__(self): |
| self.balance = 0.0 |
| self.last_tax = 0.0 |
|
|
| def add(self, x): |
| self.balance += x |
|
|
| def remove(self, x): |
| assert x <= self.balance |
| self.balance -= x |
|
|
| def get_balance(self): |
| return self.balance |
|
|
|
|
| class savings_accnt(checkings_accnt): |
| def __init__(self, interest_rate=0.005): |
| super().__init__() |
| self.interest_rate = interest_rate |
|
|
| def get_interest(self): |
| return self.balance * (self.interest_rate) |
|
|
|
|
| |
| |
| |
|
|
| STOCK_GAINS_RATE = 0.06 |
| BONDS_GAINS_RATE = 0.04 |
|
|
|
|
| def default_stock_alloc_strategy(i): |
| age = INIT_AGE + i |
| alloc = min(100.0, max(0.0, 100.0 - age)) |
| return alloc |
|
|
|
|
| class investment_accnt(object): |
| def __init__( |
| self, |
| stock_gains_rate=STOCK_GAINS_RATE, |
| bonds_gains_rate=STOCK_GAINS_RATE, |
| stock_alloc_strategy=default_stock_alloc_strategy, |
| ): |
| super().__init__() |
| self.i = 0 |
| self.principal = 0.0 |
| self.gains = 0.0 |
| self.stock_gains_rate = stock_gains_rate |
| self.bonds_gains_rate = bonds_gains_rate |
| self.stock_alloc_strategy = stock_alloc_strategy |
| self.change_stock_alloc_perc(self.stock_alloc_strategy(self.i)) |
|
|
| def add(self, x): |
| self.principal += x |
|
|
| def remove(self, x): |
| total = max(1, self.gains + self.principal) |
| assert x <= total |
| |
| remove_from_principal = x * self.principal / total |
| remove_from_gains = x * self.gains / total |
| self.principal -= remove_from_principal |
| self.gains -= remove_from_gains |
| |
| return remove_from_principal, remove_from_gains |
|
|
| def change_stock_alloc_perc(self, perc): |
| self.stock_alloc = perc / 100.0 |
| eff_rate = self.stock_gains_rate * self.stock_alloc |
| eff_rate += self.bonds_gains_rate * (1.0 - self.stock_alloc) |
| self.effective_gains_rate = eff_rate |
|
|
| def increment_year(self): |
| self.i += 1 |
| self.gains += (self.principal + self.gains) * (self.effective_gains_rate) |
| self.change_stock_alloc_perc(self.stock_alloc_strategy(self.i)) |
|
|
| def get_balance(self): |
| return self.principal + self.gains |
|
|
|
|
| def default_401k_roth_alloc_strategy(i, retired=False): |
| age = INIT_AGE + i |
| if age < 40 or retired: |
| roth_perc = 100.0 |
| else: |
| roth_perc = 4.0 * abs(age - 60.0) + 20.0 |
|
|
| return roth_perc |
|
|
|
|
| class i401k_accnt: |
| def __init__( |
| self, |
| pretax_cap=20.5e3, |
| total_cap=66e3, |
| roth_stock_alloc_strategy=default_stock_alloc_strategy, |
| pretax_stock_alloc_strategy=default_stock_alloc_strategy, |
| i401k_roth_alloc_strategy=default_401k_roth_alloc_strategy, |
| ): |
| self.roth = investment_accnt(stock_alloc_strategy=roth_stock_alloc_strategy) |
| self.pretax = investment_accnt(stock_alloc_strategy=pretax_stock_alloc_strategy) |
| self.pretax_cap = pretax_cap |
| self.total_cap = total_cap |
| self.i401k_roth_alloc_strategy = i401k_roth_alloc_strategy |
| self.roth_perc = 100.0 |
|
|
| def add(self, x): |
| pretax_x = (1.0 - self.roth_perc / 100.0) * x |
| pretax_x = max(self.pretax_cap, pretax_x) |
| roth_x = x - pretax_x |
| assert roth_x <= self.total_cap - pretax_x |
|
|
| self.pretax.add(pretax_x) |
| self.roth.add(roth_x) |
|
|
| |
| return pretax_x, roth_x |
|
|
| |
| def remove(self, x): |
| assert x <= self.get_balance() |
| |
| pretax_x = min(self.pretax.get_balance(), x) |
| roth_x = min(self.roth.get_balance(), x - pretax_x) |
| untaxed_y, taxed_y = self.pretax.remove(pretax_x) |
| if roth_x > 0: |
| untaxed_y += np.sum(self.roth.remove(roth_x)) |
|
|
| return untaxed_y, taxed_y |
|
|
| def increment_year(self, retired=False): |
| self.roth.increment_year() |
| self.pretax.increment_year() |
| i = self.roth.i |
| self.roth_perc = self.i401k_roth_alloc_strategy(i, retired=retired) |
| self.pretax_cap *= 1.0 + INFLATION_RATE |
| self.total_cap *= 1.0 + INFLATION_RATE |
|
|
| def get_balance(self): |
| return self.roth.get_balance() + self.pretax.get_balance() |
|
|
|
|
| def default_IRA_roth_alloc_strategy(i, pay): |
| roth_perc = 100.0 if pay < 115e3 else 0.0 |
| return roth_perc |
|
|
|
|
| class IRA_accnt(i401k_accnt): |
| def __init__( |
| self, |
| cap=6e3, |
| roth_stock_alloc_strategy=default_stock_alloc_strategy, |
| pretax_stock_alloc_strategy=default_stock_alloc_strategy, |
| IRA_roth_alloc_strategy=default_IRA_roth_alloc_strategy, |
| ): |
| super().__init__( |
| pretax_cap=cap, |
| total_cap=cap, |
| roth_stock_alloc_strategy=roth_stock_alloc_strategy, |
| pretax_stock_alloc_strategy=pretax_stock_alloc_strategy, |
| i401k_roth_alloc_strategy=IRA_roth_alloc_strategy, |
| ) |
| self.cap = cap |
|
|
| def increment_year(self, pay=0.0): |
| self.roth.increment_year() |
| self.pretax.increment_year() |
| i = self.roth.i |
| self.roth_perc = self.i401k_roth_alloc_strategy(i, pay=pay) |
| self.pretax_cap *= 1.0 + INFLATION_RATE |
| self.total_cap *= 1.0 + INFLATION_RATE |
| |
| if i - INIT_AGE == 65: |
| self.pretax_cap *= 6.0 / 7.0 |
| self.total_cap *= 6.0 / 7.0 |
|
|
|
|
| |
| |
| |
|
|
| EMERGENCY_TO_SPENDING_FACTOR = 0.7 |
|
|
|
|
| class portfolio(object): |
| def __init__( |
| self, |
| retirement_age=65, |
| init_checkings=1e3, |
| init_savings=1e3, |
| init_brokerage=0.0, |
| init_roth_401k=1e3, |
| init_pretax_401k=0.0, |
| init_roth_IRA=1e3, |
| init_pretax_IRA=0.0, |
| ): |
| self.init_age = INIT_AGE |
| self.age = INIT_AGE |
| self.retirement_age = retirement_age |
| self.tax_agent = IRS_agent() |
|
|
| self.checkings = checkings_accnt() |
| self.savings = savings_accnt() |
| self.brokerage = investment_accnt() |
| self.i401k = i401k_accnt() |
| self.IRA = IRA_accnt() |
| self.retired = False |
|
|
| self.target_emergency_balance = 10e3 |
| self.checkings.balance = init_checkings |
| self.savings.balance = init_savings |
| self.brokerage.principal = init_brokerage |
| self.i401k.roth.principal = init_roth_401k |
| self.i401k.pretax.principal = init_pretax_401k |
| self.IRA.roth.principal = init_roth_IRA |
| self.IRA.pretax.principal = init_pretax_IRA |
|
|
| self.effective_self_pay_tax_rate = 0.2 |
| self.effective_tax_rate = 0.2 |
|
|
| def set_target_emergency_balance(self, x): |
| self.target_emergency_balance = x |
|
|
| def get_interest(self): |
| return self.savings.interest() |
|
|
| def get_emergency_contributions(self, x): |
| |
| delta_target_emergency_checkings = self.target_emergency_balance * 0.1 |
| delta_target_emergency_checkings -= self.checkings.balance |
| delta_target_emergency_checkings = min( |
| delta_target_emergency_checkings, max(1e3, 0.05 * x) |
| ) |
| delta_target_emergency_savings = self.target_emergency_balance * 0.9 |
| delta_target_emergency_savings -= self.savings.balance |
| delta_target_emergency_savings = min( |
| delta_target_emergency_savings, max(9e3, 0.2 * x) |
| ) |
| delta_target_emergency_checkings = max(0.0, delta_target_emergency_checkings) |
| delta_target_emergency_savings = max(0.0, delta_target_emergency_savings) |
| return delta_target_emergency_checkings, delta_target_emergency_savings |
|
|
| def contribute(self, x, verbose=False): |
|
|
| untax = 0 |
| delta_checkings, delta_savings = self.get_emergency_contributions(x) |
| if verbose: |
| print(f" ADD TO CASH: {pretty_num(delta_checkings+delta_savings)}") |
| self.checkings.add(delta_checkings) |
| self.savings.add(delta_savings) |
| x -= delta_checkings + delta_savings |
| if x > 0: |
| i401k_contribution = min(x, self.i401k.total_cap) |
| if verbose: |
| print(f" 401k CONTRIBUTION: {pretty_num(i401k_contribution)}") |
| untax += self.i401k.add(i401k_contribution)[0] |
| x -= i401k_contribution |
| if x > 0: |
| IRA_contribution = min(x, self.IRA.cap) |
| if verbose: |
| print(f" IRA CONTRIBUTION: {pretty_num(IRA_contribution)}") |
| untax += self.IRA.add(IRA_contribution)[0] |
| x -= IRA_contribution |
| if x > 0: |
| if verbose: |
| print(f" BROKERAGE CONTRIBUTION: {pretty_num(x)}") |
| self.brokerage.add(x) |
|
|
| return untax |
|
|
| def cash_disburse(self, x): |
| |
| total_emergency = self.checkings.balance + self.savings.balance |
| assert x <= total_emergency |
| checkings_x = x * self.checkings.balance / total_emergency |
| savings_x = x * self.savings.balance / total_emergency |
| self.checkings.remove(checkings_x) |
| self.savings.remove(savings_x) |
| untaxed_y = checkings_x + savings_x |
| return untaxed_y |
|
|
| def disburse(self, x, verbose=False): |
| untaxed_y = 0.0 |
| taxed_y = 0.0 |
|
|
| x = abs(x) |
| if x > 0 and self.brokerage.get_balance() > 0: |
| brokerage_x = min(x, self.brokerage.get_balance()) |
| if verbose: |
| print(f" BROKERAGE WITHDRAWL: {pretty_num(brokerage_x)}") |
| ut_y, t_y = self.brokerage.remove(brokerage_x) |
| x -= brokerage_x |
| untaxed_y += ut_y |
| taxed_y += t_y |
| if (x > 0) and self.retired and self.i401k.get_balance() > 0: |
| i401k_x = min(x, self.i401k.get_balance()) |
| if verbose: |
| print(f" 401k WITHDRAWL: {pretty_num(i401k_x)}") |
| ut_y, t_y = self.i401k.remove(i401k_x) |
| x -= i401k_x |
| untaxed_y += ut_y |
| taxed_y += t_y |
| if (x > 0) and self.retired and self.IRA.get_balance() > 0: |
| IRA_x = min(x, self.IRA.get_balance()) |
| if verbose: |
| print(f" IRA WITHDRAWL: {pretty_num(IRA_x)}") |
| ut_y, t_y = self.IRA.remove(IRA_x) |
| x -= IRA_x |
| untaxed_y += ut_y |
| taxed_y += t_y |
| if x > 0: |
| if verbose: |
| print(f" CASH WITHDRAWL: {pretty_num(x)}") |
| untaxed_y += self.cash_disburse(x) |
|
|
| return untaxed_y, taxed_y |
|
|
| def increment_year(self, pay=1e3, spending=1e3, verbose=True): |
| if verbose: |
| print(f"PAY: {pretty_num(pay)}") |
| if verbose: |
| print(f"SPENDING: {pretty_num(spending)}") |
| self.age += 1 |
| year = self.age - self.init_age |
| deflate = (1.0 + INFLATION_RATE) ** (-year) |
| interest = self.savings.get_interest() |
| if verbose: |
| print(f"INTEREST: {pretty_num(interest)}") |
| msg = f"age:{self.age}\n" |
| msg += ( |
| f"Pay: {pretty_num(pay)} \t(t0 {pretty_num(pay*deflate)})\n" |
| ) |
| msg += ( |
| f"Spending: {pretty_num(spending)} \t(t0 {pretty_num(spending*deflate)})\n" |
| ) |
| msg += ( |
| f"Interest: {pretty_num(interest)} \t(t0 {pretty_num(interest*deflate)})\n" |
| ) |
| take_home = pay + interest |
|
|
| if self.age >= self.retirement_age: |
| self.retired = True |
| self.set_target_emergency_balance(spending * EMERGENCY_TO_SPENDING_FACTOR) |
| if self.checkings.balance > self.target_emergency_balance * 0.1: |
| checkings_x = self.checkings.balance - self.target_emergency_balance * 0.1 |
| self.checkings.remove(checkings_x) |
| take_home += checkings_x |
| if verbose: |
| print(f"EXTRA IN CHECKINGS: {pretty_num(checkings_x)}") |
| if self.savings.balance > self.target_emergency_balance * 0.9: |
| savings_x = self.savings.balance - self.target_emergency_balance * 0.9 |
| self.savings.remove(savings_x) |
| take_home += savings_x |
| if verbose: |
| print(f"EXTRA IN SAVINGS: {pretty_num(savings_x)}") |
|
|
| self.brokerage.increment_year() |
| self.i401k.increment_year(retired=self.retired) |
| self.IRA.increment_year(pay=pay) |
| self.tax_agent.inflate_tax_brackets() |
|
|
| estimated_tax = self.effective_tax_rate * max(spending, take_home) |
| estimated_tax = max(estimated_tax, self.tax_agent.combined_tax(take_home)) |
| estimated_tax *= 1.1 |
| if verbose: |
| print(f"ESTIMATED TAX BILL: {pretty_num(estimated_tax)}") |
|
|
| net_self_pay = 0.0 |
| self_pay = 0.0 |
| if take_home - estimated_tax < spending: |
| |
| withdrawl = 1.0 + self.effective_self_pay_tax_rate |
| withdrawl *= spending - take_home - estimated_tax |
| withdrawl *= 1.03 |
| if verbose: |
| print(f"WITHDRAWING: {pretty_num(withdrawl)}") |
| untaxable_disbursement, taxable_disbursement = self.disburse( |
| withdrawl, verbose |
| ) |
| untaxable_contribution = 0.0 |
| self_pay = taxable_disbursement + untaxable_disbursement |
| net_self_pay = self_pay |
| else: |
| withdrawl = 0.0 |
| untaxable_disbursement = 0.0 |
| taxable_disbursement = 0.0 |
| if verbose: |
| print( |
| f"CONTRIBUTING: {pretty_num(take_home - spending - estimated_tax)}" |
| ) |
| untaxable_contribution = self.contribute( |
| take_home - spending - estimated_tax, verbose |
| ) |
|
|
| tax = self.tax_agent.combined_tax( |
| pay + interest - untaxable_contribution, taxable_disbursement |
| ) |
| income_tax = self.tax_agent.combined_tax( |
| pay + interest - untaxable_contribution, 0.0 |
| ) |
| disbursement_tax = tax - income_tax |
| net_self_pay = self_pay - disbursement_tax |
| if disbursement_tax > 0: |
| if verbose: |
| print(f"DISBURSEMENT TAX: {pretty_num(disbursement_tax)}") |
| if verbose: |
| print(f"NET SELF PAY: {pretty_num(net_self_pay)}") |
|
|
| tax_return = estimated_tax - tax |
| if verbose: |
| print(f"FULL TAX BILL: {pretty_num(tax)}") |
| if tax_return < 0: |
| tax_return = abs(tax_return) |
| self.cash_disburse(tax_return) |
| if verbose: |
| print(f"TAX OWED: {pretty_num(tax_return)}") |
| else: |
| self.savings.add(0.9 * tax_return) |
| self.checkings.add(0.1 * tax_return) |
| if verbose: |
| print(f"TAX RETURNED: {pretty_num(tax_return)}") |
|
|
| if self_pay > 0: |
| self.effective_self_pay_tax_rate = disbursement_tax / (withdrawl + 1e-3) |
| effective_tax_rate = tax / (pay + interest + self_pay) |
| self.effective_tax_rate = effective_tax_rate |
|
|
| msg += f"Taxed: {pretty_num(tax)} \t(t0 {pretty_num(tax*deflate)})" |
| msg += f"\t[{round(100*effective_tax_rate, 1)}%]\n" |
| msg += f"Self-paid: {pretty_num(net_self_pay)} " |
| msg += f"\t(t0 {pretty_num(net_self_pay*deflate)})\n" |
| msg += f"Checkings: {pretty_num(self.checkings.balance)}" |
| msg += f" \t(t0 {pretty_num(self.checkings.balance*deflate)})\n" |
| msg += f"Savings: {pretty_num(self.savings.balance)}" |
| msg += f" \t(t0 {pretty_num(self.savings.balance*deflate)})\n" |
| msg += f"Brokerage: {pretty_num(self.brokerage.get_balance())}" |
| msg += f" \t(t0 {pretty_num(self.brokerage.get_balance()*deflate)})\n" |
| msg += f"401k: {pretty_num(self.i401k.get_balance())}" |
| msg += f" \t(t0 {pretty_num(self.i401k.get_balance()*deflate)})\n" |
| msg += f"IRA: {pretty_num(self.IRA.get_balance())}" |
| msg += f" \t(t0 {pretty_num(self.IRA.get_balance()*deflate)})\n" |
|
|
| msg += f"NET WORTH: {pretty_num(self.net_worth())} " |
| msg += f"\t(t0 {pretty_num(self.net_worth()*deflate)})\n\n" |
| return msg |
|
|
| def net_worth(self): |
| net_worth = self.checkings.balance |
| net_worth += self.savings.balance |
| net_worth += self.brokerage.get_balance() |
| net_worth += self.i401k.get_balance() |
| net_worth += self.IRA.get_balance() |
| return net_worth |
|
|
|
|
| if __name__ == "__main__": |
|
|
| p = portfolio() |
| p.increment_year(pay=1e6, spending=1e5) |
|
|