Files
finplan/calc.py

387 lines
18 KiB
Python

# calc.py
from datetime import datetime, timedelta
from defines import END_YEAR
# GLOBAL CONSTANTS
LEASE = 0
# Dates that don't change
car_balloon_date = datetime(2026, 11, 15)
new_fin_year_25 = datetime(2025, 7, 1)
new_fin_year_26 = datetime(2026, 7, 1)
end_date = datetime(END_YEAR, 4, 15)
school_fees_date = datetime(2025, 12, 5)
mich_present_date = datetime(2026,10,15)
first_pay_date = datetime(2025,1,8)
def bill_amount_today(finance, day, bill_data, bt_id_name, total ):
amt=0
day_str = day.strftime("%Y-%m-%d")
for b in bill_data:
# there may be more than one bill on this day, keep add amount and keep going in loop
if b['bill_date'] == day_str:
amt += b['amount']
if b['amount'] > 1000:
n=bt_id_name[ b['bill_type'] ]
print( f"bill_amt_today {n} for {day_str} has amt={amt}" )
add_annotation(finance, day, total-amt, amt, f"Pay {n}" )
# bills are desc order so if the bill is before the day we are after then stop looking
if b['bill_date'] < day_str:
return amt
#failsafe, doubt this even can occur with bills older than today
return amt
def add_annotation(finance, dt, total, delta, text):
# dont add an annotation for small changes (jic)
tm = dt.timestamp() * 1000
if delta > 0:
text += f": ${int(abs(delta))}"
else:
text += f": -${int(abs(delta))}"
finance['annotations'].append( { 'label': text, 'x': tm, 'y': total } )
return
def calculate_savings_depletion(finance, bill_data, bill_type):
# Extract all the financial data from the database
D_Salary = finance['D_Salary']
D_Num_fortnights_pay = finance['D_Num_fortnights_pay']
School_Fees = finance['School_Fees']
Car_loan_via_pay = finance['Car_loan_via_pay']
Car_loan = finance['Car_loan']
Car_balloon = finance['Car_balloon']
Car_buyout = finance['Car_buyout']
Living_Expenses = finance['Living_Expenses']
Savings = finance['Savings']
Interest_Rate = finance['Interest_Rate']
Inflation = finance['Inflation']
Mich_present = finance['Mich_present']
Overseas_trip = finance['Overseas_trip']
Mark_reno = finance['Mark_reno']
D_leave_owed_in_days = finance['D_leave_owed_in_days']
Sell_shares = finance['Sell_shares']
D_TLS_shares = finance['D_TLS_shares']
M_TLS_shares = finance['M_TLS_shares']
D_CBA_shares = finance['D_CBA_shares']
TLS_price = finance['TLS_price']
CBA_price = finance['CBA_price']
Ioniq6_future = finance['Ioniq6_future']
### COMPLEX tax implications with my leave I have not taken. It will be taxed in the year I 'quit' ###
# leave in days, 10 business days to a fortnight,
# paid before tax I earn $7830.42 / fortnight. Tax on that will be at 37% or $4933.16 after tax
# if we could stretch this to July 2026, then would be more (due to less tax)
bus_days_in_fortnight=10
# this is what I now earn before-tax (and I *THINK* vehicle allowance won't be paid X 12 weeks)
pre_tax_D_earning = 8143.65
# whenever I leave, I get 12 weeks (or 60 business days) + whatever leave they owe me
payout = ((60+D_leave_owed_in_days)/bus_days_in_fortnight) * pre_tax_D_earning
# just use redundancy calc...
payout = 83115.84
print( f"leave payout gross={payout}" )
# as the leave is just on top of my existing earnings and if in 2024 fin year, just take tax at 37% for the extra leave amount
# hardcoded 6 represents the 12 weeks or 6 fornights of pay owed to me when I give notice or they sack me
D_leave_after_tax = payout * (1-0.37)
# However, if I quit in the next fin year - tax for 2025 will be: $4,288 plus 30c for each $1 over $45,000
# (assuming the 7830.42 * ~90/bus_days_in_fortnight = ~ $64k - > 45k and < $135k bracket is 30%)
# Given, I probably can't stop Deakin doing PAYG deductions, I won't get
# the tax back until the end of the financial year, so work out the
# amount of tax I will get back info: tax_diff_D_leave
tax_on_leave = (payout - 45000)*.37 + 4288
D_leave_after_tax_new_fin_year = payout - tax_on_leave
# just use redunancy calc...
D_leave_after_tax_new_fin_year = 56518.77
tax_diff_D_leave = payout - D_leave_after_tax_new_fin_year
print( f"tax_diff_D_leave: {tax_diff_D_leave}")
### leave / tax items finished ###
# convenience vars to make it easier to read conditional leave tax/payment logic below
D_has_quit = False
D_quit_year = 0
claim_tax_on_leave = False
# Constants for interest calculations
annual_interest_rate = Interest_Rate / 100.0
daily_interest_rate = annual_interest_rate / 365
# main loop range -- start from now, and simulate till D is 60 (April 2031)
current_date = datetime.today()
# work out which bill_types relate to future bills
for bt in bill_type:
if 'Hyundai' in bt['name']:
if 'Car Ins' in bt['name']:
ioniq6_ins_bt = bt
if 'Car Rego' in bt['name']:
ioniq6_rego_bt = bt
if 'Health Ins' in bt['name']:
health_ins_bt = bt
if 'Phone' in bt['name'] and 'Damien' in bt['name']:
phone_d_bt = bt
# TODO: need to refactor Living_Expenses to exclude bills
total=0
yr=str(current_date.year)
for b in bill_data:
if b['bill_type'] == ioniq6_rego_bt['id']:
ioniq6_rego = b['amount']
if b['bill_type'] == ioniq6_ins_bt['id']:
ioniq6_ins = b['amount']
if b['bill_type'] == health_ins_bt['id']:
health_ins = b['amount']
if b['bill_type'] == phone_d_bt['id']:
phone_d = b['amount']
if yr in b['bill_date']:
total += b['amount']
print( f"this yr={current_date.year} - total={total} -- hi={health_ins}, phone_d={phone_d}" )
Living_Expenses -= total
print( f"LE is now={Living_Expenses}" )
# Calculate daily living expenses
daily_living_expenses = Living_Expenses / 365
# Start the calculation
current_savings = Savings
depletion_date = None
savings_per_fortnight = []
# significant dates - but who knows when? :)
overseas_trip_date = datetime.strptime( finance['Overseas_trip_date'], "%Y-%m-%d")
mark_reno_date = datetime.strptime( finance['Mark_reno_date'], "%Y-%m-%d")
car_buyout_date = datetime.strptime( finance['Car_buyout_date'], "%Y-%m-%d")
# to force deakin pay cycles to match reality, we work from the 8th of Jan as our "day-zero" so we are paid on the 8/1/25, 22/1/25, etc.
days_count = ( current_date - datetime(2025,1,1) ).days
# Track the fortnight, and monthly interest
fortnight_income = 0
monthly_interest = 0
# Create an empty dict to store annotations to display in the GUI
# (key is date, text is for larger spend items by hand)
finance['annotations']=[]
#quick convenience lookup of bill types name for annotations.
bt_id_name = {row["id"]: row["name"] for row in bill_type}
while current_date <= end_date:
#paid on 8th or 22nd of Jan (so 8th day of fortnight)
is_fortnight = (days_count % 14 == 7)
is_end_of_month = (current_date.day == 1)
# Subtract daily living expenses
current_savings -= daily_living_expenses
# if we have a bill for today, pay for it
current_savings -= bill_amount_today( finance, current_date, bill_data, bt_id_name, current_savings )
# KLUDGE for future bills for now:
# if I have quit, pay monthly future bills - assume 1st day of month for health ins
if D_has_quit and current_date.day == 1:
health_ins = health_ins * 1+((health_ins_bt['ann_growth_simple'])/100)*12
current_savings -= health_ins
# once a year from when I quit, pay the phone bill (year I quit comes below)
if D_has_quit and current_date.month == D_quit_date.month and current_date.day == D_quit_date.day:
amt=phone_d
for y in range( D_quit_date.year, current_date.year):
amt = amt * (1+phone_d_bt['ann_growth_simple']/100)
current_savings -= amt
# Calculate daily interest but apply at the end of the month
monthly_interest += current_savings * daily_interest_rate
# Apply fortnightly salary and handle car loan deduction
if is_fortnight:
if D_Num_fortnights_pay > 0:
fortnight_income += D_Salary
D_Num_fortnights_pay -= 1
# keep paying car off fortnightly until the end of the car loan (while I am still working) - once I quit, pay reverts to monthly on the 15th
if not D_has_quit and current_date < car_balloon_date:
current_savings -= Car_loan_via_pay
print( f"{current_date}: making car loan pay as pre-tax lease: ${Car_loan_via_pay}" )
if D_Num_fortnights_pay == 0 and D_leave_after_tax > 0:
D_has_quit = True
D_quit_date = current_date
D_quit_year = current_date.year
# okay, if we leave before Jun 30th 2024, then I pay full tax, otherwise I get 'extra', but have to await end of next fin year
if current_date > new_fin_year_25:
claim_tax_on_leave = True
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax_new_fin_year}" )
current_savings += D_leave_after_tax_new_fin_year
add_annotation(finance, current_date, current_savings, D_leave_after_tax_new_fin_year, "D quit" )
else:
claim_tax_on_leave = False
print(f"{current_date}: D has resigned - get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
current_savings += D_leave_after_tax
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
D_leave_after_tax = 0
# pay for 1st year of phone for Damien
current_savings -= phone_d
# its end of 'next' fin year, if tax_diff > 0, then ddp quit after new tax year and gets back the overpaid tax
if current_date > new_fin_year_26 and claim_tax_on_leave:
current_savings += tax_diff_D_leave
print( f"I quit last fin year, so now its 1st July {current_date.year}, get tax back of {tax_diff_D_leave}" )
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
# can only claim the tax back once :)
claim_tax_on_leave=False
if fortnight_income:
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
current_savings += fortnight_income
fortnight_income = 0 # reset for next fortnight
savings_per_fortnight.append((current_date.strftime("%Y-%m-%d"), round(current_savings, 2)))
# if I have quit, then car lease payments are made on the 15th of the month for full Car_loan
if D_has_quit and current_date.day == 15:
if Ioniq6_future == LEASE and current_date <= car_balloon_date:
current_savings -= Car_loan
print( f"{current_date}: making car loan pay (after quitting): ${Car_loan}" )
elif Ioniq6_future != LEASE and current_date <= car_buyout_date:
current_savings -= Car_loan
print( f"{current_date}: making car loan pay (after quitting): ${Car_loan}" )
if is_end_of_month:
current_savings += monthly_interest
#print(f"{current_date}: interest paid - ${monthly_interest}")
monthly_interest = 0
# monthly increase living expenses by a monthly inflation multiplier
Living_Expenses += (Inflation/100.0)/12 * Living_Expenses
daily_living_expenses = Living_Expenses / 365
#print(f"{current_date}: Living Exp inceased - ${Living_Expenses}")
if current_date.date() == school_fees_date.date():
current_savings -= School_Fees
add_annotation(finance, current_date, current_savings, -School_Fees, "School Fees")
if Ioniq6_future == LEASE and current_date.date() == car_balloon_date.date():
current_savings -= Car_balloon
add_annotation(finance, current_date, current_savings, -Car_balloon, "car balloon")
print(f"{current_date}: car balloon - ${Car_balloon}" )
if Ioniq6_future != LEASE and current_date.date() == car_buyout_date.date():
current_savings -= Car_buyout
add_annotation(finance, current_date, current_savings, -Car_buyout, "car buyout")
print(f"{current_date}: car buyout - ${Car_buyout}" )
# Anniversary of Car purchase/balloon so potentially insurance/rego
# when I quit, the if we haven't paid the car outright, then need to add rego, but not insurance
# if we pay-out the car, then add insurace and rego
if current_date.month == car_balloon_date.month and current_date.day == car_balloon_date.day:
# staying with the lease (0), if I have quit, then pay monthly rego only up to lease date, but full cost after car balloon date
if Ioniq6_future == LEASE:
if current_date.year >= car_balloon_date.year:
ins_amt=ioniq6_ins
for y in range( car_balloon_date.year, current_date.year):
ins_amt = ins_amt * (1+ioniq6_ins_bt['ann_growth_simple']/100)
rego_amt=ioniq6_rego
for y in range( car_balloon_date.year, current_date.year):
rego_amt = rego_amt * (1+ioniq6_rego_bt['ann_growth_simple']/100)
current_savings -= (ins_amt+rego_amt)
add_annotation(finance, current_date, current_savings, -(ins_amt+rego_amt), "IONIQ 6 ins/rego" )
# if we buy car outright, then as long as this anniversary is after buyout date, pay ins and rego
elif current_date.year >= car_buyout_date.year:
current_savings -= (ioniq6_ins + ioniq6_rego)
add_annotation(finance, current_date, current_savings, -(ioniq6_ins+ioniq6_rego), "IONIQ 6 ins/rego" )
# if current_date.date() == overseas_trip_date.date():
# current_savings -= Overseas_trip
# add_annotation(finance, current_date, current_savings, -Overseas_trip, "O/S trip")
if current_date.date() == mich_present_date.date():
current_savings -= Mich_present
add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" )
if current_date.date() == mark_reno_date.date():
current_savings -= Mark_reno
add_annotation(finance, current_date, current_savings, -Mark_reno, "Mark/reno" )
if current_savings < 0:
depletion_date = current_date
break
# twice a year, CBA has a dividend of $2-2.5 DRP gives back around 20ish shares twice a year, estimate this...
# and on average they exceed interest rate, but lets assume at least int.rate increase (remember its twice a year, so /2)
if current_date.day == 1 and (current_date.month == 4 or current_date.month == 10):
CBA_price = CBA_price+ CBA_price * (Interest_Rate/2)/100
drp = int( (2.25*D_CBA_shares/CBA_price) )
print( f"DRP {current_date} - adding {drp} CBA shares" )
D_CBA_shares += int( (2.25*D_CBA_shares/CBA_price) )
# if selling shares, and its 1st of July...
# BUT not if D quits before end of financial year - as I won't be able to sell CBA shares for no cap gains
# so wait until the following year
if current_date.month == 7 and current_date.day == 1 and D_has_quit and Sell_shares>0 and (current_date.year > D_quit_year or current_date.year == D_quit_year and claim_tax_on_leave == False):
# 2024 Govt. value
tax_threshold = 18200
# cap-gains is 50% of profit (lazy profit calc here, just assume its all profit)
can_sell = 2*tax_threshold
actual_sell = 0
# sell off TLS first - and they are way under the limit, so just sell them all in one hit
if D_TLS_shares > 0:
actual_sell += TLS_price*D_TLS_shares
D_TLS_shares = 0
if M_TLS_shares > 0:
actual_sell += TLS_price*M_TLS_shares
M_TLS_shares = 0
while actual_sell + CBA_price < can_sell and D_CBA_shares > 0:
actual_sell += CBA_price
D_CBA_shares -= 1
Sell_shares -= 1
current_savings += actual_sell
add_annotation(finance, current_date, current_savings, actual_sell, "Sell shares" )
current_date += timedelta(days=1)
days_count += 1
finance['CBA']=D_CBA_shares
finance['TLS']=D_TLS_shares+M_TLS_shares
return depletion_date, savings_per_fortnight, current_savings
################################################################################
# work out the date D quits and when we own the car, so we can then use it to
# handle future bills
################################################################################
def calc_key_dates( finance ):
key_dates={}
now = datetime.today()
# this will be 0 to 13 days - how far into this fortnights pay cycle are we now
days_in_pay_fortnight= ( now - first_pay_date ).days % 14
# add 1 less fortnight than we continue to work, then add rest of pay cycle (14-days_in_pay_fortnight)
key_dates['D_quit_date'] = (now+timedelta(weeks=2*(finance['D_Num_fortnights_pay']-1))+timedelta(days=(14-days_in_pay_fortnight))).strftime('%Y-%m-%d')
# use lease date
if finance['Ioniq6_future'] == LEASE:
key_dates['D_hyundai_owned'] = car_balloon_date.strftime('%Y-%m-%d')
# use buyout date
else:
key_dates['D_hyundai_owned'] = finance['Car_buyout_date']
print( f"kd={key_dates}" )
return key_dates