Modelling Personal Finance in Python

This is for people who want to understand how to make choices in personal finance and know how to program.  Normally, it's easy enough to use rules of thumb, and to just follow advice / well trodden paths in finance - but if you're inclined you can model things, and actually predict where you would be in a few years, dependent on choices! 

This post is not financial advice - do not make any real world decisions based on your models without consulting someone who knows what they're doing and can check what you've done. I am not responsible for any decisions you make or any errors in this code.

Most of the time you can predict how a single investment or debt will behave using Excel, but if you want to see how various choices could combine, it quickly becomes difficult to control, and so I decided to make python classes that behave as accounts, mortgages etc so I could model the questions I had.

First I created an account class, that I can use to model savings and investments. It should be fairly self explanatory, it allows transferring between accounts, and has a few extra methods important in evaluating for any goal, the ROI (return on investment) and passive income. Most people's goal is not simply to amass as much money as possible for no reason. Passive income is important, as the sum of passive income from all your assets is what you could potentially live off (although it's more complex, see Safe Withdrawal Rates).

# Simple class to model an account with transactions and interest
class Account:

    def __init__(self, balance, apr, name):
        self.balance = balance
        self.apr = apr
        self.name = name

    def applyMonthlyInterest(self):
        self.balance = self.balance + self.passive_income()

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def transfer(self, amount, to):
        self.withdraw(amount)
        to.deposit(amount)

    def passive_income(self):
        return (self.balance * self.apr / 1200)

    def roi(self):
        return self.apr

This class is about as simple as you get for the basic purpose - I subclass it for various accounts which have withdrawal / purchase fees (e.g. funds) and would do also if my accounts had any other weird features (e.g. monthly charges etc). The idea is that with the class format you can lay out the rules for how the account should behave, and you can now run simulations, by iterating through months / years and checking balances.

Say that you want to decide whether to pay off a student loan as a priority or not, you can run a few simulations and edit the behaviour to see what the outcome would be.

# Simulation 1 - pay off student loan with 1000 from wage,
# switch to Investment ISA after done

n_years = 15
account1 = Account(-30000, 1.5, 'Student Loan')
account2 = Account(0, 5, 'Investment ISA')
accounts = [account1, account2]

for y in range(n_years):
        for m in range(12):

            save = 1000
            loan_left = 0 - account1.balance
            into_student_loan = save if save < loan_left else loan_left
            into_isa = save - into_student_loan
            account1.deposit(into_student_loan)
            account2.deposit(into_isa)

            for account in accounts:
                account.applyMonthlyInterest()

print('Simulation 1')               
for account in accounts:  
        print(account.name, account.value())              

print('Total:', sum(map(lambda x: x.value(), accounts)))

# Simulation 2 - pay of interest only on student loan,
# and put rest of monthly savings in ISA

account1 = Account(-30000, 1.5, 'Student Loan')
account2 = Account(0, 5, 'Investment ISA')
accounts = [account1, account2]

for y in range(n_years):
        for m in range(12):

            save = 1000
            interest_to_pay =  0 - (account1.balance + 30000)
            account1.deposit(interest_to_pay)
            account2.deposit(save - interest_to_pay)

            for account in accounts:
                account.applyMonthlyInterest()

print('Simulation 2')               
for account in accounts:  
        print(account.name, account.value())              

print('Total:',sum(map(lambda x: x.value(), accounts)))

After 15 years, simulation 1 gives you a final balance of 20,7623, whereas simulation 2 gives you a final balance of 22,8379. However this account doesn't take into account the fact that I might not make 5% returns - I am not a financial advisor so I will not give you any advice there! There are other things to take into account, tax is a big one; I calculate my tax bill after carefully researching all the rules for allowances etc, I just add them to the simulation. Inflation is another one, I take it into account in my simulations - certain things inflate - costs, and the price of property - whereas balances do not - which is important when evaluating whether property is an investment. Obviously inflation rates are not guaranteed, so I run the simulation in best and worst case scenarios...

The other thing you can do asides from running a month by month strategy of putting cash here or there, is to have triggers at certain points - such as when you are saving for a house. Calculating everything by hand would be a nightmare. Here is a hypothetical situation, one is buying a house as soon as you have a 5% deposit, or waiting a certain amount of time to get a larger deposit and a better value mortgage. Here is a very simple class to deal with a house.

# Simple class to manage a house (rental or mortgage)
class House(Account):
    def __init__(self, balance, apr, name, monthlyExpenditure, borrowedAmount, investedAmount, yearsLeft):
        self.balance = balance
        self.apr = apr
        self.name = name
        self.monthlyExpenditure = monthlyExpenditure
        self.borrowedAmount = borrowedAmount
        self.investedAmount = investedAmount
        self.yearsLeft = yearsLeft

    def monthlyMortgageToPay(self):
        interest = self.borrowedAmount * (self.apr / 1200)
        if (self.yearsLeft > 0):
            repay = (self.borrowedAmount / self.yearsLeft) / 12
            return interest + repay
        else:
            return interest

    def doMonthlyUpdate(self):
        self.balance += self.passive_income()

        if (self.yearsLeft > 0):
            repay = (self.borrowedAmount / self.yearsLeft) / 12
            self.borrowedAmount -= repay
            self.investedAmount += repay
            self.yearsLeft -= (1/12)

    def value(self):
        return self.investedAmount + self.balance

    def passive_income(self):
        return 0 - self.monthlyExpenditure -self.monthlyMortgageToPay()

    def roi(self):
        return 1200 * self.passive_income() / self.investedAmount

In these models, we start off with a rental property (no loan but huge outgoing monthly expense) and wait for a trigger to purchase a property with a mortgage. I put the simulation in it's own function so I can call it with different parameters.

# Run a simple simulation of saving, and once reaching a threshold, purchasing
# a house, to see how well I would do over 5 years

def purchase_simulation(deposit_percentage, mortgage_rate, mortgage_cost):

    # Starting situation with cash savings and rental outgoing.
    cashISA = Account(16000,1.5,'Cash ISA')
    house = House(0,0,'Rental flat',1250,0,0,0)

    accounts = [cashISA]

    # Calculate price of house transaction - rough UK estimate
    # for properties under 250000
    house_price = 200000
    stamp_duty = (0.02 * (house_price - 125000))
    house_purchase_cost = 4000 + stamp_duty + mortgage_cost
    deposit = house_price * deposit_percentage / 100
    saving_threshold = house_purchase_cost + deposit
    print('Saving threshold:',saving_threshold)

    bought_house = False
    n_years = 5

    for y in range(n_years):
        for m in range(12):

            # 1500 from wage, which is spent on living costs / savings.
            save = 1500
            cashISA.deposit(save)

            for account in accounts:
                account.applyMonthlyInterest()

            # Use the cash ISA to balance the house account
            house.doMonthlyUpdate()
            house.transfer(house.balance, cashISA)

            # Purchase the house immediately after saving enough
            if not bought_house and cashISA.balance >= saving_threshold:
                cashISA.withdraw(saving_threshold)
                house = House(0,mortgage_rate,'House',100,house_price - deposit, deposit, 25)
                bought_house = True
                print('Bought house year:', y + (m/12))

    print('Total assets:', sum(map(lambda x: x.balance, accounts)) + house.investedAmount)

# One simulation is based on saving a 5% deposit
print('Simulation - 5% deposit')    
purchase_simulation(5, 2.97, 1000)

# The other simulation is based on saving a 10% deposit, which gets you a better rate
print('Simulation - 10% deposit') 
purchase_simulation(10, 2.24, 1000)

I find that in the second simulation, not only does it take three more years to save, but that I am actually way worse off after 5 years:

Simulation - 5% deposit
Saving threshold: 16500.0
Bought house year: 0.08333333333333333
Total assets: 67365.21932369476
Simulation - 10% deposit
Saving threshold: 26500.0
Bought house year: 3.0833333333333335
Total assets: 43921.627199638795

I think I can say in this case that buying the house sooner is the better strategy.

My recommendation if you decide to do your own models: Use verbose code - you do not want to make any mistakes - spell everything out and double check everything!