Files
xtb-investment-tools/README.md
T
2026-06-21 13:00:30 +03:00

259 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# XTB Portfolio Review & Wealthfolio Exporter
A set of Python tools that turn an **XTB brokerage report** (`.xlsx` export) into:
1. A complete, human-readable **portfolio review** (console and a self-contained HTML report with interactive, offline charts and analysis tables).
2. A **Wealthfolio-compatible CSV** so the same XTB history can be imported into the [Wealthfolio](https://wealthfolio.app/) portfolio tracker.
The parser is generic for XTB exports in this format. Tests generate a small
synthetic workbook at runtime, while personal brokerage exports should stay
local and untracked.
---
## Background: the XTB export format
An XTB report is an `.xlsx` file with a fixed layout:
- **Rows 14**: metadata (account number, report period).
- **Row 5** (`header=4`): the actual column headers.
- **Sheets**:
- `Closed Positions` — realized trades, with a `Profit/Loss` column. May contain a
`Profit/loss` summary row and/or be empty (all positions still open).
- `Cash Operations` — every cash flow: stock purchases/sales, deposits, withdrawals,
dividends, dividend tax, free-funds interest, currency conversions. Each trade row
carries a comment like `OPEN BUY 6 @ 301.50` or `CLOSE SELL 2 @ 100.00`, and the
sheet ends with a `Total` row (the broker-reported ending cash balance).
Two quirks the code handles explicitly:
- **Header is on row 5**, not row 1.
- **Split-fill quantity notation**: `OPEN BUY 1/100 @ 14.3130` means *1 share out of a
100-share parent order* — the numerator is the executed quantity. The tools use the
numerator (falling back to `cash / price`) rather than mis-reading `1/100` as `0.01`.
- **Stock-sale close notation**: some XTB stock-sale rows are written as
`CLOSE BUY ...` while the row type is `Stock sell` and the amount is positive
sale proceeds. The tools treat these as sales for holdings, cash flows, and
Wealthfolio export.
---
## Files
### Source code
| File | Purpose |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skills/xtb-portfolio-review/scripts/main.py` | **Portfolio review generator.** Parses the XTB report, reconstructs trades from Cash Operations comments, runs FIFO lot-matching for realized P/L, computes cash flows, holdings (cost basis), performance metrics, contribution/risk/income analysis, and reconciliation against the broker's `Total` row. Outputs a console report and a self-contained HTML report with interactive Chart.js charts and offline table tools (bundled inline, no internet required). |
| `skills/xtb-wealthfolio-export/scripts/exporter.py` | **XTB → Wealthfolio CSV exporter.** Maps each Cash Operation to a Wealthfolio row (`date,symbol,quantity,activityType,unitPrice,currency,fee`). |
| `main.py`, `exporter.py`, `html_charts.py` | Thin compatibility entry points that preserve the original repo commands/imports while delegating to the bundled skill implementations. |
### Tests
| File | Purpose |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test_portfolio.py` | Unit + integration tests for `main.py` (parsing, FIFO realized P/L, cash-flow categorization, income, open positions, performance, analysis helpers, reconciliation against the generated synthetic workbook, HTML structure and interactions). |
| `test_exporter.py` | Tests for `exporter.py` (activity-type classification, the split-fill quantity parser, full row mapping, schema validation on the generated synthetic workbook, empty-input handling). |
### Local inputs
Personal `.xlsx` exports are not committed. Place your XTB report in the repo
folder when running the tools locally, or pass its path explicitly.
### Generated outputs (regenerated by running the tools)
All generated files are written to the **`results/`** folder (created
automatically) and **named after the input report**: for input
`EUR_demo_report.xlsx` every output uses that stem plus a descriptor, e.g.
`EUR_demo_report_review.html`.
| File | Produced by | Content |
| ------------------------------------------------- | ------------- | --------------------------------------------------------------------------- |
| `results/<stem>_review.html` | `main.py` | Self-contained HTML report with interactive Chart.js charts, analysis sections, sortable/filterable tables, sticky navigation, and print/PDF styles; works offline. |
| `results/<stem>_holdings.csv` | `main.py` | Open holdings: ticker, shares, avg cost, cost basis, return %, allocation %.|
| `results/<stem>_cash_flows.csv` | `main.py` | Aggregated cash flows (deposits, interest, dividends, invested, …). |
| `results/<stem>_realized_pl.csv` | `main.py` | Realized P/L per ticker. |
| `results/<stem>_open_positions.csv` | `main.py` | Live market value / unrealized P/L (when an `Open Positions` sheet exists). |
| `results/<stem>_performance.csv` | `main.py` | Performance metrics (portfolio value, returns, yield). |
| `results/<stem>_income.csv` | `main.py` | Income (dividends + interest) by month. |
| `results/<stem>_evolution.csv` | `main.py` | Daily cost / market value / realized P/L series (drives the evolution chart).|
| `results/<stem>_wealthfolio.csv` | `exporter.py` | Wealthfolio-importable transaction history. |
---
## Setup
Requires Python 3.10+.
```bash
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -r requirements.txt
```
> The HTML report bundles Chart.js v4.5.1 (vendored inside each relevant skill at `scripts/assets/chartjs.umd.min.js`, version pinned in `scripts/assets/chartjs.VERSION`) so its charts render interactively with no internet connection.
## Agent skills
This repository includes harness-neutral agent skills under `skills/` for users
who want an LLM or coding agent to operate the tools consistently. Each skill is
a self-contained folder with a `SKILL.md`, `references/`, and bundled
`scripts/`, so users can copy a skill folder to another machine and still run
the relevant XTB workflow without cloning the full repository.
Agents should start with [`INSTALL_FOR_AGENTS.md`](INSTALL_FOR_AGENTS.md) for
copy/install/use instructions.
| Skill | Purpose |
| ----- | ------- |
| `xtb-portfolio-review` | Generate and verify XTB portfolio review reports, including reconciliation, holdings, performance, income, risk, and data-quality caveats. |
| `xtb-wealthfolio-export` | Export and validate Wealthfolio-compatible CSV files from XTB reports, including activity mappings and import-readiness checks. |
Use the skill folder directly, or copy it into the skill/instruction directory
for your harness. With a generic LLM, ask it to read the relevant `SKILL.md`.
For Codex, you can also copy either folder into `~/.codex/skills/`, then invoke
it in a new session:
```text
Use $xtb-portfolio-review to generate and verify an XTB portfolio report.
Use $xtb-wealthfolio-export to create and validate a Wealthfolio CSV from an XTB report.
```
Each copied skill folder includes `scripts/requirements.txt` plus shell wrappers
for environment setup, validation, and execution. From the directory where you
want `.venv` and `results/` to live, install dependencies with:
```bash
skills/xtb-portfolio-review/scripts/setup-env.sh
skills/xtb-wealthfolio-export/scripts/setup-env.sh
```
## Usage
### Generate the portfolio review
```bash
.venv/bin/python main.py # auto-detects the only .xlsx in the folder
.venv/bin/python main.py EUR_demo_report.xlsx # explicit report
.venv/bin/python main.py --csv # also write the CSV outputs
```
By default only the self-contained **HTML report** (with inline interactive
charts and table tools) is written to `results/`. Pass `--csv` to additionally
export the per-section CSVs (holdings, cash flows, performance, …).
If no path is given and exactly one `.xlsx` is present in the current
directory, it is used automatically; if there are none or several, pass the
path explicitly. Any same-format XTB export works — the currency is
auto-detected from the filename prefix (e.g. `EUR_…`, `USD_…`).
### HTML report features
The generated review HTML is a single offline file. It includes:
- **Executive Summary** — largest holding, top unrealized winner/loser, cash
allocation, pricing warnings, and reconciliation status.
- **Concentration & Risk** — top-1/top-3/top-5 position weights, cash weight,
positions above 20%, and cost-priced position count.
- **Income Quality** — gross income, dividend tax, net income, tax drag, net
income yield, and dividend/interest mix.
- **Methodology & Data Quality** — live-vs-cost pricing coverage, cost fallback
tickers, reconciliation status, and the main calculation assumptions.
- **Return Contribution** — per-ticker market value, unrealized P/L, realized
P/L, total contribution, and contribution % of total gain.
- **Interactive charts** — portfolio evolution, holdings allocation, cash flows,
and income over time when data exists.
- **Offline table tools** — data tables are sortable and filterable in the
browser without any external JavaScript.
- **Navigation and print support** — a sticky section nav for browsing and
print/PDF styles for cleaner exported reports.
### Export to Wealthfolio CSV
```bash
.venv/bin/python exporter.py # uses the default report -> results/<stem>_wealthfolio.csv
.venv/bin/python exporter.py EUR_other.xlsx -o my.csv # explicit input/output
```
### Run the tests
```bash
.venv/bin/python -m pytest -q
```
---
## How the review is computed
- **Trades** are reconstructed from the `OPEN/CLOSE BUY/SELL … @ price` comments in
Cash Operations (the `Closed Positions` sheet is often empty for still-open accounts).
Trades are keyed by the **real `Ticker`** column (e.g. `SPYL.DE`), so descriptive
variants of the same instrument merge into a single holding. Trades are processed in
**chronological order** — XTB sheets sometimes list a position's close leg before its
open leg, so date-ordering is required for correct FIFO lot matching.
- **Holdings** are the net open lots per ticker at cost basis, with allocation %.
- **Live market value** is fetched via [`yfinance`](https://github.com/ranaroussi/yfinance)
for the last trading day on/before the report's `Date to`. The close is taken in the
symbol's native currency and converted to the account currency when needed. Any ticker
that can't be priced (delisted / not on Yahoo) falls back to **cost basis** and is
flagged `price_source = "cost"` in the holdings CSV and report.
- **Realized P/L** prefers the broker's `Closed Positions` `Profit/Loss` column; when that
is absent, it falls back to **FIFO lot matching** from CLOSE trades.
- **Cash flows** are categorized (deposits, withdrawals, interest, dividends, dividend tax,
FX fees, invested, proceeds) and reconciled against the broker's `Total` (ending cash).
- **Performance** combines **live market value** (or cost basis fallback) with cash to give
portfolio value, total gain, total return %, money-weighted return (XIRR), and
income yield. XIRR uses external deposits/withdrawals plus terminal portfolio
value; dividends and interest are not treated as external cash flows unless
they leave the account as withdrawals.
- **Return contribution** combines each open holding's unrealized P/L with any
realized P/L by ticker, then expresses the result as a share of total gain.
- **Concentration & risk** is derived from market-value weights, cash weight, and
pricing source coverage; it flags large top holdings and cost-priced positions.
- **Income quality** separates gross income, dividend tax, net income, tax drag,
net income yield, and the dividend-vs-interest mix.
- **Evolution chart** replays the trades chronologically and, for each trading day,
computes the open cost basis, the open market value (from historical closes via
yfinance, falling back to cost for unpriced tickers), and cumulative realized P/L.
The gap between the **Cost** and **Value (realized + unrealized)** lines is the total
investment gain/loss. Daily series is persisted to `results/<stem>_evolution.csv`
when `--csv` is used.
### Wealthfolio activity mapping
| XTB operation | Wealthfolio `activityType` |
| ------------------------------------------------- | -------------------------- |
| `Stock purchase` / `OPEN BUY` | `BUY` |
| `Stock sale` / `CLOSE SELL` / `OPEN SELL` (short) | `SELL` |
| `Deposit` / `Withdrawal` | `DEPOSIT` / `WITHDRAWAL` |
| `Dividend` | `DIVIDEND` |
| `Free funds interest` | `INTEREST` |
| `Dividend tax` | `TAX` |
| `Currency conversion` | `FEE` |
Per the Wealthfolio [CSV spec](https://wealthfolio.app/docs/guide/csv-import/), cash
activities (`DEPOSIT`/`WITHDRAWAL`/`DIVIDEND`/`INTEREST`/`TAX`/`FEE`) carry their total
value in the `amount` column with `quantity = 1` and `unitPrice = 1`; the `fee` column is
only used for inline `BUY`/`SELL` commissions. Pure-cash rows use the `$CASH-<CCY>` symbol
(e.g. `$CASH-EUR`), while `DIVIDEND` keeps the security's real ticker. `BUY`/`SELL` leave
`amount` blank — Wealthfolio auto-calculates it as `quantity × unitPrice`.
---
## Notes & limitations
- **Live prices** are daily closes from yfinance, taken for the last trading day on or
before the report's `Date to`. A symbol that can't be resolved (e.g. some proprietary
XTB instrument codes) is valued at cost and flagged with `price_source = "cost"`.
- **Cost fallback positions** carry zero unrealized P/L in the report, contribution
table, and evolution chart. The methodology section lists every cost fallback ticker.
- **Money-weighted return (XIRR)** requires at least one external cash outflow and
one inflow. When the dated cash-flow series cannot be solved, the report shows `n/a`.
- **Reconciliation** compares computed ending cash against the XTB `Total` row; it reports
`[OK]` when they match within €0.01.
- **HTML interactions** (charts, sorting, filtering, sticky navigation) are all inline
and offline; no CDN or network access is required to open the generated report.
- Thousand-separators are intentionally **not** parsed in numeric fields (ambiguous with
decimal dot); XTB's plain decimal format is handled correctly.
- All generated artifacts go to `results/` (git-ignored via `.gitignore`).