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
128 lines
5.1 KiB
Python
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())
|