VOOZH about

URL: https://dev.to/soytuber/01-building-a-personal-alm-system-your-life-as-a-database-schema-4lmo

⇱ [01] Building a Personal ALM System — Your Life as a Database Schema - DEV Community


What Is ALM and Why Should You Care?

ALM — Asset-Liability Management — is how banks avoid going bankrupt. They model every asset (loans they've made, securities they hold) alongside every liability (deposits, bonds they've issued) and stress-test the relationship between them.

Your personal finances have the same structure. You just never drew the diagram.

YOU, INC.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ASSETS LIABILITIES
├─ Stocks (portfolio) ├─ Margin loan
├─ Cash reserves ├─ Credit lines
├─ Real estate ├─ Monthly expenses (recurring)
├─ Future dividends ├─ Taxes (deferred)
└─ Labor income (opt.) └─ Inflation (hidden)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 NET WORTH = Δ

Most people track the left side. This series tracks both.


The Schema

We start with SQLite — no servers, no dependencies, one file forever. This is the database that every subsequent article in this series will read from.

# alm_schema.py
import sqlite3
from datetime import date

DB_PATH = "life_alm.db"

SCHEMA = """
-- ASSETS: What you own
CREATE TABLE IF NOT EXISTS holdings (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 ticker TEXT NOT NULL,
 name TEXT,
 shares INTEGER NOT NULL,
 cost_basis REAL, -- total cost, not per-share
 asset_class TEXT DEFAULT 'equity',
 added_at TEXT DEFAULT (date('now'))
);

-- Market prices (daily snapshot)
CREATE TABLE IF NOT EXISTS prices (
 ticker TEXT NOT NULL,
 price_date TEXT NOT NULL,
 close_price REAL NOT NULL,
 PRIMARY KEY (ticker, price_date)
);

-- Dividend history and projections
CREATE TABLE IF NOT EXISTS dividends (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 ticker TEXT NOT NULL,
 ex_date TEXT,
 pay_date TEXT,
 amount_per_share REAL NOT NULL,
 is_projection INTEGER DEFAULT 0 -- 1 = forecast, 0 = actual
);

-- LIABILITIES: What you owe
CREATE TABLE IF NOT EXISTS loans (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL, -- 'securities_margin', 'credit_line', etc.
 balance REAL NOT NULL,
 rate REAL NOT NULL, -- annual interest rate (0.02 = 2%)
 collateral_type TEXT, -- 'portfolio', 'real_estate', NULL (unsecured)
 margin_call_pct REAL, -- ratio that triggers margin call (e.g., 0.70)
 forced_liq_pct REAL, -- ratio that triggers forced liquidation (e.g., 0.85)
 freeze_pct REAL, -- ratio that freezes new borrowing (e.g., 0.60)
 updated_at TEXT DEFAULT (date('now'))
);

-- BURN RATE: Recurring liabilities
CREATE TABLE IF NOT EXISTS expenses (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 category TEXT NOT NULL, -- 'housing', 'food', 'insurance', 'tax', etc.
 monthly_amount REAL NOT NULL,
 is_fixed INTEGER DEFAULT 1, -- 1 = fixed, 0 = variable
 notes TEXT
);

-- INCOME: Active earnings (optional/side-FIRE)
CREATE TABLE IF NOT EXISTS income (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 source TEXT NOT NULL, -- 'ai_consulting', 'spouse', etc.
 monthly_amount REAL NOT NULL,
 is_guaranteed INTEGER DEFAULT 0,
 notes TEXT
);

-- SNAPSHOTS: Daily balance sheet
CREATE TABLE IF NOT EXISTS snapshots (
 snapshot_date TEXT PRIMARY KEY,
 total_assets REAL,
 total_liabilities REAL,
 net_worth REAL,
 margin_ratio REAL, -- total_loan_balance / portfolio_value
 monthly_cashflow REAL,
 fire_ratio REAL -- passive_income / expenses
);
"""

def init_db():
 conn = sqlite3.connect(DB_PATH)
 conn.executescript(SCHEMA)
 conn.close()
 print(f"Database initialized: {DB_PATH}")

if __name__ == "__main__":
 init_db()

Run it once. You now have a financial database.

$ python alm_schema.py
Database initialized: life_alm.db

Loading Your Data

Here's a loader that populates the database with your actual positions. Replace the numbers with your own.

# load_portfolio.py
import sqlite3
from alm_schema import DB_PATH

def load_sample_data():
 conn = sqlite3.connect(DB_PATH)
 c = conn.cursor()

 # === HOLDINGS ===
 holdings = [
 ("2674", "Hard Off Corp", 15000, 22_500_000, "equity"),
 ("8291", "Nissan Tokyo HD", 50000, 20_000_000, "equity"),
 ("5869", "Waseda Gakken", 20000, 18_000_000, "equity"),
 ("2411", "Gendai Agency", 50000, 15_000_000, "equity"),
 ("7201", "Nissan Motor", 50000, 12_000_000, "equity"),
 ]
 c.executemany(
 "INSERT INTO holdings (ticker, name, shares, cost_basis, asset_class) VALUES (?,?,?,?,?)",
 holdings
 )

 # === CURRENT PRICES ===
 prices = [
 ("2674", "2026-04-08", 2057.0),
 ("8291", "2026-04-08", 515.0),
 ("5869", "2026-04-08", 1326.0),
 ("2411", "2026-04-08", 475.0),
 ("7201", "2026-04-08", 365.2),
 ]
 c.executemany(
 "INSERT INTO prices (ticker, price_date, close_price) VALUES (?,?,?)",
 prices
 )

 # === LOANS ===
 loans = [
 ("securities_margin", 50_000_000, 0.02, "portfolio", 0.70, 0.85, 0.60),
 ("consumer_credit", 8_000_000, 0.02, None, None, None, None),
 ]
 c.executemany(
 """INSERT INTO loans (name, balance, rate, collateral_type,
 margin_call_pct, forced_liq_pct, freeze_pct) VALUES (?,?,?,?,?,?,?)""",
 loans
 )

 # === MONTHLY EXPENSES ===
 expenses = [
 ("housing", 30_000, 1, "Mortgage paid off — maintenance only"),
 ("food", 100_000, 0, None),
 ("insurance", 50_000, 1, None),
 ("utilities", 30_000, 1, None),
 ("transport", 40_000, 0, None),
 ("education", 100_000, 1, "Children"),
 ("misc", 150_000, 0, None),
 ("tax_social", 300_000, 1, "Income tax + social insurance"),
 ]
 c.executemany(
 "INSERT INTO expenses (category, monthly_amount, is_fixed, notes) VALUES (?,?,?,?)",
 expenses
 )

 # === INCOME ===
 income = [
 ("ai_consulting", 1_000_000, 0, "Side-FIRE: work by choice"),
 ("spouse", 200_000, 1, None),
 ]
 c.executemany(
 "INSERT INTO income (source, monthly_amount, is_guaranteed, notes) VALUES (?,?,?,?)",
 income
 )

 conn.commit()
 conn.close()
 print("Portfolio data loaded.")

if __name__ == "__main__":
 load_sample_data()

The Balance Sheet Query

Now the payoff — a single query that gives you your complete financial picture:

# balance_sheet.py
import sqlite3
from alm_schema import DB_PATH

def get_balance_sheet(price_date="2026-04-08"):
 conn = sqlite3.connect(DB_PATH)
 c = conn.cursor()

 # Total portfolio value
 c.execute("""
 SELECT SUM(h.shares * p.close_price)
 FROM holdings h
 JOIN prices p ON h.ticker = p.ticker
 WHERE p.price_date = ?
 """, (price_date,))
 portfolio_value = c.fetchone()[0] or 0

 # Cash reserves (simplified — you'd track this separately)
 cash = 4_500_000 # or query from a cash table

 # Total assets
 total_assets = portfolio_value + cash

 # Total loan balances
 c.execute("SELECT SUM(balance) FROM loans")
 total_loans = c.fetchone()[0] or 0

 # Monthly expenses
 c.execute("SELECT SUM(monthly_amount) FROM expenses")
 monthly_expenses = c.fetchone()[0] or 0

 # Monthly income
 c.execute("SELECT SUM(monthly_amount) FROM income")
 monthly_income = c.fetchone()[0] or 0

 # Annual dividends (projected)
 c.execute("""
 SELECT SUM(d.amount_per_share * h.shares)
 FROM dividends d
 JOIN holdings h ON d.ticker = h.ticker
 WHERE d.is_projection = 1
 """)
 annual_dividends = c.fetchone()[0] or 0

 # Derived metrics
 net_worth = total_assets - total_loans
 margin_ratio = total_loans / portfolio_value if portfolio_value > 0 else float('inf')
 monthly_cashflow = monthly_income - monthly_expenses
 monthly_passive = annual_dividends / 12
 fire_ratio = (annual_dividends + 0) / (monthly_expenses * 12) if monthly_expenses > 0 else 0

 # Margin loan specific
 c.execute("""
 SELECT balance, freeze_pct, margin_call_pct, forced_liq_pct
 FROM loans WHERE collateral_type = 'portfolio'""")
 margin_loan = c.fetchone()

 conn.close()

 report = f"""
╔══════════════════════════════════════════════════╗
║ PERSONAL BALANCE SHEET ║
║ {price_date} ║
╠══════════════════════════════════════════════════╣
║ ASSETS ║
║ Portfolio ¥{portfolio_value:>14,.0f} ║
║ Cash ¥{cash:>14,.0f} ║
║ Total Assets ¥{total_assets:>14,.0f} ║
╠══════════════════════════════════════════════════╣
║ LIABILITIES ║
║ Total Loans ¥{total_loans:>14,.0f} ║
║ Monthly Burn ¥{monthly_expenses:>14,.0f} ║
╠══════════════════════════════════════════════════╣
║ NET WORTH ¥{net_worth:>14,.0f} ║
╠══════════════════════════════════════════════════╣
║ KEY RATIOS ║
║ Margin Ratio {margin_ratio:>8.1%} ║
║ Monthly Cashflow ¥{monthly_cashflow:>14,.0f} ║
║ FIRE Ratio {fire_ratio:>8.1%} ║
╚══════════════════════════════════════════════════╝
"""
 print(report)

 # Margin safety analysis
 if margin_loan:
 bal, freeze, call, liq = margin_loan
 print("MARGIN SAFETY ANALYSIS")
 print("" * 50)
 for label, threshold in [("Freeze", freeze), ("Margin Call", call), ("Forced Liq", liq)]:
 if threshold:
 trigger_value = bal / threshold
 drop_pct = (1 - trigger_value / portfolio_value) * 100
 print(f"{label:.<20} ratio>{threshold:.0%} triggers at ¥{trigger_value:,.0f} ({drop_pct:+.1f}%)")

if __name__ == "__main__":
 get_balance_sheet()

Running this prints:

MARGIN SAFETY ANALYSIS
──────────────────────────────────────────────────
 Freeze.............. ratio>60% triggers at ¥83,333,333 (-33.4%)
 Margin Call......... ratio>70% triggers at ¥71,428,571 (-42.9%)
 Forced Liq.......... ratio>85% triggers at ¥58,823,529 (-53.0%)

One command. Complete picture. Every morning.


The Snapshot Recorder

Automate daily snapshots with cron. This builds the historical record that powers the trend analysis in later articles.

# snapshot.py
import sqlite3
from datetime import date
from alm_schema import DB_PATH

def record_snapshot():
 conn = sqlite3.connect(DB_PATH)
 c = conn.cursor()

 today = date.today().isoformat()

 # Get latest prices for each ticker
 c.execute("""
 SELECT SUM(h.shares * p.close_price)
 FROM holdings h
 JOIN prices p ON h.ticker = p.ticker
 WHERE p.price_date = (SELECT MAX(price_date) FROM prices)
 """)
 portfolio = c.fetchone()[0] or 0

 c.execute("SELECT SUM(balance) FROM loans")
 loans = c.fetchone()[0] or 0

 c.execute("SELECT SUM(monthly_amount) FROM income")
 income = c.fetchone()[0] or 0

 c.execute("SELECT SUM(monthly_amount) FROM expenses")
 expenses = c.fetchone()[0] or 0

 net_worth = portfolio - loans
 margin_ratio = loans / portfolio if portfolio > 0 else 0
 cashflow = income - expenses

 c.execute("""
 INSERT OR REPLACE INTO snapshots
 (snapshot_date, total_assets, total_liabilities, net_worth,
 margin_ratio, monthly_cashflow)
 VALUES (?, ?, ?, ?, ?, ?)
 """, (today, portfolio, loans, net_worth, margin_ratio, cashflow))

 conn.commit()
 conn.close()
 print(f"Snapshot recorded: {today} | NW: ¥{net_worth:,.0f} | Margin: {margin_ratio:.1%}")

if __name__ == "__main__":
 record_snapshot()

Add to cron:

# crontab -e
30 16 * * 1-5 cd /home/user/alm && python snapshot.py

Market closes at 15:00 JST. Snapshot at 16:30 after data settles.


Why SQLite?

You might wonder why not PostgreSQL, or a spreadsheet, or a SaaS tool.

SQLite is the only database that will still work in 20 years without maintenance. It's a single file. No server process. No authentication. No network dependency. Backup is cp. Migration is scp.

For a system that manages your life's finances, that property — zero operational burden — is not a nice-to-have. It's a requirement.

Your FIRE system should be the most boring piece of infrastructure you own. Boring means reliable. Reliable means you trust it. Trust means you act on it.


What We Built

  • A 7-table SQLite schema covering assets, liabilities, expenses, and income
  • A data loader for real portfolio positions
  • A balance sheet generator with margin safety analysis
  • A daily snapshot recorder for cron automation

This database is the foundation. Every article that follows reads from it.