Jeremy Lloyd

Tracking Personal Finances

Updated on 2026-05-16.

I made a small application to automatically categorise my expenses and keep me on top of my finances.

finance-detail

The Problem

I wanted an easy way to categorise my expenses, so I could quickly compare my actual spending with my budget.

The workflow I wanted was:

  1. I manually download transaction data from my bank.
  2. ????
  3. I receive a spreadsheet with the same transaction data categorised and summarised

My bank already applies its own categorisations online, but it’s unreliable and doesn’t map to the categories I wanted.

The Solution

A spreadsheet and a few formulas would have sufficed.

Instead, I’ve completely over-engineered a solution as a chance to practice my dev skills. I’ve implemented domain models to validate the raw transaction data from the bank export, save it to a sqlite database, and categorise them according to rules I define through a CLI interface.

It revolves around allowing us to create different kinds of CategorisationRule, which can automatically determine each transaction’s category from its description and amount. For example:

  • Description includes ’Anytime Fitness’ -> categorise as Health/Fitness
  • External transfers exceeding $5k -> categorise as Investment/Transfers
  • Multiple rules match: highest priority rule applies

How it works

First thing I do is save all my raw bank data to a folder:

Transaction data

I then run my application, and create a new categorisation rule for any unknown expenses:

Categorisation

Any rules I create are automatically applied to the remaining transactions on my list. Over time, the number of new rules I need to create decreases.

Each CategorisationRule I create looks like this:

class CategorisationRule(BaseModel, table=True):
    """Rule for categorising a transaction based on description and/or amount."""

    id: int | None = Field(primary_key=True, default=None)
    user_id: int | None = Field(foreign_key="user.id", default=None, ondelete="CASCADE")
    priority: int = 0
    description_contains: str | None = None
    amount_min: float | None = None
    amount_max: float | None = None
    transaction_id: int | None = None

    category_id: int | None = Field(foreign_key="category.id", ondelete="SET NULL")
    subcategory_id: int | None = Field(
        foreign_key="subcategory.id", ondelete="SET NULL"
    )

Features to highlight:

  • Rules are user-specific. Any rule created is applied to transactions from all of their bank accounts.
  • Rules have priorities. If one transaction matches multiple rules, we can control which one takes precedence.
  • Transactions below a certain amount are ignored. Coffees won’t move the needle, so I needn’t waste my time categorising them.

Once transactions are categorised, we save the categorised data into our transaction database and print the results for whatever resolution or time period we like:

Finance Summary

I can then visualise the data in Excel and share the results with my partner.

Next steps

This was a good chance to work on my skills in data modelling and managing database transactions safely with SQLAlchemy.

It’s a bit of a pain to use when I want to make changes though… currently, I need to override any mistakes by making manual edits to my database. A simple frontend to make these edits and print the results would make this much nicer to use.

Thanks for reading! I'd love to hear what you thought - shoot me a DM on LinkedIn.

© 2026 Jeremy Lloyd. All rights reserved.