11 KiB
accounts.srf reference
accounts.srf describes each account referenced by your portfolio:
its tax treatment, the institution it lives at, and a few flags that
tune analysis and reconciliation. It powers the By Tax Type and
By Account breakdowns in zfin analysis, the
umbrella-exposure estimate, the audit staleness checks, and broker
reconciliation.
zfin loads accounts.srf from the same directory as the resolved
portfolio file. It is optional -- without it, accounts show up as
"Unknown" in the tax-type breakdown and everything else still works.
File format
One record per account. The account name must match the
account:: value used on your portfolio lots exactly.
#!srfv1
account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401
account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
account |
string | Yes | -- | Account name; must match account:: on lots exactly. |
tax_type |
string | Yes | -- | taxable, roth, traditional, or hsa. |
institution |
string | No | -- | Broker key, e.g. fidelity, schwab, vanguard, wells_fargo. Used by zfin audit to match export files. |
account_number |
string | No | -- | Account identifier used with institution for audit matching. Use a placeholder, not a full real number. |
update_cadence |
string | No | weekly |
How often you refresh this account's manual data: weekly, monthly, quarterly, or none. Drives the audit staleness nag. |
cash_is_contribution |
bool | No | false |
When true, raw cash-balance increases on this account count as real external contributions (see below). |
direct_indexing |
bool | No | false |
Marks an account whose lots track a benchmark with tracking-error drift (loosens contribution/audit tolerances). |
shielded |
bool | No | (derived) | Umbrella-exposure override (see below). |
audit_large_lot_threshold |
num | No | 10000 |
Per-account dollar cutoff for the audit "Large new lots" nudge (see below). Must be positive. |
harvested |
num | No | -- | Hand-declared cumulative tax-loss-harvested figure, for accounts whose realized P&L zfin cannot derive (see below). |
harvested_date |
date | No | -- | YYYY-MM-DD you last refreshed harvested. Required for it to display. |
Tax types
| Value | Display label |
|---|---|
taxable |
Taxable |
roth |
Roth (Post-Tax) |
traditional |
Traditional (Pre-Tax) |
hsa |
HSA (Triple Tax-Free) |
Any other value is shown as-is. Accounts missing from accounts.srf
appear as "Unknown".
audit_large_lot_threshold
When zfin audit runs flagless, its Large new
lots - confirm source section flags any newly-appeared lot worth at
least this many dollars, nudging you to confirm whether it's a real
external contribution or an unrecorded internal transfer. Smaller new
lots pass silently so routine payroll/ESPP accruals and weekly deposits
don't spam the report.
The threshold is per account, because the noise it fights is account-specific: an ESPP or payroll account that accrues routine large lots wants a high bar, while a taxable brokerage where any sizeable new lot deserves a look wants the default (or lower). Set it on the account's own record:
#!srfv1
account::Sample ESPP,tax_type::taxable,audit_large_lot_threshold:num:50000
account::Sample Brokerage,tax_type::taxable
Here the ESPP account stays quiet until a new lot tops $50k, while
Sample Brokerage (no override) uses the built-in $10,000 default.
Accounts you don't list, or list without the field, use that default.
The value must be positive -- zero or a negative number is rejected
at load time and the account falls back to the default.
harvested and harvested_date
Some accounts hold a number that matters to you but that zfin has no
way to compute. The motivating case is a synthetic direct-indexing
account: a tax-loss-harvesting sleeve holding hundreds of individual
positions that churn constantly. Rather than model all of them, you
track the sleeve as a single aggregate lot with a ticker:: alias:
#!srfv1
symbol::DI-SPX,ticker::SPY,shares:num:1000,open_date::2024-01-15,open_price:num:400,account::Sample Tax Loss
That keeps the portfolio readable, but it means there are no closed
lots, so zfin cannot derive realized gain/loss -- and the harvested
total is the entire point of such an account. harvested is where you
park the figure you read off the brokerage site:
#!srfv1
account::Sample Tax Loss,tax_type::taxable,direct_indexing:bool:true,harvested:num:45300,harvested_date::2026-06-24
It renders as a compact annotation next to the account name:
By Account
Sample Tax Loss █████████████████████████████ 96.7% $738,930.00 (45k 6/24)
It appears in four places:
- the By Account breakdown of
zfin analysis - the same breakdown in the TUI's Analysis tab
- the TUI's account picker (
a), beside the account number - the TUI Portfolio tab header when an account filter is active
Rules
-
harvested_dateis required for it to display. A hand-copied figure with no date is indistinguishable from a stale one, so zfin refuses to show it.zfin doctorwarns whenharvestedis set without a date, and when a date is in the future. -
zfin auditnags after 90 days. A flaglesszfin auditlists any account whose figure is more than 90 days old under Stale harvested figures, so it's part of your normal maintenance sweep. The threshold isn't configurable, and doesn't need to be: only accounts that declareharvestedare ever considered, so not wanting the nag and not wanting the field are the same choice. -
Entries older than 12 months stop displaying. Harvest data that old isn't decision-useful. This is designed behavior, not an error --
doctorstays quiet about it. Refresh the figure and the date to bring it back.The audit nag keeps firing past 12 months, and says
no longer displayedonce the annotation has retired -- otherwise the annotation would simply vanish with nothing to explain why. -
The sign doesn't matter.
harvested:num:45300andharvested:num:-45300are equivalent; the annotation's parentheses already carry the accounting "this is a loss" convention. -
It is display-only. It never enters a total, a weight, a breakdown value, or the contributions attribution. It is an annotation, not an input.
The two thresholds together: fresh for 90 days, nagged from 90 to 365, nagged and hidden past 365.
Why not put it in the account name?
Because the account name is a join key, not a label. It is matched
byte-exactly against account:: on every lot, and it is written into
every historical snapshot. Encoding a changing number in it means
renaming the account every time the number changes, and every rename
costs you history -- see
Renaming an account.
update_cadence and the audit nag
zfin audit (run flagless) flags accounts you
haven't refreshed within their cadence window: weekly = 7 days,
monthly = 30, quarterly = 90, none = never nag. The default is
weekly, so every account reminds you until you silence it -- set
update_cadence::none for accounts that update themselves (a live
brokerage feed) or that you simply don't track closely.
cash_is_contribution
Most cash-balance movement is internal noise -- interest postings,
dividend credits, CD coupons, settlement sweeps -- which would inflate
the zfin contributions attribution total
if counted as new money. So cash deltas are ignored by default. Set
cash_is_contribution:bool:true only on accounts whose cash movement
is dominated by external deposits (payroll ESPP accrual, direct 401k
cash contributions).
shielded (umbrella exposure)
The umbrella-exposure estimate in zfin analysis
splits your liquid net worth into "shielded" (retirement accounts,
assumed judgment-protected) and "exposed" (taxable). The default proxy
is "anything not taxable is shielded." Override it when that's wrong:
shielded:bool:falseon a pre-tax account that is not ERISA-protected (deferred-comp plans, non-qualified annuities), or on IRAs in states with weak IRA protection.shielded:bool:trueto mark a taxable account as shielded (rare; e.g. some asset-protection trusts).
IRA protection varies by state and is not modeled automatically; set this explicitly if it matters to you.
Example (from examples/pre-retirement-both)
#!srfv1
account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401
account::Pat Roth,tax_type::roth,institution::fidelity,account_number::PROTH
account::Sam 401k,tax_type::traditional,institution::vanguard,account_number::S401
account::Sam Roth,tax_type::roth,institution::vanguard,account_number::SROTH
account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01
account::Family HSA,tax_type::hsa,institution::fidelity,account_number::HSA01
account::Kids 529,tax_type::taxable,institution::vanguard,account_number::C529
See also
- Map your accounts -- the walkthrough.
zfin analysis-- tax-type and account breakdowns.zfin audit-- staleness checks and broker reconciliation.