230 lines
9.5 KiB
Markdown
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)
|