zfin/docs/guides/track-contributions.md
Emil Lerch 74fc219afd
All checks were successful
Generic zig build / build (push) Successful in 5m48s
Generic zig build / publish-macos (push) Successful in 11s
Generic zig build / deploy (push) Successful in 23s
add docs/guides
2026-06-22 14:53:53 -07:00

4.3 KiB

Track contributions

Goal: see how much new money you've added (or withdrawn) over a period, separated from market movement -- and stop internal transfers between your own accounts from being counted as contributions.

You'll need: your portfolio under git version control, with commits over time. contributions works by diffing two revisions of your portfolio.srf, so it only sees money movement you've committed.

How it works

Market gains change your portfolio's value; contributions change its shares and lots. zfin contributions diffs your portfolio file between two git revisions, attributes the share/lot changes to contributions vs. withdrawals, and ignores price movement.

This means the workflow is: keep portfolio.srf in a git repo, and commit it whenever you update it. A natural cadence is a commit per account update or per weekly review.

Do this bookkeeping when the market is closed -- a weekend is ideal. Prices and balances have settled, your brokerage statements are final, and zfin's cached closing prices won't shift under you mid-run, so the numbers are stable and reproducible. The same applies to audit: reconciling against an export lines up cleanly when both sides reflect the same settled close, rather than a moving intraday price.

The default modes

With no flags, the comparison depends on your working tree:

zfin contributions
  • Clean working tree: compares HEAD~1 against HEAD -- i.e. "what changed in my last commit."
  • Dirty working tree: compares HEAD against the working copy -- i.e. "what have I edited but not yet committed."

Against a freshly-checked-out example (clean, nothing to diff) you'll see:

Portfolio contributions report
  Working tree clean — comparing HEAD~1 against HEAD

  No changes detected.

Choosing a window

Use --since (and optionally --until) to pick the endpoints. Dates accept YYYY-MM-DD or relative shortcuts (1W, 1M, 1Q, 1Y):

zfin contributions --since 1Y                 # a year ago vs. now
zfin contributions --since 2025-01-01 --until 2025-12-31

Each date resolves to the commit at or before it. When a review date and its commit date diverge (you committed two days after your Sunday review), pin commits directly with --commit-before / --commit-after (which accept HEAD, HEAD~N, a SHA, or working).

Don't double-count transfers

Moving money between two accounts you own isn't a contribution, but a naive diff sees the receiving account gain lots and counts it as new money. transaction_log.srf is how you tell zfin "this was a transfer, not new money."

Like the other sibling files, it's optional and additive: you only need it if you move money between your own accounts and want clean attribution. Without it nothing breaks -- those transfers just show up as contributions, inflating the total by the amount moved. If you never shuffle money between accounts, skip the file entirely.

When you do, declare each move in transaction_log.srf so it cancels out:

#!srfv1
transfer::2026-05-02,type::cash,amount:num:50000,from::Joint taxable,to::Pat Roth,dest_lot::cash

zfin matches the transfer against the diff and removes it from the attribution total. Only type::cash is wired up today; in_kind parses but isn't yet supported.

Cash that is a contribution

By default, raw cash-balance increases are treated as internal noise (interest, dividends, sweeps). For accounts whose cash movement is dominated by real external deposits (payroll ESPP, direct 401k cash), set cash_is_contribution:bool:true in accounts.srf so those increases count.

zfin compare shows the same attribution alongside value and per-symbol price moves between two dates. See Snapshots and history.

Next steps


Previous: Read your portfolio | Next: Snapshots and history | Documentation home