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
- Snapshots and history
history-- the full timeline.contributions-- the attribution detail.