mirror of
https://github.com/farcasclaudiu/xtb-investment-tools.git
synced 2026-06-22 07:01:58 +03:00
259 lines
16 KiB
Markdown
259 lines
16 KiB
Markdown
# 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 1–4**: 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`).
|