
Building a Simple Personal Finance App
A minimalist personal finance app built with Python and SQLite.
Background
It's all well and good to keep playing around with the newest technologies; make everything serverless, highly-available, and AI-driven. But sometimes a simple application of a couple really old tools is all you need to get a job done and it's always good to stay in practice with them too.
I'd been considering potential designs for a personal finance app. Thinking thoughts like: how do I secure automated exports from multiple financial institutions? How do I manage authentication for the database access? Can I do the whole thing serverlessly? After considering all this for a while, I came to thinking that for this a simplest form solution might actually be best. One where I import data manually, don't have to worry about auth because it's all local, and is portable to many other potential personal-data-storage-and-viewing-type applications.
The GitHub repository can be found here.
The Data Model
For starters, the two high level data models I created were CreditCard and Account. The distinction here is that an Account holds Holdings, with each Holding being some asset with a ticker symbol (including things like cash or crypto) and a CreditCard is just associated with a balance and a rewards balance. Both CreditCards and Accounts are snapshotted each month with AccountHoldings and CreditCardHoldings, recording the current date, the current quantity, and the current value. All of this is implemented in SQLAlchemy for compatibility with whatever backend we choose.
models.py
class Account(Base):
__tablename__ = "accounts"
name = Column(
String, nullable=False, primary_key=True
) # e.g., "My Brokerage Account"
type = Column(String, nullable=False) # e.g., brokerage, roth_ira, 403b
institution = Column(String, nullable=True) # e.g., Vanguard, Fidelity, Coinbase
holdings = relationship("AccountHolding", back_populates="account")
class Holding(Base):
__tablename__ = "holdings"
symbol = Column(String, nullable=False, primary_key=True) # e.g., AAPL, BTC
name = Column(String, nullable=True)
asset_type = Column(String, nullable=False) # e.g., Stock, ETF, Crypto, Cash
account_holdings = relationship("AccountHolding", back_populates="holding")
class AccountHolding(Base):
__tablename__ = "account_holdings"
__table_args__ = (
UniqueConstraint(
"account_id",
"holding_id",
"date",
"purchase_date",
name="uix_account_holding_date",
),
)
account_id = Column(
String, ForeignKey("accounts.name"), primary_key=True, nullable=False
)
holding_id = Column(
String, ForeignKey("holdings.symbol"), primary_key=True, nullable=False
)
date = Column(Date, primary_key=True, nullable=False)
purchase_date = Column(Date, primary_key=True, nullable=True)
quantity = Column(Float, nullable=False) # Number of shares/units
price = Column(Float, nullable=False) # Price per share/unit
purchase_price = Column(
Float, nullable=True
) # Price at which the asset was purchased
# Optional fields
percentage_cash = Column(Float, nullable=True) # Percentage of cash in the holding
percentage_bond = Column(Float, nullable=True) # Percentage of bond in the holding
percentage_large_cap = Column(
Float, nullable=True
) # Percentage of large cap stock in the holding
percentage_mid_cap = Column(
Float, nullable=True
) # Percentage of mid cap stock in the holding
percentage_small_cap = Column(
Float, nullable=True
) # Percentage of small cap stock in the holding
percentage_international = Column(
Float, nullable=True
) # Percentage of international stock in the holding
percentage_other = Column(
Float, nullable=True
) # Percentage of other assets in the holding
notes = Column(String, nullable=True) # Notes about the holding
account = relationship("Account", back_populates="holdings")
holding = relationship("Holding", back_populates="account_holdings")
@property
def total_value(self):
return self.quantity * self.price
@property
def gain_loss(self):
if self.purchase_price:
return (self.price - self.purchase_price) * self.quantity
return None
class CreditCard(Base):
__tablename__ = "credit_cards"
name = Column(String, nullable=False, primary_key=True) # e.g., "Chase Sapphire"
institution = Column(String, nullable=True) # e.g., Chase, American Express
card_type = Column(String, nullable=False) # e.g., Visa, Mastercard
holdings = relationship("CreditCardHolding", back_populates="credit_card")
class CreditCardHolding(Base):
__tablename__ = "credit_card_holdings"
credit_card_id = Column(
String, ForeignKey("credit_cards.name"), primary_key=True, nullable=False
)
date = Column(Date, primary_key=True, nullable=False)
balance = Column(Float, nullable=False) # Current balance
rewards = Column(Float, nullable=True) # Current rewards balance
credit_card = relationship("CreditCard", back_populates="holdings")
@property
def total_value(self):
return (self.rewards if self.rewards else 0) - self.balance
Ingest and Storage
Now that we have these data models, creating ingest and storage functions is a piece of cake. We can use pandas to read CSVs into DataFrames and then the built in to_sql() to load those DataFrames to tables. Behind the scenes, there's a bit more going on to setup a database connection, create the database, and process the CSVs to add things like the current date. You can check out how that works in the GitHub.
One of the keys to the simplicity of the whole project is SQLite, a widely used, lightweight database solution that lives directly on the filesystem. You can specify the connection to such a database with the URI sqlite:////path/to/db.sqlite and then interact with it as a file and as a database (a sort of file/database superposition).
Views
Now we have a database full of data and a set of models which provide us clean ways of querying it. It's time to start looking at our data. The first thing I wanted was a function that would provide a list of accounts and their values. We can make use of our SQLAlchemy models to query the database for all AccountHoldings in the month/year of interest and then sum their values per Account.
views.py
account_holdings = (
session.query(AccountHolding)
.filter(
extract("year", AccountHolding.date) == datetime.now().year,
extract("month", AccountHolding.date) == datetime.now().month,
)
.all()
)
accounts = [
(
a.name,
a.type,
a.institution,
round(
sum(
[
ah.total_value
for ah in account_holdings
if ah.account_id == a.name
]
),
2,
),
)
for a in accounts
]
With that we have our per-account valuation! Next we can create an easy net worth function by summing over the results of the accounts and then subtracting the net CreditCard balance. There are a million other things we might investigate down the line, portfolio distribution over different asset classes, net rate of growth, performance of different investments. But that should all be much easier now that we have this simple infrastructure in place to do it. Another addition to this collection might be a quick Flask site, giving us a quick and dirty method for displaying certain stats and graphs that keeps with the theme of running everything locally. I hope this can be useful for you, it certainly is for me! And remember that this framework can be applied to just about any application you want to build that involves data you want to keep track of and have easy access to!