name: tune-income
description: Use whenever the user wants to set up, enhance, or double-check the income estimate of this portfolio repo — the income:, cash_interest_pct, and tax: blocks in data/config.yaml that drive portfolio income (gross) and portfolio income --net (after-tax). Trigger on "estimate my dividends/interest", "my income shows 0 / n/a", "add a proxy", "set the cash interest rate", "show income net of tax", "set up Teilfreistellung", "is my income/tax config right?", "which distributing twin for ", or any request to verify/fix why an accumulating ETF reports no income or why the net figure looks off. Use this even when the user just says "check my income config" without naming the fields.
⚠️ Outdated. The portfolio repo moved to a snapshot-based net-worth model (multiple institutions, ISIN categories, PDF ingest). This skill references the removed transaction/income CLI (
add-buy,add-sell,import,income) and needs a rewrite. Seedocs/superpowers/plans/2026-06-19-multi-source-networth.md.
Tune income config
The portfolio income command estimates portfolio income as two figures —
economic income (what holdings earn, market value × yield) and cash
distribution (what's actually paid out) — gross by default, or after-tax with
--net (a dashboard toggle does the same). It reads optional config:
cash_interest_pct: 2.0 # broker's annual interest rate on the cash balance
income: # per-ticker overrides; omit a ticker → use its own live yield
WEBG.DE: { proxy: VWRL.DE } # accumulating → borrow a distributing twin's live yield
EUNA.DE: { yield_pct: 2.4 } # no live twin → manual annual yield %
tax: # only used by `--net`; sensible German defaults if omitted
rate_pct: 26.375 # Kapitalertragsteuer 25% + Soli (church tax → ~27.8–28%)
default_teilfreistellung: equity # class for holdings not listed below
teilfreistellung: # German partial exemption by fund type
VOW.DE: none # individual stock — no exemption
EUNA.DE: none # bond fund — no exemption
Each income entry sets exactly one of proxy or yield_pct. Two things are
silently wrong unless configured: accumulating ETFs (common in EU/Trade Republic
portfolios) distribute ~nothing, so yfinance reports ~0% and the economic line
needs a proxy or manual yield; and in --net mode every holding inherits
default_teilfreistellung (equity → 30% tax-free) unless listed, so individual
stocks and bond funds left on the default get an exemption they aren't owed and
their net is overstated. This skill helps set both up and verify they're right.
What this skill does NOT do
No fabricated numbers. Never invent a yield, a cash interest rate, or assert a proxy ticker is correct without verifying it resolves. The cash rate is something only the user knows (their broker's current rate); proxy tickers must be confirmed against yfinance via the audit script, not pasted from memory. When you genuinely don't know, say so and ask or point at the factsheet — a confidently wrong financial number is worse than an admitted gap.
General config edits (categories, target weights, tickers, cash balance,
adding/selling positions) belong to the assess-portfolio skill, not this one.
This skill is only about the income fields.
Resources
scripts/audit-income.sh— runs the income pipeline and emits JSON findings with actionable flags. Prefer this over reading theportfolio incometable when deciding what to change. Usage from repo root:./.claude/skills/tune-income/scripts/audit-income.shreferences/proxy-map.md— how to choose a distributing-twin proxy, a starting table of twins for common accumulating UCITS ETFs, and when to use a manual yield instead. Read it whenever picking or verifying a proxy.references/tax.md— the German net-of-tax model: rate, Teilfreistellung classes per fund type, church-tax rates, and the default-equity trap. Read it whenever setting up or checking thetax:block.
Process: double-check the config
This is the default for "is my income config right?" / "why is my income 0?".
Step 1 — Run the audit
./.claude/skills/tune-income/scripts/audit-income.sh
It returns JSON: cash, holdings[], proxy_checks[], manual_yields[], and a
summary. Each holding and proxy carries a flags array. If it errors, the
config or data is broken first — run uv run portfolio check and fix that.
Step 2 — Interpret the flags
Walk the findings and translate each flag into a concrete recommendation:
zero_yield_native— a holding using its own yield reports ~0%. For an ETF this almost always means it's accumulating → recommend a proxy (seereferences/proxy-map.md) or a manualyield_pct. For a single stock it may just mean no dividend was paid in the last 12 months (annual/suspended payer) → likely a genuine zero, no action. Distinguish by whether the ticker is a fund or a stock, and say which you think it is.unresolved— no yield at all (not even 0). Either no override and yfinance has no data, or a configured proxy/source failed. Needs a proxy oryield_pct.broken_proxy— a configured proxy ticker returns no yield: delisted or mis-typed (theAGGHbond-twin trap). Pick another listing from the proxy map, or fall back to a manualyield_pct.cash_interest_unset— there's a cash balance butcash_interest_pctis 0, so cash income reads €0. Ask the user for their broker's current rate; don't guess it.teilfreistellung_default— the holding has no explicit Teilfreistellung class, so--nettaxes it as the default (usuallyequity, 30% exempt). That's right for an equity ETF but wrong for an individual stock or a bond fund, which get no exemption → recommend setting those tononeso their net isn't overstated. Thetaxblock in the audit shows the gross-vs-net totals so you can quote the impact. Only relevant if the user cares about the net figure.
Step 3 — Report
Lead with whether the estimate is trustworthy as-is, then a short bulleted list of gaps, each with the exact config edit that would fix it. Quote EUR figures from the audit (e.g. "SXR8.DE holds €2,058 but reports €0 income — add a proxy"). End by offering to apply the fixes.
Process: enhance / set up the config
For "set up income", "add a proxy for X", "make my income estimate accurate":
- Audit first (Step 1 above) to see what's actually missing — don't add overrides a holding doesn't need.
- For each flagged holding, pick the fix using
references/proxy-map.md: prefer a live proxy twin; use a manualyield_pctonly where no twin resolves (typically bond ETFs and cash). - Verify before committing. After adding a
proxy, re-run the audit and confirm it nowresolveswith a plausible yield (a broad-equity proxy showing 8% means you grabbed the wrong fund). This verify-the-proxy loop is the whole point — a proxy that doesn't resolve silently understates income. - Set
cash_interest_pctfrom the rate the user gives you. - If the user wants net-of-tax, set up the
tax:block — readreferences/tax.md. The key move: list everyteilfreistellung_defaultholding that is an individual stock or bond fund asnone; leave equity ETFs on the default. Ask whether they pay church tax (it changesrate_pct); don't assume it. - Validate:
uv run portfolio check(structural) then re-run the audit until thesummaryshows 0 unresolved / 0 broken proxies (and, if net matters, no strayteilfreistellung_defaulton stocks/bond funds). - Offer to show the before/after with
uv run portfolio income(add--netif tax was configured).
Editing notes
- The config loader rejects an
incomeentry that sets bothproxyandyield_pct, or neither, or names a ticker not in any category — so a failedportfolio check/load after your edit usually means one of those. - Keep edits minimal and idiomatic: only add
incomeentries for holdings that need them; leave distributing stocks out entirely. - Changes write to
data/config.yaml;git restore data/config.yamlundoes them.