Finances / portfolio.py
Darvin's picture
Create set_INIT_AGE function.
9d66444
Raw
History Blame Contribute Delete
20.5 kB
import numpy as np
import copy
###############################
# INFLATION RATE and INIT_AGE #
###############################
INFLATION_RATE = 0.03
INIT_AGE = 31
################
# UTILITY FUNC #
################
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
#############
# TAX STUFF #
#############
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#min(margin, 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):
# Federal treats gains and pay differently (insert political opinion here).
tax = marginal_tax(gain, self.federal_gains_brackets)
tax += marginal_tax(pay, self.federal_income_brackets)
# But New York treats them the same? Tax is so complicated.
tax += marginal_tax(pay + gain, self.state_income_brackets)
tax += marginal_tax(pay + gain, self.city_income_brackets)
return tax
#################
# BANK ACCOUNTS #
#################
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): # Gotta get a better savings rate...
super().__init__()
self.interest_rate = interest_rate
def get_interest(self):
return self.balance * (self.interest_rate)
#####################
# INVESTMENTS STUFF #
#####################
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
# Not a correct removal strategy
remove_from_principal = x * self.principal / total
remove_from_gains = x * self.gains / total
self.principal -= remove_from_principal
self.gains -= remove_from_gains
# First element is not gains taxed, the second is.
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, # With the megabackdoor, assuming size correlated with inflation
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)
# Remove pretax_x from taxed amount.
return pretax_x, roth_x
# Only allowed after retirement
def remove(self, x):
assert x <= self.get_balance()
# Remove from pretax first
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
# After retirement, cap raises to from $6k to $7k
if i - INIT_AGE == 65:
self.pretax_cap *= 6.0 / 7.0
self.total_cap *= 6.0 / 7.0
###########################
# PUTTING IT ALL TOGETHER #
###########################
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):
# Build up emergency savings slowly because we gamble against fat tails.
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):
# Removing in ratio to preserve emergency contribution strategy.
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 # fudge_factor
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:
# Overshoot to account for taxes:
withdrawl = 1.0 + self.effective_self_pay_tax_rate
withdrawl *= spending - take_home - estimated_tax
withdrawl *= 1.03 # fudge factor
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)