zfin/docs/reference/cli/compare.md

4.5 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). Useful when a review date and its commit diverge.

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