zfin/docs/guides/periodic-review.md

8.6 KiB

A periodic review

Goal: on a regular cadence -- weekly works well -- make zfin agree with your brokerages, see what changed since last time, and commit the reconciled state so the next review has a clean baseline to compare against.

This is the loop that ties the other guides together. Each step has its own guide for the details; this one is the routine you actually run. It settles into well under an hour once it's habit.

   reconcile  ──►  what changed?  ──►  (projections)  ──►  commit
   (audit)         (compare)           (optional)          (baseline)
      ▲                                                        │
      └────────────────  next review  ◄────────────────────────┘

1. Reconcile against your brokerages

Pull a fresh export from each brokerage that offers one and drop it in your audit/ folder (or wherever you've pointed $ZFIN_AUDIT_FILES -- see Audit against your brokerage for the export steps and auto-discovery rules). Then:

zfin audit

Fix any flagged share or cash discrepancy in portfolio.srf and re-run until it reconciles. A tight edit loop helps: if you have watchexec installed,

watchexec -- zfin audit

re-runs the audit on every save, so the remaining-discrepancy list shrinks live as you fix lots -- much faster than alt-tab, rerun, read, alt-tab, edit.

Reconcile first, for a reason. Step 2 splits your change in value into contributions vs. market gains, and that split is only as honest as your share counts. Reconcile before you read the headline or the attribution will lie to you.

Blind spots the audit can't see

  • Accounts with no export (some insurers, some 401(k) recordkeepers) -- check the latest statement and update portfolio.srf by hand.
  • Payroll-driven cash -- e.g. ESPP contributions that haven't purchased yet won't appear in a positions export until the purchase posts. Reconcile those against a paystub.
  • A small, expected standing discrepancy -- some accounts just sit a few dollars off every week. Note it and move on rather than chasing it each time.
  • Lagging transaction views. A brokerage's positions view can update overnight before its transaction view posts the dividend, interest, or option assignment that caused the change. Trust the positions numbers and reconcile the totals; the cause-side record catches up later and isn't needed to get today's counts right.

2. The headline -- what changed since last time

One command gives you the whole "since last review" picture:

zfin compare 1W --projections
  • 1W is the point of comparison -- the snapshot from one week ago. Any relative shortcut or an explicit YYYY-MM-DD works.
  • --projections folds in projected-return and safe-withdrawal (SWR@99%) deltas, then vs. now. (Costs ~1-2s per endpoint for the Monte Carlo search; add --no-events to exclude life events.)
  • No --commit-before needed. Attribution now anchors itself on the commit that recorded the comparison snapshot. See Attribution and commit timing.

Read off the liquid-total delta, the contributions-vs-gains split, the per-symbol winners and losers, and the projection deltas. For most weeks, this single command is the review. See Snapshots and history for a full walk through compare output.

3. (Optional) Full projections

compare --projections gives you the deltas but not the full benchmark table or every scenario row. When you want the complete picture:

zfin projections                 # default: with life events (SS, college, ...)
zfin projections --no-events     # baseline: life events excluded
zfin projections --as-of 1W      # the same table as of last review
zfin projections --vs 1W         # both ends of the window in one run

See Plan for retirement for what these rows mean.

4. Commit -- the baseline for next time

git add portfolio.srf metadata.srf history/
git commit -m "review 2026-06-20"

Committing does double duty:

  • It snapshots the reconciled portfolio.srf, and the day's snapshot file in history/ rides along, so future --as-of runs can read it.
  • zfin walks the git history of portfolio.srf for contributions analysis, treating each commit as a reconciliation point. Commit timing sets next review's baseline -- a same-day commit keeps a weekly cadence clean.

If you keep your portfolio directory in git (recommended), this is also your backup and your audit trail.

Attribution and commit timing

compare and contributions work out "contributions vs. gains" by walking the git history of portfolio.srf. The positional date (1W) picks the snapshot whose prices you compare against; the attribution needs a commit to anchor on. If those two describe different periods, the split is wrong -- and it is wrong silently, because gains are derived as a residual (delta - contributions), so anything the attribution window misses is reported as market performance.

This is now handled for you. The default anchor is the commit that ADDED history/<date>-portfolio.srf for the snapshot being compared against. Because a review commits its snapshots together with the reconciled portfolio.srf, that commit is that week's reconciliation -- no naming convention, no dependence on how many times you committed, and unaffected by later edits to the file.

Do not pass --commit-before HEAD. It used to be the recommended workaround for reconciling on Saturday and committing on Monday, and it worked only while HEAD happened to be the previous week's reconciliation commit. Commit anything during a review -- a data fix, a metadata tweak -- and the attribution window collapses to "uncommitted edits only" while the value window still spans the week. One observed case reported ~$23k of vested shares as market gains.

The flag remains for pinning a window deliberately; it is no longer a habit worth having.

Re-snapshot after reconciling

One ordering detail matters more than it looks. If a scheduled job writes daily snapshots, the one for the last market day is captured before you reconcile -- so it holds pre-reconcile share counts while portfolio.srf ends up holding post-reconcile ones. The two then disagree at the same commit, permanently, by one reconciliation:

history/2026-08-21-portfolio.srf   AMZN 1569   (pre-reconcile)
portfolio.srf @ that same commit   AMZN 1656   (post-reconcile)

compare reads the snapshot for "then" and the live portfolio for "now", so those 87 shares appear in the value delta. Attribution walks portfolio.srf across commits, where they were already present -- so no contribution is found and the residual absorbs them.

Fix: run zfin snapshot --force after reconciling and before committing. On a non-trading day it rewrites the last trading day's file, so it never invents a weekend snapshot.

Relative dates

Every date-accepting command takes the same shorthand, so you rarely type a full date:

Shortcut Means
1W one week ago
3W three weeks ago
1M one month ago
1Q one quarter ago
1Y one year ago

compare, contributions --since/--until, projections --as-of/--vs, snapshot --as-of, and history --since/--until all accept it (plus an explicit YYYY-MM-DD).

If there's no snapshot yet

The comparison needs a snapshot to compare against. If your daily snapshot didn't run, or you're reviewing off your normal cadence, make one first:

zfin portfolio --refresh    # fresh close prices for tracked symbols
zfin snapshot               # writes history/<today>-portfolio.srf

See Snapshots and history for the cron setup that automates daily snapshots.

Next steps


Previous: Audit against your brokerage | Next: Customize the TUI | Documentation home