This also provides ensurance through zig build test that all fields are documented in markdown so they do not get out of sync
115 lines
5.5 KiB
Markdown
115 lines
5.5 KiB
Markdown
# `transaction_log.srf` reference
|
|
|
|
`transaction_log.srf` is an optional sibling of `portfolio.srf` that
|
|
declares real-world transactions which change how zfin interprets the
|
|
portfolio diff. It holds exactly one kind of record: **transfers** --
|
|
money or securities you moved between accounts you own.
|
|
|
|
## Why it exists
|
|
|
|
[`zfin contributions`](../cli/contributions.md) infers "new money" by
|
|
diffing your portfolio over time. A plain diff can't tell an external
|
|
contribution apart from an internal transfer, so moving (say) $50k from
|
|
one account to another would otherwise be **double-counted**: the
|
|
receiving account's new lots look like contributions while the sending
|
|
account's removed lots are ignored. Declaring the transfer here cancels
|
|
that out.
|
|
|
|
Missing file -> the matcher is a no-op; nothing changes.
|
|
|
|
## File format
|
|
|
|
One record per **destination**. A record pins the money to exactly one
|
|
landing spot: either a specific lot (`SYMBOL@open_date`) or the literal
|
|
token `cash`. A sweep that lands in several lots becomes several records
|
|
sharing the same `(date, from, to)` but differing in `dest_lot`.
|
|
|
|
```srf
|
|
#!srfv1
|
|
# Simple cash transfer between two accounts
|
|
transfer::2026-05-02,type::cash,amount:num:5000,from::Sample IRA,to::Sample Brokerage,dest_lot::cash
|
|
|
|
# Transfer that was invested into a single stock lot on arrival
|
|
transfer::2026-05-02,type::cash,amount:num:7000,from::Sample IRA,to::Sample Brokerage,dest_lot::VTI@2026-05-03
|
|
|
|
# Sweep into a basket plus a cash residual (two records, same date/from/to)
|
|
transfer::2026-05-02,type::cash,amount:num:145300,from::Sample IRA,to::Sample Brokerage,dest_lot::VTI@2026-05-03
|
|
transfer::2026-05-02,type::cash,amount:num:4700,from::Sample IRA,to::Sample Brokerage,dest_lot::cash
|
|
```
|
|
|
|
## Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|------------|--------|----------|---------------------------------------------------------------------|
|
|
| `transfer` | date | Yes | Transfer date (`YYYY-MM-DD`); the record key. |
|
|
| `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. |
|
|
| `note` | string | No | Free-text reminder of why the transfer happened. |
|
|
|
|
## `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. You do **not** need a record for selling one
|
|
holding to buy another inside a single account -- that is detected
|
|
automatically, and the sale's proceeds offset the repurchase. See
|
|
[`zfin contributions`](../cli/contributions.md#internal-movement) for
|
|
how sales are valued and when a record is still required.
|
|
- 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
|
|
|
|
- [Track contributions](../../guides/track-contributions.md) -- the walkthrough.
|
|
- [`zfin contributions`](../cli/contributions.md) -- the command that consumes this file.
|
|
|
|
---
|
|
|
|
[Documentation home](../../README.md#reference)
|