Files
Predictalytics/UMSETZUNGSPLAN.md
T
Richard 340bfdaa82 B1: EF Core migrations baseline validated against real dev DB
- Design-time AppDbContextFactory now builds its connection string via
  MySqlConnectionStringBuilder from PREDICTALYTICS_DB_* env vars instead of a
  hardcoded local default, so no secret needs to live in source/config to run
  migrations against any target database.
- InitialBaseline migration applied end-to-end against a fresh dev MySQL DB
  and confirmed via `dotnet ef migrations list`.
2026-07-03 10:30:13 +02:00

184 lines
20 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.
# Predictalytics Umsetzungsplan (Phase 1 & 2 + Storage-Optimierung)
> **Wie wir das nutzen:** Wir arbeiten diese Liste Stück für Stück ab. Erledigte Punkte werden von `[ ]` auf `[x]` gesetzt. Reihenfolge ist absichtlich so gewählt, dass spätere Schritte auf früheren aufbauen — nicht einfach querbeet abhaken, siehe Abschnitt "Empfohlene Reihenfolge" ganz unten für die Kurzfassung.
>
> Phase 3 (Monetarisierung: Auth, Multi-Tenant, Billing) ist bewusst **nicht** Teil dieses Plans.
---
## 0. Vorab-Bugfund (betrifft Phase 1 direkt)
Beim Review ist aufgefallen, dass `TraderAnalyticsWorker.CalculatePnL` **jeden Trade, der kein `Buy` ist, wie ein `Sell` behandelt** (`else pnl += t.Amount`). Das ist falsch für `Split`, `Merge`, `Redeem`, `AddLiquidity`, `RemoveLiquidity` — das sind keine gewöhnlichen Verkäufe. Dieser Fix gehört zwingend in die neue PnL-Engine (siehe A1/A6), nicht als Extra-Task.
---
## Phase 1 Kernanalytik korrigieren
### A1. Positionsbasierte PnL-Engine
- [ ] Entscheidung dokumentieren: **Average-Cost-Methode** statt FIFO (einfacher, und wichtig kompatibel mit späterer Trade-Kompaktierung in Abschnitt C, weil Average-Cost nur Gesamtstückzahl & Gesamtkosten braucht, keine Einzel-Trade-Reihenfolge)
- [ ] Neue Domain-Struktur `TraderPosition` (TraderId, MarketOutcomeId, SharesHeld, AvgCost, RealizedPnl, zuletzt aktualisiert) — inkrementell fortschreibbar statt bei jeder Berechnung die komplette Trade-Historie neu zu scannen
- [ ] Buchungslogik je `TradeSide` sauber definieren:
- [ ] `Buy`: Shares += Size, AvgCost neu gewichten
- [ ] `Sell`: RealizedPnl += Size × (Price AvgCost), Shares = Size
- [ ] `Redeem` (Marktauflösung): RealizedPnl += verbleibende Shares × (1 oder 0 je nach Gewinn-Outcome AvgCost), Shares = 0
- [ ] `Split` / `Merge`: als neutrale Positionsumwandlung behandeln (kein PnL-Effekt), nicht wie `Sell`
- [ ] `AddLiquidity` / `RemoveLiquidity`: getrennt von Trading-PnL betrachten (eigene Kategorie, fließt nicht in "Trading-Skill"-Bewertung ein)
- [ ] Unrealisierten PnL für offene Positionen berechnen: `Shares × (MarketOutcome.CurrentPrice AvgCost)`
- [ ] Gesamt-PnL = realisiert + unrealisiert (ersetzt `OverallPnL`, `PnL30d/7d/24h` Felder in `TraderAnalytics`)
- [ ] Prüfen, ob wir zusätzlich/alternativ Polymarkets eigenen `/positions`-Endpoint (`GetTraderPositionsAsync`, liefert `CurrentValue`/`PercentPnl`) als Plausibilitäts-Check oder sogar als primäre Quelle für **offene** Positionen nutzen (aktuell komplett ungenutzt)
- [ ] `CalculatePnL`-Bug aus Abschnitt 0 im Zuge dessen mit erledigen
### A2. Win-Rate korrekt berechnen
- [ ] Win/Loss ist **pro Markt**, nicht pro Trade, definiert: ein Markt zählt als "Win", wenn der realisierte PnL aus diesem Markt (nach Redeem/vollständigem Exit) positiv ist
- [ ] `WinRate = Anzahl gewonnener Märkte / Anzahl abgeschlossener Märkte` (offene Positionen zählen nicht mit)
- [ ] Placeholder `return 0;` in `TraderAnalyticsWorker.CalculateWinRate` ersetzen
### A3. Deep-Dive-Kennzahlen mit echten Werten füllen
- [ ] **Voraussetzung klären:** Für Entry/Exit-Qualität und Timing-Accuracy brauchen wir eine **Preis-Historie** pro `MarketOutcome`, nicht nur den aktuellen Preis (`CurrentPrice`). Aktuell existiert keine Historisierung.
- [ ] Prüfen, ob Polymarkets CLOB-API einen Preishistorie-Endpoint (`/prices-history`) hergibt, den wir zum Backfill nutzen können
- [ ] Falls ja: neue Tabelle `MarketOutcomePriceSnapshot` (MarketOutcomeId, Timestamp, Price) einführen, periodisch befüllt (z.B. durch bestehenden `MarketSyncWorker`/`MarketHistoryWorker` erweitern)
- [ ] `AvgHoldDurationHours` echt berechnen: gewichtete Haltedauer zwischen Einstieg (Buy-Zeitpunkte, gewichtet nach Größe) und Ausstieg (Sell/Redeem) pro Position
- [ ] `EntryQuality`: Einstiegspreis im Vergleich zur nachfolgenden Preisentwicklung (z.B. Perzentil des Einstiegspreises innerhalb eines Zeitfensters danach)
- [ ] `ExitQuality`: analog für Ausstiegspreis
- [ ] `TimingAccuracy`: z.B. Anteil der Trades, die kurz vor einer für den Trader günstigen Preisbewegung platziert wurden
- [ ] Hardcodierte `50`-Neutralwerte in `AnalyticsService.PerformDeepDive` entfernen
### A4. Strategie-Klassifikation verbessern
- [ ] Zusätzliche Signale einbeziehen statt nur "Ø-Größe" und "Hedging-Rate":
- [ ] Verteilung der Haltedauern (kurz/lang, Varianz)
- [ ] Diversität der Marktkategorien
- [ ] Anteil der Trades kurz vor Marktauflösung vs. früh im Marktleben
- [ ] Nutzung gegenläufiger Positionen (Arbitrage-Muster über mehrere Outcomes/Märkte)
- [ ] Trade-Größen-Varianz (regelmäßig gleich große Orders = evtl. automatisiert)
- [ ] Überlegen, ob `StrategyType` als reines Einzel-Enum ausreicht oder ob wir zusätzlich Mehrfach-Signale/Tags parallel speichern (z.B. "primär Hedger, aber auch Whale-Größenordnung") — Enum bleibt als Primär-Tag, Zusatzsignale als eigene Felder/Scores
- [ ] Sicherstellen, dass nicht der Großteil der Trader dauerhaft bei "Unknown" landet (aktuell strukturell der Fall)
### A5. Copytrading-Eignungs-Score (neu)
- [ ] Neue Metrik-Dimension definieren, unabhängig vom bestehenden `PriorityScore`:
- [ ] **Liquiditäts-Fit**: durchschnittliche Positionsgröße im Verhältnis zur Marktliquidität/zum Volumen zum Handelszeitpunkt (Slippage-Risiko für Nachahmer)
- [ ] **Reaktionsfenster**: wie viel Zeit bliebe einem Copytrader realistisch zum Nachziehen (Trader mit Sekunden/Millisekunden-Kadenz sind nicht kopierbar)
- [ ] **Frequenz/Konzentration**: sehr hochfrequente/bot-artige Trader senken den Score automatisch
- [ ] **Track-Record-Länge & Konsistenz**: mehr abgeschlossene Märkte mit konsistent positivem PnL = höheres Vertrauen
- [ ] Kombinierten `CopytradingScore` (0100) berechnen und persistieren (neues Feld auf `TraderScore` oder eigene Entity `TraderCopytradingProfile`)
- [ ] Im Dashboard/API sichtbar machen (getrennt von der bisherigen "Priorität", da unterschiedliche Fragestellung: *gut* vs. *kopierbar*)
### A6. (siehe Abschnitt 0) TradeSide-Bugfix in PnL-Berechnung
- [ ] Erledigt sich durch A1, hier nur als Häkchen zur Nachverfolgung
- [ ] Kurzer Änderungsvermerk/Commit-Hinweis, damit klar ist, dass dieser Bug bewusst behoben wurde
---
## Phase 2 Robustheit & Wartbarkeit
### B1. EF Core Migrationen einführen
- [x] Aktuellen Ist-Stand des Schemas exakt erfassen (inkl. aller manuellen `ALTER TABLE`-Patches aus `DependencyInjection.cs`) — abgeglichen, Entities/`OnModelCreating` deckten den Patch-Stand bereits vollständig ab
- [x] Erste **Baseline-Migration** erzeugt (`20260701102311_InitialBaseline`, in `src/Predictalytics.Infrastructure/Migrations/`), entspricht exakt dem aktuellen Schema
- [x] `EnsureCreatedAsync` + handgeschriebene `ExecuteIfColumnMissing`/`ALTER`-Helfer aus `DependencyInjection.cs` entfernt, durch `db.Database.MigrateAsync()` ersetzt (Platform-Seed bleibt als `INSERT IGNORE`)
- [x] Migration end-to-end gegen eine echte, frische DEV-Datenbank getestet (`lqf7.your-database.de`/`bergisnu_db0`, vom Nutzer bereitgestellt) — `dotnet ef database update` lief fehlerfrei durch, `dotnet ef migrations list` bestätigt sie als angewendet
- [x] `AppDbContextFactory` (Design-Time-Factory für `dotnet ef`) liest Zielverbindung jetzt aus Env-Vars (`PREDICTALYTICS_DB_SERVER/_NAME/_USER/_PASSWORD`, via `MySqlConnectionStringBuilder` statt roher String-Konkatenation, da Passwörter Sonderzeichen wie `;`/`{}` enthalten können) statt fest codiert — kein Secret mehr im Repo nötig, um Migrationen gegen eine beliebige DB zu fahren
- [x] ~~Live-Produktions-DB manuell als "bereits migriert" markieren~~ — vorerst zurückgestellt: Da wir laut Abschnitt D ohnehin einen kompletten Neustart der Produktions-DB planen (Altdaten sind jederzeit nachladbar, siehe C0), richten wir die neue Produktions-DB am Ende genauso ein wie die DEV-DB (leer anlegen + `dotnet ef database update`) — der fummelige Stamp-Schritt auf die bestehende Live-DB entfällt dann komplett. Nur falls wir uns doch gegen den Neustart entscheiden, müsste dieser Schritt nachgeholt werden
- [ ] Alle künftigen Schemaänderungen aus Phase 1 (z.B. `TraderPosition`, `MarketOutcomePriceSnapshot`, `TraderCopytradingProfile`) als reguläre Migrationen anlegen — direkt gegen die DEV-DB entwickeln/testen, da wir dort laut Nutzer frei experimentieren dürfen
### B2. Secrets-Management (Release-Vorbereitung, kein akuter Risikofall)
> Software läuft aktuell nur lokal, kein Fremdzugriff — Passwort bleibt vorerst wie es ist, keine Rotation nötig. Dieser Punkt ist reine **Vorbereitung**, damit das Projekt bei Bedarf später auch ohne die aktuellen Secrets veröffentlicht/geteilt werden könnte, ohne den Code nochmal anfassen zu müssen. Dadurch niedrigere Priorität als vorher angenommen — kann später in der Reihenfolge stehen.
- [ ] Produktiven MySQL-Connection-String aus `appsettings.json` in Umgebungsvariablen/User Secrets verschieben
- [ ] `appsettings.json` im Repo künftig nur Platzhalter/Dev-Default enthalten, damit ein Klon des Repos ohne die echten Zugangsdaten lauffähig bleibt (mit eigener lokaler DB)
- [ ] Keine Passwort-Rotation nötig, solange rein lokale Nutzung
### B3. Scoring-Pipeline entkoppeln
- [ ] `PollingWorker` soll **nicht** bei jedem 60-Sekunden-Zyklus alle Trader neu bewerten
- [ ] Nur Trader neu bewerten, die seit letzter Berechnung neue Trades bekommen haben (Dirty-Flag oder Vergleich `LastTradesUpdatedAt` vs. `TraderScore.CalculatedAt`)
- [ ] N+1-Datenbankzugriffe in `ScoringService.RecalculateAllScoresAsync` durch Batch-Queries ersetzen
- [ ] Eigenes Intervall für volle Neuberechnung/Ranking (z.B. alle 15 Min) getrennt vom reinen Trade-Polling (60s)
### B4. Testabdeckung für die kritische Logik
- [ ] Neues Testprojekt (z.B. `Predictalytics.Application.Tests`) anlegen — aktuell existiert **kein einziges** Testprojekt
- [ ] Unit-Tests für die neue PnL-Engine (A1) — insbesondere Grenzfälle: nur offene Position, nur geschlossene Position, Split/Merge, Redeem-Verlust vs. -Gewinn
- [ ] Unit-Tests für Win-Rate (A2)
- [ ] Unit-Tests für Deep-Dive-Kennzahlen (A3) und Copytrading-Score (A5)
- [ ] Diese Tests idealerweise **parallel zu A1A5** schreiben, nicht erst am Ende nachziehen
### B5. Kleine verwandte Aufräumarbeit
- [ ] Log-Rotation/Retention der Serilog-Datei-Sinks (`WinFormsHost/logs`) prüfen — wächst potenziell unbegrenzt, ähnliches Prinzip wie die DB-Speicherplatzfrage unten
---
## Datenbank-Speicherplatz-Optimierung
### C0. Kritische Bewertung deiner beiden Ideen (überarbeitet nach Diskussion 2026-07-01)
**Neue Rahmenbedingungen, die die Bewertung ändern:**
- Alle Rohdaten sind jederzeit erneut abrufbar — über Polymarkets `/activity`-Endpoint ([Doku](https://docs.polymarket.com/api-reference/core/get-user-activity), keine sichtbare Limitierung), im Zweifel über die Blockchain selbst. Wir müssen also **nichts "für immer" sichern** — Löschen ist kein unwiderruflicher Datenverlust.
- Analyseziel ist laut Auftrag explizit die **aktuelle** Strategie und aktueller Erfolg/Misserfolg, nicht die Historie von vor Jahren. Wir brauchen also gar keine unbegrenzte Detailtiefe — nur genug, um "aktuelles Verhalten" zu charakterisieren.
- Kern-Einsicht aus A1: Sobald ein Trade in `TraderPosition` (SharesHeld/AvgCost/RealizedPnl) eingerechnet ist, wird die **Rohzeile für die PnL-Fortführung nie wieder gebraucht** — die Position ist bereits die komprimierte Zusammenfassung. Rohdaten braucht es nur noch für die Strategie-/Deep-Dive-Analyse (Haltedauer, Timing, Hedging-Muster), und die soll ohnehin nur das aktuelle Fenster betrachten.
→ Das ersetzt die alte Idee 1 (Archivierung nur inaktiver Trader + Reaktivierungs-Baseline) durch ein einfacheres, einheitliches Prinzip: siehe **C1 Rollierendes Zeitfenster**.
**Idee 2 (Hochfrequenz-Trader alle 10 Min. aggregieren)** bleibt weiterhin sinnvoll — allerdings in überarbeiteter Form, da die ursprüngliche Beschreibung zu grob war:
- "Min/Max/Ø Buy-In" ohne Trennung nach Buy/Sell und nach Markt/Outcome zerstört genau die Information, die die PnL-Engine (A1) braucht.
- Eine feste 10-Minuten-Uhrzeit-Bucket-Grenze für *alle* Trades eines als "Bot" eingestuften Traders würde auch die wenigen möglicherweise bedeutsamen Trades eines Mischtyps mit-aggregieren.
- Relevant bleibt sie, weil auch *innerhalb* des neuen rollierenden Zeitfensters (siehe C1) ein aktiver Bot enorme Mengen an Trades erzeugen kann — das ist jetzt eine Optimierung fürs "heiße" Fenster, nicht mehr fürs Langzeitarchiv.
→ Vorschlag: **Burst-Erkennung statt starrer Zeit-Buckets** (nur tatsächlich dichte Trade-Sequenzen kompaktieren) und **Aggregation getrennt nach Trader+Markt/Outcome+Seite** mit **VWAP** (mengengewichteter Durchschnittspreis) statt einfachem Durchschnitt — bleibt unter der Average-Cost-Methode aus A1 nahezu verlustfrei für die PnL-Berechnung.
### C1. Rollierendes Zeitfenster statt Archivierung (ersetzt die alte "Idee 1")
- [ ] Konfigurierbares Retention-Fenster einführen (Default-Vorschlag: 36 Monate) — gilt **für alle Trader gleichermaßen**, nicht nur für inaktive. Länge sollte sich daran orientieren, wie weit die Deep-Dive-/Strategie-Analyse (A3/A4) tatsächlich zurückschaut, um "aktuelle Strategie" zu charakterisieren
- [ ] Reihenfolge pro Trade zwingend einhalten: **erst** in `TraderPosition`/Monats-Aggregat (s.u.) einrechnen und das sicher persistieren, **dann erst** die Rohzeile löschen — rein zeitbasiert, unabhängig davon ob die betroffene Position noch offen oder schon geschlossen ist
- [ ] Kein Export/Cold-Storage nötig, da jederzeit über die Polymarket-API bzw. im Zweifel über die Blockchain nachladbar — vereinfacht C1 gegenüber der ursprünglichen Idee erheblich (keine Baseline-Felder, keine Reaktivierungs-Sonderfälle)
- [ ] Optionales, leichtgewichtiges Langzeit-Signal (nice-to-have, niedrige Priorität): ein grobes Monats-Aggregat pro Trader (Monat, realisierter PnL, Trade-Anzahl, Volumen) für einen "war er über Monate hinweg konsistent profitabel"-Trend im Copytrading-Score (A5) — **ohne** Trade-Detailtiefe, nur wenige Kennzahlen pro Monat
- [ ] Neuer periodischer Cleanup-Job (`TradeRetentionWorker`), der Trades außerhalb des Fensters findet und löscht, nachdem die Voraussetzung (Position/Aggregat aktuell) erfüllt ist
- [ ] Kurzer Stichprobentest, wie weit `/activity` pro Wallet tatsächlich zurückreicht (bestätigt/verifiziert nur die schon vorliegende Doku-Aussage, geringe Priorität da schon durch Nutzer-Recherche plausibilisiert)
### C2. Kompaktierung von Hochfrequenz-Tradern innerhalb des Zeitfensters (überarbeitete "Idee 2")
- [ ] Burst-Erkennung statt globalem Zeitraster: Sequenz von Trades mit Abstand kleiner als Schwellwert (z.B. 60s, konfigurierbar) zwischen aufeinanderfolgenden Trades **desselben Traders, Outcomes und derselben Seite (Buy/Sell)** gilt als "Burst"
- [ ] Mindestlänge für Kompaktierung festlegen (z.B. erst ab 20+ Trades im Burst lohnt sich das)
- [ ] Bestehende Bot-Heuristik (`intervals.Average() < 10` in `AnalyticsService`) als Ausgangspunkt wiederverwenden/verallgemeinern statt eine zweite, unabhängige Definition einzuführen
- [ ] Aggregat-Datensatz pro Burst: Anzahl Trades, Summe Size, Summe Amount, Min-Preis, Max-Preis, **VWAP** (nicht einfacher Durchschnitt)
- [ ] Design-Entscheidung: Aggregat als zusätzliche nullable Spalten auf der bestehenden `Trade`-Tabelle (`AggregateCount`, `AggregateMinPrice`, `AggregateMaxPrice`) statt separater Tabelle — bestehender Code (PnL, Deep-Dive) muss dadurch kaum angepasst werden, ein Aggregat-Datensatz ist einfach ein "Trade" mit `Size = Summe`, `Price = VWAP`
- [ ] **Reihenfolge beachten:** Diese Kompaktierung erst implementieren, nachdem A1 (neue PnL-Engine) steht und validiert ist — sonst kompaktieren wir Daten weg, bevor wir wissen, was die neue Engine wirklich braucht
### C3. Weitere eigene Vorschläge
- [ ] **Spaltentypen verkleinern**: `TransactionHash`, `MarketId`, `AssetId` sind Hex-/Dezimal-Strings fester Länge (z.B. `0x` + 64 Hex-Zeichen) — als `BINARY(32)` statt `VARCHAR(66/80)` speichern spart ca. 3050% Platz auf diesen stark indizierten Spalten und ist schneller
- [ ] **Tabellen-Partitionierung** von `Trades` nach Monat (`ExecutedAt`) prüfen — erlaubt später das Archivieren/Droppen ganzer Partitionen statt teurer zeilenweiser `DELETE`s
- [ ] **Materialitätsschwelle**: Mikro-Trades unterhalb eines Betrags (z.B. < $0.50) unabhängig von Bot-Klassifizierung direkt aggregiert erfassen, da sie für Copytrading/Strategieanalyse ohnehin kaum Aussagekraft haben
- [ ] ~~Kaltarchiv außerhalb der DB~~ — nicht mehr nötig: da Rohdaten jederzeit über die API/Blockchain nachladbar sind (siehe C1), erübrigt sich ein separates Cold-Storage-Export. Nur falls sich die API-Nachladbarkeit später als doch eingeschränkt herausstellt, hier nochmal aufgreifen
- [ ] **InnoDB-Kompression** (`ROW_FORMAT=COMPRESSED`) für die `Trades`-Tabelle als schneller Zwischenschritt prüfen — kein Datenverlust, kombinierbar mit allem anderen
- [ ] `Split`/`Merge`/`AddLiquidity`/`RemoveLiquidity`-Ereignisse (siehe A1) ggf. separat und kompakter ablegen, da sie für die Trader-Bewertung meist weniger relevant sind als `Buy`/`Sell`
### C4. Abhängigkeit zu Phase 1
- [ ] Merksatz: **Erst A1 (neue PnL-Engine) fertigstellen, dann C1/C2 umsetzen.** Sonst laufen wir Gefahr, Rohdaten wegzuoptimieren, die die neue Engine noch gebraucht hätte.
---
## Entscheidung: Frischer Datenbank-Neustart nach Abschluss?
**Neu bewertet (2026-07-01):** Da wir laut C0/C1 ohnehin nichts an Alt-Historie "für immer" brauchen und alles bei Bedarf über die API/Blockchain nachladbar ist, ist ein sauberer Reset am Ende von Phase 1+2 **risikoarm und klar empfehlenswert** — nicht mehr nur eine Option mit Vorbehalt.
### D1. Kurzer Bestätigungs-Check (geringe Priorität, kein Blocker mehr)
- [ ] Stichprobenartig verifizieren, dass `/activity` für ein bekanntes, lange aktives Wallet tatsächlich vollständig zurückreicht — bestätigt nur die bereits über die [Doku](https://docs.polymarket.com/api-reference/core/get-user-activity) plausibilisierte Annahme, ist kein Show-Stopper mehr für die Entscheidung
### D2. Ablauf des Neustarts
- [ ] Vollständiges Backup/Dump der aktuellen Datenbank sichern (reine Vorsichtsmaßnahme, wird voraussichtlich nicht gebraucht) und einige Wochen aufbewahren
- [ ] EF-Migrationen-Baseline + alle Phase-1/2-Schemaänderungen fertigstellen (B1)
- [ ] Neue leere Datenbank anlegen, Migrationen anwenden
- [ ] Liste bereits bekannter Trader-Wallets aus dem alten Bestand als Startpunkt für die Re-Discovery übernehmen
- [ ] Re-Import über die Worker-Pipeline (mit korrigierter PnL-Logik, direkt im Rahmen des neuen Retention-Fensters aus C1) laufen lassen
- [ ] **Wichtig:** Dieser Schritt kommt ganz am Ende von Phase 1 + 2 — nicht vorher, damit wir nicht zweimal migrieren/importieren müssen
---
## Empfohlene Reihenfolge (Kurzfassung, Stand 2026-07-01)
1. **B1** EF-Migrationen-Baseline einführen (Fundament für alle weiteren Schemaänderungen)
2. **A1 + A6** Positionsbasierte PnL-Engine inkl. TradeSide-Bugfix
3. **A2** Win-Rate
4. **A3** Deep-Dive-Kennzahlen (inkl. Preis-Historie-Voraussetzung klären)
5. **A4** Strategie-Klassifikation verbessern
6. **A5** Copytrading-Eignungs-Score
7. **B4** Tests (idealerweise begleitend zu 2.6., hier als Nachhol-Punkt falls übersprungen)
8. **B3** Scoring-Pipeline entkoppeln/optimieren
9. **C1 + C2 + C3** Speicherplatz-Optimierung: rollierendes Zeitfenster + Burst-Kompaktierung (bewusst erst jetzt, siehe C4)
10. **D** Datenbank-Neustart durchführen (ganz am Ende, jetzt als klar empfohlener Schritt statt offener Entscheidung)
(B2 Secrets-Vorbereitung und B5 Log-Rotation sind risikoarme Nebenpunkte ohne akute Dringlichkeit, da rein lokale Nutzung; können jederzeit zwischendurch erledigt werden, wenn Zeit ist.)