zfin/docs/reference/cli/contributions.md
Emil Lerch c463d94ea5
All checks were successful
Generic zig build / build (push) Successful in 6m58s
Generic zig build / publish-macos (push) Successful in 11s
Generic zig build / deploy (push) Successful in 19s
contributions: track matured CDs properly
2026-09-29 07:20:24 -07:00

113 lines
5.2 KiB
Markdown

# `zfin contributions`
Show contributions, withdrawals, and lot-level changes between two
points in your portfolio's **git history**.
```
Usage: zfin contributions [opts]
```
`contributions` diffs two git revisions of your `portfolio*.srf` files
and attributes the share/lot changes to new money vs. market movement.
Every file matching the glob is read at both revisions and merged, so a
sold lot archived into a sibling `portfolio_closed.srf` is still seen.
Your portfolio must be under git with commits over time.
## Modes
| Invocation | Window |
|-----------------------------|-----------------------------------------------------------|
| (no flags), dirty tree | `HEAD` vs. working copy |
| (no flags), clean tree | `HEAD~1` vs. `HEAD` (review the last commit) |
| `--since <DATE>` | commit at/before DATE vs. HEAD (or working copy if dirty) |
| `--since <D1> --until <D2>` | commit at/before D1 vs. commit at/before D2 |
`--until` alone is rejected (the window is ambiguous). Dates accept
`YYYY-MM-DD` or `1W`/`1M`/`1Q`/`1Y`.
## Options
| Flag | Effect |
|--------------------------|-------------------------------------------------------------------------------------|
| `--since <DATE>` | Earliest side (resolves to commit at/before). |
| `--until <DATE>` | Latest side (pair with `--since`). |
| `--commit-before <SPEC>` | Pin the before commit directly (same grammar as `--commit-after`, minus `working`). |
| `--commit-after <SPEC>` | Pin the after commit: `YYYY-MM-DD`, relative, `HEAD`, `HEAD~N`, SHA, or `working`. |
Pass at most one of `--since`/`--commit-before` (same axis), and at
most one of `--until`/`--commit-after`.
## Example
```bash
zfin contributions --since 1Y
```
## Internal movement
Money that was already inside an account is not a contribution -- it
just changed form. Two shapes are detected automatically, with no
bookkeeping on your part, and both report under **Internal purchases**
rather than counting toward the total:
- **Buying with cash already in the account.** The buy appears
alongside the account's cash going down.
- **Reallocating -- selling one holding to buy another in the same
account.** The sale's proceeds offset the repurchase.
- **A CD paying out.** A CD's payout counts like a sale: rolling it
into a new CD, or buying with the payout, isn't new money.
A sale is valued at `close_price` when you record one (see
[`portfolio.srf`](../config/portfolio-srf.md)), which is what the sale
actually realized. If you delete the lot outright instead, or record the
close without a `close_price`, the current market price stands in --
accurate for a recent sale, less so for one made long before the end of
the window. The report labels which was used: `at close` only when every
lot on the line had a `close_price`, otherwise `at mark`.
An option removed after it expired is worth $0 -- whether it expired
worthless or was exercised, the premium isn't proceeds. (An exercise
shows up separately, as the stock and cash that moved.)
A partial sale is recorded by splitting the lot: keep the shares you
still hold on one line, and put the sold shares on another with
`close_date` and `close_price`. Only the sold shares count, at their
`close_price`, whichever order the lines are in and whether the sold one
stays in `portfolio.srf` or moves to `portfolio_closed.srf`. Deleting a
lot you had already closed changes nothing, and neither does adding old
closed lots to the file -- though an added one appears in the report as
a purchase funded by its own sale.
Closing a position that has accumulated a lot per dividend
reinvestment retires many lots at once, so sales collapse to one line
per account and symbol, carrying the lot count and the total.
Proceeds still sitting in cash at the end of the window cannot have
funded anything, and are treated accordingly. On an account marked
`cash_is_contribution::true` they also cancel that account's cash
credit, since the sale is not new money even though cash arrived.
A CD that matures pays out in the window containing its maturity date
-- between the dates of the two commits being compared, or today for
uncommitted changes -- whether you close it,
delete it, archive it to `portfolio_closed.srf`, or leave it where it
is. Closing or removing it in a later window changes nothing. A CD
redeemed before maturity pays out in the window you close or remove it.
On an account marked `cash_is_contribution::true`, update its cash
balance in that same window: a payout that reaches the cash line a
commit later reads as new money there.
Movement **between** accounts is a different matter -- zfin cannot tell
it from a contribution, so declare it in
[`transaction_log.srf`](../config/transaction-log-srf.md). An explicit
record always wins over the automatic netting above.
## See also
- [Track contributions](../../guides/track-contributions.md) -- the full workflow.
- [`compare`](compare.md) -- attribution alongside value and price moves.
---
[CLI command reference](index.md)