zfin/docs/reference/config/accounts-srf.md
Emil Lerch 0e1a4862db
All checks were successful
Generic zig build / build (push) Successful in 4m56s
Generic zig build / publish-macos (push) Successful in 17s
Generic zig build / deploy (push) Successful in 24s
add stale harvesting date checks to audit command
2026-07-25 10:45:12 -07:00

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_date is 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 doctor warns when harvested is set without a date, and when a date is in the future.

  • zfin audit nags after 90 days. A flagless zfin audit lists 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 declare harvested are 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 -- doctor stays 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 displayed once the annotation has retired -- otherwise the annotation would simply vanish with nothing to explain why.

  • The sign doesn't matter. harvested:num:45300 and harvested:num:-45300 are 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:false on 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:true to 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


Documentation home