Files

354 lines
26 KiB
Markdown
Raw Permalink 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.
# Finance Dashboard
Privates, schlankes Dashboard für passives Einkommen auf pinguAurora. FastAPI, Jinja2 und SQLite; Python 3.13 im Docker-Image. Kein Node, keine externen Finanz-APIs. Docker-Port **8081 → 8080**, Daten-Volume **./data → /data**, RAM-Limit **128 MiB** bleiben bestehen.
## Funktionen
- Acht Kennzahlen, dynamischer Monats-/Jahresvergleich und drei Chart.js-Diagramme.
- Zahlungen anlegen, bearbeiten, mit Bestätigung löschen; Positionen ergänzen.
- Vollständige Historie unter `/income`, nach Jahr, Monat, Position und Kategorie filterbar, 100 Buchungen pro Seite.
- CSV-Export, wiederholbarer XLSX-Import und getrennte Erfassung erwarteter/erhaltener Zahlungen.
- Alle Summen basieren ausschließlich auf `received=1`. Der Jahresvergleich vergleicht das aktuelle Kalenderjahr mit dem **gesamten** Vorjahr, kein YTD-Vergleich. Bei Vorjahreswert 0 erscheint ``.
- Jahres-Spalten entstehen aus den vorhandenen Buchungsjahren, dem aktuellen Jahr und dem nächsten Kalenderjahr als Ausblick; Lücken werden ergänzt. Es sind keine konkreten Jahreszahlen im Dashboard fest programmiert.
## Lokale Entwicklung
```bash
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
export FINANCE_DB_PATH="$PWD/data/finance.db"
uvicorn main:app --app-dir app --reload --host 127.0.0.1 --port 8081
```
`requirements.txt` enthält nur die Web-Abhängigkeiten, einschließlich `python-multipart` für HTML-Formulare. SQLite ist Teil von Python. `openpyxl` wird nur beim Import benötigt; `httpx` nur für Tests. Templates und statische Dateien werden relativ zum Anwendungscode gefunden, im Container also unter `/app/templates` und `/app/static`.
```bash
.venv/bin/python -m unittest discover -s tests -v
docker compose config
```
Tests verwenden ausschließlich temporäre Datenbanken. Die Entwicklungsbibliotheken werden nicht im Produktionsimage installiert.
## Docker Build und Start
```bash
DOCKER_BUILDKIT=0 docker build \
-t finance-dashboard-finance-dashboard:latest \
.
docker compose up -d --no-build
curl --fail http://127.0.0.1:8081/health
```
Die App ist unter `http://localhost:8081` erreichbar. `/health` liefert `{"status":"ok"}`. `python:3.13-slim` wird ohne feste CPU-Plattform verwendet; auf dem Raspberry wird nativ für ARM64 gebaut.
## SQLite und Geldbeträge
Standardpfad: **/data/finance.db**, persistent auf dem Host als **./data/finance.db**. Für lokale Entwicklung/Import ist `FINANCE_DB_PATH` oder beim Import `--db` verfügbar. Tabellen und die neun Grundpositionen werden beim Start automatisch angelegt.
`income_entries.amount` enthält **ganze Cent (INTEGER)**, keine Euro-Floats. Python summiert Integer und berechnet Prozentwerte mit Decimal. Formulare akzeptieren `0,04`, `0.04` und `28,00`, ohne Tausendertrennzeichen. Mehr als zwei Nachkommastellen werden abgelehnt. Negative Beträge sind für Korrekturen erlaubt. Nur Chart.js verwendet für die grafische Anzeige JavaScript-Zahlen; das ändert keine Finanzwerte in SQLite.
SQLite nutzt Foreign Keys, WAL, kurze Transaktionen und fünf Sekunden Wartezeit bei Locks. Referenzierte Positionen können nicht physisch gelöscht werden; `active=0` deaktiviert sie für neue Buchungen, vorhandene Historie bleibt erhalten. Bei temporären Datenbankproblemen antwortet die App mit HTTP 503. Schema-Version 3 wird über `PRAGMA user_version` geführt.
`expected=1, received=0` bezeichnet eine offene/ausgefallene erwartete Zahlung. Der Betrag enthält dann die Erwartung, fließt aber **nicht** in tatsächliche Summen ein. Eine teilweise erhaltene Zahlung wird als erhaltene Buchung plus separate offene Restbuchung erfasst. Es gibt noch keine automatische Prognose oder Fälligkeitsverwaltung.
## Excel-Import
SQLite ist die primäre Datenquelle. Excel wird nach dem Import nicht im laufenden Dashboard gelesen. Die XLSX-Datei bleibt lokal im ignorierten Verzeichnis `import/`.
```bash
source .venv/bin/activate
pip install -r requirements-import.txt
python scripts/import_excel.py import/tr_verbessert_2027_vergleiche_enbridge.xlsx \
--db ./data/finance.db --dry-run
python scripts/import_excel.py import/tr_verbessert_2027_vergleiche_enbridge.xlsx \
--db ./data/finance.db
```
Ohne `--db` verwendet das Script `FINANCE_DB_PATH` bzw. `/data/finance.db`:
```bash
python scripts/import_excel.py /pfad/datei.xlsx
```
Es wird nur das **erste Tabellenblatt** gelesen. Der Import sucht den Buchungskopf `Datum`, `Art des Ertrags`/`Position`, `Betrag (€)` und `Kategorie`. Dashboard-Zellen davor und weitere Blätter werden ignoriert. Ungültige Buchungen oder Formeln innerhalb der Buchungsfelder brechen den gesamten Import mit Zeilenangabe ab; es werden keine Teilimporte gespeichert. `--dry-run` rollt Buchungsänderungen zurück, legt jedoch bei Bedarf Datenbank und Seed an.
Bekannte Aliase werden normalisiert: **MSC → Main Street Capital**, CSW/CSWC → Capital Southwest, PC → Prospect Capital, Stoxx → STOXX Global Select Dividend 100, Air Baltic → airBaltic. Präfixe wie „Dividende“ werden entfernt. Groß-/Kleinschreibung, Leerzeichen und Satzzeichen führen nicht zu neuen Positionen. Unbekannte Positionen werden angelegt; unsichere Namen werden nicht anhand von Ähnlichkeit zusammengelegt. Historische airBaltic-Buchungen mit Dividendenkategorie werden als Anleihezinsen übernommen und mit einer Notiz gekennzeichnet.
Dublettenidentität: Datum, normalisierte Position, Kategorie, Centbetrag, Erwartet-/Erhalten-Status und Vorkommensnummer. Dadurch bleiben mehrere identische echte Zahlungen in einer Datei erhalten, während wiederholte Imports und passende manuelle Buchungen wiederverwendet werden. `import_records` merkt sich den Import auch nach Bearbeiten/Löschen einer Buchung; ein erneuter Import stellt gelöschte Buchungen nicht wieder her. Änderungen an identitätsbildenden Excel-Feldern gelten als neue Buchungen: Korrekturen nach dem Erstimport deshalb im Dashboard vornehmen. Die Excel anschließend als Archiv behandeln.
Die vorliegende Excel besitzt im Ertragsbuch keine Erwartet-/Erhalten-Spalten. Diese historischen Buchungen gelten als erhalten. Aus widersprüchlichen Dashboard-Texten werden **keine zusätzlichen Zahlungen oder Ausfälle erfunden**. Falls eine historische Buchung tatsächlich ausgefallen ist, ihren Status nach fachlicher Prüfung im Dashboard korrigieren. Optionale Importspalten `Erwartet`/`Erhalten` unterstützen Ja/Nein, true/false und 1/0.
Das Script zeigt einen Plausibilitätscheck: September 2026 **3,71 €**, einschließlich Enbridge am **02.09.2026 mit 0,04 €**. Abweichungen werden gemeldet, nicht durch erfundene Buchungen ausgeglichen.
Falls `data/` bereits durch Docker angelegt wurde und für deinen lokalen Benutzer nicht beschreibbar ist, kann der Import im temporären Container laufen. Die Importbibliothek wird dabei nicht im Produktionsimage gespeichert:
```bash
docker compose run --rm --no-deps -v "$PWD:/workspace:ro" finance-dashboard sh -c \
'pip install --no-cache-dir --target /tmp/import-deps openpyxl && PYTHONPATH=/tmp/import-deps python /workspace/scripts/import_excel.py /workspace/import/tr_verbessert_2027_vergleiche_enbridge.xlsx'
```
## CSV-Export
`/export/income.csv` exportiert **alle** Buchungen einschließlich offener Zahlungen: Datum, Position, Kategorie, Betrag, Notiz, Erwartet, Erhalten. UTF-8 mit BOM, Semikolon, Dezimalkomma und CRLF für Excel. Gefährliche Formelpräfixe in Textfeldern erhalten ein schützendes Apostroph. Export ist eine Buchungsliste, kein vollständiges Datenbankbackup.
## Backup und Wiederherstellung
`data/`, SQLite-Dateien, Excel-Dateien und `.env` werden von Git ausgeschlossen. `.dockerignore` hält sie auch aus dem Build-Kontext fern. **Nie nur die laufende SQLite-Hauptdatei kopieren:** Im WAL-Modus können neuere Transaktionen noch in `-wal` liegen.
Konsistentes Backup während des Betriebs mit der SQLite-Backup-API:
```bash
mkdir -p backups
docker compose exec -T finance-dashboard python -c 'import sqlite3; src=sqlite3.connect("/data/finance.db"); dst=sqlite3.connect("/data/finance-backup.db"); src.backup(dst); dst.close(); src.close()'
cp data/finance-backup.db "backups/finance-$(date +%Y%m%d-%H%M%S).db"
```
Backup zusätzlich auf einem anderen Datenträger sichern. Wiederherstellung nur bei gestoppter App: vorhandenes `data/` vollständig beiseite sichern (inkl. WAL/SHM), ein frisches `data/` anlegen, die Backup-Datei dort als `finance.db` einsetzen und Dateirechte prüfen. Anschließend `docker compose up -d --no-build`. Das Deploy-Script führt keine Wiederherstellung durch und löscht keine Daten.
## Deployment auf pinguAurora
Wegen des aktuellen Buildx-Versionskonflikts **nicht `docker compose up -d --build` verwenden**. Im bestehenden Checkout auf dem Raspberry:
```bash
git pull
DOCKER_BUILDKIT=0 docker build \
-t finance-dashboard-finance-dashboard:latest \
.
docker compose up -d --no-build
```
Oder im Checkout einfach:
```bash
./deploy.sh
```
Das ausführbare Script wechselt in sein eigenes Projektverzeichnis, nutzt `git pull --ff-only`, führt genau den klassischen Build und den Start ohne Build aus, zeigt den Containerstatus und prüft `http://127.0.0.1:8081/health` mit Wiederholungen. Bei einem Fehler bricht es ab. Es verändert keine Datenbankdateien. Zugriff: `http://pinguAurora:8081`.
Die lokale Datenbank wird **nicht mit Git übertragen**. Für die Erstübernahme auf dem Raspberry entweder die XLSX-Datei separat übertragen und das Import-Script dort in einer Python-Umgebung mit `requirements-import.txt` gegen `./data/finance.db` ausführen, oder ein konsistentes SQLite-Backup vor dem ersten Start in das dortige `data/` übernehmen. Vor einem Import in einen vorhandenen Datenbestand ein Backup erstellen.
## Betrieb und Quellen
Nur für das private LAN, ohne Benutzerverwaltung. Validierung, SQL-Parameterbindung, Jinja-Autoescaping und Prüfung fremder Browser-Formularursprünge sind enthalten. Chart.js wird fest versioniert vom CDN geladen; ohne Internet funktionieren Buchungen, Kennzahlen und Tabellen weiter. Die Diagramme benötigen Zugang zum CDN.
Implementierungsreferenzen: [FastAPI Templates](https://fastapi.tiangolo.com/advanced/templates/) und [Chart.js Integration](https://www.chartjs.org/docs/latest/getting-started/integration.html).
## REST-API aktivieren
Die API läuft im bestehenden FastAPI-Prozess unter **`/api/v1`** und verwendet dieselben Services und dieselbe SQLite-Datenbank wie die Weboberfläche. Es gibt keine neue Datenhaltung und keine zusätzlichen Laufzeit-Abhängigkeiten.
1. Ein langes zufälliges Token erzeugen:
```bash
python -c 'import secrets; print(secrets.token_urlsafe(32))'
```
2. Im Projektverzeichnis eine `.env` anlegen bzw. die vorhandene Datei ergänzen:
```dotenv
FINANCE_API_TOKEN=<langes-zufälliges-token>
```
`.env.example` enthält nur den Platzhalter `change-me`. Diesen durch das erzeugte Token ersetzen; `change-me` aktiviert die API ausdrücklich nicht. `.env` bleibt in `.gitignore` und `.dockerignore`. Ein echtes Token gehört niemals ins Repository.
3. Den Container nach einer Tokenänderung neu erstellen:
```bash
docker compose up -d --no-build --force-recreate
```
Compose liest `.env` automatisch und reicht `FINANCE_API_TOKEN` weiter. Bei direktem Uvicorn-Start muss die Variable in der Prozessumgebung gesetzt sein. Ohne Token, bei leerem Token oder beim Beispiel-Platzhalter antwortet die API mit **503**. Weboberfläche und `/health` funktionieren weiterhin. Ein fehlender/falscher Bearer-Header bei aktivierter API führt zu **401** mit `WWW-Authenticate: Bearer`. Der Vergleich erfolgt mit `secrets.compare_digest`. Tokens werden nicht protokolliert. CORS bleibt deaktiviert.
Für die folgenden curl-Beispiele muss `FINANCE_API_TOKEN` auch in der aufrufenden Shell gesetzt sein. Im eigenen Projektverzeichnis kann die selbst angelegte `.env` geladen werden:
```bash
set -a
. ./.env
set +a
curl \
-H "Authorization: Bearer $FINANCE_API_TOKEN" \
http://pinguAurora:8081/api/v1/assets
```
### Endpunkte
Alle folgenden Pfade beginnen mit `/api/v1` und erfordern den Bearer-Header, auch `/meta`.
| Methode | Pfad | Verhalten |
| --- | --- | --- |
| GET | `/assets` | Alle Positionen, optional `active=true/false` |
| GET | `/assets/{id}` | Einzelne Position, sonst 404 |
| POST | `/assets` | Position erstellen, 201; normalisierte Duplikate 409 |
| PATCH | `/assets/{id}` | Nur übergebene Felder ändern |
| DELETE | `/assets/{id}` | Position deaktivieren (`active=false`), 204; Historie bleibt erhalten |
| GET | `/income` | Historie, neueste zuerst; Filter und Pagination |
| GET | `/income/{id}` | Einzelne Buchung, sonst 404 |
| POST | `/income` | Zahlung erstellen und zurückgeben, 201 |
| PATCH | `/income/{id}` | Teiländerung, aktualisierte Zahlung zurückgeben |
| DELETE | `/income/{id}` | Buchung löschen, 204 ohne Inhalt |
| GET | `/stats/summary` | Dieselben acht Kennzahlen wie im Dashboard |
| GET | `/stats/monthly` | Monatswerte je Jahr, optional `year` |
| GET | `/stats/by-asset` | Betrag und Anteil je Position, alle Jahre |
| GET | `/stats/by-category` | Betrag und Anteil für alle vier Kategorien |
| GET | `/meta` | Name, API-Version und Datenbankstatus ohne Systemdetails |
`GET /income` unterstützt `year`, `month`, `asset_id`, `category`, `received`, `expected`, `limit` (Standard 100, maximal 1000) und `offset` (Standard 0). Sortierung: Datum absteigend, bei gleichem Datum ID absteigend. Listen werden als JSON-Arrays geliefert. `asset` in Buchungen und Anteilen ist der normalisierte Positionsname; `asset_id` identifiziert die Position eindeutig.
Geldbeträge werden im JSON **ausschließlich als Strings in Euro** mit zwei Nachkommastellen ausgegeben, z. B. `"0.04"`. Auch Eingaben müssen Dezimalstrings sein (`"0.04"` oder `"0,04"`); JSON-Fließkommazahlen werden abgelehnt. Intern bleibt es bei Decimal und Integer-Cent. Prozentwerte sind ebenfalls Strings mit zwei Nachkommastellen; bei Nenner 0 ist der Wert `null`. Ein Anteil wird relativ zur tatsächlichen Gesamtsumme berechnet, inklusive negativer Korrekturen.
`/stats/monthly` liefert ein Array aus `{year, months: [{month, amount}], total}` mit zwölf Monaten pro Jahr. Ein explizit angefragtes Jahr ohne Buchungen liefert Nullbeträge. Alle Statistik-Endpunkte zählen ausschließlich `received=true`; `expected=true, received=false` erhöht keine tatsächliche Einnahme. Der Jahresvergleich bleibt aktuelles Jahr gegen gesamtes Vorjahr. `/stats/by-category` trennt `dividend` und `distribution`; das Dashboard-Diagramm fasst sie zur Anzeige zusammen.
Unbekannte IDs liefern 404, doppelte Positionsnamen 409 und ungültige Felder/Referenzen 422. `PATCH` lässt fehlende Felder unverändert; `note: null` bzw. `ticker: null` leert optionale Felder. Andere Felder dürfen nicht `null` sein. Historische Buchungen deaktivierter Positionen bleiben lesbar und bearbeitbar; neue Buchungen für inaktive Positionen werden abgelehnt. Reaktivierung erfolgt über `PATCH /assets/{id}` mit `{"active":true}`. Datenbank-Locks führen nach der bestehenden Wartezeit zu 503 mit Wiederholungshinweis; interne Fehler zu 500 ohne Stacktrace oder interne Details.
### Zahlung per API erstellen
Zuerst die passende `asset_id` über `/assets` ermitteln; die ID im folgenden Beispiel ersetzen:
```bash
curl -X POST \
-H "Authorization: Bearer $FINANCE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"date":"2026-09-02",
"asset_id":1,
"category":"dividend",
"amount":"0.04",
"note":"Test",
"expected":false,
"received":true
}' \
http://pinguAurora:8081/api/v1/income
```
POST legt jeweils eine echte neue Zahlung an; die Dublettenerkennung des Excel-Imports gilt nicht für manuelle/API-Buchungen.
Swagger: **http://pinguAurora:8081/docs**. Unter **Authorize** nur das Token eingeben; Swagger ergänzt `Bearer`. OpenAPI: `/openapi.json`. Die Tags heißen Assets, Income und Stats. Die Dokumentation ist lesbar, die dokumentierten API-Aufrufe erfordern Authentifizierung.
### API-Deployment auf pinguAurora
Einmalig `.env` mit einem echten Token auf dem Raspberry anlegen. Dann weiterhin der bestehende Weg ohne Buildx:
```bash
git pull
DOCKER_BUILDKIT=0 docker build \
-t finance-dashboard-finance-dashboard:latest \
.
docker compose up -d --no-build
curl --fail http://127.0.0.1:8081/health
```
`./deploy.sh` bleibt unverändert nutzbar. Die vorhandene `/data/finance.db` wird weiterverwendet und weder gelöscht noch überschrieben. API-Tests setzen ein zufälliges Testtoken in der Testumgebung und verwenden ausschließlich temporäre Datenbanken.
Technische Referenzen: [FastAPI HTTPBearer](https://fastapi.tiangolo.com/reference/security/) und [Pydantic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/).
## Positionsänderung: Anleihezinsen und Notgroschen
Beim ersten Start dieser Version wird automatisch eine einmalige Datenmigration ausgeführt:
- **Anleihezinsen** (Anleihe) und **Zinsen NG** (Notgroschen/Zinskonto) werden angelegt bzw. aktiviert.
- **Zinsen** und **Steuerrückzahlung** werden deaktiviert und sind für neue Zahlungen nicht mehr auswählbar.
- Vorhandene Buchungen und ihre Zuordnung bleiben unverändert; sie bleiben in Historie und Auswertungen sichtbar.
Die Migration wird zusammen mit ihrer Ausführungsmarkierung in einer SQLite-Transaktion gespeichert. Spätere Starts überschreiben keine danach vorgenommenen Positionsänderungen. Auf pinguAurora reicht das normale Deployment mit `./deploy.sh`; die Datenbank wird nicht über Git übertragen.
## Trading-Buch und Positionshistorie
Das **Trading-Buch** (`/trading`) erfasst Käufe und Verkäufe getrennt von Dividenden/Zinsen. `/trading/positions` zeigt offene Positionen, `/trading/transactions` die vollständige filterbare Historie und `/trading/assets/{asset_id}` die Historie einer Position einschließlich Bestand **vor und nach** jeder Buchung. Geschlossene Positionen bleiben über ihre Transaktionen erreichbar. Neue Positionen lassen sich direkt aus dem Trading-Formular ergänzen.
Unterstützte Asset-Typen: Aktien (`stock`), ETFs (`etf`), Anleihen (`bond`) und Krypto (`crypto`). Transaktionstypen: `buy` und `sell`. Es gibt keine automatische Kursabfrage, keine Performance auf Basis aktueller Marktpreise und keine steuerliche Gewinnermittlung.
### Genauigkeit und Durchschnittseinstand
Stückzahlen und Kurse werden als kanonische Dezimalstrings in SQLite gespeichert, mit bis zu **12 Vor- und 12 Nachkommastellen**. Gebühren haben höchstens zwei Nachkommastellen. Eingaben akzeptieren Komma oder Punkt, keine Tausendertrennzeichen. JSON-Zahlen/Floats werden für diese Felder abgelehnt; die API erwartet Strings.
Berechnungen verwenden Python Decimal mit 60 Stellen Rechenpräzision:
- `gross_amount = quantity × price_per_unit`, kaufmännisch auf zwei Nachkommastellen gerundet (`ROUND_HALF_UP`).
- Kauf: `total_cost = gross_amount + fees`.
- Verkauf: `net_proceeds = gross_amount - fees`.
- Offenes investiertes Kapital ist der verbleibende Einstand inklusive Kaufgebühren, **nicht** die Summe aller historischen Einzahlungen oder der aktuelle Marktwert.
- Einstand je Stück = offenes investiertes Kapital / aktueller Bestand. Die API gibt den Durchschnitt mit zwölf Nachkommastellen aus, die UI zeigt acht.
- Beim Verkauf wird der anteilige Durchschnittseinstand auf Cent gerundet ausgebucht. Realisierter G/V = Nettoerlös minus ausgebuchter Einstand. Beim vollständigen Verkauf wird der gesamte restliche Einstand ausgebucht, ohne Rundungsrest.
Beispiel: 10 Stück zu 10,00 mit 2,00 Gebühren plus 10 Stück zu 20,00 mit 2,00 Gebühren ergeben 304,00 Einstand und einen Durchschnitt von 15,20 pro Stück. Verkauf von 5 Stück zu 30,00 mit 1,00 Gebühren: Nettoerlös 149,00, ausgebuchter Einstand 76,00, realisierter Gewinn 73,00. Offen bleiben 15 Stück mit 228,00 Einstand.
**Dies ist keine deutsche steuerliche FIFO-Berechnung.** FIFO, Steuerberechnung und steuerliche Verlusttöpfe sind nicht implementiert.
Die Reihenfolge ist deterministisch: Datum aufsteigend, bei gleichem Datum ID aufsteigend (Erfassungsreihenfolge). Jede Änderung wird in einer Schreibtransaktion gegen die gesamte Historie der betroffenen Position(en) geprüft. Überverkäufe werden auch bei Rückdatierung, Änderung des Assets oder Löschen eines früheren Kaufs verhindert. Solche Änderungen werden vollständig zurückgerollt. Quantity muss positiv sein; Kurs und Gebühren dürfen 0, aber nicht negativ sein. Ohne bekannten Kurs keine Buchung speichern; 0 ist nur für tatsächlich kostenlose Erwerbe gedacht.
### Währungen
Jede Position wird in genau einer Währung geführt, auch über zwischenzeitliche Komplettverkäufe hinweg. Für denselben Asset-Datensatz dürfen keine unterschiedlichen Währungen gemischt werden. Es gibt **keine Wechselkursumrechnung**. Die Trading-KPIs und Diagramme beziehen sich auf die gewählte Währung (Standard EUR); Bestandslisten zeigen die Währung pro Position. Der dreistellige Währungscode muss zum dokumentierten Abrechnungskurs passen. Monetäre Beträge werden in dieser Version für alle Codes auf zwei Nachkommastellen geführt.
### Source und Strategie
| Feld | Werte |
| --- | --- |
| `source` | `manual` (Manuell), `savings_plan` (Sparplan), `roundup` (Round-up), `cashback`, `rebalancing`, `other` |
| `strategy_tag` (optional) | `core`, `income`, `conviction`, `dip_buy`, `speculation`, `rebalancing`, `other` |
Beispiele: SpaceX Round-up → `roundup` / `conviction`; normaler SpaceX-Nachkauf → `manual` / `conviction`; FTSE-Sparplan → `savings_plan` / `core`; CSWC-Sparplan → `savings_plan` / `income`.
Die Diagramme zeigen **den noch offenen Einstand nach Source und Strategie der ursprünglichen Käufe**. Ein Verkauf reduziert diese Anteile proportional. Cent-Reste werden deterministisch nach dem größten Nachkomma-Rest verteilt, sodass die Anteile zusammen exakt dem offenen Einstand entsprechen. Tags des Verkaufs verändern nicht die Herkunft des bisherigen Einstands. Ohne Strategie wird `untagged`/„Ohne Tag“ als separate Auswertungsgruppe gezeigt. Das dritte Diagramm zählt echte Käufe pro Monat/Jahr. Alle Diagramme besitzen Tabellen als Alternative ohne CDN-Zugriff.
### Trading-API und Export
Alle Endpunkte verwenden die vorhandene Bearer-Authentifizierung mit `FINANCE_API_TOKEN`, dokumentiert unter `/docs` im Tag **Trading**:
| Methode | Pfad |
| --- | --- |
| GET, POST | `/api/v1/transactions` |
| GET, PATCH, DELETE | `/api/v1/transactions/{id}` |
| GET | `/api/v1/positions` |
| GET | `/api/v1/trading/stats` |
| GET | `/api/v1/trading/by-source` |
| GET | `/api/v1/trading/by-strategy` |
Transaktionsfilter in Web und API: `year`, `month`, `asset_id`, `transaction_type`, `source`, `strategy_tag`. `strategy_tag=untagged` findet Einträge ohne Strategie. API zusätzlich `limit` (11000, Standard 100) und `offset` (Standard 0). Listen kommen neueste zuerst, nach Datum und ID absteigend. `POST` liefert 201, `PATCH` 200 und `DELETE` 204. Überschrittene Bestände oder unzulässige Änderungen liefern 422 mit einer fachlichen Fehlermeldung. Ein API-POST ist immer eine neue Buchung, nicht idempotent.
`/positions` liefert aktuelle Positionen aller Währungen; optional `currency=EUR` und `include_closed=true`. Felder: Asset-ID/-Name, Ticker, Asset-Typ, Währung, Stückzahl, Durchschnittseinstand, offenes Kapital, Summe Käufe inkl. Gebühren, Summe Verkäufe nach Gebühren, realisierter G/V, Kauf-/Verkaufsanzahl und erstes/letztes Datum. `/trading/stats`, `/by-source` und `/by-strategy` unterstützen `currency` (Standard EUR). Anteils-Endpunkte liefern `{currency, items: [{key, amount, percentage}]}`. Prozentwerte sind Strings bzw. `null` bei Gesamteinstand 0. Alle Geld- und Stückzahlwerte der API sind Dezimalstrings.
`PATCH` ändert nur übergebene Felder; nur `strategy_tag` und `note` dürfen explizit `null` sein. Inaktive Assets bleiben für Verkäufe und die Korrektur vorhandener Trades verfügbar; neue Käufe für inaktive Positionen sind gesperrt.
CSV unter **`/export/trading.csv`** enthält Datum, Position, Typ, Stückzahl, Kurs, Währung, Gebühren, Gesamtbetrag, Source, Strategie und Notiz. UTF-8 mit BOM, Semikolon, Dezimalkomma und CRLF; Textfelder sind gegen Excel-Formelinjektion geschützt. Gesamtbetrag bedeutet beim Kauf Gesamtkosten und beim Verkauf Nettoerlös.
### Migration, Backup und Deployment
Schema-Version **3** ergänzt automatisch die Tabelle `transactions`, zwei Indizes und einen Eintrag in `data_migrations`. DDL und Migrationsmarkierung werden atomar ausgeführt. Bestehende Tabellen werden weder gelöscht noch ersetzt. **Income-Einträge werden nicht verändert.** Es werden keine Trades und keine aktuellen Bestände automatisch eingetragen.
Das oben beschriebene konsistente SQLite-Backup sichert jetzt auch Trading-Daten. Ein CSV-Export ersetzt weiterhin kein vollständiges Backup. Das Deployment auf pinguAurora bleibt unverändert:
```bash
git pull
DOCKER_BUILDKIT=0 docker build \
-t finance-dashboard-finance-dashboard:latest \
.
docker compose up -d --no-build
curl --fail http://127.0.0.1:8081/health
```
Alternativ `./deploy.sh`. Niemals `docker compose up -d --build` auf dem Raspberry verwenden, solange der bekannte Buildx-Konflikt besteht. `/data/finance.db` bleibt persistent.
### Referenzbestand nicht importiert
Diese vom Nutzer genannten Stückzahlen sind ausschließlich eine spätere Referenz, **keine Buchungen und kein verifizierter aktueller Depotstand**:
| Position | Stückzahl |
| --- | ---: |
| AGNC | 153,816 |
| FTSE Global All Cap | 244,284 |
| STOXX Global Select Dividend 100 | 18 |
| Main Street Capital | 14,4758 |
| AI ETF | 2,14 |
| SpaceX, nach Round-up | 2,692262 |
| Capital Southwest | 8,28 |
| Ares Capital | 7,33 |
| Enbridge | 0,22 |
SpaceX-Beispiel: 2,600000 vor Round-up + 0,092262 = 2,692262 danach; `buy`, `source=roundup`, `strategy_tag=conviction`. **Kein verlässlicher Kaufkurs liegt vor, deshalb wurde kein Preis und keine Transaktion eingetragen.** Eine technische Anfangsposition (Opening Balance) ist noch nicht implementiert. Sie müsste künftig separat von echten Käufen modelliert werden und darf keine Kaufstatistiken erhöhen. Bis echte historische Käufe mit Datum und Kurs erfasst sind, zeigt das Trading-Buch entsprechend keine daraus abgeleiteten Bestände.