zfin/docs/guides/set-up-accounts.md
Emil Lerch 4f1ca3d34d
All checks were successful
Generic zig build / build (push) Successful in 5m19s
Generic zig build / publish-macos (push) Successful in 12s
Generic zig build / deploy (push) Successful in 22s
add ability to handle opaque 401(k)s with new tax_mix fields
2026-08-01 15:32:33 -07:00

230 lines
9.5 KiB
Markdown

# 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](set-up-your-portfolio.md)). Full field list:
[`accounts.srf` reference](../reference/config/accounts-srf.md).
## 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:
```srf
#!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:
```bash
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](audit-against-brokerage.md)), add the institution and a
(placeholder) account number so zfin can match export files to
accounts:
```srf
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`](../reference/cli/audit.md) (run with no flags) nags you
about accounts you haven't refreshed recently. The default cadence is
`weekly`; relax or silence it per account:
```srf
account::Family HSA,tax_type::hsa,update_cadence::quarterly
account::Old Rollover,tax_type::traditional,update_cadence::none
```
## 4. Advanced flags
Five 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](read-your-portfolio.md#umbrella-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 in
[`zfin contributions`](track-contributions.md), 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.
- **`tax_mix_roth:num:22.4`** (and the `tax_mix_taxable` /
`tax_mix_traditional` / `tax_mix_hsa` siblings) -- split an account
that holds more than one kind of money. The classic case is a 401(k)
whose single reported balance lumps pre-tax deferrals, the employer
match, and Roth contributions together. The carve-out is what the
account is *not*; `tax_type` keeps the remainder:
```srf
account::Sample 401k,tax_type::traditional,tax_mix_roth:num:22.4,tax_mix_date::2026-08-01
```
See [Mixed tax treatment](../reference/config/accounts-srf.md#mixed-tax-treatment)
for the rules and the drift caveat.
- **`audit_large_lot_threshold:num:50000`** -- raise (or lower) the
dollar cutoff at which a flagless [`zfin audit`](../reference/cli/audit.md)
nudges 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:
```srf
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:
```srf
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:
```srf
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`](../reference/cli/doctor.md) will tell you. The sign is
ignored, so `-45300` works too.
You don't have to remember to refresh it. A flagless
[`zfin audit`](../reference/cli/audit.md) 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`](track-contributions.md) 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`](../reference/cli/audit.md)
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`](../reference/config/accounts-srf.md#harvested-and-harvested_date)),
not in the key.
## Example (from `examples/pre-retirement-both`)
```srf
#!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](read-your-portfolio.md) -- the breakdowns this unlocks.
- [Audit against your brokerage](audit-against-brokerage.md) -- put `institution`/`account_number` to work.
---
[Previous: Classify your holdings](classify-holdings.md) | [Next: Read your portfolio](read-your-portfolio.md) | [Documentation home](../README.md)