document in-kind transfer implementation
This commit is contained in:
parent
0e1a4862db
commit
dcd5885fcc
3 changed files with 66 additions and 11 deletions
|
|
@ -777,7 +777,7 @@ command.
|
|||
|
||||
- **Portfolio auto-detection.** Both CLI and TUI resolve `portfolio*.srf` the same way, and it is ZFIN_HOME-exclusive when set: if `$ZFIN_HOME` is set, only that directory is searched (cwd is NOT a fallback - a project directory that incidentally ships a `portfolio.srf` must not shadow the user's canonical data); if `$ZFIN_HOME` is unset, cwd is searched. An explicit `-p` path resolves the same exclusive way. `watchlist.srf` and the `.env` file follow the same rule (`.env` adds process environment variables as a higher-priority tier on top). `metadata.srf` and `accounts.srf` are loaded from the same directory as the resolved portfolio file.
|
||||
|
||||
- **`transaction_log.srf` is a sibling file.** Optional. Lives next to `portfolio.srf` / `accounts.srf`. Holds user-declared `transfer::` records so the contributions pipeline can tell internal account-to-account movement apart from real external contributions. Only `type::cash` is wired in v1 - `type::in_kind` parses but is rejected downstream. Missing file -> matcher is a no-op. See `REPORT.md` section 5 "Transfer log" for the user-facing guide.
|
||||
- **`transaction_log.srf` is a sibling file.** Optional. Lives next to `portfolio.srf` / `accounts.srf`. Holds user-declared `transfer::` records so the contributions pipeline can tell internal account-to-account movement apart from real external contributions. Both `type::cash` (verified against the destination's dollar value, $1 tolerance) and `type::in_kind` (verified against share counts, 1%/0.01-share tolerance) are wired into the classifier. Missing file -> matcher is a no-op. See `docs/reference/config/transaction-log-srf.md` for the user-facing guide.
|
||||
|
||||
- **Server sync is optional.** The `ZFIN_SERVER` env var enables parallel cache syncing from a remote zfin-server instance. All server sync code silently no-ops when the URL is null.
|
||||
|
||||
|
|
|
|||
|
|
@ -89,8 +89,21 @@ transfer::2026-05-02,type::cash,amount:num:50000,from::Joint taxable,to::Pat Rot
|
|||
```
|
||||
|
||||
zfin matches the transfer against the diff and removes it from the
|
||||
attribution total. Only `type::cash` is wired up today; `in_kind`
|
||||
parses but isn't yet supported.
|
||||
attribution total.
|
||||
|
||||
If the **securities** moved rather than cash -- an ACAT transfer or an
|
||||
in-kind rollover -- use `type::in_kind` and point `dest_lot` at the lot
|
||||
that arrived:
|
||||
|
||||
```srf
|
||||
transfer::2026-05-02,type::in_kind,amount:num:87000,from::Old 401k,to::Pat Roth,dest_lot::VTI@2024-01-15
|
||||
```
|
||||
|
||||
In-kind records are verified by share count rather than by `amount`
|
||||
(which is informational there), so the shares that left have to match
|
||||
the shares that arrived. See the
|
||||
[reference](../reference/config/transaction-log-srf.md#typecash-vs-typein_kind)
|
||||
for the exact tolerances and what happens when they don't line up.
|
||||
|
||||
## Cash that *is* a contribution
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
`transaction_log.srf` is an optional sibling of `portfolio.srf` that
|
||||
declares real-world transactions which change how zfin interprets the
|
||||
portfolio diff. In v1 it holds exactly one kind of record:
|
||||
**transfers** -- money you moved between accounts you own.
|
||||
portfolio diff. It holds exactly one kind of record: **transfers** --
|
||||
money or securities you moved between accounts you own.
|
||||
|
||||
## Why it exists
|
||||
|
||||
|
|
@ -42,21 +42,63 @@ transfer::2026-05-02,type::cash,amount:num:4700,from::Sample IRA,to::Sample Brok
|
|||
| Field | Type | Required | Description |
|
||||
|------------|--------|----------|---------------------------------------------------------------------|
|
||||
| `transfer` | date | Yes | Transfer date (`YYYY-MM-DD`); the record key. |
|
||||
| `type` | string | Yes | `cash` or `in_kind` (see v1 scope below). |
|
||||
| `type` | string | Yes | `cash` (money moved) or `in_kind` (securities moved). See below. |
|
||||
| `amount` | num | Yes | Dollar amount transferred to this destination. |
|
||||
| `from` | string | Yes | Source account name (matches an `account::` in your portfolio). |
|
||||
| `to` | string | Yes | Destination account name. |
|
||||
| `dest_lot` | string | Yes | Where it landed: `cash`, or `SYMBOL@YYYY-MM-DD` for a specific lot. |
|
||||
|
||||
## v1 scope and limits
|
||||
## `type::cash` vs `type::in_kind`
|
||||
|
||||
Both are matched by the contributions classifier; they differ in what
|
||||
moved and therefore in how the record is verified.
|
||||
|
||||
**`type::cash`** -- dollars left one account and arrived at another,
|
||||
possibly getting invested on arrival. The `amount` is load-bearing: it's
|
||||
matched against the destination's value, and the three outcomes are
|
||||
|
||||
- **within $1** -- fully a transfer; contributes $0 to attribution.
|
||||
- **destination worth more than `amount`** -- a *partial* transfer. Only
|
||||
`amount` is cancelled out; the excess is still counted as new money.
|
||||
That's the "I moved $5k in and also added $2k of my own" case.
|
||||
- **`amount` exceeds the destination's value by more than $1** --
|
||||
rejected, and reported under **Flagged for review**. You can't have
|
||||
moved more into a lot than the lot is worth, so the record is
|
||||
presumed wrong rather than trusted.
|
||||
|
||||
**`type::in_kind`** -- the securities themselves moved (an ACAT
|
||||
transfer, an in-kind rollover); no cash changed hands. Requires
|
||||
`dest_lot::SYMBOL@YYYY-MM-DD` -- `dest_lot::cash` is rejected, since
|
||||
nothing can land as cash in a transfer where no cash moved.
|
||||
|
||||
```srf
|
||||
#!srfv1
|
||||
# 300 shares of VTI moved from one account to the other, no cash involved
|
||||
transfer::2026-05-02,type::in_kind,amount:num:87000,from::Sample IRA,to::Sample Brokerage,dest_lot::VTI@2024-01-15
|
||||
```
|
||||
|
||||
Here `amount` is informational only. The moved value comes from the
|
||||
destination lot itself, and verification is by **share count**: the
|
||||
shares that left `from` must match the shares that arrived at `to`,
|
||||
within 1% or 0.01 shares (whichever is larger). A mismatch is reported
|
||||
under **Flagged for review** rather than silently absorbing what might
|
||||
be a real contribution.
|
||||
|
||||
A missing source side is not an error for either type -- the sending
|
||||
account may be untracked (an external rollover origin, a 401k you don't
|
||||
model). Only the destination is required.
|
||||
|
||||
Because no cash funded an in-kind move, there is no partial outcome: a
|
||||
destination is either fully a transfer or not one at all.
|
||||
|
||||
## Scope and limits
|
||||
|
||||
- Only `transfer::` records. Buys, sells, and dividends stay inferred
|
||||
from the portfolio diff.
|
||||
- Only `type::cash` is wired into the contributions classifier.
|
||||
`type::in_kind` parses (the format is forward-compatible) but the
|
||||
matcher rejects it with an "in-kind transfers not yet supported"
|
||||
message.
|
||||
- Forward-looking only -- there is no historical reconstruction.
|
||||
- Account names are matched byte-exactly, so a
|
||||
[renamed account](../../guides/set-up-accounts.md#renaming-an-account)
|
||||
breaks records that reference the old name.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue