Add trading journal and portfolio tracking

This commit is contained in:
kai
2026-09-09 19:01:30 +02:00
parent 1db3b008ec
commit c683bc74c0
24 changed files with 1043 additions and 8 deletions
+97 -1
View File
@@ -48,7 +48,7 @@ Standardpfad: **/data/finance.db**, persistent auf dem Host als **./data/finance
`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 2 wird über `PRAGMA user_version` geführt.
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.
@@ -255,3 +255,99 @@ Beim ersten Start dieser Version wird automatisch eine einmalige Datenmigration
- 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.