zfin/docs/reference/cli/compare.md

4.6 KiB

zfin compare

Compare your portfolio at two points in time: liquid totals, per-symbol price moves, and contribution attribution.

Usage:
  zfin compare <DATE>             # compare DATE vs. live portfolio
  zfin compare <DATE1> <DATE2>    # compare two historical dates

Arguments can be given in any order; output always reads older -> newer. Dates accept YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y). Historical dates resolve against your history/*-portfolio.srf snapshots; on a missing date, compare prints the nearest available dates and exits rather than snapping silently.

Options

Flag Effect
--projections Add projected-return and 99% safe-withdrawal deltas (adds ~1-2s per endpoint).
--no-events With --projections, exclude life events.
--snapshot-before <DATE> / --snapshot-after <DATE> Override a side's snapshot (--snapshot-after live for the current portfolio).
--commit-before <SPEC> / --commit-after <SPEC> Pin the git commit for the attribution block (HEAD, HEAD~N, SHA, or working). Rarely needed: by default each side anchors on the commit that recorded its snapshot. Passing HEAD is usually wrong -- see Attribution and commit timing.

Example

ZFIN_HOME=examples/post-retirement zfin compare 2024-04-01 2025-04-01
Portfolio comparison: 2024-04-01 -> 2025-04-01 (365 days)

Liquid:  $2,350,000.00 -> $2,580,000.00    +$230,000.00   +9.79%

With symbols held on both dates, a per-symbol price-change table appears, sorted by percentage move.

Mixed share classes

A symbol can cover holdings in more than one share class -- for example a direct-indexing sleeve, a 401(k) collective trust and plain retail shares all priced through the same ticker:: alias with different price_ratio values. Those trade at different per-share prices, so the group has no single price to show and the price columns render as —. The percentage and dollar figures come from the change in market value instead.

Whether that percentage is trustworthy depends on one thing: did the share count move?

Share count unchanged -- the percentage is a real return, and the row sorts inline with everything else. A group's value is its underlying price times a fixed basket, so with the basket held still the change in value is the change in price. You just don't get a per-share price beside it:

  SPYM               —   ->         —    +9.40%     +$110,000.00

Shares bought or sold -- the figure now mixes market movement with your own cash flow and there is no way to separate them. Those rows are tallied separately, named in a footnote, and always sorted last so you can disregard them as a block:

  SPYM               —   ->         —   +30.69%    +$419,605.63
  18 gainers, 3 losers, 1 share count changed
  SPYM: spans share classes AND the share count moved, so the figure is total value change - part market, part shares bought or sold.

In that second example much of the +30.69% is a retail purchase made partway through the window, not market movement.

Ordinary (non-mixed) rows are never affected by this: their percentage is a pure price ratio, so buying or selling during the window cannot distort it.

One caveat the tool cannot detect: restating a price_ratio without changing shares also breaks the equivalence, because the ratio itself is not recorded in a snapshot. If you have just re-based a proxied holding, treat percentages spanning that date with suspicion.

This split is a limitation of what snapshots record rather than a display choice: price_ratio is folded into each lot's stored price and the underlying base price is never written, so a single meaningful price for the group cannot be reconstructed after the fact.

See also


CLI command reference