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>
16 KiB
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 inTradeRetentionWorker.cs:89;MaxDatabaseSizeGb/MinRetentionDayswerden gelesen (:74,75). Tests inStorageGovernorTests.csundTradeRetentionWorkerTests.cs.- G2 Retention räumt auch
Aggregated-Trader —TradeRetentionWorker.cs, Test vorhanden.- G3
UsdcSizeaufTrade,OutcomeIndexaufMarketOutcome— beide persistiert, Test inTradeRepositoryTests.cs.- G4 Per-Kategorie-Edge —
AvgReturnPctbis 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)
- Test-Baseline:
dotnet test src/Predictalytics.Application.Testsliefert 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. - Bestehende Test-Assertions NIEMALS ändern. Wird ein alter Test rot, ist DEIN Code falsch.
- Arbeite eine Aufgabe komplett fertig (Code + Test +
dotnet build+dotnet testgrün), danngit commit, dann erst die nächste. - Migrationen: mit
dotnet ef migrations add <Name> -p src/Predictalytics.Infrastructure -s src/Predictalytics.Apierzeugen. 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. - Neue Entity-Felder müssen auch in
src/Predictalytics.Infrastructure/Data/AppDbContext.cskonfiguriert werden, falls dort für die Entity bereits einmb.Entity<...>(e => {...})-Block steht. - Zahlen: Prozente auf 0–100-Skala, Beträge in USD, Zeiten in UTC.
- 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:
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:
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):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):
(Hinweis:
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."); }SqlQueryRaw<double>brauchtusing Microsoft.EntityFrameworkCore;. Die Spalte mussValueheißen, damit das Scalar-Mapping funktioniert.) - Das effektive Fenster berechnen und statt
retentionDaysverwenden:Den bestehendenvar 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);retentionCutoffdurch 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
&& t.Trader.IngestMode == IngestMode.Full
ändern zu
&& 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.
// 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.cszwei nullable Felder ergänzen:public decimal? UsdcSize { get; set; } public int? OutcomeIndex { get; set; } - Migration
AddTradeUsdcAndOutcomeIndexerzeugen.
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:
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:
- Die Spaltenliste im Header-String um
, UsdcSize, OutcomeIndexam Ende ergänzen. - 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 Strideint pIdx = i * 15;aufi * 17ändern. - Nach den bestehenden
parameters.Add(...)-Zeilen pro Trade zwei neue in gleicher Reihenfolge: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:
// 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):
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:
// 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:
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, ~40–60 % kleiner):
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:
-- 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)
dotnet buildfehlerfrei.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.- Storage-Governor greift: bei simulierter DB-Größe nahe
MaxDatabaseSizeGbwird das Fenster verkürzt (G1-Test beweist die Rechenlogik). Aggregated-Trader-Trades werden von der Retention erfasst (G2-Test).- 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.