Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions wellsfargo/net-worth-tracker/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# Generated SkillForge environment template for net-worth-tracker
# No required secrets for this skill.

# Optional override
WF_SERENDB_URL=
6 changes: 6 additions & 0 deletions wellsfargo/net-worth-tracker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
config.json
.env
artifacts/
__pycache__/
*.pyc
.pytest_cache/
65 changes: 51 additions & 14 deletions wellsfargo/net-worth-tracker/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
---
name: net-worth-tracker
description: "Track account balances from Wells Fargo statement data with optional manual asset and liability entries to produce a simplified balance sheet and net worth trajectory over time."
description: "Track net worth trends over time from Wells Fargo transaction data stored in SerenDB."
---

# Net Worth Tracker
# Wells Fargo Net Worth Tracker

## When to Use
## When To Use

- track my net worth over time
- generate balance sheet from wells fargo data
- show net worth trajectory
- Track net worth trends from cumulative transaction balances over time.
- Compute monthly inflow/outflow totals and running balance.
- Visualize net worth trajectory across configurable periods.
- Persist net worth snapshots into SerenDB for trend analysis.

## Workflow Summary
## Prerequisites

1. `resolve_serendb` uses `connector.serendb.connect`
2. `query_balances` uses `connector.serendb.query`
3. `load_manual_entries` uses `transform.load_manual_entries`
4. `compute_balance_sheet` uses `transform.compute_balance_sheet`
5. `compute_net_worth_trajectory` uses `transform.compute_net_worth_trajectory`
6. `render_report` uses `transform.render`
7. `persist_networth_data` uses `connector.serendb.upsert`
- The `bank-statement-processing` skill must have completed at least one successful run with SerenDB sync enabled.
- SerenDB must contain populated `wf_transactions` and `wf_txn_categories` tables.

## Safety Profile

- Read-only against SerenDB source tables.
- Writes only to dedicated `wf_networth_*` tables (never modifies upstream data).
- No browser automation required.
- No credentials stored or transmitted.

## Quick Start

```bash
cd wellsfargo/net-worth-tracker
python3 -m pip install -r requirements.txt
cp .env.example .env && cp config.example.json config.json
python3 scripts/run.py --config config.json --months 12 --out artifacts/net-worth-tracker
```

## Commands

```bash
python3 scripts/run.py --config config.json --months 12 --out artifacts/net-worth-tracker
python3 scripts/run.py --config config.json --start 2025-01-01 --end 2025-12-31 --out artifacts/net-worth-tracker
python3 scripts/run.py --config config.json --months 12 --skip-persist --out artifacts/net-worth-tracker
```

## Outputs

- Markdown report: `artifacts/net-worth-tracker/reports/<run_id>.md`
- JSON report: `artifacts/net-worth-tracker/reports/<run_id>.json`
- Monthly export: `artifacts/net-worth-tracker/exports/<run_id>.monthly.jsonl`

## SerenDB Tables

- `wf_networth_runs` - net worth tracking runs
- `wf_networth_monthly` - monthly inflow/outflow/balance per run
- `wf_networth_snapshots` - summary snapshot per run

## Reusable Views

- `v_wf_networth_latest` - most recent net worth snapshot
- `v_wf_networth_trend` - monthly net worth trend from latest run
25 changes: 13 additions & 12 deletions wellsfargo/net-worth-tracker/config.example.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"connectors": [
"serendb"
],
"dry_run": false,
"inputs": {
"end": "",
"manual_entries_file": "config/manual_entries.json",
"months": 12,
"out": "artifacts/net-worth-tracker",
"skip_persist": false,
"start": ""
"runtime": { "artifacts_subdir": "net-worth-tracker" },
"serendb": {
"enabled": true,
"database_url_env": "WF_SERENDB_URL",
"auto_resolve_via_seren_cli": true,
"pooled_connection": true,
"project_id": "",
"branch_id": "",
"schema_path": "sql/schema.sql",
"project_name": "",
"branch_name": "",
"database_name": "serendb"
},
"skill": "net-worth-tracker"
"starting_balance": 0.0
}
2 changes: 2 additions & 0 deletions wellsfargo/net-worth-tracker/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
psycopg[binary]>=3.2.0
python-dateutil>=2.9.0
142 changes: 142 additions & 0 deletions wellsfargo/net-worth-tracker/scripts/networth_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Pure-logic net worth builder (no DB dependencies)."""
from __future__ import annotations

from collections import defaultdict
from datetime import date
from typing import Any


def compute_monthly_totals(transactions: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Group transactions by month and compute inflows, outflows, net, and txn_count.

Returns a sorted list of dicts:
{month_start, inflows, outflows, net, txn_count}
"""
buckets: dict[str, dict[str, Any]] = defaultdict(
lambda: {"inflows": 0.0, "outflows": 0.0, "txn_count": 0}
)

for txn in transactions:
txn_date = txn.get("txn_date")
if isinstance(txn_date, str):
txn_date = date.fromisoformat(txn_date)
month_key = txn_date.replace(day=1).isoformat()
amount = float(txn.get("amount", 0))
if amount >= 0:
buckets[month_key]["inflows"] = round(buckets[month_key]["inflows"] + amount, 2)
else:
buckets[month_key]["outflows"] = round(buckets[month_key]["outflows"] + amount, 2)
buckets[month_key]["txn_count"] += 1

result: list[dict[str, Any]] = []
for month_key in sorted(buckets):
b = buckets[month_key]
net = round(b["inflows"] + b["outflows"], 2)
result.append(
{
"month_start": month_key,
"inflows": b["inflows"],
"outflows": b["outflows"],
"net": net,
"txn_count": b["txn_count"],
}
)
return result


def compute_running_balance(
monthly_totals: list[dict[str, Any]],
starting_balance: float = 0.0,
) -> list[dict[str, Any]]:
"""Add a running_balance field to each month dict (cumulative from starting_balance)."""
balance = starting_balance
for month in monthly_totals:
balance = round(balance + month["net"], 2)
month["running_balance"] = balance
return monthly_totals


def build_networth_summary(
transactions: list[dict[str, Any]],
starting_balance: float = 0.0,
) -> dict[str, Any]:
"""Build a complete net worth summary from transactions.

Returns a dict with:
monthly: list of monthly dicts (with running_balance)
total_inflows, total_outflows, net_change, ending_balance, starting_balance
"""
monthly = compute_monthly_totals(transactions)
monthly = compute_running_balance(monthly, starting_balance)

total_inflows = round(sum(m["inflows"] for m in monthly), 2)
total_outflows = round(sum(m["outflows"] for m in monthly), 2)
net_change = round(total_inflows + total_outflows, 2)
ending_balance = monthly[-1]["running_balance"] if monthly else starting_balance

return {
"monthly": monthly,
"total_inflows": total_inflows,
"total_outflows": total_outflows,
"net_change": net_change,
"starting_balance": starting_balance,
"ending_balance": ending_balance,
}


def render_markdown(
summary: dict[str, Any],
period_start: date,
period_end: date,
run_id: str,
txn_count: int,
) -> str:
"""Render a net worth report as Markdown."""
lines: list[str] = []
lines.append("# Wells Fargo Net Worth Report")
lines.append("")
lines.append(f"**Period:** {period_start.isoformat()} to {period_end.isoformat()}")
lines.append(f"**Run ID:** {run_id}")
lines.append(f"**Transactions analyzed:** {txn_count}")
lines.append(f"**Starting Balance:** ${summary['starting_balance']:,.2f}")
lines.append("")

lines.append("## Monthly Breakdown")
lines.append("")
lines.append("| Month | Inflows | Outflows | Net | Running Balance | Txns |")
lines.append("|-------|--------:|---------:|----:|----------------:|-----:|")
for m in summary["monthly"]:
lines.append(
f"| {m['month_start']} "
f"| ${m['inflows']:,.2f} "
f"| ${m['outflows']:,.2f} "
f"| ${m['net']:,.2f} "
f"| ${m['running_balance']:,.2f} "
f"| {m['txn_count']} |"
)
lines.append("")

lines.append("## Summary")
lines.append("")
lines.append("| | Amount |")
lines.append("|--|-------:|")
lines.append(f"| Starting Balance | ${summary['starting_balance']:,.2f} |")
lines.append(f"| Total Inflows | ${summary['total_inflows']:,.2f} |")
lines.append(f"| Total Outflows | ${summary['total_outflows']:,.2f} |")
lines.append(f"| Net Change | ${summary['net_change']:,.2f} |")
lines.append(f"| **Ending Balance** | **${summary['ending_balance']:,.2f}** |")
lines.append("")

lines.append("## Net Worth Trend")
lines.append("")
if summary["monthly"]:
max_balance = max(m["running_balance"] for m in summary["monthly"])
min_balance = min(m["running_balance"] for m in summary["monthly"])
lines.append(f"- **Peak:** ${max_balance:,.2f}")
lines.append(f"- **Trough:** ${min_balance:,.2f}")
lines.append(f"- **Final:** ${summary['ending_balance']:,.2f}")
else:
lines.append("No data for the selected period.")
lines.append("")

return "\n".join(lines)
Loading