Files
Predictalytics/FIXPLAN-G-Speicher.md
T
RichardandClaude Opus 5 1f230fe12c Fruehjahrsputz 2/2: Dokumentation auf den tatsaechlichen Stand gebracht
Die Plandokumente waren durchweg veraltet: 85 von 91 Punkten im
UMSETZUNGSPLAN standen auf offen, obwohl der Code sie enthielt, und in
FIXPLAN-UI-Ranglisten war keine einzige der 18 Aufgaben abgehakt, obwohl
beide zugehoerigen Commits laengst im Zweig stecken. Alles abschnittsweise
gegen den Code geprueft und die Haekchen gesetzt - mit Belegstellen, damit
die naechste Pruefung nicht wieder bei null anfaengt.

Neu: STATUS.md als Einstiegsseite - wo das Projekt steht, was fertig ist,
was offen ist, und was beim Aufraeumen bewusst stehengeblieben ist. CLAUDE.md
verweist darauf.

Nachgezogen:
* UMSETZUNGSPLAN.md - 66 Punkte abgehakt. Offen bleiben A4 (Strategie-
  Klassifikation), B2 (Secrets) sowie C3/D1/D2 (SQL-Arbeiten des Nutzers).
* FIXPLAN-UI-Ranglisten.md - abgeschlossen bis auf den als "Optional"
  markierten Pfeilrichtungs-Punkt.
* FIXPLAN-TODO.md - Teil D und F abgehakt; die Test-Baseline "16 gruen"
  auf die heutigen 126 korrigiert. Offen: F5 und zwei Tests aus F6.
* FIXPLAN-G-Speicher.md - G1 bis G4 als erledigt vermerkt, Baseline "39/1"
  korrigiert, Pfad auf die nicht mehr existierende WinFormsHost/appsettings.json
  richtiggestellt.
* docs/PLAN-Linux-Portierung.md - der Watchdog-Warnhinweis war ueberholt
  (DcHeartbeatService meldet an /api/watchdog/v1/ping). Abschnitt 11 empfahl
  noch, mit Phase 0 zu beginnen; jetzt benennt er Phase 5 und 6 als das, was
  wirklich aussteht. Die Randnotiz zu Zugangsdaten in LicenseGuard.cs ist
  gegenstandslos, die Datei laeuft ueber den Deploymentcenter.Client.

Zwei Befunde, die keine Aufraeumarbeit sind und deshalb nur dokumentiert
wurden - beide in STATUS.md Abschnitt 3:

1. In src/Predictalytics.Api/appsettings.json steht ein echter
   OpenRouter-API-Key im Klartext, versioniert seit 7045002. Nicht
   eigenmaechtig entfernt: ohne Rotation beim Anbieter bringt das nichts
   (die History behaelt ihn), wuerde aber die KI-Analyse abschalten.
   Der Schluessel muss zurueckgezogen und neu ausgestellt werden.
2. Die Portierung ist auf keinem Linux-System je ausgefuehrt worden. Die
   Verifikation vom 2026-08-08 lief unter Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:30:42 +02:00

336 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.
# FIXPLAN Teil G — Speicher-Budget & Kategorie-Edge (für Gemini 3.5 Flash)
> **✅ ABGESCHLOSSEN — G1 bis G4 sind umgesetzt.** Am 2026-08-23 gegen den Code geprüft:
> * **G1** Speicher-Governor — `Application/Services/StorageGovernor.cs`, angewendet in
> `TradeRetentionWorker.cs:89`; `MaxDatabaseSizeGb`/`MinRetentionDays` werden gelesen
> (`:74,75`). Tests in `StorageGovernorTests.cs` und `TradeRetentionWorkerTests.cs`.
> * **G2** Retention räumt auch `Aggregated`-Trader — `TradeRetentionWorker.cs`, Test vorhanden.
> * **G3** `UsdcSize` auf `Trade`, `OutcomeIndex` auf `MarketOutcome` — beide persistiert,
> Test in `TradeRepositoryTests.cs`.
> * **G4** Per-Kategorie-Edge — `AvgReturnPct` bis in die Trader-Detailansicht
> (`TraderEndpoints.cs:80`, `app.js:913`).
>
> Der **Anhang** („Einmalige DB-Optimierungen") bleibt offen: das sind SQL-Schritte, die
> laut Absprache der Nutzer selbst gegen die Datenbank fährt, kein Code.
>
> Die unten genannte Test-Baseline (39/1) ist überholt — Stand 2026-08-23 sind es
> **126 grün + 1 übersprungen**.
> Eigenständiger Plan. Von oben nach unten abarbeiten. Nach **jeder** Aufgabe bauen + testen + committen.
---
## 0. Regeln für den Umsetzer (WICHTIG, zuerst lesen)
1. **Test-Baseline:** `dotnet test src/Predictalytics.Application.Tests` liefert aktuell
**39 erfolgreich, 1 übersprungen**. Nach jeder Aufgabe muss gelten: **alle bisher grünen Tests
bleiben grün** + die neuen Tests der Aufgabe sind grün. Die eine übersprungene bleibt übersprungen.
2. **Bestehende Test-Assertions NIEMALS ändern.** Wird ein alter Test rot, ist DEIN Code falsch.
3. **Arbeite eine Aufgabe komplett fertig** (Code + Test + `dotnet build` + `dotnet test` grün),
dann `git commit`, dann erst die nächste.
4. **Migrationen:** mit `dotnet ef migrations add <Name> -p src/Predictalytics.Infrastructure -s src/Predictalytics.Api`
erzeugen. Die Design-Time-Factory liest die Ziel-DB aus Umgebungsvariablen
(`PREDICTALYTICS_DB_SERVER/_NAME/_USER/_PASSWORD`). **Keine** SQL-DDL von Hand schreiben —
immer EF-Migrationen. Neue DB-Spalten immer **nullable** oder mit Default anlegen.
5. **Neue Entity-Felder** müssen auch in `src/Predictalytics.Infrastructure/Data/AppDbContext.cs`
konfiguriert werden, falls dort für die Entity bereits ein `mb.Entity<...>(e => {...})`-Block steht.
6. **Zahlen:** Prozente auf 0100-Skala, Beträge in USD, Zeiten in UTC.
7. Wenn eine Anweisung unklar ist: die **kleinste, sicherste** Variante wählen, nichts „drumherum" umbauen.
---
## Aufgabe G1 — Speicher-Governor (budget-basierte Retention)
**Ziel:** Die Datenbank soll unter einer konfigurierbaren Grenze (Default 90 GB) bleiben. Nähert sie
sich der Grenze, wird das Retention-Fenster automatisch verkürzt.
### G1.1 — Reine Rechenfunktion (zuerst, voll testbar)
Neue Datei `src/Predictalytics.Application/Services/StorageGovernor.cs`:
```csharp
namespace Predictalytics.Application.Services;
/// <summary>
/// Pure helper: shrinks the retention window as the database approaches its size budget.
/// No DB access here — fully unit-testable.
/// </summary>
public static class StorageGovernor
{
/// <summary>
/// Returns the retention window (in days) to actually use.
/// - Below 80% of the budget: the configured window (no tightening).
/// - Between 80% and 100%: linearly interpolated down towards minRetentionDays.
/// - At or above 100%: minRetentionDays.
/// </summary>
public static int ComputeEffectiveRetentionDays(
double currentSizeGb, double maxSizeGb, int configuredRetentionDays, int minRetentionDays)
{
if (maxSizeGb <= 0 || configuredRetentionDays <= minRetentionDays)
return configuredRetentionDays;
double softStart = 0.80 * maxSizeGb;
if (currentSizeGb <= softStart) return configuredRetentionDays;
if (currentSizeGb >= maxSizeGb) return minRetentionDays;
// linear interpolation between softStart (=configured) and maxSizeGb (=min)
double t = (currentSizeGb - softStart) / (maxSizeGb - softStart); // 0..1
double days = configuredRetentionDays - t * (configuredRetentionDays - minRetentionDays);
return (int)System.Math.Round(days);
}
}
```
### G1.2 — Test
Neue Datei `src/Predictalytics.Application.Tests/Services/StorageGovernorTests.cs`:
```csharp
using Predictalytics.Application.Services;
using Xunit;
namespace Predictalytics.Application.Tests.Services;
public class StorageGovernorTests
{
[Theory]
[InlineData(10.0, 90.0, 90, 30, 90)] // far below budget -> full window
[InlineData(72.0, 90.0, 90, 30, 90)] // exactly at 80% -> still full
[InlineData(90.0, 90.0, 90, 30, 30)] // at budget -> min window
[InlineData(100.0, 90.0, 90, 30, 30)] // over budget -> min window
[InlineData(81.0, 90.0, 90, 30, 87)] // just into the zone -> slightly tightened
public void ComputeEffectiveRetentionDays_Interpolates(
double sizeGb, double maxGb, int configured, int min, int expected)
{
Assert.Equal(expected, StorageGovernor.ComputeEffectiveRetentionDays(sizeGb, maxGb, configured, min));
}
[Fact]
public void HalfwayIntoZone_IsBetweenMinAndConfigured()
{
// 85 GB of 90 (softStart 72) -> t ~0.72 -> days between 30 and 90
var days = StorageGovernor.ComputeEffectiveRetentionDays(85.0, 90.0, 90, 30);
Assert.InRange(days, 31, 89);
}
}
```
### G1.3 — In den Worker einbauen
In `src/Predictalytics.Worker/Services/TradeRetentionWorker.cs`, Methode `RunOptimizationAsync`:
- Zwei neue Config-Werte lesen (neben dem bestehenden `RetentionSettings:RetentionDays`):
```csharp
var maxSizeGb = _config.GetValue("RetentionSettings:MaxDatabaseSizeGb", 90.0);
var minRetention = _config.GetValue("RetentionSettings:MinRetentionDays", 30);
```
- Aktuelle DB-Größe per Scalar-Query ermitteln (Pomelo/MySQL):
```csharp
double currentSizeGb = 0;
try {
currentSizeGb = (await db.Database.SqlQueryRaw<double>(
"SELECT COALESCE(SUM(data_length + index_length),0) / 1073741824.0 AS Value " +
"FROM information_schema.tables WHERE table_schema = DATABASE()").ToListAsync(ct)).FirstOrDefault();
} catch (Exception ex) { _logger.LogWarning(ex, "Could not read DB size; using configured retention."); }
```
(Hinweis: `SqlQueryRaw<double>` braucht `using Microsoft.EntityFrameworkCore;`. Die Spalte muss
`Value` heißen, damit das Scalar-Mapping funktioniert.)
- Das effektive Fenster berechnen und **statt** `retentionDays` verwenden:
```csharp
var effectiveRetentionDays = StorageGovernor.ComputeEffectiveRetentionDays(
currentSizeGb, maxSizeGb, retentionDays, minRetention);
if (effectiveRetentionDays != retentionDays)
_logger.LogWarning("Storage governor: DB {Size:F1} GB, retention tightened {From}d -> {To}d",
currentSizeGb, retentionDays, effectiveRetentionDays);
var retentionCutoff = utcNow.Date.AddDays(-effectiveRetentionDays);
```
Den bestehenden `retentionCutoff` durch diese Berechnung ersetzen. Der Rest der Methode bleibt.
### G1.4 — Config-Defaults
In `src/Predictalytics.Api/appsettings.json` unter `RetentionSettings` (Objekt anlegen falls
fehlt) ergänzen: `"MaxDatabaseSizeGb": 90, "MinRetentionDays": 30`. (Nur Defaults; nichts Bestehendes löschen.)
**G1 fertig, wenn:** neue Tests grün, alte grün, Build grün. Commit: „G1: storage governor (budget-based retention)".
---
## Aufgabe G2 — Retention räumt auch `Aggregated`-Trader auf
**Problem:** In `TradeRetentionWorker.RunOptimizationAsync` filtert das Pruning heute auf
`t.Trader.IngestMode == IngestMode.Full`. Dadurch werden die (stündlich aggregierten) Trades von
`Aggregated`-Tradern **nie** gelöscht und wachsen unbegrenzt. `SnapshotOnly`-Trader haben ohnehin
kaum Trades. `Watchlist`-Trader bleiben ausgenommen.
**Fix:** Beide Vorkommen von
```csharp
&& t.Trader.IngestMode == IngestMode.Full
```
ändern zu
```csharp
&& t.Trader.IngestMode != IngestMode.SnapshotOnly
```
(damit werden `Full` **und** `Aggregated` geprunt; `SnapshotOnly` weiterhin nicht, Watchlist weiterhin ausgenommen).
**Test** (`src/Predictalytics.Application.Tests/Services/`, neue Datei oder in `TradeRetentionWorkerTests`):
Baue mit dem vorhandenen SQLite-Muster (siehe `TradeRetentionWorkerTests`) einen `Aggregated`-Trader
mit einem alten Trade (älter als Retention, kein Watchlist-Eintrag) und rufe `RunOptimizationAsync`
per Reflection auf. Danach: der alte Trade ist gelöscht.
```csharp
// Trader: IngestMode = IngestMode.Aggregated, keine WatchlistEntries
// Trade: ExecutedAt = baseDate.AddDays(-200), MarketOutcomeId gesetzt
// nach RunOptimizationAsync: Assert.Empty(db.Trades ... für diesen Trade)
```
**G2 fertig, wenn:** neuer Test grün, alte grün. Commit: „G2: retention prunes Aggregated tier too".
---
## Aufgabe G3 — `usdcSize` + `outcomeIndex` persistieren (günstig, optional zuerst überspringbar)
> ⚠️ Diese Aufgabe fasst den **Massen-Insert-Pfad** an. Wenn du dir bei der Parameter-Indizierung
> unsicher bist, überspringe G3 und mach G4 zuerst — G3 ist nicht kritisch. Der Test unten fängt
> einen kaputten Insert ab.
**Ziel:** Die Felder `usdcSize` (exakter Cash-Betrag) und `outcomeIndex` (0/1) werden von der API
bereits geparst (`PolymarketTradeResponse.UsdcSize`, `PolymarketTradeResponse.OutcomeIndex`), aber
nicht gespeichert.
### G3.1 — Entity + Config + Migration
- In `src/Predictalytics.Domain/Entities/Trade.cs` zwei nullable Felder ergänzen:
```csharp
public decimal? UsdcSize { get; set; }
public int? OutcomeIndex { get; set; }
```
- Migration `AddTradeUsdcAndOutcomeIndex` erzeugen.
### G3.2 — Im Provider mappen
In `src/Predictalytics.Infrastructure/Providers/Polymarket/PolymarketProvider.cs` in **beiden**
Trade-Mappings (`GetTraderTradesAsync` ~Zeile 42, `GetMarketTradesAsync` ~Zeile 80) im
`new Trade { ... }` ergänzen:
```csharp
UsdcSize = (decimal)r.UsdcSize,
OutcomeIndex = r.OutcomeIndex,
```
### G3.3 — Massen-Insert erweitern (VORSICHT)
In `src/Predictalytics.Infrastructure/Data/Repositories/TradeRepository.cs`, Methode `AddRangeAsync`:
Der handgeschriebene `INSERT IGNORE` listet die Spalten explizit auf. Du musst **drei** Dinge synchron ändern:
1. Die Spaltenliste im Header-String um `, UsdcSize, OutcomeIndex` **am Ende** ergänzen.
2. Die pro Zeile erzeugte Klammer hat aktuell **15** Parameter (`@p{pIdx}`..`@p{pIdx+14}`) → auf **17**
erhöhen (`@p{pIdx+15}`, `@p{pIdx+16}`), und den Stride `int pIdx = i * 15;` auf `i * 17` ändern.
3. Nach den bestehenden `parameters.Add(...)`-Zeilen pro Trade **zwei** neue in gleicher Reihenfolge:
```csharp
parameters.Add(t.UsdcSize ?? (object?)null);
parameters.Add(t.OutcomeIndex ?? (object?)null);
```
Wenn die Spaltenanzahl im Header nicht exakt zur Parameteranzahl pro Zeile passt, schlägt der Insert fehl.
### G3.4 — Test (Sicherheitsnetz gegen kaputten Insert)
Neue Datei `src/Predictalytics.Application.Tests/Services/TradeRepositoryInsertTests.cs` — SQLite,
füge über `AddRangeAsync` einen Trade mit `UsdcSize=12.5m, OutcomeIndex=1` ein und lies ihn zurück:
```csharp
// Assert: der zurückgelesene Trade hat UsdcSize == 12.5m und OutcomeIndex == 1
```
(Falls `AddRangeAsync` MySQL-spezifisches `INSERT IGNORE` nutzt, das SQLite nicht kennt: dann diesen
Test mit der In-Memory- oder MySQL-Testinfrastruktur ausführen, ODER den Test auf das Provider-Mapping
beschränken — Hauptsache, die Persistenz der zwei Felder ist einmal geprüft.)
**G3 fertig, wenn:** Test grün, alte grün. Commit: „G3: persist usdcSize + outcomeIndex on Trade".
---
## Aufgabe G4 — Per-Kategorie-Edge (hoher Analysewert, speicher-günstig)
**Ziel:** Pro Trader und Kategorie soll sichtbar werden, **wie viel Edge** der Trader dort hat
(Rendite in % des eingesetzten Kapitals). Das beantwortet „in welcher Kategorie ist er gut" und ist
direkt für Copytrading nutzbar. Voraussetzung ist der Kategorie-Fix aus Teil F — G4 funktioniert aber
auch schon davor, nur mit vielen `Other`-Einträgen.
### G4.1 — Entity + Migration
In `src/Predictalytics.Domain/Entities/TraderCategoryPerformance.cs`:
- Neues gespeichertes Feld: `public decimal TotalInvested { get; set; }` (Summe der Buy-Amounts in
abgeschlossenen Märkten dieser Kategorie).
- Neue **berechnete** Property (nicht gespeichert):
```csharp
public decimal AvgReturnPct => TotalInvested > 0 ? TotalPnL / TotalInvested * 100m : 0m;
```
- Migration `AddCategoryTotalInvested`.
### G4.2 — Berechnung in der Engine
In `src/Predictalytics.Infrastructure/Services/PositionPnLEngine.cs`, Methode
`CalculateCategoryPerformances`: dort wird pro abgeschlossenem Markt bereits `marketPnl` bestimmt und
`perf.TotalPnL += marketPnl` gesetzt. **An derselben Stelle** zusätzlich das investierte Kapital
summieren:
```csharp
// invested = Summe der Buy-Amounts der Trades dieses Markts (nur Buy-Seite)
var invested = marketGroup.Where(t => t.Side == TradeSide.Buy).Sum(t => t.Amount);
perf.TotalInvested += invested;
```
Und beim Upsert der bestehenden Kategorien-Zeile (im `RecalculateTraderPositionsAsync`, wo
`existing.TotalPnL = kvp.Value.TotalPnL;` etc. gesetzt wird) ergänzen:
```csharp
existing.TotalInvested = kvp.Value.TotalInvested;
```
### G4.3 — DTO + API
In `src/Predictalytics.Application/DTOs/TraderDto.cs` das Record `TraderCategoryPerformanceDto` um
`decimal AvgReturnPct` erweitern (am Ende anhängen). In
`src/Predictalytics.Application/Services/AnalyticsService.cs` beim Erzeugen des DTOs
(`new TraderCategoryPerformanceDto(...)`) `p.AvgReturnPct` mitgeben.
### G4.4 — Test
Neue Datei `src/Predictalytics.Application.Tests/Services/CategoryEdgeTests.cs` (SQLite-Muster wie in
`MarketRepositoryTests`): ein Trader, ein aufgelöster Markt einer Kategorie, Buy 100 @ 0.40 (=40 USD),
Gewinn-Auflösung (Payout 1.0) → RealizedPnl 60, TotalInvested 40 → nach Recalc muss die
`TraderCategoryPerformance` dieser Kategorie `TotalInvested == 40` und `AvgReturnPct == 150` haben.
Zweiter Fall: Kategorie mit `TotalInvested == 0` → `AvgReturnPct == 0` (keine Division durch 0).
**G4 fertig, wenn:** Test grün, alte grün. Commit: „G4: per-category edge (AvgReturnPct)".
---
## Anhang — Einmalige DB-Optimierungen (führt der NUTZER als SQL aus, NICHT als Code)
> Diese DDL macht Richard selbst auf der Live-DB (er betreibt DB-SQL ohnehin selbst). **Nicht** als
> EF-Migration umsetzen. Vorher Backup. Nacheinander, außerhalb der Stoßzeiten.
**A) InnoDB-Kompression auf der größten Tabelle (verlustfrei, ~4060 % kleiner):**
```sql
ALTER TABLE Trades ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
```
**B) Monats-Partitionierung von `Trades` (macht Retention zu instant `DROP PARTITION`):**
> Voraussetzung: alle Unique-/Primary-Keys müssen die Partitionsspalte (`ExecutedAt`) enthalten.
> Das ist ein größerer Umbau der Schlüssel — nur mit sorgfältigem Test auf der DEV-DB
> (`lqf7.your-database.de`) durchführen. Grobskizze:
```sql
-- Beispielhaft; exakte Schlüsselanpassung vorher auf DEV testen!
ALTER TABLE Trades
PARTITION BY RANGE (TO_DAYS(ExecutedAt)) (
PARTITION p_2026_06 VALUES LESS THAN (TO_DAYS('2026-07-01')),
PARTITION p_2026_07 VALUES LESS THAN (TO_DAYS('2026-08-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Alte Daten löschen wird dann: ALTER TABLE Trades DROP PARTITION p_2026_06;
```
**C) (Optional, fortgeschritten) Hex-Spalten binär speichern:** `TransactionHash`/`ConditionId` als
`BINARY(32)` statt `VARCHAR(66)`. Spart ~50 % auf diesen Spalten + Indizes, aber erfordert
Datenmigration (Hex→Binär) und Code-Anpassung beim Lesen/Schreiben. **Erst angehen, wenn A+B nicht reichen.**
---
## Gesamt-Abnahme (alles zusammen)
1. `dotnet build` fehlerfrei.
2. `dotnet test src/Predictalytics.Application.Tests`: **alle vorher grünen Tests weiter grün
(Baseline 39/1)** + alle neuen G-Tests grün, 1 übersprungen bleibt übersprungen. Keine
Assertion eines Alt-Tests geändert.
3. Storage-Governor greift: bei simulierter DB-Größe nahe `MaxDatabaseSizeGb` wird das Fenster
verkürzt (G1-Test beweist die Rechenlogik).
4. `Aggregated`-Trader-Trades werden von der Retention erfasst (G2-Test).
5. Trader-Detail zeigt je Kategorie einen `avgReturnPct` (G4).
**Reihenfolge:** G1 → G2 → G4 → (G3 optional zuletzt). G3 nur machen, wenn der Massen-Insert-Test
danach grün ist — sonst zurückrollen.