Featured image of post DuckDB Personal Finance Automation: Build an Intelligent Financial Assistant with SQL

DuckDB Personal Finance Automation: Build an Intelligent Financial Assistant with SQL

Build a personal finance automation system with DuckDB — from bank statement import to expense categorization, budget monitoring, and one-click monthly reports with full Python + SQL code.

DuckDB Personal Finance Automation: Build an Intelligent Financial Assistant with SQL

The Problem: Where Did Your Money Go?

Many people earn a decent salary each month but have no idea where it all went by the end of the month. The traditional approaches each have significant pain points:

  • Excel manual entry: Tedious to enter every transaction, easy to abandon; hard to aggregate data across months
  • Budgeting apps: Your data lives on someone else’s server — privacy concerns; want to export raw data for custom analysis? Not happening
  • Bank apps: Data is siloed per bank, can’t get a unified view across accounts

DuckDB offers a fundamentally different approach: download your bank statements as CSV files locally, analyze everything with SQL, and keep all your data completely private. The entire pipeline from data import to report generation runs in seconds, powered by pure SQL.

Personal Finance Automation Architecture


Step 1: Import Bank Statement Data

Most banks in China support exporting transaction histories as CSV files. Taking China Merchants Bank as an example, the exported format typically includes: transaction date, transaction time, transaction amount, balance, counterparty account, and transaction description.

import duckdb
from pathlib import Path

# Connect to in-memory database (no installation needed)
con = duckdb.connect(':memory:')

# Read China Merchants Bank CSV export
# Assuming file path: ~/Downloads/CMB_202609.csv
cmb_csv = Path.home() / 'Downloads' / 'CMB_202609.csv'

con.execute(f"""
    CREATE TABLE cmb_transactions AS
    SELECT * FROM read_csv_auto('{cmb_csv}', 
        header=true, 
        columns={{
            '交易日期': 'VARCHAR',
            '交易时间': 'VARCHAR', 
            '交易金额': 'DECIMAL(12,2)',
            '余额': 'DECIMAL(12,2)',
            '对方账户': 'VARCHAR',
            '交易摘要': 'VARCHAR'
        }}
    )
""")

print(f"CMB transaction records: {con.execute('SELECT COUNT(*) FROM cmb_transactions').fetchone()[0]}")

💡 Pro tip: read_csv_auto auto-detects column types, but if the bank CSV format is irregular (mixed date formats), you can manually specify types via the columns parameter. Different banks have slightly different export formats, but DuckDB handles them all intelligently.

Merging Multiple Bank Accounts

If you have accounts at multiple banks (CMB, ICBC, Alipay, WeChat Pay), import each separately then UNION ALL them together:

-- Read ICBC transactions
CREATE TABLE icbc_transactions AS
SELECT * FROM read_csv_auto('~/Downloads/ICBC_202609.csv', header=true);

-- Read Alipay statements
CREATE TABLE alipay_transactions AS
SELECT * FROM read_csv_auto('~/Downloads/ALIPAY_202609.csv', header=true);

-- Merge all into one unified table
CREATE TABLE all_transactions AS
SELECT 'CMB' AS bank, * FROM cmb_transactions
UNION ALL
SELECT 'ICBC' AS bank, * FROM icbc_transactions
UNION ALL
SELECT 'Alipay' AS bank, * FROM alipay_transactions;

Step 2: Intelligent Expense Categorization

The biggest challenge with bank statements is that transaction descriptions only tell you “transferred out 50 yuan” but don’t tell you whether it was for “food” or “transportation”. We need to build a categorization rule engine:

-- Create category rules table
CREATE TABLE category_rules AS
SELECT * FROM (VALUES
    ('Food', '%takeout%'),
    ('Food', '%Meituan%'),
    ('Food', '%Ele.me%'),
    ('Food', '%McDonald%'),
    ('Food', '%KFC%'),
    ('Food', '%Starbucks%'),
    ('Transport', '%DiDi%'),
    ('Transport', '%subway%'),
    ('Transport', '%bus%'),
    ('Shopping', '%Taobao%'),
    ('Shopping', '%JD%'),
    ('Shopping', '%Pinduoduo%'),
    ('Entertainment', '%iQiyi%'),
    ('Entertainment', '%Netflix%'),
    ('Entertainment', '%movie%'),
    ('Housing', '%rent%'),
    ('Housing', '%property%'),
    ('Housing', '%utilities%'),
    ('Salary', '%salary%'),
    ('Salary', '%bonus%'),
    ('Investment', '%fund%'),
    ('Investment', '%stock%'),
    ('Transfer', '%transfer%'),
    ('Refund', '%refund%'),
    ('Refund', '%return%')
) AS t(category_keyword, pattern);

-- Classify each transaction
CREATE TABLE classified_transactions AS
SELECT 
    t.*,
    COALESCE(
        (SELECT r.category_keyword 
         FROM category_rules r 
         WHERE t.交易摘要 ILIKE r.pattern 
         LIMIT 1),
        'Other'
    ) AS category
FROM all_transactions t;

Expanding Category Rules

As you use the system, you can continuously add new rules. If you notice a restaurant is always misclassified, just add a rule:

-- Add new category rules
INSERT INTO category_rules (category_keyword, pattern) VALUES
    ('Food', '%Haidilao%'),
    ('Food', '%Xibei%'),
    ('Fitness', '%Keep%'),
    ('Fitness', '%gym%');

Step 3: Monthly Expense Analysis

With categorized data, you can generate reports across multiple dimensions:

-- Monthly spending by category
SELECT 
    category,
    SUM(ABS(交易金额)) AS total_spent,
    COUNT(*) AS transaction_count,
    AVG(ABS(交易金额)) AS avg_amount,
    ROUND(100.0 * SUM(ABS(交易金额)) / SUM(SUM(ABS(交易金额))) OVER(), 1) AS percentage
FROM classified_transactions
WHERE 交易金额 < 0
GROUP BY category
ORDER BY total_spent DESC;

Sample output:

categorytotal_spenttransaction_countavg_amountpercentage
Housing4500.0014500.0045.0
Food2800.508732.1928.0
Shopping1200.002352.1712.0
Transport680.004515.116.8
Entertainment450.001237.504.5
Other369.501524.633.7

Month-over-Month Comparison

-- Compare spending between consecutive months
WITH monthly_spending AS (
    SELECT 
        strftime(交易日期, '%Y-%m') AS month,
        category,
        SUM(ABS(交易金额)) AS total_spent
    FROM classified_transactions
    WHERE 交易金额 < 0
    GROUP BY strftime(交易日期, '%Y-%m'), category
),
with_change AS (
    SELECT *,
        LAG(total_spent) OVER (PARTITION BY category ORDER BY month) AS prev_month_spent,
        ROUND(total_spent - LAG(total_spent) OVER (PARTITION BY category ORDER BY month), 2) AS change_amount,
        ROUND(100.0 * (total_spent - LAG(total_spent) OVER (PARTITION BY category ORDER BY month)) 
              / NULLIF(LAG(total_spent) OVER (PARTITION BY category ORDER BY month), 0), 1) AS change_pct
    FROM monthly_spending
)
SELECT month, category, total_spent, change_amount, change_pct
FROM with_change
WHERE month = (SELECT MAX(month) FROM monthly_spending)
ORDER BY total_spent DESC;

Budget Monitoring: Are You Overspending?

-- Set monthly budgets and check for overspending
CREATE TABLE budgets AS
SELECT * FROM (VALUES
    ('Food', 3000.00),
    ('Transport', 800.00),
    ('Shopping', 1500.00),
    ('Entertainment', 500.00),
    ('Housing', 5000.00),
    ('Other', 500.00)
) AS t(category, budget_amount);

-- Check which categories exceeded budget
SELECT 
    b.category,
    b.budget_amount,
    COALESCE(s.total_spent, 0) AS actual_spent,
    b.budget_amount - COALESCE(s.total_spent, 0) AS remaining,
    CASE 
        WHEN COALESCE(s.total_spent, 0) > b.budget_amount THEN 'OVER BUDGET'
        WHEN COALESCE(s.total_spent, 0) > b.budget_amount * 0.8 THEN 'WARNING'
        ELSE 'OK'
    END AS status
FROM budgets b
LEFT JOIN (
    SELECT category, SUM(ABS(交易金额)) AS total_spent
    FROM classified_transactions
    WHERE 交易金额 < 0
    GROUP BY category
) s ON b.category = s.category
ORDER BY remaining ASC;

Step 4: Automated Monthly Financial Report

Integrate everything into a Python script for one-click report generation:

import duckdb
from pathlib import Path

def generate_monthly_report(year_month: str):
    """Generate monthly financial report"""
    con = duckdb.connect(':memory:')
    
    # 1. Import data
    con.execute(f"""
        CREATE TABLE all_transactions AS
        SELECT 'CMB' AS bank, * FROM read_csv_auto(
            '{Path.home()}/Downloads/CMB_{year_month}.csv', header=true)
        UNION ALL
        SELECT 'Alipay' AS bank, * FROM read_csv_auto(
            '{Path.home()}/Downloads/ALIPAY_{year_month}.csv', header=true);
    """)
    
    # 2. Classify transactions
    con.execute("""
        CREATE TABLE category_rules AS
        SELECT * FROM (VALUES
            ('Food', '%takeout%'), ('Food', '%Meituan%'),
            ('Transport', '%DiDi%'), ('Transport', '%subway%'),
            ('Shopping', '%Taobao%'), ('Shopping', '%JD%'),
            ('Entertainment', '%iQiyi%'), ('Entertainment', '%movie%'),
            ('Housing', '%rent%'), ('Housing', '%property%'),
            ('Salary', '%salary%'), ('Salary', '%bonus%'),
            ('Investment', '%fund%'), ('Investment', '%stock%'),
            ('Refund', '%refund%')
        ) AS t(category_keyword, pattern);
    """)
    
    con.execute("""
        CREATE TABLE classified AS
        SELECT t.*, COALESCE(
            (SELECT r.category_keyword FROM category_rules r 
             WHERE t.交易摘要 ILIKE r.pattern LIMIT 1), 'Other'
        ) AS category
        FROM all_transactions t;
    """)
    
    # 3. Generate report
    print(f"{'='*50}")
    print(f"📊 {year_month} Monthly Financial Report")
    print(f"{'='*50}")
    
    overview = con.execute("""
        SELECT 
            SUM(CASE WHEN 交易金额 >= 0 THEN 交易金额 ELSE 0 END) AS income,
            SUM(CASE WHEN 交易金额 < 0 THEN ABS(交易金额) ELSE 0 END) AS expense,
            SUM(交易金额) AS net_savings
        FROM classified
    ").fetchone()
    
    print(f"\n💰 Income: ${overview[0]:,.2f}")
    print(f"💸 Expenses: ${overview[1]:,.2f}")
    print(f"📈 Net Savings: ${overview[2]:,.2f} ({overview[2]/overview[0]*100:.1f}%)")
    
    print(f"\n📋 Top 5 Spending Categories:")
    top_spending = con.execute("""
        SELECT category, SUM(ABS(交易金额)) AS total
        FROM classified WHERE 交易金额 < 0
        GROUP BY category ORDER BY total DESC LIMIT 5
    ").fetchall()
    for i, (cat, amt) in enumerate(top_spending, 1):
        print(f"  {i}. {cat}: ${amt:,.2f}")
    
    print(f"\n🏪 Top 5 Frequent Merchants:")
    top_merchants = con.execute("""
        SELECT 交易摘要, COUNT(*) AS cnt, SUM(ABS(交易金额)) AS total
        FROM classified WHERE 交易金额 < 0
        GROUP BY 交易摘要 ORDER BY cnt DESC LIMIT 5
    ").fetchall()
    for i, (merch, cnt, total) in enumerate(top_merchants, 1):
        print(f"  {i}. {merch}: {cnt} times, ${total:,.2f}")
    
    print(f"\n{'='*50}")
    con.close()

# Usage
generate_monthly_report('202609')

Expected output:

==================================================
📊 202609 Monthly Financial Report
==================================================

💰 Income: $15,000.00
💸 Expenses: $9,900.00
📈 Net Savings: $5,100.00 (34.0%)

📋 Top 5 Spending Categories:
  1. Housing: $4,500.00
  2. Food: $2,800.50
  3. Shopping: $1,200.00
  4. Transport: $680.00
  5. Entertainment: $450.00

🏪 Top 5 Frequent Merchants:
  1. Meituan: 32 times, $1,280.00
  2. DiDi: 18 times, $540.00
  3. Taobao: 8 times, $890.00
  4. Ele.me: 15 times, $620.00
  5. JD.com: 5 times, $310.00

==================================================

Comparison: Traditional Tools vs DuckDB

DimensionExcel Manual EntryBudgeting AppsDuckDB Automation
Data ImportManual entry or copy-pasteManual entryOne-click CSV import
Multi-account AggregationSwitch between multiple sheetsSingle account onlyUNION ALL merge
Expense CategorizationManual tagging per transactionApp auto-categorizes (not customizable)SQL rule engine, fully extensible
Privacy & SecurityLocal files, relatively secureData uploaded to cloud, privacy riskCompletely local, data never leaves your machine
Analysis FlexibilityLimited by Excel featuresOnly charts the app providesArbitrary SQL analysis
Automation LevelLow, manual operations requiredMedium, but limited analysisHigh, one-script report generation
Learning CurveLowZeroModerate (basic SQL sufficient)
CostFreeSome features paidCompletely free

Why DuckDB?

  1. Zero installation: pip install duckdb and you’re ready — no database server needed
  2. Blazing fast: Columnar storage + vectorized execution, million-row statements analyzed in seconds
  3. Local-first: All data stays local, no network transmission, complete privacy
  4. SQL-native: No new APIs to learn — standard SQL handles 90% of analysis needs
  5. Highly extensible: Easily scales from monthly analysis to yearly trends and predictive modeling

💰 Monetization Strategies

Productize this personal finance system with several monetization paths:

Low Barrier (Free/Low-cost startup)

  • Template sales: Package categorization rules and SQL templates as a “Finance Analysis Template Pack” on Gumroad or Xiaohongshu (¥19.9-49.9)
  • Tutorial monetization: Create a “DuckDB Personal Finance Automation” video series on Bilibili/YouTube, monetize through ads and paid courses
  • Paid consulting: Offer one-click “data import + classification setup” service for non-technical users, ¥100-300 per session

Medium Investment (Requires some development/operations)

  • SaaS dashboard: Build a web app with DuckDB + Streamlit where users upload bank CSVs and get instant visual reports, subscription model ¥29/month
  • Multi-user version: Support couples/family shared financial data with real-time synchronized analysis, annual fee ¥199/family
  • Enterprise edition: Provide expense analysis and team budget management SaaS for small businesses, ¥500-2000/month per client

High Investment (Requires team/funding)

  • AI financial advisor: Integrate LLMs for natural language Q&A (“How much did I spend on food this month? Is it more than last month?”), DuckDB handles queries, LLM explains results
  • Financial data platform: Aggregate multiple bank APIs (via OAuth), enable auto-sync and real-time analysis, compete with MoneyWiz and YNAB
  • White-label solutions: Provide white-label analysis platforms for financial advisors/planners, charge ¥50-200/user/month

Summary

DuckDB transforms personal finance management from “manual bookkeeping” to “automated intelligent analysis”. Each month, you only need to download your bank statements once, and a single SQL pipeline handles data import, intelligent categorization, and report generation. All your data stays local — privacy guaranteed, analysis unlimited.

Start today: import your bank CSV and use DuckDB to see exactly where your money went this month.

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy