110 lines
5.1 KiB
Markdown
110 lines
5.1 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. |
|
|
|
|
## `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.
|
|
- 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)
|