IBKR eTax Statement Johannes von Scheidt
winget install --id=jvonscheidt.ibkr-etaxstatement -e Converts Interactive Brokers FlexQuery XML exports into eCH-0196 XML and eCH-0270 barcode PDFs for import into Swiss cantonal tax software.
winget install --id=jvonscheidt.ibkr-etaxstatement -e Converts Interactive Brokers FlexQuery XML exports into eCH-0196 XML and eCH-0270 barcode PDFs for import into Swiss cantonal tax software.
Convert an Interactive Brokers (IBKR) FlexQuery XML export into a Swiss eCH-0196 E-Steuerauszug — both as validated XML and as an eCH-0270 barcode PDF that imports directly into cantonal tax software (e.g. ZHPrivateTax / TaxMe) via "Steuerausweis importieren".
Swiss residents holding securities at IBKR get no Swiss tax statement from the broker. This tool builds a standards-compliant Wertschriftenverzeichnis from the data IBKR already exports, including foreign-withholding amounts for DA-1 review and deductible debit interest, so the positions and income can be imported instead of typed in by hand.
> Disclaimer. This is an independent, unofficial tool. It is not > affiliated with or endorsed by Interactive Brokers, the Verein eCH, or any > Swiss tax authority. It generates a self-issued tax statement — you are > responsible for checking every figure against your own records before filing. > No warranty of any kind (see LICENSE).
grossRevenueA / withHoldingTaxClaim)
from foreign dividend income and withholding (grossRevenueB /
lumpSumTaxCreditAmount), plus deductible debit interest under listOfLiabilities.Swiss private capital gains are tax-exempt and have no eCH-0196 element, so realised trade gains/losses are intentionally not parsed or reported.
requirements.txt: lxml (XSD
validation), and for the barcode PDF pdf417gen (vroonhof fork, installed
from git), reportlab, Pillow, python-barcode, pypdf.pip install -r requirements.txt
Download the standalone ibkr-etaxstatement.exe from the
latest release.
After the package is accepted into the Windows Package Manager repository,
install it with:
winget install --id jvonscheidt.ibkr-etaxstatement --exact
The executable uses the same command line as python convert.py.
The eCH-0196 XSD is not redistributed here. At startup, the converter refreshes
the latest official v2.2 schema and its eCH dependencies, using local
schemaLocation references. It searches for documentation/eCH-0196-2-2.xsd
beside convert.py or the Windows executable, then in the working directory.
An existing cache is refreshed in place; otherwise the application directory is
used. After a successful refresh, superseded dependency folders older than one
hour are removed, so runs in parallel do not delete each other's schemas. If
downloading fails, the cached copy is retained. Validation is explicitly skipped
if lxml or the main XSD is absent. If the cached XSD is unusable, the run fails
without writing output; delete the documentation folder and rerun online.
# eCH-0196 XML only:
python convert.py data/Tax.xml output.xml
# Override the year-end EUR→CHF rate with the official ESTV Jahresendkurs:
python convert.py data/Tax.xml output.xml --eur-chf-rate 0.9311
# Also produce the eCH-0270 barcode PDF for import into ZHPrivateTax:
python convert.py data/Tax.xml output.xml --eur-chf-rate 0.9311 \
--barcode-pdf output_barcode.pdf
Import output_barcode.pdf into your tax application via "Steuerausweis
importieren".
The year-end rate override affects holdings only. Dividends, withholding, cash interest and debt interest retain their payment-date FX rates. CHF amounts need no conversion-rate rows.
In IBKR Client Portal, create a FlexQuery covering the tax year with Open
Positions, Cash Transactions, Change in Dividend Accruals, and Conversion
Rates, run it, and save the XML as data/Tax.xml. Trades may be included but are
not used to infer dividend entitlement. Dates in the export are DD/MM/YYYY; the base currency is
assumed to be EUR. The converter derives the tax period from the FlexStatement
dates and rejects partial-year exports. Export one account in one
FlexStatement per file; multi-statement exports are rejected rather than
silently omitting accounts.
Enable the Summary level of detail for Open Positions and the Detail
level for Cash Transactions; Summary cash rows are ignored so income is not
counted twice. Include ibEntity in Account Information: it sets the bank
account country (IB-UK → GB, IB-LLC → US, IB-IE → IE, …), and an unknown or
missing entity falls back to GB with a warning.
Tax data the converter cannot report stops conversion with an error instead of
being left out: open positions without an ISIN (for example options or
futures), bond interest, 871(m) Withholding, and dividends without an ISIN.
Deposits and fees are ignored; other unrecognised cash transaction types
produce a warning.
Enable ISIN, Currency, Quantity, Ex Date, Pay Date, Gross Amount, and the account/contract/model/action identifiers in Change in Dividend Accruals. Include the corresponding identifiers and Ex Date in Cash Transactions when available. IBKR defines the accrual quantity as the quantity held before ex-date; that quantity and ex-date are preserved for payouts and linked adjustments, including securities sold before year-end.
Accrual postings and reversals are metadata, not additional income. Consistent records are used once; conflicting records, mismatched gross amounts, missing entitlements and unlinked adjustments stop conversion with an error, before writing output. Refunds and dividend reversals need an action ID or ex-date link to their entitlement. Separate same-day events remain separate when their identifiers differ. A split dividend/payment-in-lieu event is accepted only when its combined gross amount matches one unambiguous accrual.
No closing-quantity, zero-quantity or trade-history fallback is used. Older
exports must be regenerated with the required metadata; non-positive accrual
quantities and unresolved corrections require manual reconciliation.
The anonymized data/Tax.xml sample has no security dividend payments and
therefore needs no accrual rows. To exercise historical dividend quantities,
use the synthetic example tests/fixtures/dividend_accruals.xml:
python convert.py tests/fixtures/dividend_accruals.xml output.xml
Foreign withholding is recorded, but the FlexQuery does not establish the
treaty-limited, non-recoverable amount or your DA-1 eligibility. The converter
warns when manual confirmation is needed: it does not set the DA-1 eligibility
flag or per-payment non-recoverable amount, and the required
totalNonRecoverableTax subtotal remains zero (no credit claimed). Confirm and
enter the eligible amount in your tax application before filing. Foreign tax
must not be interpreted as a Swiss withHoldingTaxClaim.
Bank-account payments have no foreign-tax amount field in eCH-0196. Foreign tax on cash interest is therefore preserved in XML payment annotations, with a warning, rather than included in Swiss claims or securities-tax totals.
Withholding refunds are signed adjustments, converted on their own booking
dates; they reduce the corresponding tax totals. A withholding row is linked to
same-day income when none of the identifiers both rows report (action ID,
contract, model, ex-date) differ, since IBKR may omit the ex-date or action ID
on tax rows; refunds need a shared action ID or ex-date. Tax in the income's
currency is reported on the income's own payment. Payments in different
currencies remain separate, but Swiss withholding still classifies its linked
income as grossRevenueA regardless of currency. Other income on the same day,
such as a withholding-free distribution, stays grossRevenueB, and refunds
alone do not reclassify new positive income. Unmatched or ambiguous Swiss
withholding produces a warning to confirm the A/B classification manually.
Multiple holdings with the same ISIN are
consolidated in the first listing's currency so their combined value is retained
and dividend income is reported only once.
src/parse_ibkr.py # FlexQuery XML → dataclasses
src/dividend_entitlements.py # Match cash payments to historical accrual metadata
src/generate_ech196.py # dataclasses → eCH-0196 XML tree (+ XSD validation)
src/generate_barcode_pdf.py # eCH-0196 XML → eCH-0270 barcode PDF
convert.py # CLI: parse → build → validate → write [→ PDF]
tests/ # pytest suite
The barcode PDF compresses the eCH-0196 XML with ZLIB, encodes it as PDF417
Structured Append (13×35, EC level 4, native 290×35 px, 4-integer Macro file id
per BEIL2 §2.2), and lays out a portrait statement page followed by rotated
barcode sheet(s). These details were confirmed by decoding real accepted
reference statements; see AGENTS.md for the full technical notes
and links to the eCH-0196 / eCH-0270 specifications.
The statement page uses the built-in Helvetica font, which only covers cp1252:
letters outside it print as their base letter (Dvořák → Dvorák), while the
XML keeps the exact text. Its totals list security income, account interest
and debt interest separately, so they reconcile with the statement totals.
pip install -r requirements-dev.txt
black --check .
ruff check .
python -m pytest
requirements-dev.txt pins the Black and Ruff versions CI uses; Dependabot
bumps them weekly.
The suite covers parsing, FX→CHF conversion, eCH-0196 generation, an end-to-end
XSD validation, and barcode round-trip/structure (the barcode tests self-skip if
their optional decode dependencies are absent). XSD tests skip without
documentation/eCH-0196-2-2.xsd; CI downloads it first and fails if it cannot.
CI exercises Python 3.12.
Release-script tests mock GitHub APIs and require Node.js 24; they skip locally
when Node is absent, while CI installs it explicitly.
pip install -r requirements-build.txt
python -m PyInstaller --noconfirm --clean ibkr-etaxstatement.spec
The portable x64 executable is written to dist\ibkr-etaxstatement.exe.
.github/workflows/release.yml runs when a stable vMAJOR.MINOR.PATCH tag is
pushed. The tagged commit must belong to main, and its version must match
convert.py and both string and numeric versions in
packaging/windows-version-info.txt. Before tagging, merge the release changes
to main with green CI. Do not move an existing published tag.
The workflow runs the reusable CI workflow against the tagged source, builds a
Windows x64 executable, checks its CLI version, generates manifests from
packaging/winget/templates, and validates them with WinGet. The executable's
SHA-256 is calculated from that exact build, not copied from an older release.
The runner uses a checksum-pinned official WinGet CLI bundle before validation;
preinstalled runner versions may not recognize the manifest schema headers.
The native validator runs without AppX registration, which can fail on hosted
images. Its version and SHA-256 are pinned in .github/scripts/setup-winget.ps1.
It uploads the executable, SHA256SUMS, and three manifest files to a draft
GitHub release, publishes it only after every asset upload succeeds, then opens
the microsoft/winget-pkgs pull request in the same workflow. Building and
uploading an executable manually is no longer required.
To prepare and validate assets locally without publishing (use a fresh output directory):
python .github\scripts\prepare_release.py --tag v0.4.1
python -m PyInstaller --noconfirm --clean ibkr-etaxstatement.spec
python .github\scripts\prepare_release.py --tag v0.4.1 --installer dist\ibkr-etaxstatement.exe --output dist\release-0.4.1
winget validate --manifest dist\release-0.4.1\winget
Once ready, push a new version tag to trigger publication. No release is created by the local preparation commands above.
WinGet submission requires:
jvonscheidt/winget-pkgs fork.WINGET_TOKEN repository secret containing a classic GitHub PAT with the
public_repo scope.The submission branches from the fork's existing master; it does not sync or
modify the fork's default branch or any workflow files. The PAT therefore does
not need the workflow scope. Initial packages and subsequent versions use the
same generated-manifest submission path.
If WinGet submission fails after GitHub publication, rerun only failed jobs in that tag's Actions run. An existing open/merged PR or accepted version is not submitted twice. Published release assets are never overwritten; rerunning the entire workflow stops at an already-published release. A failed upload leaves a draft that can be completed by retrying.
After submission, complete the upstream checklist and CLA, and resolve any installer/Defender findings before merge. Successful GitHub publication or PR creation does not mean the package is available in WinGet. The existing v0.3.0 submission's installation block is not bypassed by this workflow.