Files
fin/.claude/skills/fin-diagnostics-and-tooling/scripts/audit_holdings.py
T
EislM0203andClaude Opus 5 cb97d97d81 docs: check in the fin skill library, ignore local Claude settings
Twelve skills that had been sitting uncommitted in the working tree: the
architecture contract, accounting reference, change-control rulebook, failure
archaeology, debugging playbook, diagnostics scripts, ledger-analysis toolkit,
research frontier, run/operate runbook, build-and-env, test-coverage campaign,
and validation/QA guide.

Updates three frontier statuses to match what actually shipped today rather
than checking in a roadmap that describes closed work as open:

- A1 double-entry hardening -> partly-done. The create half is enforced; what
  remains is running the existing audit_linked_legs.py against a prod backup.
- B3 safety rails -> partly-done. Idempotency shipped, including the note that
  the index is not unique-when-not-null and why. dry_run is still open.
- D3 CSV import -> unblocked but still gated. Its stated blocker was B3's
  idempotency keys, which now exist.

Also gitignores .claude/settings.local.json, which is per-machine and was
showing up as untracked on every status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3qYLuu9jgXsCRXmJ18YGY
2026-08-22 22:09:22 +00:00

128 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Recompute every holding from stock_transactions and diff against holdings rows.
READ-ONLY. Usage: python3 audit_holdings.py /path/to/fin.db [--json]
Faithful replication of crud.recompute_holding_from_stock_transactions
(api/app/crud.py): iterate stock_transactions for each (account_id, ticker)
ordered by date ASC, id ASC;
buy / rsu_vest : shares += tx.shares; total_cost += tx.shares*price_per_share + fee
sell : error if tx.shares > shares held at that point;
total_cost -= tx.shares * (total_cost/shares); shares -= tx.shares;
total_cost = max(0, total_cost)
avg_cost_basis = total_cost/shares if shares > 0 else 0
currency = first transaction's currency
If no stock transactions remain, crud deletes the holding — so a holding row
with zero stock transactions is stale.
Tolerances: shares 1e-6, avg cost basis 0.005 (float accumulation).
Currency mismatch is a WARNING only (crud never updates currency on an
existing holding, so drift is possible without a bug).
Exit 0 = clean, 1 = anomalies found.
"""
import argparse
import json
import sqlite3
import sys
SHARES_EPS = 1e-6
COST_EPS = 0.005
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("db")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
con = sqlite3.connect(f"file:{args.db}?mode=ro", uri=True)
con.row_factory = sqlite3.Row
txns_by_key: dict[tuple, list] = {}
for r in con.execute(
"SELECT id, account_id, ticker, type, shares, price_per_share, fee, "
"date, currency FROM stock_transactions ORDER BY date ASC, id ASC"):
txns_by_key.setdefault((r["account_id"], r["ticker"]), []).append(dict(r))
holdings = {(r["account_id"], r["ticker"]): dict(r) for r in con.execute(
"SELECT id, account_id, ticker, shares, avg_cost_basis_per_share, currency "
"FROM holdings")}
anomalies: list[dict] = []
warnings: list[str] = []
def flag(check, key, detail):
anomalies.append({"check": check, "account_id": key[0],
"ticker": key[1], "detail": detail})
for key, txns in txns_by_key.items():
shares = 0.0
total_cost = 0.0
currency = txns[0]["currency"]
invalid = None
for tx in txns:
t = (tx["type"] or "").lower()
if t in ("buy", "rsu_vest"):
shares += tx["shares"]
total_cost += tx["shares"] * tx["price_per_share"] + tx["fee"]
elif t == "sell":
if tx["shares"] > shares:
invalid = (f"stock_txn {tx['id']} sells {tx['shares']} but only "
f"{round(shares, 6)} held at that point in history")
break
if shares > 0:
total_cost -= tx["shares"] * (total_cost / shares)
shares -= tx["shares"]
total_cost = max(0.0, total_cost)
else:
flag("unknown-stock-txn-type", key,
f"stock_txn {tx['id']} has type {tx['type']!r} "
f"(crud silently ignores it)")
if invalid:
flag("invalid-history", key, invalid)
continue
avg = (total_cost / shares) if shares > 0 else 0.0
h = holdings.get(key)
if h is None:
flag("missing-holding", key,
f"{len(txns)} stock txns imply shares={round(shares, 6)}, "
f"avg={round(avg, 4)}, but no holdings row exists")
continue
if abs(h["shares"] - shares) > SHARES_EPS:
flag("shares-mismatch", key,
f"holding {h['id']}: stored {h['shares']}, recomputed {round(shares, 6)}")
if abs(h["avg_cost_basis_per_share"] - avg) > COST_EPS:
flag("cost-basis-mismatch", key,
f"holding {h['id']}: stored {h['avg_cost_basis_per_share']}, "
f"recomputed {round(avg, 6)}")
if h["currency"] != currency:
warnings.append(f"WARNING [currency-drift] account {key[0]} {key[1]}: "
f"holding says {h['currency']}, first txn says {currency}")
for key, h in holdings.items():
if key not in txns_by_key:
flag("stale-holding", key,
f"holding {h['id']} ({h['shares']} shares) has no stock "
f"transactions — recompute would delete it")
if args.json:
print(json.dumps({"holdings": len(holdings),
"keys_with_txns": len(txns_by_key),
"warnings": warnings, "anomalies": anomalies}, indent=2))
else:
print(f"audit_holdings: {len(holdings)} holdings, "
f"{len(txns_by_key)} (account,ticker) keys with stock txns")
for w in warnings:
print(w)
for a in anomalies:
print(f"ANOMALY [{a['check']}] account {a['account_id']} "
f"{a['ticker']}: {a['detail']}")
print("CLEAN" if not anomalies else f"{len(anomalies)} anomalies")
return 1 if anomalies else 0
if __name__ == "__main__":
sys.exit(main())