8.9 KiB
Map your accounts
Goal: create an accounts.srf that tags each account with its tax
treatment (and, optionally, its institution and maintenance cadence).
This unlocks the By Tax Type breakdown, an umbrella-insurance
exposure estimate, and broker reconciliation.
You'll need: a portfolio.srf whose lots use account:: labels
(build one first). Full field list:
accounts.srf reference.
1. List your accounts with a tax type
One record per account. The account:: name must match the
account:: value on your lots exactly. The minimum is a tax type:
#!srfv1
account::Pat 401k,tax_type::traditional
account::Pat Roth,tax_type::roth
account::Joint taxable,tax_type::taxable
account::Family HSA,tax_type::hsa
The four recognized tax types are taxable, roth, traditional,
and hsa. Run analysis to see the breakdown:
ZFIN_HOME=~/finance zfin analysis
By Tax Type
Traditional (Pre-Tax) █████████████████▋ 58.9% $815,290.06
Taxable ██████▍ 21.6% $299,010.60
Roth (Post-Tax) █████ 16.9% $233,732.95
HSA (Triple Tax-Free) ▊ 2.5% $35,104.20
Accounts you don't list show up as "Unknown."
2. Add institution and account number (for auditing)
If you plan to reconcile against brokerage exports (audit guide), add the institution and a (placeholder) account number so zfin can match export files to accounts:
account::Pat 401k,tax_type::traditional,institution::fidelity,account_number::P401
account::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01
Recognized institution keys include fidelity, schwab, vanguard,
and wells_fargo.
3. Tune the maintenance cadence
zfin audit (run with no flags) nags you
about accounts you haven't refreshed recently. The default cadence is
weekly; relax or silence it per account:
account::Family HSA,tax_type::hsa,update_cadence::quarterly
account::Old Rollover,tax_type::traditional,update_cadence::none
4. Advanced flags
Four optional fields change how analysis and the audit treat an account -- see the reference for details:
-
shielded:bool:false-- mark a pre-tax account that is not judgment-protected (deferred comp, a weak-state IRA) so it counts toward your umbrella-insurance exposure -- the slice of net worth a personal umbrella liability policy is meant to cover. -
cash_is_contribution:bool:true-- treat cash increases on this account as real external contributions inzfin contributions, instead of internal noise. -
direct_indexing:bool:true-- mark an account whose lots track a benchmark with tracking-error drift rather than holding it directly, so routine share reconciliation stops registering as money movement. -
audit_large_lot_threshold:num:50000-- raise (or lower) the dollar cutoff at which a flaglesszfin auditnudges you to confirm a new lot's source. The default is $10,000; bump it on a noisy ESPP/payroll account so routine accruals stop spamming the report, while leaving quieter accounts at the default:account::Sample ESPP,tax_type::taxable,audit_large_lot_threshold:num:50000
5. Track tax-loss harvested on a synthetic account
If you run a tax-loss-harvesting sleeve (direct indexing, a managed
separate account), you probably don't want its hundreds of churning
positions in portfolio.srf. The usual shape is one aggregate lot with
a ticker:: alias standing in for the whole sleeve:
symbol::DI-SPX,ticker::SPY,shares:num:1000,open_date::2024-01-15,open_price:num:400,account::Sample Tax Loss
That keeps the file readable, but it means zfin has no closed lots to work from, so it can't compute realized gain/loss -- and the harvested total is the whole reason the account exists. Record it by hand instead:
account::Sample Tax Loss,tax_type::taxable,direct_indexing:bool:true,harvested:num:45300,harvested_date::2026-06-24
Next time you check the figure on the brokerage site, update both
fields. It shows up as a compact annotation beside the account name in
zfin analysis, the TUI Analysis tab, the TUI account picker (a),
and the TUI Portfolio tab header when you filter to that account:
By Account
Sample Tax Loss █████████████████████████████ 96.7% $738,930.00 (45k 6/24)
The date is required -- an undated hand-copied number is
indistinguishable from a stale one, so zfin won't display it and
zfin doctor will tell you. The sign is
ignored, so -45300 works too.
You don't have to remember to refresh it. A flagless
zfin audit lists anything more than 90
days old, so it rides along with your normal maintenance sweep:
Stale harvested figures (>90 days - refresh 'harvested' in accounts.srf)
Sample Tax Loss last updated 146 days ago
Past 12 months the annotation retires itself -- data that old isn't
decision-useful. The audit nag keeps going and says
no longer displayed, so the figure never disappears silently.
Don't put the number in the account name. It's tempting -- the name already prints in a lot of places -- but the name is a join key, not a label. See the next section for what a rename costs you.
Renaming an account
Renaming an account is a bigger deal than it looks, because the account
name is zfin's only identifier for an account. There is no separate
account ID. The name is matched byte-exactly against account:: on
every lot, and it is written into every historical snapshot at the time
that snapshot is taken.
To rename, change it in both accounts.srf and every account::
on your portfolio.srf lots, in a single commit. Then expect the
following:
One-time noise in contributions and compare. zfin identifies a lot
by (security_type, symbol, account, open_date, open_price). Changing
the account breaks that key on every lot in the account, so
zfin contributions and zfin compare see
the old lots vanish and identical new ones appear -- indistinguishable
from closing one account and opening another. Any review window
spanning the rename commit will report phantom new lots and an inflated
contribution total, and a flagless zfin audit
will list them under Large new lots - confirm source. This happens
exactly once, for the one window that straddles the rename. Nothing is
corrupted; just ignore that run.
Truncated audit-staleness history. The cadence nag derives each
account's last-updated time by walking portfolio.srf's git history.
Older commits only contain the old name, so the account's update
history restarts at the rename commit. The rename itself registers as
an update, so the account reads as freshly refreshed rather than
falsely overdue.
A permanent split in per-account snapshot history. Snapshots already on disk record the old name and are immutable historical records -- zfin does not rewrite them. Any per-account historical series therefore has a discontinuity at the rename date. This one does not heal.
Possible renumbering in the TUI account picker. Shortcut keys are
assigned by list position (accounts.srf order, then alphabetical for
anything not listed), so a name that sorts differently can shift the
keys. Cosmetic.
zfin import is unaffected: it resolves accounts by
(institution, account_number) and looks the name up in
accounts.srf, so a consistent rename in both files keeps working.
The practical advice: pick account names that never need to change.
Don't encode anything that varies -- balances, dates, harvested totals,
"current"/"old" markers. Values that change belong in fields
(see harvested),
not in the key.
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::Joint taxable,tax_type::taxable,institution::schwab,account_number::JT01
account::Family HSA,tax_type::hsa,institution::fidelity,account_number::HSA01
Next steps
- Read your portfolio -- the breakdowns this unlocks.
- Audit against your brokerage -- put
institution/account_numberto work.
Previous: Classify your holdings | Next: Read your portfolio | Documentation home