Eine Roadmap statt neun Plandokumente; alte Plaene ins Archiv
Die offenen Punkte lagen ueber neun Dokumente verstreut, teils widersprechend, teils mit Punkten, die laengst umgesetzt waren. ROADMAP.md fuehrt sie zusammen: sechs Stufen, jeder Punkt gegen den Code geprueft. Markierung ueber eine Legende, damit Konzepte nicht mit Aufgaben verwechselt werden: dringend, eingeplant, Backlog, Konzept (durchdacht, aber bewusst nicht eingeplant), liegt beim Nutzer, verworfen. Stufen: 0 Sofort (OpenRouter-Key) - 1 Aufraeumen abschliessen (Merge nach main, Nullable-Warnungen, Startup-Backfill) - 2 Portierung abschliessen (Linux-Erstlauf und Verifikation, dann Deployment/CI) - 3 Analytik schaerfen (Strategie- Klassifikation, Backtest-Harness, Track-Record im Score, Ranking) - 4 Daten- haushalt (SQL des Nutzers) - 5 Ingest skalieren - 6 Monetarisierung vorbereiten. Ein Anhang haelt fest, was geprueft und bewusst NICHT auf die Roadmap kam, damit es nicht versehentlich wieder als Aufgabe auftaucht (Azuro/Limitless, die Marketing-Seiten, Kaltarchiv, EF Core 10). Archiv: FIXPLAN-DONE, FIXPLAN-G-Speicher, FIXPLAN-TODO, FIXPLAN-UI-Ranglisten, UMSETZUNGSPLAN sowie ANALYSE-Linux-Portierung, PLAN-Linux-Portierung, PLAN-Architektur-WebUI-Backend und PLAN-DatenIngest-Skalierung liegen jetzt unter docs/archiv/ mit einer README, die jedes Dokument einordnet. Sie bleiben als Begruendungs- und Detailquelle - die Roadmap nennt jeden Punkt knapp, die Herleitung steht dort. Dabei aufgefallen: die beiden nie gepflegten Plaene (Architektur, DatenIngest) zeigten 40 offene Punkte, von denen die Haelfte umgesetzt war - Read/Control- Split, /api/capabilities, CORS-Whitelist, Egress-Kanaele mit Cooldown und Per-Kanal-Limiter, Ingest-Tiering. Nachgeprueft, abgehakt und mit Statusblock eingeordnet, sonst waere das Archiv selbst eine Fehlerquelle. Was dabei nur teilweise umgesetzt war, ist als Roadmap 5.3 aufgenommen (Health-Statistik je Kanal, multi-homed pruefen, DB-Guard, Plausibilitaets- pruefung), ebenso der nie durchgefuehrte Audit auf versteckte Writes in Read-Endpunkten (6.1). Header-Rotation ist als verworfen markiert statt offen zu bleiben: Verschleierung gegenueber Polymarket riskiert genau den Zugang, auf dem das Projekt aufsetzt. STATUS.md beschreibt jetzt nur noch den Ist-Stand und verweist fuer die offenen Punkte auf die Roadmap, damit nichts doppelt gepflegt wird. CLAUDE.md nennt beide Einstiege. Build gruen, 126 Tests gruen, keine toten Links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
# Analyse: Linux-Fähigkeit von Predictalytics
|
||||
|
||||
**Stand:** 2026-08-06 · Branch `main` @ `aa19a89` · reine Ist-Analyse, keine Codeänderungen
|
||||
|
||||
---
|
||||
|
||||
## 0. Kernbefund vorab
|
||||
|
||||
> **Avalonia UI und LiveCharts2 werden nicht benötigt.**
|
||||
|
||||
Die Annahme im Auftrag war, dass eine WinForms-Oberfläche auf Avalonia + LiveCharts2 portiert
|
||||
werden muss. Das trifft auf dieses Projekt nicht zu:
|
||||
|
||||
* Die **eigentliche Analyse-Oberfläche ist bereits eine Browser-Anwendung** —
|
||||
`src/Predictalytics.Api/wwwroot/` (3.745 Zeilen HTML/CSS/JS), Charts über
|
||||
**Chart.js 4.4.4** (`index.html:9`, CDN). Sie ist damit heute schon plattformunabhängig.
|
||||
* `Predictalytics.WinFormsHost` ist **kein UI-Projekt im fachlichen Sinne**, sondern eine
|
||||
Bedien-Hülle für den Betrieb: Start/Stop der Worker, Start/Stop des Kestrel-Webservers,
|
||||
ein `PropertyGrid` für Einstellungen, eine `RichTextBox` als Log-Terminal und vier
|
||||
manuelle Wartungsaktionen. **1.698 LOC inkl. Designer-Datei.** Kein einziges Diagramm.
|
||||
|
||||
Die Aufgabe lautet damit nicht „UI-Framework migrieren", sondern
|
||||
**„Bedien-Hülle durch einen headless Host ersetzen"** — eine Größenordnung weniger Aufwand.
|
||||
|
||||
---
|
||||
|
||||
## 1. Portierungsstatus je Projekt
|
||||
|
||||
| Projekt | TFM | Status | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| `Predictalytics.Domain` | `net8.0` | ✅ portabel | 42 Dateien, reine Fachlogik |
|
||||
| `Predictalytics.Application` | `net8.0` | ✅ portabel | 39 Dateien |
|
||||
| `Predictalytics.Infrastructure` | `net8.0` | ✅ portabel | 94 Dateien, EF Core + Pomelo MySQL |
|
||||
| `Predictalytics.Api` | `net8.0` | ✅ portabel | Minimal-APIs + wwwroot |
|
||||
| `Predictalytics.Worker` | `net8.0` | ✅ portabel | 13 Worker-Services |
|
||||
| `Predictalytics.Application.Tests` | `net10.0` | ⚠️ Inkonsistenz | s. Punkt 4.8 |
|
||||
| `Predictalytics.WinFormsHost` | `net8.0-windows` | ❌ **muss ersetzt werden** | einziger Blocker |
|
||||
| `LicenseLabrador.Client` (Schwester-Repo) | `netstandard2.0` | ⚠️ teilportabel | s. Punkt 3.2 |
|
||||
|
||||
**54.056 Zeilen C# gesamt. Davon sind 1.698 (3,1 %) windows-gebunden — und davon ist
|
||||
etwa die Hälfte (`EmbeddedWebServer.cs`, Serilog-Setup) inhaltlich bereits portabel und
|
||||
nur im falschen Projekt beheimatet.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Was bereits sauber ist (und keinen Aufwand erzeugt)
|
||||
|
||||
Diese Punkte wurden explizit geprüft, weil sie erfahrungsgemäß die teuren Überraschungen
|
||||
einer Linux-Portierung sind. Der Code ist hier durchweg in gutem Zustand:
|
||||
|
||||
### 2.1 Zeitzonen — überwiegend sauber, mit einer wichtigen Ausnahme ⚠️
|
||||
|
||||
> **Korrektur (2026-08-06, nach Phase 0):** Die erste Fassung dieser Analyse stufte den
|
||||
> Punkt als „vollständig sauber, Aufwand 0" ein. Das war falsch — siehe unten. Die
|
||||
> explizite Zeitzonen-*API*-Nutzung ist sauber, die *implizite* über `DateTime.TryParse`
|
||||
> war es nicht.
|
||||
|
||||
```
|
||||
166 × DateTime.UtcNow / DateTimeOffset.UtcNow
|
||||
0 × DateTime.Now · DateTime.Today · DateTimeOffset.Now
|
||||
0 × ToLocalTime() · DateTimeKind.Local
|
||||
0 × TimeZoneInfo / FindSystemTimeZoneById
|
||||
13 × DateTime.TryParse ohne Culture/Styles ← Problem
|
||||
```
|
||||
|
||||
Der klassische Linux-Killer `TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time")`
|
||||
(Windows-IDs existieren unter Linux nicht, dort gilt IANA `Europe/Berlin`) **kommt nicht
|
||||
vor**. Insoweit ist die Codebasis sauber.
|
||||
|
||||
**Übersehen hatte ich die implizite Variante:** `DateTime.TryParse("...Z")` ohne
|
||||
`DateTimeStyles` liefert `Kind=Local` und **rechnet den Wert in Lokalzeit um**. In beiden
|
||||
Providern wurden so ISO-Zeitstempel der Plattform-APIs eingelesen:
|
||||
|
||||
* `PolymarketProvider.cs` — `StartDate`, `EndDate`, `CreatedAt`, `ClosedAt` (10 Stellen)
|
||||
* `LimitlessProvider.cs` — `ExecutedAt`, `ExpirationDate` (2 Stellen)
|
||||
|
||||
Dass UTC gemeint war, belegen die Fallbacks: jede dieser Zuweisungen fällt bei
|
||||
Parse-Fehler auf `DateTime.UtcNow` zurück, und direkt daneben steht `DbCreatedAt =
|
||||
DateTime.UtcNow`. Auf der Berliner Entwicklungsmaschine landeten die geparsten Werte
|
||||
dadurch um 1–2 Stunden verschoben in der Datenbank, die Fallback-Werte dagegen korrekt.
|
||||
|
||||
Für die Portierung ist das doppelt relevant: derselbe Code hätte auf einem Linux-Server
|
||||
mit `TZ=UTC` **andere Werte** produziert als auf dem Windows-Rechner.
|
||||
|
||||
**Behoben in Phase 0** über `Infrastructure/Helpers/DateParsing.cs`
|
||||
(`InvariantCulture` + `AdjustToUniversal | AssumeUniversal`).
|
||||
|
||||
⚠️ **Offen: Altdaten.** Bereits gespeicherte Zeilen tragen die Verschiebung weiter. Ob ein
|
||||
Backfill nötig ist, hängt daran, wie stark `StartDate`/`EndDate`/`ClosedAt` in die
|
||||
Auswertung eingehen — siehe „Nächste Schritte" im Portierungsplan.
|
||||
|
||||
### 2.2 Kulturabhängige Zahlenformatierung — bis auf eine Stelle sauber ✅
|
||||
|
||||
Alle Parser für Fremd-APIs (Polymarket, Limitless) verwenden explizit `InvariantCulture`:
|
||||
|
||||
* `LimitlessProvider.cs:59,60,110,113,114,115`
|
||||
* `PolymarketModels.cs:17` · `PolymarketProvider.cs:386`
|
||||
* `TradeHistoryWorker.cs:466,467`
|
||||
|
||||
**Eine Ausnahme (echter Latent-Bug, plattformunabhängig):**
|
||||
|
||||
```csharp
|
||||
// Predictalytics.Infrastructure/Providers/Limitless/LimitlessProvider.cs:268
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
|
||||
```
|
||||
|
||||
Ohne `InvariantCulture`. Auf einem System mit deutscher Locale (`de_DE.UTF-8`, unter Linux
|
||||
sehr üblich) wird `"1234.56"` als **123456** interpretiert — Faktor 100.000 zu hoch.
|
||||
Der Fehler existiert schon heute auf einem deutschsprachigen Windows; Linux macht ihn nur
|
||||
wahrscheinlicher, weil die Locale dort oft explizit gesetzt wird.
|
||||
|
||||
**Aufwand: 5 Minuten. Sollte unabhängig von der Portierung gefixt werden.**
|
||||
|
||||
Zusätzliche Absicherung empfohlen: `InvariantGlobalization` bzw. ein explizites
|
||||
`CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture` im neuen Host.
|
||||
|
||||
### 2.3 Pfade und Dateisystem ✅
|
||||
|
||||
* Keine hartkodierten Laufwerksbuchstaben im Code — **eine** Ausnahme, siehe 4.5.
|
||||
* `Path.Combine()` wird konsequent verwendet, keine manuell zusammengesetzten `\`-Pfade.
|
||||
* **Kein einziger Datei-/Verzeichniszugriff** in `Infrastructure`, `Worker`, `Api`,
|
||||
`Application`, `Domain`. Sämtliches File-IO liegt in `WinFormsHost` (Settings, Logs)
|
||||
und im Lizenz-Client.
|
||||
|
||||
### 2.4 Groß-/Kleinschreibung (case-sensitive Dateisystem) ✅
|
||||
|
||||
Linux-Dateisysteme sind case-sensitiv, Windows nicht — ein häufiger Stolperstein bei
|
||||
statischen Web-Assets. Geprüft:
|
||||
|
||||
| Referenz | Datei | Passt |
|
||||
|---|---|---|
|
||||
| `href="css/style.css?v=20260715"` | `wwwroot/css/style.css` | ✅ |
|
||||
| `src="js/app.js?v=20260715"` | `wwwroot/js/app.js` | ✅ |
|
||||
| `href="./docs.html"` | `wwwroot/docs.html` | ✅ |
|
||||
| `href="./index.html"` | `wwwroot/index.html` | ✅ |
|
||||
|
||||
Alles durchgängig kleingeschrieben und deckungsgleich. **Aufwand: 0.**
|
||||
|
||||
Hinweis: MySQL-Tabellennamen sind unter Linux abhängig von `lower_case_table_names`
|
||||
ebenfalls case-sensitiv. Da Prod-DB und Dev-DB bereits auf Linux-Servern laufen
|
||||
(`lqf7.your-database.de`) und der Code über EF Core mit konsistenter Schreibweise
|
||||
zugreift, besteht hier kein zusätzliches Risiko durch die Client-Portierung.
|
||||
|
||||
### 2.5 Keine Windows-spezifischen APIs außerhalb des Hosts ✅
|
||||
|
||||
Geprüft auf `Registry`, `Microsoft.Win32`, `ManagementObject`/WMI, `ProtectedData`,
|
||||
`DllImport`, `System.Drawing`, `Environment.OSVersion`: **keine Treffer** außerhalb von
|
||||
`Predictalytics.WinFormsHost`. Auch `Encoding.Default` (unter Linux immer UTF-8, unter
|
||||
Windows Codepage) kommt nirgends vor.
|
||||
|
||||
### 2.6 NuGet-Abhängigkeiten ✅
|
||||
|
||||
Alle verwendeten Pakete sind plattformneutral: EF Core 8 + Pomelo MySQL, Serilog (+ File/
|
||||
Console/Compact), Swashbuckle, `Microsoft.Extensions.*`, xunit, Moq. Keine
|
||||
windows-exklusiven Pakete außerhalb des WinForms-Hosts.
|
||||
|
||||
### 2.7 Ein bereits vorhandener Vorteil: der Composition Root existiert schon ✅
|
||||
|
||||
`Predictalytics.Api/ApiConfiguration.cs` (`ConfigureApi`) ist eine **vollständige,
|
||||
eigenständige Host-Konfiguration** — DI, CORS, Swagger, Static Files, Endpoint-Mapping.
|
||||
Ihr fehlt nur eine `Main`-Methode. Der neue headless Host kann darauf direkt aufsetzen,
|
||||
statt die Verdrahtung neu zu schreiben.
|
||||
|
||||
Ebenso ist `Infrastructure/Logging/RichTextBoxSink.cs` trotz des Namens **vollständig
|
||||
portabel** — es ist ein reiner Delegate-Sink (`Action<string, LogEventLevel>`) ohne
|
||||
WinForms-Referenz. Lediglich umbenennen (z. B. `DelegateSink`) wäre sinnvoll.
|
||||
|
||||
---
|
||||
|
||||
## 3. Die tatsächlichen Blocker
|
||||
|
||||
### 3.1 `Predictalytics.WinFormsHost` — Ersatz durch headless Host ⛔ **Hauptaufwand**
|
||||
|
||||
Das Projekt ist der einzige Einstiegspunkt der Anwendung (`static void Main` existiert nur
|
||||
hier). Es leistet fünf Dinge, die im neuen Host anders gelöst werden müssen:
|
||||
|
||||
| Funktion heute | Ersatz unter Linux | Aufwand |
|
||||
|---|---|---|
|
||||
| **Serilog-Setup** (`Program.cs:35-148`) — Datei-Sinks nach Level und Plattform | 1:1 übernehmbar, ist bereits portabel. Nur `RichTextBoxSink` entfällt bzw. wird Console-Sink. | trivial |
|
||||
| **`EmbeddedWebServer.cs`** (365 Z.) — Kestrel- und Worker-Lifecycle | Inhaltlich portabel. Wird zum normalen `IHost` mit `WebApplication` + `AddWorkerServices()`. Vereinfacht sich sogar deutlich, weil Start/Stop-Buttons entfallen. | klein |
|
||||
| **`AppSettings.cs` + `PropertyGrid`** — Laufzeit-Konfiguration in `settings.json` | `appsettings.json` + Umgebungsvariablen + `IOptions<T>`. Die `[Category]`/`[DisplayName]`-Attribute entfallen ersatzlos. | klein–mittel |
|
||||
| **Manuelle Aktionen** — „Sync Markets", „DB Update", „Recalculate All Traders" (Bestätigungsdialoge + `MessageBox`) | Entweder CLI-Verben (`--sync-markets`, `--recalc-all`) oder geschützte Admin-Endpunkte in der bestehenden Web-UI. Die Rückfrage-Logik („nicht während laufender Worker") muss erhalten bleiben. | mittel |
|
||||
| **Log-Terminal** (`RichTextBox`) + DB-Größen-Anzeige + Build-Version | Entfällt / Console + `journalctl`. Die DB-Größenabfrage (`MainForm.cs:317-343`) kann als Health-Endpunkt weiterleben. | klein |
|
||||
|
||||
`WatchdogHeartbeatService.cs` (141 Z.) ist reines `HttpClient`-Verhalten und **vollständig
|
||||
portabel** — sollte nach `Infrastructure` oder in den neuen Host wandern.
|
||||
|
||||
**Empfehlung:** WinFormsHost **nicht löschen**, sondern beide Hosts parallel führen.
|
||||
Er ist die etablierte Betriebsoberfläche unter Windows; beide teilen sich denselben
|
||||
Composition Root. Der Wartungsaufwand für die dünne Hülle ist gering.
|
||||
|
||||
### 3.2 Lizenzierung — der unterschätzte Blocker ⛔
|
||||
|
||||
Der `LicenseLabrador.Client` ist erfreulich gut vorbereitet, aber die **Aktivierung** ist es nicht.
|
||||
|
||||
**Was bereits funktioniert:**
|
||||
|
||||
* `HardwareId.cs:52-84` behandelt Linux explizit: liest `/etc/machine-id` bzw.
|
||||
`/var/lib/dbus/machine-id`, mit MAC-Adressen-Fallback. Sauber gelöst.
|
||||
* Zielframework `netstandard2.0`, `Microsoft.Win32.Registry` wird per `IsWindows()`-Guard
|
||||
nur unter Windows angefasst.
|
||||
|
||||
**Was nicht funktioniert:**
|
||||
|
||||
1. **`LicenseDialog` ist ein blockierender WinForms-Modaldialog.**
|
||||
`Program.cs:16` → `LicenseGuard.EnsureLicensed()` → `dialog.ShowDialog()`.
|
||||
Auf einem headless Server gibt es niemanden, der einen Key eintippt. Es braucht einen
|
||||
**nicht-interaktiven Aktivierungspfad**: Lizenzschlüssel aus Umgebungsvariable oder
|
||||
Konfigurationsdatei, Aktivierung beim ersten Start, sauberer Abbruch mit Exit-Code und
|
||||
klarer Logmeldung, wenn keine gültige Lizenz vorliegt.
|
||||
|
||||
2. **`StateStore.cs:43,92` — DPAPI-Verschlüsselung fällt unter Linux still auf Klartext zurück.**
|
||||
```csharp
|
||||
try { dataToSave = ProtectedData.Protect(plainBytes, null, DataProtectionScope.CurrentUser); }
|
||||
catch { dataToSave = plainBytes; } // ← Linux landet immer hier
|
||||
```
|
||||
Der Lizenz-Cache (inkl. Lizenzschlüssel) liegt dann unverschlüsselt auf der Platte.
|
||||
Funktional unkritisch, aber eine bewusste Entscheidung — mindestens sollten die
|
||||
Dateirechte (`0600`) und das Verzeichnis abgesichert werden.
|
||||
|
||||
3. **`LicenseConfig.StorageDirectory`** nutzt `Environment.SpecialFolder.ApplicationData`
|
||||
→ unter Linux `~/.config`. Unter systemd mit `DynamicUser=yes` oder ohne `$HOME`
|
||||
existiert das nicht. Muss explizit gesetzt werden (z. B. `/var/lib/predictalytics`).
|
||||
|
||||
4. **`LicenseGuard.StartPeriodicRevalidation`** verwendet `System.Windows.Forms.Timer`
|
||||
→ `PeriodicTimer` oder `IHostedService`. `Application.Exit()` → `IHostApplicationLifetime.StopApplication()`.
|
||||
|
||||
**Das ist der Punkt mit der größten Restunsicherheit**, weil er das Schwester-Repo
|
||||
LicenseLabrador berührt und serverseitig geprüft werden muss (Aktivierung ohne UI).
|
||||
|
||||
### 3.3 Kestrel bindet nur auf `localhost` ⚠️
|
||||
|
||||
```csharp
|
||||
// EmbeddedWebServer.cs:122
|
||||
builder.WebHost.UseUrls($"http://localhost:{port}");
|
||||
```
|
||||
|
||||
Das ist für einen Desktop-Host korrekt, für einen Linux-Server aber falsch: von außen
|
||||
nicht erreichbar. Muss konfigurierbar werden (`0.0.0.0` bzw. `ASPNETCORE_URLS`), inkl.
|
||||
Anpassung der CORS-`AllowedOrigins` (heute fest `http://localhost:5000`).
|
||||
|
||||
**Sicherheitshinweis:** Sobald der Dienst nicht mehr nur auf `localhost` lauscht, ist die
|
||||
bisher implizite Absicherung weg. `MapPredictalyticsControlEndpoints()` (schreibende
|
||||
Operationen) und Swagger sind dann exponiert. Es gibt bereits ein `CanControl`/`AuthRequired`-
|
||||
Konzept (`ApiConfiguration.cs:66`) — das muss vor dem Linux-Deployment scharf geschaltet
|
||||
werden, plus Reverse Proxy (nginx/Caddy) mit TLS davor.
|
||||
|
||||
---
|
||||
|
||||
## 4. Weitere Fundstellen (klein, aber notwendig)
|
||||
|
||||
### 4.1 `Process.Start("explorer.exe", …)` — 3 Stellen
|
||||
`MainForm.cs:192,207,209` (Browser öffnen, Log-Ordner öffnen). Betrifft nur den
|
||||
WinForms-Host; im headless Host entfallen die Aktionen ersatzlos.
|
||||
|
||||
### 4.2 `System.Windows.Forms.Timer` — 2 Stellen
|
||||
`MainForm.cs:71` (DB-Größe), `LicenseGuard.cs:70` (Revalidierung).
|
||||
→ `PeriodicTimer` / `IHostedService`.
|
||||
|
||||
### 4.3 Schriftart `Cascadia Code`
|
||||
`MainForm.cs:25`. Nur WinForms-relevant, entfällt.
|
||||
|
||||
### 4.4 Log-Verzeichnis
|
||||
`Program.cs:33`: `Path.Combine(AppContext.BaseDirectory, "logs")`.
|
||||
Technisch portabel, aber unter Linux ist das Installationsverzeichnis üblicherweise nicht
|
||||
beschreibbar. → konfigurierbar machen, Default `/var/log/predictalytics` bzw.
|
||||
`journalctl` über den Console-Sink.
|
||||
|
||||
Nebenbefund: `src/Predictalytics.WinFormsHost/logs/Predictalytics-20260428.json` ist
|
||||
offenbar **versehentlich eingecheckt** und enthält Stacktraces mit lokalen Pfaden. Sollte
|
||||
aus der Versionierung entfernt und `.gitignore` ergänzt werden.
|
||||
|
||||
### 4.5 `FindWwwrootPath()` — hartkodierter Absolutpfad ⚠️
|
||||
```csharp
|
||||
// EmbeddedWebServer.cs:314-321
|
||||
var candidates = new[] {
|
||||
Path.Combine(baseDir, "wwwroot"),
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..","..","..","..","Predictalytics.Api","wwwroot")),
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..","..","..","..","..","src","Predictalytics.Api","wwwroot")),
|
||||
@"j:\Softwareprojekte\Predictalytics\Predictalytics\src\Predictalytics.Api\wwwroot" // ←
|
||||
};
|
||||
```
|
||||
Die Suchheuristik über Build-Output-Verzeichnisse funktioniert im Deployment nicht
|
||||
zuverlässig, der `j:\`-Fallback gar nicht. Sauber: `wwwroot` als `Content` mit
|
||||
`CopyToOutputDirectory` in die Host-`.csproj` aufnehmen (in der Api-`.csproj` bereits
|
||||
vorhanden) und nur noch `Path.Combine(AppContext.BaseDirectory, "wwwroot")` verwenden.
|
||||
|
||||
### 4.6 MySQL-Verbindung: `SslMode = None` fest verdrahtet ⚠️
|
||||
`AppSettings.cs:100`. Bei einem lokal laufenden Windows-Client zu einer entfernten DB war
|
||||
das schon fragwürdig; bei einem Serverbetrieb sollte SSL konfigurierbar (und
|
||||
standardmäßig aktiv) sein. Kein Linux-spezifisches Problem, fällt aber bei der
|
||||
Konfigurationsmigration ohnehin an.
|
||||
|
||||
### 4.7 Egress-Kanäle (`SourceIp`) — verhält sich unter Linux anders ⚠️
|
||||
`EgressPoolService.cs:70-88` bindet einen Socket per `socket.Bind(new IPEndPoint(ip, 0))`
|
||||
an eine Quell-IP. Das funktioniert unter Linux grundsätzlich, aber:
|
||||
* Die IP muss auf einem Interface tatsächlich konfiguriert sein (sonst `EADDRNOTAVAIL`);
|
||||
Windows ist hier toleranter.
|
||||
* Für nicht-lokale IPs bräuchte es `IP_FREEBIND`/`CAP_NET_ADMIN`.
|
||||
|
||||
Falls Egress-Kanäle produktiv genutzt werden, ist das ein expliziter Testpunkt.
|
||||
Proxy-Kanäle (`WebProxy`) sind unproblematisch.
|
||||
|
||||
### 4.8 Zielframework-Inkonsistenz ⚠️
|
||||
`Predictalytics.Application.Tests` steht auf **`net10.0`**, alle anderen auf `net8.0`.
|
||||
Das funktioniert (Test-Assembly darf höher liegen), erzwingt aber ein .NET-10-SDK auf dem
|
||||
Build-Server. Bei einer CI-Einrichtung für Linux vereinheitlichen — entweder alles auf
|
||||
`net8.0` oder ein bewusster Sprung der gesamten Solution auf `net10.0`.
|
||||
|
||||
### 4.9 `Predictalytics.slnx` und Build
|
||||
Die Solution referenziert den WinFormsHost. Auf Linux schlägt ein `dotnet build` der
|
||||
Gesamtsolution fehl (`net8.0-windows` + `UseWindowsForms`). Optionen:
|
||||
* Den WinFormsHost mit `<TargetFrameworks>` bzw. einer Condition auf
|
||||
`$([MSBuild]::IsOSPlatform('Windows'))` versehen, oder
|
||||
* eine zweite Solution-Datei für Linux-Builds pflegen.
|
||||
|
||||
Zusätzlich: Die `.csproj` des Hosts referenziert das Schwester-Repo über einen
|
||||
**relativen Pfad mit vier Ebenen nach oben** (`..\..\..\..\LicenseLabrador\…`). Das setzt
|
||||
eine feste Verzeichnisstruktur voraus und ist für CI/Container fragil — besser über ein
|
||||
NuGet-Paket oder ein Git-Submodul lösen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was zusätzlich neu entsteht (Deployment)
|
||||
|
||||
Kein Portierungsproblem im engeren Sinn, aber Teil der Aufgabe „Linux-fähig":
|
||||
|
||||
* **systemd-Unit** (`Restart=always`, `User=`, `WorkingDirectory=`, `EnvironmentFile=`)
|
||||
oder alternativ **Dockerfile** + `docker-compose` (es existiert bereits ein
|
||||
`J:\Softwareprojekte\dockerCompose`-Verzeichnis — vermutlich anknüpfbar).
|
||||
* **Reverse Proxy** (nginx/Caddy) mit TLS vor Kestrel.
|
||||
* **Secrets-Handling**: DB-Passwort und Watchdog-API-Key liegen heute im Klartext in
|
||||
`settings.json` neben der Exe. Unter Linux → Environment-File mit `0600` oder
|
||||
Secret-Store.
|
||||
*Randnotiz:* In `LicenseGuard.cs:16-17` stehen Basic-Auth-Zugangsdaten im Quelltext.
|
||||
Bei einem Server-Deployment (und erst recht bei der angedachten Monetarisierung) sollte
|
||||
das überdacht werden — unabhängig von dieser Portierung.
|
||||
* **EF-Migrationen**: `EnsureDatabaseAsync()` existiert bereits und wird beim Start
|
||||
aufgerufen — für den Serverbetrieb ist zu entscheiden, ob Migrationen automatisch
|
||||
laufen sollen oder als separater Schritt.
|
||||
* **CI**: Build/Test auf Linux, Publish als `linux-x64` (ggf. self-contained).
|
||||
* `docs/BETRIEB-Watchdog-Lizenz.md` muss um den Linux-Betrieb ergänzt werden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Aufwandsschätzung
|
||||
|
||||
| Arbeitspaket | Aufwand (PT) | Risiko |
|
||||
|---|---|---|
|
||||
| **A** Neues Host-Projekt `Predictalytics.Host` (net8.0): Serilog, Kestrel, Worker, Lifecycle | 2–3 | niedrig — Composition Root existiert bereits |
|
||||
| **B** Konfigurationsmigration `AppSettings`/`PropertyGrid` → `appsettings.json` + Env + `IOptions` | 1 | niedrig |
|
||||
| **C** Manuelle Wartungsaktionen → CLI-Verben bzw. Admin-Endpunkte | 1–1,5 | niedrig |
|
||||
| **D** Lizenzierung headless: nicht-interaktive Aktivierung, StorageDirectory, Timer, Cache-Absicherung | 1,5–3 | **mittel–hoch** — berührt LicenseLabrador, serverseitig zu verifizieren |
|
||||
| **E** Kestrel-Binding + CORS + Absicherung der Control-Endpunkte | 0,5–1 | mittel (Sicherheitsentscheidung nötig) |
|
||||
| **F** Kleinkram: `wwwroot`-Deployment, Log-Pfad, Culture-Bug, TFM-Vereinheitlichung, Solution-Split, eingecheckte Logs | 0,5–1 | niedrig |
|
||||
| **G** Deployment: systemd/Docker, Reverse Proxy, Secrets, CI | 1–2 | niedrig–mittel |
|
||||
| **H** Verifikation auf einer echten Linux-Instanz (Worker-Dauerlauf, DB, Egress, Watchdog, Lizenz) | 2 | mittel |
|
||||
| **Summe** | **~9,5–14,5 PT** | |
|
||||
|
||||
**Realistisch: 2–3 Wochen bei Teilzeit, ca. 2 Wochen fokussiert.**
|
||||
|
||||
Zum Vergleich: Der ursprünglich angenommene Weg über **Avalonia + LiveCharts2** hätte
|
||||
grob **4–8 Wochen** gekostet — für eine Oberfläche, die es fachlich bereits als Web-UI
|
||||
gibt. Diese Arbeit wäre vollständig verloren gewesen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Empfohlenes Vorgehen
|
||||
|
||||
1. **F zuerst** (halber Tag): Culture-Bug, TFM, eingecheckte Logs, `wwwroot`-Deployment.
|
||||
Wirkt sofort, unabhängig von der Portierung, reduziert Rauschen.
|
||||
2. **A + B parallel** — das neue Host-Projekt entsteht neben dem WinFormsHost, nicht
|
||||
an seiner Stelle. Beide teilen sich `ApiConfiguration` und `AddWorkerServices()`.
|
||||
Ab hier läuft der Stack unter Linux (noch ohne Lizenz-Gate).
|
||||
3. **E** — Binding und Absicherung, bevor irgendetwas exponiert wird.
|
||||
4. **D** — Lizenzierung. Am besten früh anfangen zu klären, weil es das Schwester-Repo
|
||||
und ggf. Serveränderungen betrifft; die Umsetzung kann dann parallel laufen.
|
||||
5. **C**, **G**, **H** zum Abschluss.
|
||||
|
||||
**Zwei Entscheidungen sind vorab zu treffen:**
|
||||
|
||||
* **Bleibt der WinFormsHost erhalten?** Empfehlung: **ja**, als Windows-Betriebsoberfläche.
|
||||
Er ist dünn, funktioniert und beide Hosts teilen sich denselben Kern. Der Web-UI-Zugang
|
||||
ist ohnehin identisch.
|
||||
* **Wie wird headless lizenziert?** Lizenzschlüssel per Umgebungsvariable/Config und
|
||||
Aktivierung beim ersten Start ist der pragmatische Weg — muss aber mit LicenseLabrador
|
||||
abgestimmt werden.
|
||||
|
||||
---
|
||||
|
||||
## 8. Zusammenfassung in drei Sätzen
|
||||
|
||||
Predictalytics ist in deutlich besserem Zustand für eine Linux-Portierung als erwartet:
|
||||
96,9 % des Codes sind bereits plattformneutral, es gibt keine Windows-Zeitzonen-IDs und
|
||||
keine Windows-APIs außerhalb des Hosts, und die Analyse-Oberfläche ist längst eine
|
||||
Browser-Anwendung mit Chart.js — LiveCharts2 wird nicht gebraucht.
|
||||
|
||||
Zwei Defekte an der Grenze zu den Plattform-APIs (13 × lokalzeit-verzerrendes
|
||||
Datums-Parsing, 1 × kulturabhängiges Zahlen-Parsing) waren allerdings vorhanden und sind
|
||||
in Phase 0 behoben.
|
||||
|
||||
Der eigentliche Aufwand liegt im Ersetzen der 1.698 Zeilen umfassenden WinForms-
|
||||
Bedienhülle durch einen headless Host und im Umbau der Lizenzaktivierung, die heute
|
||||
zwingend einen Modaldialog erfordert.
|
||||
|
||||
Geschätzt **9,5–14,5 Personentage**, mit dem einzigen nennenswerten Risiko im
|
||||
Lizenzierungspfad.
|
||||
@@ -0,0 +1,560 @@
|
||||
# Fix- und Datenreparatur-Plan (Stand 2026-07-09, Übergabe an Gemini)
|
||||
|
||||
> **Abnahmekriterium für alle Code-Änderungen:** `dotnet test src/Predictalytics.Application.Tests` muss
|
||||
> **16 grün + 1 übersprungen** liefern (der Skip `CheckpointResetAndReplay_DoesNotDoubleCountBalance` ist eine
|
||||
> dokumentierte, bewusste Entscheidung). Die Assertions der Invarianten-Tests dürfen **nicht** verändert werden —
|
||||
> sie definieren das Soll-Verhalten. Wenn ein Test rot wird, ist der Code falsch, nicht der Test.
|
||||
|
||||
## Hintergrund
|
||||
|
||||
Die Engine-Fixes vom 09.07. sind korrekt (Tests grün). Die im WebUI sichtbaren Probleme haben drei andere Ursachen:
|
||||
|
||||
1. Die Buttons der **Trader-Detailseite** nutzen alte, Job-lose Endpoints (die Listen-Buttons nutzen bereits das Job-System).
|
||||
2. Die **abgeleiteten Daten in der DB stammen aus der Bug-Ära** (Snapshots/Positionen wurden von den alten, fehlerhaften
|
||||
Engine-Versionen berechnet). Beispiel aus dem Live-System: `PnL30d = 244,0K` bei `TotalPnL = 158,5K`, weil der
|
||||
Basis-Snapshot `-85,5K` enthält (korrupter Altwert). Kein Code-Fix ändert das — die Daten müssen einmalig repariert werden.
|
||||
3. **Deadlocks + Shutdown-Fehlerkaskaden** in den Workern (unbatchtes Reconciliation-UPDATE, fehlende Cancellation-Behandlung).
|
||||
|
||||
**Ein DB-Reset ist NICHT nötig.** Die Rohdaten (`Trades`) sind größtenteils intakt; Positionen, Analytics, Snapshots und
|
||||
Scores sind abgeleitet und lokal neu berechenbar. Nur Trader, deren Alt-Trades die Retention bereits gelöscht/kompaktiert
|
||||
hat, brauchen einen gezielten API-Re-Import (kleine Teilmenge, siehe Teil B).
|
||||
|
||||
---
|
||||
|
||||
## Teil A — Code-Fixes
|
||||
|
||||
### A1. Trader-Detailseite: Buttons auf das Job-System umstellen
|
||||
**Problem:**
|
||||
- `manualUpdateTrader` in `src/Predictalytics.Api/wwwroot/js/app.js` (~Zeile 110) baut die URL mit **Backslashes**:
|
||||
`` fetch(`\api\traders\${id}\refresh`) `` — in JS-Template-Literals ist `\t` ein Tab und `\${id}` unterdrückt die
|
||||
Interpolation. Der Request geht als Müll-URL raus.
|
||||
- Der „Analyze"-Button der Detailseite (~Zeile 441) ruft `POST /api/traders/{id}/force-analyze` — läuft **synchron** im
|
||||
API-Request, legt **keinen** `BackgroundJob` an (der Alert behauptet es aber) und führt nur die PnL-Engine aus,
|
||||
weder `CopytradingEstimator` noch KI.
|
||||
|
||||
**Fix:**
|
||||
- `btn-sync-trader` → `POST /api/jobs/sync/{id}`, `btn-analyze-trader` → `POST /api/jobs/analyze/{id}`
|
||||
(bestehende Funktionen `queueHistorySync(id)` / `queueTraderAnalysis(id)` wiederverwenden).
|
||||
- Alert-Texte ehrlich machen (Job-Id anzeigen oder auf die Jobs-Seite verweisen).
|
||||
- Die Endpoints `/{id}/force-analyze` und `/{id}/refresh` entweder entfernen oder intern auf Job-Enqueue umbauen —
|
||||
es darf nur noch **einen** Auslöse-Pfad geben.
|
||||
|
||||
### A2. TradeReconciliationWorker: Bulk-UPDATE batchen, Fehler pro Markt behandeln
|
||||
**Problem:** Das eine große `UPDATE Trades ... INNER JOIN ... WHERE MarketOutcomeId IS NULL` läuft über die gesamte
|
||||
Tabelle, hält minutenlang Locks und produziert Deadlocks mit den Insert-Workern. Außerdem verwirft der eine
|
||||
try/catch um den ganzen Batch bei jedem Einzelfehler (z. B. ein fehlgeschlagener `GetMarketAsync`) die komplette Restarbeit.
|
||||
|
||||
**Fix:**
|
||||
- UPDATE in Batches. Achtung: MySQL erlaubt kein `LIMIT` bei Multi-Table-UPDATE — Pattern mit Subquery verwenden:
|
||||
```sql
|
||||
UPDATE Trades t
|
||||
JOIN (
|
||||
SELECT t2.Id, o.Id AS OutcomeId, o.Label, m.Id AS MarketDbId
|
||||
FROM Trades t2
|
||||
JOIN MarketOutcomes o ON t2.AssetId = o.TokenId
|
||||
JOIN Markets m ON o.MarketId = m.Id
|
||||
WHERE t2.MarketOutcomeId IS NULL AND t2.AssetId != ''
|
||||
LIMIT 5000
|
||||
) x ON t.Id = x.Id
|
||||
SET t.MarketOutcomeId = x.OutcomeId, t.Outcome = x.Label, t.DbMarketId = x.MarketDbId;
|
||||
```
|
||||
In einer Schleife ausführen, bis 0 Zeilen betroffen sind (mit kurzem Delay zwischen den Batches).
|
||||
- `GetMarketAsync`-Fehler pro Markt fangen und loggen — die restlichen Märkte des Batches weiterverarbeiten.
|
||||
- Das Checkpoint-Reset (`LastAppliedTradeId = 0`) weiterhin **nur** für Positionen mit `IsHistoryPruned = 0`
|
||||
(ist bereits so umgesetzt — nicht regressieren, Test `PrunedPositionWithResetCheckpoint_DoesNotDoubleCount` wacht darüber).
|
||||
|
||||
### A3. Deadlock-Retry in `TradeRepository.AddRangeAsync`
|
||||
`MySqlException` mit `Number == 1213` (Deadlock) oder `1205` (Lock wait timeout) → bis zu 3 Versuche mit Backoff
|
||||
(250 ms / 500 ms / 1 s). Chunk-Größe von 1000 auf 500 Zeilen reduzieren. Bei endgültigem Fehlschlag: Fehler loggen
|
||||
inkl. Anzahl verlorener Zeilen.
|
||||
|
||||
### A4. Saubere Cancellation in allen Worker-Loops
|
||||
**Problem:** Beim Stoppen des Servers wirft jede laufende Operation `OperationCanceledException`; der
|
||||
`TraderAnalyticsWorker` fängt das **pro Trader** als ERROR und nudelt durch den restlichen 500er-Batch
|
||||
(→ hunderte Fehlerlog-Einträge pro Shutdown, verzögerter Stopp).
|
||||
|
||||
**Fix (in TraderAnalyticsWorker, PollingWorker, TradeHistoryWorker, TradeReconciliationWorker, TradeContextEnrichmentWorker):**
|
||||
- Vor jeder Batch-Iteration: `if (ct.IsCancellationRequested) break;`
|
||||
- `catch (OperationCanceledException) when (ct.IsCancellationRequested)` separat behandeln:
|
||||
als Information loggen („shutting down"), Schleife beenden — **nicht** als Error.
|
||||
|
||||
### A5. Hängengebliebene Jobs wiederbeleben
|
||||
**Problem:** Jobs, die beim Shutdown `InProgress` waren, bleiben für immer stecken (`GetNextPendingJobAsync` holt nur `Pending`).
|
||||
|
||||
**Fix:** Beim Start der Job-verarbeitenden Worker (oder einmal pro Zyklus): Jobs mit `Status = InProgress` und
|
||||
`StartedAt < UtcNow - 15min` zurück auf `Pending` setzen (Log-Hinweis).
|
||||
|
||||
### A6. Deep-Resync-Fähigkeit (Voraussetzung für die Datenreparatur in Teil B)
|
||||
**Problem:** `PolymarketApiClient.GetTradesAsync` macht genau **einen** Request (`/activity?user=X&limit=1000`,
|
||||
keine Pagination; die API cappt vermutlich ohnehin bei 500). Der „INITIAL FULL sync" holt also nur die jüngsten
|
||||
~500–1000 Aktivitäten. Für die Reparatur der Retention-/Kompaktierungs-Opfer brauchen wir die **komplette** Historie.
|
||||
|
||||
**Fix:**
|
||||
- Neue Methode `GetTradesPagedAsync(wallet, ...)` mit **Timestamp-basierter Pagination**: erste Seite normal laden,
|
||||
Folgeseiten mit `&end=<ältester Timestamp der Vorseite - 1>` bis eine leere Seite kommt. (Timestamp-Pagination ist
|
||||
robuster als `offset`, da Offset-Limits der API umgangen werden.) `limit=500` verwenden. Jede Seite über den
|
||||
vorhandenen `IRateLimiter` drosseln.
|
||||
- Neuer `JobType.DeepResync` (Migration für Enum nicht nötig, Enum ist int): Der `TradeHistoryWorker` behandelt ihn wie
|
||||
`HistorySync`, lädt aber ALLE Seiten.
|
||||
- **Vor** dem Import im DeepResync-Pfad für den Trader aufräumen (sonst Doppelzählung!):
|
||||
1. `DELETE FROM Trades WHERE TraderId = @id AND PlatformTradeId LIKE 'COMPACT_%'`
|
||||
(Re-Import bringt die Original-Trades zurück; die Aggregate dürfen nicht zusätzlich existieren),
|
||||
2. alle `TraderPositions` des Traders löschen (**inklusive** `IsHistoryPruned = 1` — die Konserve wird durch den
|
||||
vollständigen Re-Import ersetzt),
|
||||
3. nach erfolgreichem Import: `IsInitialImportComplete = true`, `LastTradesUpdatedAt = now`, `LastAnalyzedAt = NULL`.
|
||||
- Endpoints:
|
||||
- `POST /api/jobs/deep-resync/{traderId}` (einzeln),
|
||||
- `POST /api/jobs/deep-resync-pruned?take=25` — enqueued DeepResync-Jobs für Trader mit `IsHistoryPruned`-Positionen
|
||||
oder `COMPACT_`-Trades, Watchlist zuerst, dann nach `TotalTrades` absteigend.
|
||||
- WebUI: Button „Deep Resync" auf der Jobs-Seite neben „Analyze Backlog".
|
||||
|
||||
### A7. Retention pausierbar machen
|
||||
Neues Config-Flag `RetentionSettings:Enabled` (Default `true`), das der `TradeRetentionWorker` pro Zyklus prüft.
|
||||
Während der Datenreparatur steht es auf `false` — sonst prunt/kompaktiert die tägliche Runde die frisch
|
||||
re-importierten Alt-Trades wieder weg, bevor die Engine sie eingerechnet hat.
|
||||
|
||||
### A8. ⚠️ NEU (2026-07-10, höchste Priorität): ResolutionOutcome existiert in der Gamma-API nicht — alle Gewinner werden als Totalverlust gebucht
|
||||
|
||||
**Empirisch gegen die Live-API verifiziert:** Die Antwort von `gamma-api.polymarket.com/markets` enthält
|
||||
**weder** ein Feld `resolution_outcome` (so mappt es `GammaMarketResponse` aktuell) **noch** `resolutionOutcome`
|
||||
**noch** `resolved`. Folgen im Bestand und in jeder Neuberechnung:
|
||||
|
||||
- `Market.ResolutionOutcome` ist für **jeden** Markt `NULL` → `MarketOutcomeHelper.IsWinningOutcome` liefert immer
|
||||
`false` → jeder Redeem und jeder virtuelle Payout bucht Auszahlung **0** → **jeder aufgelöste Markt ist ein
|
||||
Totalverlust**. Das erzeugt exakt das Live-Bild: WinRate 0 %, Quality Edge 0.0, negative Total-PnL.
|
||||
- `IsResolved = raw.Resolved || raw.Closed` degeneriert zu `IsResolved = closed`. Märkte, die für den Handel
|
||||
geschlossen, aber noch nicht UMA-aufgelöst sind, werden **vorzeitig** zu Payout 0 ausgebucht.
|
||||
|
||||
**Wie man den Gewinner wirklich erkennt** (Live-API-Beispiele): Nach der Auflösung rasten die `outcomePrices`
|
||||
auf `["1","0"]` / `["0","1"]` ein (liegen bei uns bereits in `MarketOutcome.CurrentPrice`), und es gibt das Feld
|
||||
`umaResolutionStatus` (String, `"resolved"` bei aufgelösten Märkten; bei sehr alten Märkten fehlt es).
|
||||
|
||||
**Fix (drei Teile):**
|
||||
1. **Model:** In `GammaMarketResponse` das tote `resolution_outcome`-Mapping entfernen,
|
||||
`[JsonPropertyName("umaResolutionStatus")] public string? UmaResolutionStatus` ergänzen.
|
||||
2. **Mapper (`MapGammaMarket`):**
|
||||
- Preise parsen, dann: `pricesSnapped = alle Outcome-Preise ≤ 0.02 oder ≥ 0.98` (und mindestens ein Preis ≥ 0.98).
|
||||
- `IsResolved = raw.UmaResolutionStatus == "resolved" || (raw.Closed && pricesSnapped)`.
|
||||
- `ResolutionOutcome = Label des Outcomes mit Preis ≥ 0.98` (nur wenn `IsResolved`; sonst `NULL`).
|
||||
3. **Engine-Absicherung (Defense in depth, weil der Bestand NULL-Werte enthält):** Redeem-Buchung und virtueller
|
||||
Payout dürfen nur settlen, wenn das Ergebnis entscheidbar ist: `ResolutionOutcome` gesetzt **oder** ein
|
||||
Outcome-Preis des Marktes ≥ 0.98 (dann gilt das Outcome mit Preis ≥ 0.98 als Gewinner, z. B. via erweitertem
|
||||
`MarketOutcomeHelper`). Ist der Markt „resolved", aber nichts entscheidbar (Preise nicht eingerastet) →
|
||||
**Position offen lassen** (kein Payout zu 0!).
|
||||
|
||||
**Abnahme:** Zwei neue rote Invarianten-Tests in `PositionPnLEngineTests.cs` müssen grün werden, ohne die
|
||||
Assertions zu ändern:
|
||||
- `RecalculateTraderPositionsAsync_ResolvedMarketWithoutResolutionOutcome_PaysWinnerViaSnappedPrice`
|
||||
(aktuell: RealizedPnl −40 statt +60)
|
||||
- `RecalculateTraderPositionsAsync_ClosedButUnresolvedMarket_DoesNotBookPrematurePayout`
|
||||
(aktuell: Position wird zu 0 ausgebucht statt offen zu bleiben)
|
||||
|
||||
### A9. Trader-Namen aus der Activity-API übernehmen (Suche nach Benutzername)
|
||||
|
||||
**Problem:** Manuell hinzugefügte (und über Markt-Trades entdeckte) Trader behalten für immer den
|
||||
Platzhalter-Namen `0x2005d16a...` — die Suche findet sie nur über die Adresse, nicht über den Polymarket-Namen
|
||||
(Beispiel: `0x2005d16a84ceefa912d4e380cd32e7ff827875ea` heißt auf Polymarket „RN1").
|
||||
|
||||
**Empirisch verifiziert:** Jede Zeile der `/activity`-Antwort enthält bereits `name` („RN1") und `pseudonym`
|
||||
(„Scary-Edible") — die Felder werden nur nicht gemappt und damit bei jedem Sync weggeworfen.
|
||||
|
||||
**Fix:**
|
||||
1. `PolymarketTradeResponse`: `[JsonPropertyName("name")] public string? Name` und
|
||||
`[JsonPropertyName("pseudonym")] public string? Pseudonym` ergänzen.
|
||||
2. `Trade`: transientes Feld `[NotMapped] public string? TransientDisplayName` (analog `TransientWallet`);
|
||||
im `PolymarketProvider`-Mapping mit `name`, Fallback `pseudonym`, befüllen.
|
||||
3. `PollingWorker` und `TradeHistoryWorker`: nach dem Fetch, wenn ein nicht-leerer `TransientDisplayName`
|
||||
vorliegt und vom aktuellen `DisplayName` abweicht → `trader.DisplayName` aktualisieren
|
||||
(die Plattform ist die Quelle der Wahrheit; Platzhalter wie `0x…` heilen sich damit von selbst).
|
||||
4. `DiscoveryService.ImportTraderAsync` (manuelles Hinzufügen): direkt beim Import die erste Activity-Seite
|
||||
abrufen und den Namen setzen, statt des Wallet-Präfixes.
|
||||
5. Die Suche (`TraderRepository.SearchAsync`) durchsucht `DisplayName` bereits — funktioniert danach automatisch
|
||||
für Name **und** Adresse.
|
||||
|
||||
**Empfohlener Beifang im selben Handgriff:** Die Antwort enthält auch `usdcSize` (echter Cash-Betrag — wichtig für
|
||||
korrekte Split/Merge/Redeem-Buchungen) und `outcomeIndex` (robustes Outcome-Matching ohne Label-Vergleich).
|
||||
Mindestens im Response-Model mit erfassen; Persistierung von `usdcSize` auf `Trade` (Migration) als eigener
|
||||
kleiner Folge-Task.
|
||||
|
||||
### A10. Watchlist end-to-end reparieren + `api()`-Helper-Bug (betrifft auch die KI-Analyse!)
|
||||
|
||||
**Problem 1 — der zentrale JS-Helper verwirft alle Fetch-Optionen:**
|
||||
```js
|
||||
// app.js Zeile 133 — options-Parameter fehlt komplett:
|
||||
async function api(endpoint) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`); // ← { method: 'POST' } wird ignoriert!
|
||||
```
|
||||
Jeder Aufruf der Form `api(url, { method: 'POST'|'DELETE' })` degradiert still zu einem **GET** → 404 →
|
||||
der Fehler wird im catch geschluckt (`return null`). Betroffen: **Watchlist-Toggle** (Zeile ~449) und
|
||||
**KI-Analyse-Button** (Zeile ~605). Deshalb „passiert nichts" beim Watchlist-Button — und deshalb steht überall
|
||||
„Not analyzed yet".
|
||||
|
||||
**Problem 2 — Backslash im Route-Template (gleiche Tippfehler-Familie wie in app.js):**
|
||||
`TraderEndpoints.cs` Zeile ~52: `group.MapPost("\{id:int}/ai-analysis", ...)` — die Route ist mit dem
|
||||
Backslash unerreichbar. Der KI-Analyse-Endpoint ist damit **serverseitig ebenfalls tot** (doppelt kaputt).
|
||||
|
||||
**Problem 3 — es gibt keine Watchlist-Ansicht:** Der Toggle-Button existiert, aber nirgendwo im WebUI kann man
|
||||
die beobachteten Trader sehen. `WatchlistService.GetAllAsync` existiert im Backend, hat aber weder Endpoint noch UI.
|
||||
|
||||
**Fix:**
|
||||
1. `api()`-Helper reparieren:
|
||||
```js
|
||||
async function api(endpoint, options = {}) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const text = await res.text();
|
||||
return text ? JSON.parse(text) : true; // leere 200er (Results.Ok()) nicht crashen lassen
|
||||
} catch (err) { console.error(`API Error [${endpoint}]:`, err); return null; }
|
||||
}
|
||||
```
|
||||
2. Route-Template fixen: `"\{id:int}/ai-analysis"` → `"/{id:int}/ai-analysis"`.
|
||||
**Danach das gesamte Projekt nach weiteren Backslash-Pfaden absuchen** (`grep -rn '"\\{' src/` und
|
||||
`grep -n '\\\\api' wwwroot/js/app.js`) — das ist jetzt der dritte Fall dieser Fehlerklasse.
|
||||
3. Neuer Endpoint `GET /api/watchlist`: liefert Watchlist-Einträge mit Trader-Kerndaten
|
||||
(TraderId, DisplayName, Label, Notes, CreatedAt, TotalPnl, WinRate, CopytradingScore).
|
||||
4. WebUI: Nav-Punkt „Watchlist" + Seite mit Tabelle (Spalten wie Traders-Liste, plus Label/Notes und
|
||||
Remove-Button; Zeilenklick öffnet die Detailseite). Der Toggle auf der Detailseite muss nach dem Klick
|
||||
sichtbar den Zustand wechseln („Watchlist (Add)" ↔ „Watchlist (Remove)").
|
||||
|
||||
---
|
||||
|
||||
## Teil B — Datenreparatur ohne DB-Reset (Reihenfolge strikt einhalten)
|
||||
|
||||
**Warum kein Reset nötig ist:** `Trades` = Rohdaten, größtenteils intakt. `TraderPositions`, `TraderAnalytics`,
|
||||
`TraderDailySnapshots`, `TraderScores`, `TraderCategoryPerformances` = abgeleitet, lokal neu berechenbar.
|
||||
`Markets/Events` = unvollständig (Erbe des 10%-Sampling-Bugs), aber per Marktsync günstig nachladbar.
|
||||
Nur Pruned-/Compacted-Trader brauchen API-Re-Import — das ist eine kleine Teilmenge, nicht die ganze Import-Woche.
|
||||
|
||||
### B0. Diagnose (Umfang bestimmen — SQL führt Richard selbst aus)
|
||||
```sql
|
||||
-- Wie viele Trader brauchen Deep-Resync?
|
||||
SELECT COUNT(*) AS PrunedPositions, COUNT(DISTINCT TraderId) AS BetroffeneTrader
|
||||
FROM TraderPositions WHERE IsHistoryPruned = 1;
|
||||
SELECT COUNT(DISTINCT TraderId) FROM Trades WHERE PlatformTradeId LIKE 'COMPACT_%';
|
||||
-- Reconciliation-Backlog und Snapshot-Bestand
|
||||
SELECT COUNT(*) FROM Trades WHERE MarketOutcomeId IS NULL;
|
||||
SELECT COUNT(*) FROM TraderDailySnapshots;
|
||||
```
|
||||
|
||||
### B1. Vorbereitung
|
||||
Teil A deployen → Worker stoppen → **DB-Dump als Sicherung** → `RetentionSettings:Enabled = false`.
|
||||
|
||||
### B2. Voll-Marktsync
|
||||
Manuellen Market-Sync (inkl. geschlossener Märkte) einmal komplett durchlaufen lassen — schließt die Markt-Lücken,
|
||||
an denen die Trade-Verlinkung bisher scheiterte, **und befüllt nach A8 erstmals `ResolutionOutcome`/korrektes
|
||||
`IsResolved` für den gesamten Marktbestand**. Kostet nur Events-Endpoint-Requests (einige hundert), keine Import-Woche.
|
||||
|
||||
Optional als Sofort-Backfill vor dem Sync (nutzt die bereits gespeicherten, eingerasteten Preise):
|
||||
```sql
|
||||
UPDATE Markets m
|
||||
JOIN MarketOutcomes o ON o.MarketId = m.Id AND o.CurrentPrice >= 0.98
|
||||
SET m.ResolutionOutcome = o.Label
|
||||
WHERE m.IsResolved = 1 AND (m.ResolutionOutcome IS NULL OR m.ResolutionOutcome = '');
|
||||
```
|
||||
|
||||
### B3. SQL-Reparatur der abgeleiteten Daten (Richard führt aus, Worker sind aus)
|
||||
```sql
|
||||
-- Vergiftete Fenster-Basis komplett verwerfen (heilt über den Fallback + neue Snapshots)
|
||||
TRUNCATE TABLE TraderDailySnapshots;
|
||||
|
||||
-- Abgeleitete Kategorien-Statistik neu aufbauen lassen
|
||||
DELETE FROM TraderCategoryPerformances;
|
||||
|
||||
-- Positionen mit vollständiger lokaler Historie löschen → Engine baut sie mit gefixtem Code neu
|
||||
DELETE FROM TraderPositions WHERE IsHistoryPruned = 0;
|
||||
-- (IsHistoryPruned = 1 absichtlich behalten: Konserve bis zum Deep-Resync in B5)
|
||||
|
||||
-- Analytics nullen
|
||||
UPDATE TraderAnalytics SET OverallPnL=0, PnL30d=0, PnL7d=0, PnL24h=0,
|
||||
OverallWinRate=0, WinRate30d=0, WinRate7d=0, WinRate24h=0,
|
||||
CurrentBalance=0, EstimatedBankroll=0, Trades30d=0,
|
||||
CopytradingScore=0, CopytradingQualityScore=0, CopytradingCopyabilityScore=0;
|
||||
|
||||
-- Re-Analyse für alle triggern
|
||||
UPDATE Traders SET LastAnalyzedAt = NULL;
|
||||
|
||||
-- Optional (einmalig, teuer — außerhalb der Stoßzeiten): Zähler geradeziehen
|
||||
UPDATE Traders t SET TotalTrades = (SELECT COUNT(*) FROM Trades tr WHERE tr.TraderId = t.Id);
|
||||
```
|
||||
|
||||
### B4. Worker starten, Backlog abarbeiten lassen
|
||||
Reconciliation (jetzt gebatcht) verlinkt die Orphans; der `TraderAnalyticsWorker` rechnet alle Trader neu
|
||||
(Fortschritt über Jobs-Seite/Analyze-Backlog-Button sichtbar). Die Fenster-PnL läuft anfangs über den Fallback und
|
||||
gewinnt mit jedem Tag Snapshot-Präzision — nach 30 Tagen voll da. Das ist korrekt und erwartbar.
|
||||
|
||||
### B5. Deep-Resync der betroffenen Trader
|
||||
Für alle Trader aus B0 (Pruned/Compacted): `POST /api/jobs/deep-resync-pruned` in Häppchen (z. B. 25er-Batches),
|
||||
über Tage verteilt — der RateLimiter drosselt automatisch. Watchlist-Trader zuerst.
|
||||
Bis ein Trader dran war, zeigt er die (möglicherweise leicht verzerrte) Pruned-Konserve — akzeptierter Zwischenzustand.
|
||||
|
||||
### B6. Retention wieder aktivieren
|
||||
`RetentionSettings:Enabled = true`. Ab jetzt entsteht die Pruned-Konserve auf Basis der **korrekten** Engine —
|
||||
zukünftiges Pruning ist damit verlustfrei im Sinne der PnL-Summen.
|
||||
|
||||
### B7. Verifikation
|
||||
1. Invarianten-SQL:
|
||||
```sql
|
||||
-- Fenster-PnL darf für Trader ohne Trades im Fenster nicht = Lifetime sein
|
||||
SELECT COUNT(*) FROM TraderAnalytics a
|
||||
WHERE ABS(a.PnL30d) > 0 AND a.Trades30d = 0;
|
||||
```
|
||||
2. Plausibilitäts-Stichprobe gegen Polymarkets eigene Zahlen: kleiner Dev-Endpoint
|
||||
`GET /api/dev/verify-positions/{traderId}`, der `GetTraderPositionsAsync` (Polymarkets `/positions` liefert
|
||||
deren berechnete `size`/`avgPrice`/`percentPnl`) mit unseren `TraderPositions` vergleicht und Abweichungen
|
||||
> 5 % listet. 10–20 aktive Trader stichproben.
|
||||
3. 24 h Logs beobachten: keine 1213-Deadlocks, keine ERR-Kaskaden bei Shutdown, Jobs-Seite zeigt Durchsatz.
|
||||
|
||||
---
|
||||
|
||||
## Teil C — Abnahmekriterien (gesamt)
|
||||
|
||||
1. `dotnet test`: **18 grün + 1 skip** (inkl. der beiden A8-Tests), Assertions unverändert.
|
||||
2. „Sync"/„Analyze" auf der Detailseite erzeugen sichtbare Einträge auf der Jobs-Seite, die auch abgearbeitet werden.
|
||||
3. Nach B3/B4: kein Trader mehr mit `|PnL30d| > 0` bei `Trades30d = 0`; PnL30d/Total-Verhältnisse plausibel.
|
||||
4. Quality Edge / Copyability auf der Detailseite ≠ 0 für analysierte Trader mit verlinkten Trades.
|
||||
5. 24 h Betrieb ohne Deadlock-Errors und ohne Shutdown-Fehlerkaskaden.
|
||||
6. Die Suche nach „RN1" findet den Trader `0x2005d16a84ceefa912d4e380cd32e7ff827875ea` (nach dessen nächstem Sync). *(A9)*
|
||||
7. Watchlist: Toggle auf der Detailseite wechselt sichtbar den Zustand; die neue Watchlist-Seite listet die
|
||||
beobachteten Trader; Remove funktioniert. *(A10)*
|
||||
8. „Run Deep Analysis" (KI) füllt die AI Strategy Analysis auf der Detailseite tatsächlich. *(A10)*
|
||||
|
||||
---
|
||||
### D1. `AggregatedCount`-Spalte (Grundlage für D2/D3)
|
||||
|
||||
- Migration: `Trades.AggregatedCount INT NULL` (NULL = einzelner Roh-Trade).
|
||||
- Kompaktierung im `TradeRetentionWorker`: schreibt `AggregatedCount = Anzahl der ersetzten Trades`
|
||||
(heute geht die Original-Anzahl verloren!) und summiert beim erneuten Kompaktieren
|
||||
bestehende Aggregate (`Sum(t.AggregatedCount ?? 1)`).
|
||||
- Engine: `trader.TotalTrades = trades.Sum(t => t.AggregatedCount ?? 1)` (ersetzt `trades.Count`),
|
||||
`analytics.Trades30d` analog.
|
||||
- **Test:** Kompaktierung von 5 Trades → 1 Aggregat mit `AggregatedCount = 5`; `TotalTrades` bleibt nach
|
||||
Recalc 5, nicht 1.
|
||||
|
||||
### D2. TraderTraits — heuristische Strategie-Merkmale (ohne KI)
|
||||
|
||||
- Neue Tabelle `TraderTraits`: Id, TraderId (FK, Cascade), Trait (string ≤ 64), Value (decimal, Messwert),
|
||||
ComputedAt. Unique-Index (TraderId, Trait). Pro Analyse-Lauf upserten, nicht mehr zutreffende Traits löschen.
|
||||
- Berechnung als **pure Funktion** `TraderTraitCalculator.Compute(trader, trades, positions)` →
|
||||
Liste (Trait, Value); Aufruf im `TraderAnalyticsWorker` nach Engine + Estimator aus den **bereits geladenen**
|
||||
Daten — keine zusätzlichen API-Calls.
|
||||
- Traits v1 (Trait vergeben, wenn Bedingung erfüllt; Value = Messgröße):
|
||||
|
||||
| Trait | Regel |
|
||||
|---|---|
|
||||
| `sub_second_cadence` | Median-Intervall < 2 s bei ≥ 50 Trades (Value = Median in s) |
|
||||
| `always_on_24_7` | größte Inaktivitätslücke der letzten 7 Tage < 4 h bei ≥ 200 Trades/7d |
|
||||
| `uniform_sizes` | Variationskoeffizient der Size (letzte 200 Trades) < 0,1 |
|
||||
| `round_amounts` | > 60 % der Amounts ∈ {1,5,10,20,25,50,100,250,500,1000} ± 1 % |
|
||||
| `uses_split_merge` | Anteil Split+Merge > 10 % |
|
||||
| `both_sides_same_market` | Yes- UND No-Trades in > 20 % der Märkte |
|
||||
| `resolution_farming` | > 30 % der Buys mit Price ≥ 0,93 UND < 48 h vor `ClosedAt` (min. 10 Buys) |
|
||||
| `longshot_buyer` | > 30 % der Buys mit Price ≤ 0,10 |
|
||||
| `scalper` | mediane Haltedauer < 1 h |
|
||||
| `holds_to_resolution` | > 70 % der aufgelösten Positionen ohne vorherigen Sell |
|
||||
| `fresh_wallet` | erster Trade < 30 Tage (Value = Alter in Tagen) |
|
||||
| `stable_stake_fraction` | CV von Amount/EstimatedBankroll < 0,5 (nur bei Bankroll > 0) |
|
||||
| `possible_insider` | siehe D2b — statistische Überraschung der Ergebnisse unter Marktpreisen |
|
||||
|
||||
- Schwellwerte als Konstanten im Calculator (v1 hart kodiert ist ok).
|
||||
- API/UI: `TraderDto`/`TraderDetailDto` um `Traits` (string-Liste) erweitern; Detailseite zeigt Chips
|
||||
unter dem Strategy-Feld.
|
||||
- **Trait-Filter & -Suche (verbindlich, nicht optional):**
|
||||
- `GET /api/traders` bekommt einen `trait`-Query-Parameter (mehrfach angebbar = UND-Verknüpfung),
|
||||
serverseitig als `EXISTS`-Filter auf `TraderTraits` — nicht im Speicher filtern.
|
||||
- Neuer Endpoint `GET /api/traits`: alle vorhandenen Traits mit Trader-Anzahl (für das Dropdown).
|
||||
- Traders-Liste: Trait-Dropdown neben dem Platform-Filter, aktive Filter als entfernbare Chips.
|
||||
- Die Trait-Chips auf der Detailseite sind klickbar und springen in die entsprechend gefilterte Liste.
|
||||
- KI-Integration: `AiStrategyAnalysisService`-Prompt bekommt die Merkmalsliste; Trade-Beispiele von 50 auf
|
||||
15 repräsentative reduzieren (5 größte, 5 jüngste, 5 zufällige) — die KI verifiziert Hypothesen statt zu raten.
|
||||
- **Tests:** pure-Function-Tests je Trait, mindestens Positiv- UND Negativfall für `resolution_farming`,
|
||||
`sub_second_cadence`, `uniform_sizes`, `both_sides_same_market`.
|
||||
|
||||
### D2b. Trait `possible_insider` — Insider-Verdacht erkennen (ergänzt 2026-07-11)
|
||||
|
||||
**Kernidee (wichtig, nicht vereinfachen):** Hohe Winrate allein ist KEIN Insider-Signal — die haben auch
|
||||
Resolution-Farmer (Einstieg ≥ 0,90) und Glückspilze. Verdächtig ist, wenn die Ergebnisse unter der Annahme
|
||||
„der Marktpreis war korrekt" statistisch unplausibel sind: **gewonnene Longshots zählen, gewonnene Favoriten
|
||||
nicht.** Ein Trader, der fünfmal bei 0,15 kauft und fünfmal gewinnt, hat unter Marktpreisen eine
|
||||
Wahrscheinlichkeit von 0,15^5 ≈ 0,00008 — DAS ist der Fingerabdruck.
|
||||
|
||||
**v1 (aus vorhandenen Daten, im `TraderTraitCalculator`):**
|
||||
Grundfilter (alle müssen erfüllt sein):
|
||||
- 5 ≤ abgeschlossene Märkte ≤ 100 (seltene Wetter, keine Vieltrader),
|
||||
- Ø-Einsatz ≥ 500 USD **oder** ≥ 10 % der `EstimatedBankroll`,
|
||||
- Trade-Frequenz < 5/Tag,
|
||||
- Trait `resolution_farming` NICHT gesetzt (Ausschluss der harmlosen 95%-Winrate-Klasse).
|
||||
|
||||
Kernmetrik „Markt-Überraschung": über alle abgeschlossenen Märkte des Traders, mit p = Einstiegs-VWAP des
|
||||
gehandelten Outcomes:
|
||||
```
|
||||
L = Π p(gewonnene Märkte) × Π (1 − p)(verlorene Märkte)
|
||||
Value = −log10(L)
|
||||
```
|
||||
Trait vergeben, wenn: WinRate ≥ 85 % UND mittlerer Einstiegs-VWAP der GEWONNENEN Märkte ≤ 0,70 UND
|
||||
Value ≥ 3 (d. h. L ≤ 1:1000 unter Marktpreisen). `Value` dient als Ranking („wie verdächtig") in der
|
||||
gefilterten Liste — absteigend sortieren.
|
||||
|
||||
**v2 (späterer Ausbau, wenn Enrichment-Daten breiter vorliegen):**
|
||||
- **Kauft VOR der News, nicht danach:** Anteil der Buys, nach denen der Preis binnen 24–48 h ≥ 15 ct in
|
||||
Trader-Richtung lief, während er VOR dem Kauf flach war (`PreTradePrice1m` ≈ Einstieg). Das trennt Insider
|
||||
von News-Snipern, die erst nach der Bewegung einsteigen.
|
||||
- **Einweg-Wallet:** erster Trade < 30 Tage, ≤ 3 Märkte insgesamt, nach dem Gewinn inaktiv
|
||||
(Kombination mit `fresh_wallet`).
|
||||
- **Themen-Konzentration:** ≥ 80 % des Volumens in einem einzigen Event oder engen Themencluster.
|
||||
- **Zeitkritische Aggressivität:** hoher Taker-Anteil + hohe akzeptierte Slippage (wer weiß, dass morgen die
|
||||
News kommt, kauft das Orderbuch leer statt zu limiten).
|
||||
- **Nischenmärkte:** Einsatz groß relativ zu Marktliquidität/-volumen zum Kaufzeitpunkt.
|
||||
|
||||
**Produkt-Hinweis (Ausblick, nicht Teil dieses Auftrags):** `possible_insider`-Wallets + Watchlist + Alert
|
||||
bei neuem Trade (der `AlertService` existiert) ergibt einen „Insider-Follow-Feed" — für Copytrading sind
|
||||
diese Konten die wertvollsten Signalgeber überhaupt, auch wenn sie selten handeln.
|
||||
|
||||
**Tests:** Positivfall (5 gewonnene Longshots à 0,15, hohe Einsätze → Trait mit Value ≈ 4,1), Negativfälle:
|
||||
Resolution-Farmer (Winrate 95 %, Einstiege 0,95 → KEIN Trait), Vieltrader mit 90 % Winrate (Frequenzfilter),
|
||||
Kleinwetter (Einsatzfilter).
|
||||
|
||||
### D2c. Markt-Rendite-Metriken: Win-Edge UND Loss-Edge (ergänzt 2026-07-11)
|
||||
|
||||
**Motivation:** Die Winrate allein sagt nichts über das Auszahlungsprofil. Ein Arb-Grinder gewinnt 95 % seiner
|
||||
Märkte mit je +2 %, ein Longshot-Halter gewinnt 25 % mit je +400 % — beide können profitabel sein, aber sie
|
||||
sind völlig verschiedene Strategien und völlig verschieden zu kopieren. Die Rendite-Verteilung der
|
||||
abgeschlossenen Märkte ist damit gleichzeitig Strategie-Fingerabdruck UND Grundlage für Position-Sizing
|
||||
beim Copytrading.
|
||||
|
||||
**Zwei verbindliche Design-Entscheidungen:**
|
||||
1. **Median führend, Durchschnitt zusätzlich.** Ein einziger 10x-Ausreißer verzerrt den Durchschnitt komplett;
|
||||
der Median beschreibt den typischen Win. Die Differenz Durchschnitt↔Median ist selbst ein Signal
|
||||
(Schiefe = seltene Lottogewinne vs. konstantes Mahlen).
|
||||
2. **Die Verlustseite ist Pflicht, nicht optional.** „90 % Winrate mit +5 % pro Win" ist wertlos, wenn die
|
||||
10 % Verluste je −100 % sind (genau das Profil von Resolution-Farmern!). Ohne Loss-Edge ist die Metrik
|
||||
aktiv irreführend.
|
||||
|
||||
**Berechnung (in der Engine, im selben Durchlauf wie `CalculateMarketWinRates` — dort wird pro
|
||||
geschlossenem Markt bereits `marketPnl` ermittelt):**
|
||||
- Pro geschlossenem Markt: `invested` = Summe der Buy-Amounts (inkl. Split-Kosten) über die Outcomes des
|
||||
Markts; `ReturnPct = marketPnl / invested × 100`. Märkte mit `invested <= 0` (z. B. Buy-Historie durch
|
||||
Retention gepruned) überspringen — nicht mit 0 verfälschen.
|
||||
- Aggregation über Wins (ReturnPct > 0) und Losses (ReturnPct < 0) getrennt.
|
||||
|
||||
**Neue Felder auf `TraderAnalytics` (Migration):**
|
||||
- `MedianWinReturnPct`, `AvgWinReturnPct`
|
||||
- `MedianLossReturnPct`, `AvgLossReturnPct` (negativ gespeichert)
|
||||
- `ProfitFactor` (Σ Markt-Gewinne / Σ |Markt-Verluste|; `NULL` wenn keine Verluste)
|
||||
|
||||
**DTOs + UI:** Detailseite bekommt Karten „Median Win / Median Loss" (z. B. „+6,2 % / −38 %") und
|
||||
„Profit Factor"; die Expectancy pro Markt (`WinRate×MedianWin − (1−WinRate)×|MedianLoss|`) kann das UI
|
||||
daraus ableiten, wird nicht gespeichert.
|
||||
|
||||
**Abgeleitete Traits (in den `TraderTraitCalculator`, ergänzt die D2-Tabelle):**
|
||||
| Trait | Regel |
|
||||
|---|---|
|
||||
| `thin_margin_wins` | Median-Win-Rendite < 10 % bei ≥ 20 gewonnenen Märkten (Arb-/Grinder-Profil) |
|
||||
| `high_payoff_wins` | Median-Win-Rendite > 100 % bei ≥ 5 gewonnenen Märkten (Conviction-/Longshot-Profil) |
|
||||
|
||||
**Tests (pure Berechnung):**
|
||||
- Bekannte Markt-Menge → korrekte Median-/Avg-/ProfitFactor-Werte für Win- UND Loss-Seite.
|
||||
- Ausreißer-Robustheit: ein einzelner +1000 %-Win verschiebt den Median nicht.
|
||||
- Markt mit gepruntem Buy (invested = 0) fließt nicht ein.
|
||||
- Resolution-Farmer-Profil (viele kleine Wins, seltene −100 %-Losses) liefert ProfitFactor nahe 1 —
|
||||
der Fall, den die Winrate allein verschleiert.
|
||||
|
||||
## Teil E — Master-Auswahl-Metriken & generischer Profil-Endpoint (ergänzt 2026-07-11)
|
||||
|
||||
> Hintergrund: Ein externer Konsument (Copytrading-Client) braucht eine belastbare
|
||||
> Master-Trader-Auswahl (Prüfplan liegt in `PREDICTALYTICS-PRUEFPLAN-Master-Auswahl.md` im
|
||||
> externen Repo). **Bewusste Architektur-Entscheidung: Die API bleibt generisch.**
|
||||
> Kein kunden-spezifisches Format, kein „HOLDER/STOPLOSS"-Vokabular in der API —
|
||||
> wir exponieren neutrale Metriken, der Konsument mappt selbst.
|
||||
> Abgleich: M2/M8 existieren (D2c/CategoryPerformances), M3/M9 teilweise als Traits (D2),
|
||||
> M1/M5/M6 teilweise; **komplett neu sind M4, M7, Out-of-Sample-Fenster, Kopierbarkeits-
|
||||
> Aggregate und die Korrelations-Sicht** — das ist Teil E.
|
||||
|
||||
### E1. Generischer Profil-Endpoint `GET /api/traders/{id}/profile`
|
||||
|
||||
Ein Aufruf liefert das vollständige Analyse-Profil (statt 4+ Einzel-Calls):
|
||||
Stammdaten + alle Analytics-Felder + Traits (D2) + Rendite-Metriken (D2c) +
|
||||
Fenster-Blöcke (E2) + Fingerprint-Verteilungen (E3) + Kopierbarkeits-Aggregate (E5).
|
||||
Nur persistierte Werte ausliefern (kein On-the-fly-Rechnen wie im Deep-Dive) —
|
||||
der Endpoint muss schnell und pollbar sein. In `docs/API.md` dokumentieren.
|
||||
|
||||
### E2. Fenster-Metriken als Struktur + Out-of-Sample-Vergleich
|
||||
|
||||
**Problem:** Die 24h/7d/30d-Felder sind Einzelspalten; der Prüfplan braucht zwei frei
|
||||
definierte Vergleichsfenster (z. B. Tag −180…−60 vs. −60…heute), um Glücks-Wallets
|
||||
auszusortieren (nur wer in BEIDEN Fenstern liefert, ist ein Kandidat).
|
||||
|
||||
**Fix:**
|
||||
- Neue Tabelle `TraderWindowMetrics`: TraderId, WindowStart, WindowEnd, ClosedMarkets,
|
||||
WinRate, AvgReturnPct (M1: Ø realisierte Rendite je Markt), MedianWinReturnPct,
|
||||
MedianLossReturnPct, ProfitFactor, ComputedAt. Unique (TraderId, WindowStart, WindowEnd).
|
||||
- Berechnung im Analytics-Lauf für zwei konfigurierbare Fenster
|
||||
(`AnalysisWindows`-Sektion in appsettings, Default: −180…−60 und −60…0 Tage).
|
||||
Wiederverwendet die D2c-Logik mit Zeitfilter auf den Markt-Abschlusszeitpunkt.
|
||||
- Im Profil (E1) als `windows[]`-Array. **Achtung Retention:** Fenster A reicht weiter
|
||||
zurück als 90 Tage — Berechnung muss mit fehlender Historie ehrlich umgehen
|
||||
(`closedMarkets` klein → Konsument sieht die dünne Stichprobe). Nach der
|
||||
Retention-Verlängerung auf 180 Tage (D3) wird Fenster A tragfähig.
|
||||
|
||||
### E3. Fingerprint-Verteilungen (persistiert, im Profil)
|
||||
|
||||
Im Analytics-Lauf berechnen und als JSON-Spalte(n) auf `TraderAnalytics` oder eigene
|
||||
Tabelle persistieren:
|
||||
- **Preisband-Profil (M7):** Einsatz-Anteil je 10-¢-Einstiegspreisband über Buys,
|
||||
**plus realisierte Winrate je Band** (macht Glück von System unterscheidbar:
|
||||
„kauft 90–95-¢-Shares, gewinnt 97 %" = +Edge sichtbar pro Band).
|
||||
- **Haltedauer:** Median (nicht nur Ø) Stunden Kauf→Exit/Auflösung (M6).
|
||||
- **Positionsgrößen:** P50/P90-Amount (M9-Basis).
|
||||
- Trade-Frequenz je Woche (aus Trades30d ableitbar, im Profil ausgeben).
|
||||
|
||||
### E4. Exit-Verhalten klassifizieren (M4) — generisch als Trait + Kennzahl
|
||||
|
||||
- Kennzahl `stop_loss_ratio`: Anteil der Sells, die nach einem Preisrückgang von
|
||||
≥ 10 % unter den Einstands-AvgCost erfolgen (Sell-Preis ≤ 0,9 × AvgCost),
|
||||
bezogen auf alle geschlossenen Positionen. Braucht KEINE Preis-Historie —
|
||||
Sell-Preis vs. AvgCost der Position reicht als v1-Näherung.
|
||||
- Traits: `sells_at_loss` (Ratio > 0,15) ergänzt das vorhandene `holds_to_resolution`.
|
||||
Ein Konsument bildet daraus selbst HOLDER (`holds_to_resolution` ∧ ¬`sells_at_loss`),
|
||||
STOPLOSS, MIXED.
|
||||
|
||||
### E5. Kopierbarkeits-Aggregate (im Profil)
|
||||
|
||||
- `medianMarketVolumeUsd`: Median des `Market.Volume` (bzw. Volume24h) der vom Trader
|
||||
gehandelten Märkte — handelt er in Kleinstmärkten, bewegt der Kopierer den Preis.
|
||||
- `medianPostFillDriftPct`: Median-Preisänderung nach seinen Buys (aus vorhandenem
|
||||
`TradeContext.PriceAfter1m`/`FollowerFillPrice60s`, nur enriched Trades; Anzahl
|
||||
der Datenpunkte mit ausgeben).
|
||||
- `netEdgeAfterFeesPct`: `AvgReturnPct` (E2, Fenster B) minus kategorie-gewichteter
|
||||
Taker-Fee (aus `Market.FeeRateBps` — wird bereits erfasst!) minus konfigurierbarem
|
||||
Spread-Aufschlag (`CopyCostSettings:SpreadPct`, Default 2,0). Generisch als
|
||||
„Netto-Edge nach Kopierkosten" benannt.
|
||||
|
||||
### E6. Korrelations-/Portfolio-Sicht
|
||||
|
||||
- Neuer Endpoint `GET /api/traders/correlation?ids=1,2,3` (oder `?top=20`):
|
||||
paarweise Jaccard-Ähnlichkeit über die gehandelten ConditionIds (Fenster B) +
|
||||
Kategorie-Mix-Cosinus. Antwort: Matrix + je Paar die Overlap-Zahl.
|
||||
- Kein „Portfolio-Empfehlungs"-Endpoint in v1 — die Auswahl-Logik (max. 2 je
|
||||
Kategorie etc.) gehört zum Konsumenten. Wir liefern die Korrelationsdaten.
|
||||
|
||||
### E7. Martingale-Erkennung (M9) als Trait
|
||||
|
||||
`martingale_pattern`: mittleres Verhältnis Einsatz(nach verlorenem Markt) /
|
||||
Einsatz(nach gewonnenem Markt) über die Sequenz der abgeschlossenen Märkte;
|
||||
Trait ab Verhältnis ≥ 1,5 bei ≥ 20 Märkten. (Einsatz = investiertes Kapital je
|
||||
Markt aus D2c.)
|
||||
|
||||
### E8. Abnahme Teil E
|
||||
|
||||
1. Bestehende Tests grün (Assertions unverändert) + neue Tests: Fenster-Metriken
|
||||
(bekannte Markt-Menge, zwei Fenster → korrekte Werte je Fenster), Preisband-
|
||||
Winrate (Band-Zuordnung + Ränder 0,895/0,90), `stop_loss_ratio`
|
||||
(Positiv-/Negativfall), Jaccard-Berechnung, Martingale (steigende Einsätze nach
|
||||
Losses → Trait; konstante → kein Trait).
|
||||
2. `GET /api/traders/{id}/profile` liefert für einen analysierten Trader alle Blöcke
|
||||
gefüllt; Antwortzeit < 200 ms (nur persistierte Daten).
|
||||
3. `docs/API.md` um Profile-/Correlation-Endpoint ergänzt.
|
||||
4. Kein kunden-spezifisches Vokabular in API/DTOs.
|
||||
|
||||
Reihenfolge: E1+E3 zuerst (Profil mit vorhandenen + Fingerprint-Daten), dann E2
|
||||
(Fenster), E4/E5/E7 (Kennzahlen), E6 zuletzt. Teil E setzt D1/D2/D2c voraus.
|
||||
@@ -0,0 +1,335 @@
|
||||
# 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 0–100-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, ~40–60 % 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.
|
||||
@@ -0,0 +1,168 @@
|
||||
# Fix- und Datenreparatur-Plan (Stand 2026-07-09, Übergabe an Gemini)
|
||||
|
||||
> **Status 2026-08-23: Teil D und Teil F sind umgesetzt**, nachgeprüft gegen den Code.
|
||||
> Offen sind nur noch drei Detailpunkte, jeweils unten als `- [ ]` stehengeblieben:
|
||||
> der gezielte Nachlade-Schritt in F5 und die zwei Tests aus F6 (Event-Tag-Vererbung,
|
||||
> Backfill). Belege:
|
||||
> * **D3** — `IngestMode` (`Domain/Enums/IngestMode.cs`), Einstufung in
|
||||
> `TradeHistoryWorker.cs:174-177` inklusive der geforderten Hysterese
|
||||
> (Rückstufung erst unter halber Schwelle, Richtung `Full` zusätzlich erst nach
|
||||
> 7 Tagen), wöchentliche Biopsie in `TradeHistoryWorker.cs:127`,
|
||||
> Aggregations-Buckets in `TradeAggregation.cs`, Trait `not_copyable_hf` in
|
||||
> `TraderTraitCalculator.cs`. Tests: `IngestModeTests.cs`.
|
||||
> * **F1/F2** — `CanonicalTags` und `BlacklistTags`/`IsNoiseTag` in
|
||||
> `Infrastructure/Helpers/MarketCategoryMapper.cs`.
|
||||
> * **F3/F4** — Tag-Nachladen in `PolymarketProvider.cs:187-196`, Offline-Backfill
|
||||
> aus Event-Tags in `PredictalyticsHost.cs:114-135`.
|
||||
>
|
||||
> **Abnahmekriterium für alle Code-Änderungen:** `dotnet test` muss grün bleiben.
|
||||
> Die damals notierte Zahl (16 grün + 1 übersprungen) ist überholt — Stand
|
||||
> 2026-08-23 sind es **126 grün + 1 übersprungen** (der Skip
|
||||
> `CheckpointResetAndReplay_DoesNotDoubleCountBalance` ist weiterhin eine
|
||||
> dokumentierte, bewusste Entscheidung). Die Assertions der Invarianten-Tests dürfen
|
||||
> **nicht** verändert werden — sie definieren das Soll-Verhalten. Wenn ein Test rot
|
||||
> wird, ist der Code falsch, nicht der Test.
|
||||
|
||||
## Hintergrund
|
||||
|
||||
Die Engine-Fixes vom 09.07. sind korrekt (Tests grün). Die im WebUI sichtbaren Probleme haben drei andere Ursachen:
|
||||
|
||||
1. Die Buttons der **Trader-Detailseite** nutzen alte, Job-lose Endpoints (die Listen-Buttons nutzen bereits das Job-System).
|
||||
2. Die **abgeleiteten Daten in der DB stammen aus der Bug-Ära** (Snapshots/Positionen wurden von den alten, fehlerhaften
|
||||
Engine-Versionen berechnet). Beispiel aus dem Live-System: `PnL30d = 244,0K` bei `TotalPnL = 158,5K`, weil der
|
||||
Basis-Snapshot `-85,5K` enthält (korrupter Altwert). Kein Code-Fix ändert das — die Daten müssen einmalig repariert werden.
|
||||
3. **Deadlocks + Shutdown-Fehlerkaskaden** in den Workern (unbatchtes Reconciliation-UPDATE, fehlende Cancellation-Behandlung).
|
||||
|
||||
**Ein DB-Reset ist NICHT nötig.** Die Rohdaten (`Trades`) sind größtenteils intakt; Positionen, Analytics, Snapshots und
|
||||
Scores sind abgeleitet und lokal neu berechenbar. Nur Trader, deren Alt-Trades die Retention bereits gelöscht/kompaktiert
|
||||
hat, brauchen einen gezielten API-Re-Import (kleine Teilmenge, siehe Teil B).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Teil D — Ausbaustufe: Merkmals-Tags & HF-Trader-Tiering (ergänzt 2026-07-11)
|
||||
|
||||
> **Bereits direkt erledigt (nicht Teil dieses Auftrags):** `TotalTrades` wird jetzt von der Engine aus dem
|
||||
> echten Row-Count gesetzt; der Kategorie-Mapper klassifiziert zusätzlich über den Frage-Text und matcht kurze
|
||||
> Tokens nur an Wortgrenzen; `UpdateMarketFields` überschreibt gute Kategorien nicht mehr mit "Other".
|
||||
> Teststand: **32 grün + 1 Skip** — das ist die neue Basis, Assertions unverändert lassen.
|
||||
|
||||
### D3. Trader-Tiering (`IngestMode`) — Umgang mit Ultra-HF-Tradern (RN1, Swisstony)
|
||||
|
||||
**Hintergrund:** Ultra-HF-Trader werden heute schon NICHT vollständig erfasst (PollingWorker: 100 Trades/60 s
|
||||
gegen 300+/min) — das Trade-Replay-PnL ist für diese Klasse bereits falsch und frisst nur Speicher.
|
||||
|
||||
- Enum `IngestMode { Full = 0, Aggregated = 1, SnapshotOnly = 2 }` + Spalte auf `Trader` (Default Full), Migration.
|
||||
- **Klassifizierung** im `TradeHistoryWorker` nach jedem Fetch: Zeitspanne der letzten 500 Trades →
|
||||
Trades/Tag-Schätzung. > 5.000/Tag → SnapshotOnly; > 100/Tag → Aggregated. Hysterese: Rückstufung Richtung
|
||||
Full erst nach 7 Tagen unter der halben Schwelle (kein Flattern).
|
||||
- **SnapshotOnly (Tier C):**
|
||||
- Polling/History-Worker überspringen den Trade-Import komplett.
|
||||
- Stündlich: `GetTraderPositionsAsync` (der ungenutzte `/positions`-Endpoint!) → `TraderPositions` upserten
|
||||
(size→SharesHeld, avgPrice→AvgCost, cashPnl→RealizedPnl); `OverallPnL` aus Positions +
|
||||
`GetLeaderboardAsync`-PnL für die Zeitfenster; `TraderDailySnapshot` weiter schreiben (Equity-Kurve bleibt).
|
||||
- Wöchentliche „Biopsie": einmal 500 Trades via /activity ziehen, NUR durch den `TraderTraitCalculator`
|
||||
schicken, NICHT persistieren.
|
||||
- Engine überspringt Trade-Replay für SnapshotOnly; Estimator/Enrichment überspringen; CopytradingScore = 0
|
||||
mit Trait `not_copyable_hf`.
|
||||
- **Aggregated (Tier B):** Aggregation beim Import statt nachträglicher Kompaktierung: Bucket
|
||||
(TraderId, MarketOutcomeId, Side, Stunde) mit VWAP-Preis, Summen-Size/-Amount, `AggregatedCount`; gespeichert
|
||||
als normale Trade-Zeile mit `PlatformTradeId = "AGG_{traderId}_{outcomeId}_{side}_{yyyyMMddHH}"`, laufende
|
||||
Stunde per Upsert aktualisieren. Average-Cost-Engine bleibt damit verlustfrei.
|
||||
- Danach: `RetentionDays` für Full-Trader auf 180 erhöhen (Config) — die Bots stellen nicht mehr die Masse,
|
||||
und längerer Track-Record nützt genau den kopierbaren Tradern.
|
||||
- **Tests:** Klassifizierungs-Schwellen + Hysterese als pure Funktion; PollingWorker importiert für
|
||||
SnapshotOnly-Trader nichts; Aggregations-Upsert ist idempotent (2× dieselbe Stunde → 1 Zeile, korrekte Summen
|
||||
und `AggregatedCount`).
|
||||
|
||||
### D4. Abnahme Teil D
|
||||
|
||||
1. `dotnet test`: alle bestehenden **32 + 1 Skip** bleiben grün (Assertions unverändert) + die neuen D-Tests.
|
||||
2. RN1/Swisstony stehen nach der Einstufung auf SnapshotOnly: PnL gefüllt (aus /positions/Leaderboard),
|
||||
Traits gesetzt, **keine neuen Trade-Zeilen** mehr in der DB.
|
||||
3. Detailseite zeigt Trait-Chips; Traders-Liste filterbar nach Trait.
|
||||
3b. Detailseite zeigt Median-Win/-Loss-Rendite und Profit Factor; die D2c-Werte sind für Trader mit
|
||||
abgeschlossenen Märkten gefüllt.
|
||||
4. Tägliches DB-Wachstum sichtbar reduziert (DB-Size-Anzeige im WinForms-Statusbar beobachten).
|
||||
|
||||
Reihenfolge: **D1 → D2/D2b/D2c → D3** (D2c ist klein und gehört in denselben Engine-Durchlauf wie die Winrate; bei D3 zuerst Tier C, dann Tier B).
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Teil F — Kategorie-Erkennung strukturell reparieren (ergänzt 2026-07-13)
|
||||
|
||||
> Nutzer meldet: Markt-Kategorien und die Kategorien, in denen sich ein Trader bewegt, stimmen
|
||||
> weiterhin nicht. Ursache **live gegen die Gamma-API verifiziert** — es ist strukturell, nicht der Mapper.
|
||||
|
||||
### F0. Root Cause (verifiziert)
|
||||
|
||||
- Die **`/events`-Liste** (MarketSyncWorker-Pfad) liefert **reichhaltige Tags**
|
||||
(z. B. `['Sports','Soccer','FIFA World Cup',...]`).
|
||||
- Der **`/markets?condition_id=`-Pfad** (On-Demand, `PolymarketProvider.GetMarketAsync`, aufgerufen
|
||||
vom `PollingWorker` für jeden noch unbekannten Markt) liefert **weder `category` noch Event-Tags**
|
||||
(`raw.Events[0].Tags` ist leer, auch mit `include_tag=true`). → Solche Märkte werden nur per
|
||||
Frage-Text klassifiziert und landen sonst auf **`Other`**.
|
||||
- **Genau die vom Trader gehandelten Märkte entstehen überwiegend on-demand** → viele `Other`.
|
||||
- Verschärfend: **geschlossene/aufgelöste** Märkte deckt der aktive Events-Sync nicht laufend ab →
|
||||
historische Märkte (die für die Analyse zählen) bleiben ohne Tags = `Other`.
|
||||
- Zwei Folgefehler: (a) `GetSubcategory` nimmt **`tags[0]`** — das ist oft Müll (`"Ethiopia"`,
|
||||
`"Hide From New"`, `"exchange"`), nicht die Kategorie; (b) die Tag-**Reihenfolge** ist unzuverlässig
|
||||
(bei „Next PM of Ethiopia" steht der kanonische Tag `Politics` **zuletzt**).
|
||||
|
||||
### F1. Mapper: kanonischen Tag zuerst, dann Heuristik
|
||||
|
||||
- [x] In `MarketCategoryMapper.Map`: **zuerst** prüfen, ob **irgendein Tag exakt** einer bekannten
|
||||
Kategorie entspricht (Polymarkets Tag-Vokabular enthält fast immer den kanonischen Top-Level-Tag:
|
||||
`Sports`, `Politics`, `Crypto`, `Business`/`Economy`, `Pop Culture`, `Science`, ...). Mapping-Tabelle
|
||||
Tag→`MarketCategory` (inkl. Synonyme: `Finance`/`Business`→Economy, `Pop Culture`→PopCulture).
|
||||
Erst wenn **kein** kanonischer Tag matcht, die bestehende Keyword-Heuristik auf Frage+Tags anwenden.
|
||||
- [x] **Kategorie über die gesamte Tag-Menge** bestimmen, nie über `tags[0]`.
|
||||
|
||||
### F2. Subcategory: Noise filtern, sinnvoll wählen, leer normalisieren
|
||||
|
||||
- [x] Organisations-/Müll-Tags herausfiltern (Blacklist: `Hide From New`, `Tournament Futures`,
|
||||
`Main Election`, `Recurring`, Jahres-Tags wie `2025 Predictions`, `2026 FIFA World Cup`→ok als Sub?, …).
|
||||
- [x] Subcategory = spezifischster **verbleibender** Tag, der **nicht** die Kategorie selbst ist
|
||||
(bei World-Cup-Tags → `Soccer`, nicht `Sports`). Kein passender → leerer String.
|
||||
- [x] **NULL/`""` einheitlich als `""`** speichern (behebt die doppelten „Sports/-"-Zeilen: heute
|
||||
entstehen zwei Gruppen-Keys aus NULL vs. "").
|
||||
|
||||
### F3. On-Demand-Markt: Tags nachladen statt `Other` zu speichern
|
||||
|
||||
- [x] In `GetMarketAsync`: wenn `parentTags` leer ist, aber ein Event mit Id vorhanden ist →
|
||||
**`/events?id=<eventId>` nachladen** (liefert Tags, 1 Extra-Call pro neuem Markt, cachebar) und die
|
||||
Tags fürs Mapping verwenden. Über den `IRateLimiter` drosseln.
|
||||
- [x] Alternativ/zusätzlich: existiert das Parent-Event bereits in unserer DB (aus dem Events-Sync,
|
||||
`Event.Tags` gefüllt) → **Tags von dort erben**, ganz ohne API-Call.
|
||||
|
||||
### F4. Kategorie aus Event-Tags ableiten + Offline-Backfill (der große Hebel)
|
||||
|
||||
- [x] Markt-Kategorie primär aus den **Event-Tags** (`market.Event.Tags`) ableiten, nicht aus den
|
||||
(leeren) Markt-Feldern. On-Demand-Märkte erben so die Kategorie ihres Events.
|
||||
- [x] **Einmaliger Offline-Backfill** (keine API-Calls!): über alle Märkte iterieren, deren Event
|
||||
Tags hat, und Kategorie/Subcategory aus `Event.Tags` neu ableiten (mit F1/F2). Als Methode in
|
||||
`RunRecalculateAllTradersAsync` einhängen ODER eigener Dev-Endpoint. Danach `TraderCategoryPerformance`
|
||||
neu rechnen (passiert durch die ohnehin folgende Trader-Neuberechnung).
|
||||
|
||||
### F5. Coverage geschlossener Märkte
|
||||
|
||||
- [ ] Sicherstellen, dass der Events-Sync **geschlossene** Events (mit Tags) ausreichend abdeckt —
|
||||
mindestens für Märkte, die getrackte Trader gehandelt haben (gezielter „fehlende Event-Tags
|
||||
nachladen"-Schritt in der Reconciliation).
|
||||
|
||||
### F6. Tests
|
||||
|
||||
- [x] Mapper: kanonischer Tag gewinnt über Reihenfolge (`"Ethiopia, Elections, ..., Politics"` → Politics;
|
||||
`"Sports, Soccer, ..."` → Sports, Subcategory `Soccer`).
|
||||
- [x] Subcategory: Müll-Tags werden gefiltert; NULL und `""` erzeugen denselben Gruppen-Key
|
||||
(kein Duplikat mehr).
|
||||
- [ ] Event-Tag-Vererbung: Markt ohne eigene Tags, Event mit `['Crypto',...]` → Markt wird Crypto.
|
||||
- [ ] Backfill: Markt in DB als `Other`, Event.Tags = `['Politics',...]` → nach Backfill Politics,
|
||||
ohne API-Call.
|
||||
|
||||
### F7. Reihenfolge
|
||||
|
||||
F1+F2 (reiner Mapper, sofort, testbar) → F4-Backfill (heilt Bestand offline) → F3 (On-Demand-Tags für
|
||||
neue Märkte) → F5 (Coverage). F1/F2/F4 bringen den Großteil, ohne nennenswerte API-Last.
|
||||
@@ -0,0 +1,169 @@
|
||||
# FIXPLAN Teil UI — Ranglisten & Showcases sichtbar/korrekt machen
|
||||
|
||||
> **✅ ABGESCHLOSSEN.** Umgesetzt in den Commits *„UI-U3: server-side
|
||||
> min-winrate/min-copyability filters"* und *„UI-U1/U2/U4/U5: dashboard showcases +
|
||||
> server-side leaderboard sort"*, beide in `feature/linux-net10` enthalten.
|
||||
> Am 2026-08-23 Punkt für Punkt gegen den Code nachgeprüft: `loadShowcases()`
|
||||
> (`app.js:393`) wird aus `loadDashboard()` heraus aufgerufen (`app.js:307`),
|
||||
> Red Flags heben sich über `showcase-card--warn` ab, `take` steht auf 200 mit
|
||||
> serverseitigem `sort`-Parameter (`app.js:466,470`), `minWinRate`/`minCopyability`
|
||||
> gehen bis in `TraderEndpoints.cs:16` durch (Test in `AnalyticsServiceTests.cs:135`),
|
||||
> `#tradersSort` führt alle sieben Metriken, und der Empty-State steht auf
|
||||
> `colspan="10"`.
|
||||
>
|
||||
> **Offen bleibt nur der als „Optional" markierte Punkt in U5** (Pfeilrichtung
|
||||
> ▲/▼ in der aktiven Sortierspalte) — die Tabellenköpfe zeigen weiterhin
|
||||
> statisches ↕. Reine Politur, kein Fehlverhalten.
|
||||
|
||||
> Eigenständiger Plan für die Web-UI (`src/Predictalytics.Api/wwwroot/`). Von oben nach unten
|
||||
> abarbeiten. Nach **jeder** Aufgabe im Browser gegen die laufende API prüfen und committen.
|
||||
|
||||
---
|
||||
|
||||
## 0. Regeln (zuerst lesen)
|
||||
|
||||
1. **Backend-Test-Baseline bleibt grün.** Vor Beginn einmal `dotnet test src/Predictalytics.Application.Tests`
|
||||
laufen lassen und die aktuelle Zahl (grün/übersprungen) notieren. Keine Aufgabe hier darf sie senken.
|
||||
Assertions bestehender Tests **niemals** ändern.
|
||||
2. **Reine Frontend-Änderungen** (`index.html`, `app.js`, `style.css`) brauchen keine Migration.
|
||||
Die zwei kleinen API-Ergänzungen (U3) sind additiv und nullable/optional.
|
||||
3. **Nichts „drumherum" umbauen.** Kleinste sichere Variante wählen. Bestehende Optik/Klassen
|
||||
(`card`, `data-table`, `tier-badge`, `platform-select`, CSS-Variablen) wiederverwenden, keine neuen
|
||||
Farbwelten erfinden.
|
||||
4. **Zahlen:** Prozente 0–100, Beträge USD, Zeiten UTC — konsistent zum restlichen UI.
|
||||
5. Nach jeder Aufgabe: Browser-Check gegen `page-dashboard` bzw. `page-traders`, dann `git commit`.
|
||||
|
||||
---
|
||||
|
||||
## Kontext — was ist kaputt (im Code verifiziert 2026-07-23)
|
||||
|
||||
Der Commit *„Add showcase leaderboards endpoint + sortable trader list"* hat das **Backend** erweitert,
|
||||
aber das **Frontend nicht nachgezogen**:
|
||||
|
||||
- `GET /api/traders/showcases` liefert 7 kuratierte `ShowcaseSection` (`copy_ready`, `smooth_operators`,
|
||||
`rising_stars`, `high_conviction`, `specialists`, `insider_watch`, `red_flags` — siehe
|
||||
`ShowcaseBuilder.cs`). **Kein** Frontend-Code ruft das je auf → das Feature ist unsichtbar.
|
||||
- `GetTradersAsync(sort)` unterstützt serverseitig `pnl`, `pnl30d`, `winrate`, `copytrading`, `calmar`,
|
||||
`conviction`, `profitfactor` (`AnalyticsService.cs:184`). Das Frontend übergibt **keinen** `sort`-Param,
|
||||
holt stattdessen `take=100` in Default-Order und sortiert clientseitig (`app.js:392,420`).
|
||||
→ Die aussagekräftigen Metriken (Calmar, Conviction, Profit-Faktor) sind nicht wählbar, und jede
|
||||
Sortierung reiht nur die ersten 100 der Default-Order um statt die echten Top-N zu zeigen.
|
||||
- `filterWinrateMin` / `filterCopyabilityMin` filtern clientseitig nach dem `take=100`-Fetch
|
||||
(`app.js:416`) → ein passender Trader jenseits Rang 100 erscheint nie.
|
||||
- Zwei Sort-Dropdowns schreiben beide `currentSort`: global `#sortSelect` (`app.js:121`) und lokal
|
||||
`#tradersSort` (`index.html:209`), mit **unterschiedlichen** Optionssätzen.
|
||||
- Empty-State nutzt `colspan="11"` (`app.js:407`), die Tabelle hat aber **10** Spalten (`index.html:270`).
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe U1 — Showcases auf dem Dashboard rendern (größter Hebel)
|
||||
|
||||
**Ziel:** Die 7 kuratierten Sektionen sichtbar machen — je Sektion Titel, Beschreibung und die Top-Trader
|
||||
als klickbare Kacheln/Zeilen (Klick → `viewTrader(id)`).
|
||||
|
||||
- [x] **HTML:** In `index.html` im `#page-dashboard` einen Container `<div id="showcaseSections"></div>`
|
||||
ergänzen (unter den bestehenden Dashboard-Karten, vor/nach „Top Traders" — Platzierung so, dass es
|
||||
nicht mit den bestehenden Kacheln kollidiert).
|
||||
- [x] **JS:** Neue Funktion `loadShowcases()` in `app.js`:
|
||||
- `const sections = await api('/api/traders/showcases');`
|
||||
- Response-Shape (camelCase): `[{ key, title, description, traders: [TraderDto, ...] }]`.
|
||||
- Pro Sektion eine `card` rendern: `title` als Überschrift, `description` als `page-subtitle`-artiger
|
||||
Untertitel, darunter die Trader als kompakte Liste/Grid mit Name (+ `🤖` bei `isSuspectedBot`),
|
||||
Plattform, `combinedScore`, `copytradingCopyabilityScore`, `totalPnl` (via `fmt.pnl`), Trait-Chips
|
||||
(dieselbe Chip-Logik wie in `loadTraders`, `getTraitDisplayName/Description/Class`).
|
||||
- Leere Sektionen liefert das Backend gar nicht erst — kein Sonderfall nötig; aber wenn die ganze
|
||||
Antwort leer/`null` ist, freundlicher Empty-State.
|
||||
- Jede Trader-Zeile: `onclick="viewTrader(${t.id})"`.
|
||||
- [x] `loadShowcases()` in `loadDashboard()` aufrufen (bzw. beim Aktivieren von `page-dashboard`).
|
||||
- [x] **Optik:** `insider_watch` und `red_flags` optisch abheben (z. B. Warn-Akzent für Red Flags über
|
||||
vorhandene CSS-Variablen `--pnl-negative`), aber im bestehenden Kartenstil bleiben.
|
||||
|
||||
**U1 fertig, wenn:** Dashboard zeigt die vom Backend gelieferten Sektionen; Klick auf einen Trader öffnet
|
||||
die Detailseite; keine Konsolenfehler. Commit: „UI-U1: render dashboard showcases".
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe U2 — Server-seitige Sortierung statt Client-Sort auf 100 Zeilen
|
||||
|
||||
**Ziel:** Die Rangliste zeigt die **echten** Top-N nach der gewählten Metrik, nicht nur eine Umsortierung
|
||||
der ersten 100.
|
||||
|
||||
- [x] **JS:** In `loadTraders()` den gewählten Sort als Query-Param an die API hängen:
|
||||
`url += '&sort=' + encodeURIComponent(serverSortKey)`. Mapping UI→Server:
|
||||
`score→` (leer/default), `winrate→winrate`, `copyability→copytrading`, `pnl→pnl`,
|
||||
`pnl30d→pnl30d`, `calmar→calmar`, `conviction→conviction`, `profitfactor→profitfactor`.
|
||||
(Der Server ordnet absteigend — das ist für alle diese Metriken die sinnvolle Richtung.)
|
||||
- [x] **JS:** Den clientseitigen `data.sort(...)`-Block in `loadTraders()` entfernen bzw. nur noch als
|
||||
Fallback für die rein clientseitigen Keys `name`/`platform` behalten (die kennt der Server nicht).
|
||||
- [x] **take** von 100 auf einen sinnvollen Wert erhöhen (z. B. 200) — die serverseitige Sortierung liefert
|
||||
jetzt die richtige Reihenfolge, die Tabelle kann eine echte Bestenliste zeigen.
|
||||
- [x] Die Klick-Sortierung der Tabellenköpfe (`setTraderSort`, `app.js:376`) auf denselben Pfad umstellen:
|
||||
Sort setzen → `loadTraders()` (das nun serverseitig sortiert). Die `↕`-Header behalten.
|
||||
|
||||
**U2 fertig, wenn:** Auswahl „Calmar/Conviction/Profit-Faktor" (nach U4) verändert die Reihenfolge sichtbar
|
||||
und zeigt Trader, die vorher nicht in den ersten 100 waren. Commit: „UI-U2: server-side leaderboard sort".
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe U3 — Min-Filter serverseitig (Winrate/Copyability) — additive API-Ergänzung
|
||||
|
||||
**Ziel:** Die Kennzahl-Mindestfilter dürfen nicht an der 100/200-Grenze abschneiden.
|
||||
|
||||
- [x] **API:** `IAnalyticsService.GetTradersAsync` und die Implementierung (`AnalyticsService.cs:165`) um
|
||||
zwei **optionale nullable** Parameter erweitern: `decimal? minWinRate = null,
|
||||
decimal? minCopyability = null`. In der In-Memory-Filterkette (dort wird ohnehin schon `highlyCopyable`
|
||||
und `traitFilter` gefiltert) anwenden:
|
||||
`if (minWinRate is > 0) traders = traders.Where(t => t.WinRate >= minWinRate).ToList();` und analog
|
||||
`t.Analytics?.CopytradingCopyabilityScore >= minCopyability`.
|
||||
- [x] **Endpoint:** In `TraderEndpoints.cs` die `/api/traders`-Route um die zwei Query-Parameter durchreichen.
|
||||
- [x] **JS:** In `loadTraders()` `filterWinrateMin`/`filterCopyabilityMin` als Query-Param senden statt
|
||||
clientseitig zu filtern; den clientseitigen Filter-Block entfernen.
|
||||
- [x] **Test:** Kleiner Service-Test (SQLite-Muster wie vorhandene `AnalyticsService`/Repository-Tests):
|
||||
drei Trader mit WinRate 40/60/80, `minWinRate=55` → nur zwei zurück. Baseline bleibt grün.
|
||||
|
||||
**U3 fertig, wenn:** Ein hoher Mindest-Copyability-Wert zeigt auch Trader, die in der Default-Order weit
|
||||
hinten stehen. Commit: „UI-U3: server-side min-winrate/min-copyability filters".
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe U4 — Sort-Dropdown vereinheitlichen & vervollständigen
|
||||
|
||||
**Ziel:** Ein einziges, vollständiges Sort-Steuerelement für die Rangliste.
|
||||
|
||||
- [x] **HTML:** `#tradersSort` (`index.html:209`) um die aussagekräftigen Metriken erweitern:
|
||||
`pnl30d` (PnL 30 T), `calmar` (Rendite/Drawdown), `conviction` (Conviction-Edge),
|
||||
`profitfactor` (Profit-Faktor), `copyability` (bereits da), `score`/`winrate`/`pnl`/`name` behalten.
|
||||
- [x] **JS:** Klarstellen, welches Dropdown die Traders-Seite steuert. `#tradersSort` bleibt maßgeblich für
|
||||
`page-traders`; das globale `#sortSelect` (falls es die Traders-Seite mitsteuert) entkoppeln oder
|
||||
auf denselben State spiegeln, sodass es **kein** widersprüchliches `currentSort` mehr gibt.
|
||||
- [x] Sicherstellen, dass Header-Klick (`setTraderSort`) und Dropdown denselben `currentSort` schreiben und
|
||||
beide `loadTraders()` (serverseitig, U2) auslösen.
|
||||
|
||||
**U4 fertig, wenn:** Alle Dropdown-Optionen sortieren korrekt (serverseitig), Header-Klick und Dropdown
|
||||
bleiben synchron. Commit: „UI-U4: unify + extend leaderboard sort control".
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe U5 — Kleinkram / Konsistenz
|
||||
|
||||
- [x] **colspan-Fix:** Empty-State in `loadTraders()` (`app.js:407`) von `colspan="11"` auf die echte
|
||||
Spaltenzahl **10** korrigieren.
|
||||
- [x] **Spalten-Parität:** Prüfen, dass Tabellenkopf (`index.html:270`) und Zeilen-Template
|
||||
(`app.js:435`) dieselbe Spaltenzahl/-reihenfolge haben.
|
||||
- [ ] **Optional:** Aktive Sortierspalte im Header visuell markieren (Pfeilrichtung ▲/▼ statt neutralem ↕).
|
||||
|
||||
**U5 fertig, wenn:** Leere Tabelle rendert sauber über die volle Breite; kein Spaltenversatz.
|
||||
Commit: „UI-U5: leaderboard table consistency".
|
||||
|
||||
---
|
||||
|
||||
## Gesamt-Abnahme
|
||||
|
||||
1. `dotnet build` fehlerfrei; `dotnet test` = notierte Baseline weiter grün + neuer U3-Test grün.
|
||||
2. Dashboard zeigt die 7 Showcase-Sektionen mit echten Tradern (U1).
|
||||
3. Sortierung nach Calmar/Conviction/Profit-Faktor zeigt Trader jenseits der alten Top-100 (U2/U4).
|
||||
4. Mindest-Filter schneiden nicht mehr an der Fetch-Grenze ab (U3).
|
||||
5. Keine JS-Konsolenfehler; Tabelle ohne Spaltenversatz (U5).
|
||||
|
||||
**Reihenfolge:** U1 (Sichtbarkeit, reines Frontend) → U2 (Sort-Korrektheit) → U3 (Filter-Korrektheit,
|
||||
kleine API-Ergänzung) → U4 (Bedien-Konsistenz) → U5 (Politur).
|
||||
@@ -0,0 +1,166 @@
|
||||
# Plan: Drei-Schichten-Architektur (Core / Lokal / Public)
|
||||
|
||||
> **Archiviert am 2026-08-23** — offene Punkte siehe [`ROADMAP.md`](../../ROADMAP.md),
|
||||
> Stufe 6. Dieses Dokument wird nicht mehr gepflegt.
|
||||
>
|
||||
> **Phase 0 bis 2 sind erledigt**, überwiegend als Nebenprodukt der Linux-Portierung, und
|
||||
> hier nachträglich abgehakt:
|
||||
> * Read/Control-Split (`ApiConfiguration.MapPredictalyticsReadEndpoints()` bzw.
|
||||
> `MapPredictalyticsControlEndpoints()`), `/api/dev/*` und die Job-Trigger liegen auf der
|
||||
> Control-Seite.
|
||||
> * `GET /api/capabilities` existiert und meldet den tatsächlichen Zustand; die WebUI
|
||||
> koppelt ihre Admin-Aktionen daran (`app.js:880,1132`).
|
||||
> * `API_BASE` ist im SPA konfigurierbar (`app.js:2`), Trait-Chips sind da.
|
||||
> * CORS läuft über eine Origin-Whitelist, die Bind-Adresse ist explizit
|
||||
> (`PredictalyticsHost.cs:177,198-201`) — kein `0.0.0.0`.
|
||||
>
|
||||
> **Nicht abgehakt** ist der Deep-Dive-Audit auf versteckte Writes in Read-Endpunkten
|
||||
> (Phase 0, dritter Punkt): die Trennung steht, ob jeder Read-Endpunkt einzeln daraufhin
|
||||
> geprüft wurde, lässt sich im Nachhinein nicht belegen. Vor einem öffentlichen Host
|
||||
> nachholen.
|
||||
>
|
||||
> **Phase 3 und 4 sind offen** und bilden Roadmap-Stufe 6 — dort als Konzept markiert,
|
||||
> weil über eine Monetarisierung nicht entschieden ist.
|
||||
|
||||
> Stand: 2026-07-12 · Status: **Architektur bestätigt** (Nutzer-Entscheidung), Implementierung
|
||||
> schrittweise. Kein Code in diesem Schritt.
|
||||
> Kontext: Neues WebUI-Design (mit Claude Design vorbereitet) wird nach der Schicht-Trennung
|
||||
> schrittweise implementiert. Public-Release ist noch fern, aber alles wird dafür vorbereitet.
|
||||
|
||||
## 0. Bestätigte Architektur
|
||||
|
||||
Drei Schichten mit **genau einer** Integrationsschnittstelle zwischen intern und öffentlich:
|
||||
**der Datenbank**.
|
||||
|
||||
```
|
||||
┌─────────────────────────── LOKAL (eigener Rechner/LAN) ───────────────────────────┐
|
||||
│ │
|
||||
│ Schicht 1: CORE Schicht 2: LOKALE API + WebUI │
|
||||
│ ───────────────── ─────────────────────────── │
|
||||
│ Crawler, Worker, PnL-Engine, volle API (Read + Control), │
|
||||
│ Analyse, Ingest keine Auth (localhost), │
|
||||
│ │ schreibt serviert WebUI-SPA, │
|
||||
│ ▼ zum Testen von Analysen & Design │
|
||||
│ ┌──────────────────┐ liest/schreibt ▲ │
|
||||
│ │ Analyse-DB │◄────────────────────────┘ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ │ (nur SELECT-Grant) │
|
||||
└────────────┼──────────────────────────────────────────────────────────────────────┘
|
||||
│ ← EINZIGE Verbindung nach außen: read-only DB-Zugriff
|
||||
┌────────────┼──────────────────────── EXTERNER WEBSERVER ───────────────────────────┐
|
||||
│ ▼ │
|
||||
│ Schicht 3: PUBLIC API + WebUI │
|
||||
│ ──────────────────────────── │
|
||||
│ nur Read-Endpunkte (identisch zur lokalen Read-Seite), │
|
||||
│ Auth + Accounts, öffentliches Rate-Limiting, TLS, │
|
||||
│ serviert dasselbe WebUI-SPA │
|
||||
│ │ read-only │ read/write │
|
||||
│ ▼ ▼ │
|
||||
│ [Analyse-DB, RO] [User-DB (Accounts), separat] │
|
||||
└────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Kernprinzipien (aus der Nutzer-Entscheidung):**
|
||||
- **Core ist rein lokal** und die einzige Instanz, die in die Analyse-DB **schreibt**.
|
||||
- **Public spricht ausschließlich mit der Datenbank** — nie mit dem Core-Prozess, nie mit der
|
||||
lokalen API. Public bekommt einen **read-only** DB-User auf die Analyse-DB und eine
|
||||
**separate User-DB** für Accounts.
|
||||
- **Lokale und Public WebUI sind optisch UND funktional gleich** (auf der Analyse-/Ansichtsseite),
|
||||
damit neue Analysen und Designs lokal getestet werden und 1:1 öffentlich aussehen.
|
||||
|
||||
## 1. Wie „identisch" technisch erreicht wird
|
||||
|
||||
Damit beide WebUIs garantiert gleich sind, wird die **Read-/Analyse-Seite genau einmal** gebaut
|
||||
und von beiden Hosts wiederverwendet — kein Duplizieren:
|
||||
|
||||
1. **Geteilte Read-API-Library.** `Predictalytics.Api` ist bereits eine Library. Die
|
||||
Endpoint-Registrierung wird in zwei Gruppen gespalten:
|
||||
- `MapReadEndpoints()` — **die Analyse-/Ansichts-Endpunkte** (Trader-Liste/-Detail/-Profil/
|
||||
-Positionen, Markets, Dashboard, Search, Watchlist-Ansicht, Traits). Wird von **beiden**
|
||||
Hosts registriert.
|
||||
- `MapControlEndpoints()` — **schreibende/steuernde** Endpunkte (Jobs-Trigger, `/api/dev`,
|
||||
Trader-Anlage, Watchlist-Mutation, Priority, AI-Analyse-Trigger). Wird **nur lokal**
|
||||
registriert.
|
||||
2. **Ein einziges WebUI-SPA-Artefakt.** Dasselbe Build wird von der lokalen und der öffentlichen
|
||||
Schicht ausgeliefert. Es ist reiner API-Client (kein Server-Code, keine DB-Kenntnis).
|
||||
3. **Capability-gesteuerte Admin-Bedienelemente.** Da die Control-Endpunkte öffentlich fehlen,
|
||||
fragt das SPA beim Start eine `GET /api/capabilities` ab (`{ canControl: bool, authRequired: bool }`)
|
||||
und blendet Admin-Aktionen (Sync/Analyze/Deep-Resync/Trader-Anlegen) nur ein, wenn vorhanden.
|
||||
→ Analyse-Ansichten sind überall identisch; nur die lokalen Steuer-Buttons fehlen öffentlich.
|
||||
|
||||
> **Wichtiger Audit-Punkt vor dem Teilen:** Manche „Read"-Endpunkte haben heute versteckte
|
||||
> Nebenwirkungen. Beispiel: `GET /api/traders/{id}/deep-dive` **holt Preis-Historie vom Provider
|
||||
> und speichert Snapshots** — also ein Schreibzugriff **und** ein externer API-Call. Solche
|
||||
> Endpunkte sind **nicht** public-tauglich (read-only DB verbietet den Write, und Public darf
|
||||
> Polymarket nicht anrufen). Vor dem Aufnehmen in `MapReadEndpoints()` jeden Endpunkt auf
|
||||
> versteckte Writes/Provider-Calls prüfen; die Public-Read-Seite muss **rein aus persistierten
|
||||
> Daten** bedienbar sein (das `/profile`-Design aus FIXPLAN E ist genau deshalb „persisted-only").
|
||||
|
||||
## 2. Was heute wo läuft (Ist → Ziel)
|
||||
|
||||
- **Heute:** Core + Lokale API + WebUI sind **ein** Prozess (WinFormsHost/EmbeddedWebServer:
|
||||
Worker + volle API + `wwwroot`-SPA, localhost, keine Auth, CORS `*`). Public existiert nicht.
|
||||
- **Ziel Lokal:** bleibt bequem **ein** Prozess (Core-Worker + Lokale API + WebUI zusammen ist
|
||||
zum Testen praktisch). Wichtig ist nur die **Code-Schichtung** (Read-Library getrennt von
|
||||
Control/Core), damit Public die Read-Library **ohne** Core/Worker/Control referenzieren kann.
|
||||
- **Ziel Public:** eigener schlanker Host (neues Projekt `Predictalytics.PublicApi`), der
|
||||
`MapReadEndpoints()` + Auth registriert, das SPA ausliefert, und **nur** die read-only
|
||||
Analyse-DB + die User-DB kennt.
|
||||
|
||||
## 3. Implementierungsplan (schrittweise)
|
||||
|
||||
### Phase 0 — Endpunkte klassifizieren & Read/Control trennen (Fundament, klein)
|
||||
- [x] Jeden Endpunkt in `docs/API.md` als **Read** (public-tauglich) oder **Control** (nur lokal)
|
||||
markieren. Kandidaten Control: alle `POST/PUT/DELETE`, `/api/dev/*`, Job-Trigger.
|
||||
- [ ] Auf **versteckte Writes/Provider-Calls** in Read-Endpunkten prüfen (Deep-Dive!) und
|
||||
bereinigen oder als Control einstufen.
|
||||
- [x] `MapPredictalyticsEndpoints()` in `MapReadEndpoints()` + `MapControlEndpoints()` splitten
|
||||
(rein struktureller Refactor, Verhalten unverändert; bestehende Tests bleiben grün).
|
||||
- [x] `GET /api/capabilities` einführen.
|
||||
|
||||
### Phase 1 — WebUI als sauberes, gemeinsames SPA (parallel zum neuen Design)
|
||||
- [x] Neues Design als **ein statisches Artefakt** bauen, reiner API-Client, `API_BASE`
|
||||
konfigurierbar (heute `''` = gleiche Origin bleibt lokal gültig).
|
||||
- [x] Admin-Aktionen an `capabilities.canControl` koppeln.
|
||||
- [x] Trait-Chips/-Filter im Design vorsehen — **erscheinen erst, wenn Backend-D2 steht**
|
||||
(kein UI-Bug, das Backend liefert Traits noch nicht).
|
||||
|
||||
### Phase 2 — Lokale Grenze härten (klein, jetzt schon sinnvoll)
|
||||
- [x] CORS von `AllowAnyOrigin` auf konfigurierte Origin-Whitelist umstellen.
|
||||
- [x] Bind-Adresse explizit (localhost/LAN), nie `0.0.0.0` ohne Firewall; Konfig-Kommentar
|
||||
„NIEMALS ins Internet — das ist die interne Schicht".
|
||||
|
||||
### Phase 3 — DB für Public vorbereiten (mittel, kann früh passieren)
|
||||
- [ ] **Read-only DB-User** auf der Analyse-DB anlegen (nur `SELECT`). Der Nutzer führt das SQL
|
||||
selbst aus (Grant-Statements liefern wir).
|
||||
- [ ] `AppDbContext` public-seitig read-only konfigurieren (read-only Connection; keine
|
||||
Migrations, kein SaveChanges). Sicherstellen, dass die Read-Endpunkte ohne Writes auskommen.
|
||||
- [ ] **Schema-Kopplung dokumentieren:** Weil Public direkt auf der Analyse-DB liest, ist das
|
||||
DB-Schema jetzt eine **Vertragsfläche**. Schema-Änderungen im Core müssen die Public-Read-
|
||||
Views berücksichtigen (Views/stabile Spalten als Puffer erwägen, damit interne
|
||||
Refactorings die öffentliche Sicht nicht brechen).
|
||||
|
||||
### Phase 4 — Public-Host bauen (groß, wenn Release näher rückt)
|
||||
- [ ] Neues Projekt `Predictalytics.PublicApi` (eigener Host, TLS, öffentliche Bind-Adresse).
|
||||
- [ ] `MapReadEndpoints()` + **Auth/Accounts** (separate **User-DB**; Passwörter/Sessions nach
|
||||
Stand der Technik — Secret-Handling lebt **nur hier**, nie im Core).
|
||||
- [ ] Öffentliches **Rate-Limiting & Quotas pro Account** (getrennt vom internen Polymarket-Limiter).
|
||||
- [ ] Mandanten-Sicht: Kunde sieht seine Watchlist/kopierten Master, nicht das gesamte Universum.
|
||||
- [ ] Dasselbe WebUI-SPA ausliefern.
|
||||
|
||||
## 4. Explizite Nicht-Ziele / Fallen
|
||||
- ❌ Public bekommt **keinen** Zugriff auf Core-Prozess, lokale API oder Control-Endpunkte.
|
||||
- ❌ Public bekommt **keinen** Schreibzugriff auf die Analyse-DB (nur `SELECT`).
|
||||
- ❌ **Kein** Nachrüsten von Kunden-Auth auf die lokale Vollschicht — Auth lebt nur im Public-Host.
|
||||
- ❌ Read-Endpunkte machen **keine** Provider-Calls und **keine** Writes (sonst public-untauglich).
|
||||
- ❌ WebUI kennt die DB nicht — immer nur über die API.
|
||||
|
||||
## 5. Reihenfolge / Aufwand
|
||||
1. **Phase 0** (klein): Read/Control-Split + Capabilities + Deep-Dive-Audit. **Jetzt** — es ist
|
||||
die Voraussetzung dafür, dass das neue Design von Anfang an sauber sitzt.
|
||||
2. **Phase 1+2** (klein, parallel): neues SPA als gemeinsames Artefakt, lokale Grenze härten.
|
||||
3. **Phase 3** (mittel): read-only DB-User + Schema-als-Vertrag dokumentieren.
|
||||
4. **Phase 4** (groß, später): Public-Host mit Auth/User-DB.
|
||||
|
||||
Phase 0–2 begleiten das neue Design sofort. Phase 3 kann jederzeit vorgezogen werden (billig).
|
||||
Phase 4 erst zum Release — aber durch 0–3 ist dann nichts mehr umzubauen, nur zu ergänzen.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Plan: Daten-Ingest skalieren (Rate-Limits, Egress-Kanäle, Blockchain)
|
||||
|
||||
> **Archiviert am 2026-08-23** — offene Punkte siehe [`ROADMAP.md`](../../ROADMAP.md),
|
||||
> Stufe 5. Dieses Dokument wird nicht mehr gepflegt.
|
||||
>
|
||||
> **Erledigt und hier nachträglich abgehakt:**
|
||||
> * **1c Ingest-Tiering** — `IngestMode` mit `SnapshotOnly`/`Aggregated`, siehe
|
||||
> [`FIXPLAN-TODO.md`](FIXPLAN-TODO.md) Teil D.
|
||||
> * **1d Redundanz** — `IMemoryCache` im `PolymarketApiClient`.
|
||||
> * **2a/2b Egress-Kanäle** — `Infrastructure/Services/EgressPoolService.cs` und
|
||||
> `EgressPoolHandler.cs`: Round-Robin über die aktiven Kanäle, Umschalten rein per
|
||||
> Konfiguration, Cooldown nach drei Fehlschlägen in Folge (`:152`), sodass ein 429 nur
|
||||
> den betroffenen Kanal trifft. Der Limiter-Schlüssel enthält den Kanal
|
||||
> (`RateLimiterService.cs:36`). Tests: `EgressPoolTests.cs`.
|
||||
>
|
||||
> **Teilweise:** Health-Statistik je Kanal (Erfolgsquote, 429-Rate, Latenz) fehlt — es gibt
|
||||
> nur Fehlerzähler und Cooldown. Tote Kanäle werden also pausiert, aber nicht ausgewertet.
|
||||
>
|
||||
> **Offen:** 1a (Blockchain/Subgraph als Bulk-Quelle) und 1b (Markt-Stammdaten-Cache) sind
|
||||
> Roadmap 5.1 und 5.2. **Abschnitt 3a (Header-Rotation) ist bewusst verworfen** — siehe
|
||||
> Roadmap 5.3: Verschleierung gegenüber Polymarket riskiert genau den Zugang, auf dem das
|
||||
> Projekt aufsetzt.
|
||||
|
||||
> Stand: 2026-07-12 · Status: **Richtung bestätigt** (Nutzer: beide Egress-Wege umschaltbar
|
||||
> umsetzen, sofern Aufwand vertretbar). Kein Code in diesem Schritt.
|
||||
> Problem: Polymarket-Rate-Limits bremsen den Datenimport teils extrem aus.
|
||||
|
||||
## 0. Ist-Zustand (verifiziert)
|
||||
|
||||
- `RateLimiterService` = **globaler Singleton-Token-Bucket**, feste Delays je Endpoint-Gruppe
|
||||
(Gamma ≈28/s, Data ≈18/s, Clob ≈66/s).
|
||||
- `PolymarketApiClient` = Singleton mit 3 benannten HttpClients, **ohne** Proxy/IP-Konfig →
|
||||
ein einziger Ausgangs-IP, ein globales Budget. Das ist die echte Bremse.
|
||||
|
||||
**Zwei Hebel:** Nachfrage senken (Abschnitt 1, größter Gewinn) und Angebot erhöhen (Abschnitt 2,
|
||||
Egress-Kanäle). Der Nutzer möchte in Abschnitt 2 **beide Egress-Arten (eigene IPs UND Proxys)
|
||||
umschaltbar** — und das ist billig, weil beide **derselbe HttpClient-Seam** sind (Abschnitt 2).
|
||||
|
||||
---
|
||||
|
||||
## 1. Nachfrage senken — größter Hebel (zuerst)
|
||||
|
||||
### 1a. Historische Massendaten aus Blockchain/Subgraph statt REST
|
||||
Polymarket-Trades sind On-Chain-Events auf Polygon. Für **Backfill/Deep-Resync** (größter
|
||||
REST-Verbraucher) ist `/activity` die falsche Quelle.
|
||||
- [ ] The-Graph-/Goldsky-Subgraph für Polymarket evaluieren: ein GraphQL-Call → tausende Fills
|
||||
eines Wallets, statt seitenweiser rate-limitierter REST-Abrufe.
|
||||
- [ ] Neue `IHistoricalTradeSource` neben dem REST-Provider; DeepResync (FIXPLAN A6) zieht Bulk
|
||||
hierüber. **Effekt:** nimmt die teuerste Last komplett von der REST-API.
|
||||
|
||||
### 1b. Markt-Stammdaten aggressiv cachen
|
||||
- [ ] Metadaten (Frage, Kategorie, Outcomes, `ConditionId`↔`TokenId`) einmal ziehen, lange cachen;
|
||||
nur Volume/Liquidity/Auflösung periodisch aktualisieren. Geschlossene Märkte nie voll re-syncen.
|
||||
|
||||
### 1c. Ingest-Tiering (FIXPLAN D3)
|
||||
- [x] Ultra-HF-Trader im `SnapshotOnly`-Modus erzeugen null Trade-Calls (PnL aus `/positions` +
|
||||
Leaderboard, wöchentliche Biopsie). Entfernt genau die Wallets, die das Budget auffressen.
|
||||
|
||||
### 1d. Redundanz vermeiden
|
||||
- [x] Worker-übergreifender Kurzzeit-Cache „gerade geholt", damit nicht mehrere Worker denselben
|
||||
Markt/Trader kurz hintereinander abrufen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Angebot erhöhen: umschaltbare **Egress-Kanäle** (eigene IPs UND Proxys — eine Mechanik)
|
||||
|
||||
**Die Schlüssel-Einsicht:** Ob eine Anfrage über eine **eigene Quell-IP** oder über einen
|
||||
**Proxy** rausgeht, ist im HttpClient nur eine andere Konfiguration desselben `SocketsHttpHandler`.
|
||||
Deshalb wird **nicht** zweimal gebaut, sondern **ein** Konzept: der **Egress-Kanal**. Umschalten =
|
||||
Konfiguration, nicht Code. Damit bekommt der Nutzer „beides, umschaltbar" zu geringen Kosten.
|
||||
|
||||
### 2a. Abstraktion `EgressChannel`
|
||||
Ein Kanal ist genau eine Ausgangsroute, per Config als einer von zwei Typen definiert:
|
||||
```jsonc
|
||||
"Egress": {
|
||||
"Channels": [
|
||||
{ "id": "ip-a", "type": "SourceIp", "value": "203.0.113.10" }, // eigene IP
|
||||
{ "id": "ip-b", "type": "SourceIp", "value": "203.0.113.11" },
|
||||
{ "id": "prox-1", "type": "Proxy", "value": "http://user:pass@proxy.example:8080" }
|
||||
]
|
||||
}
|
||||
```
|
||||
- [x] Pro Kanal **ein** `SocketsHttpHandler`:
|
||||
- `SourceIp` → `ConnectCallback`, Socket vor Connect an die lokale IP binden
|
||||
(`socket.Bind(new IPEndPoint(ip, 0))`).
|
||||
- `Proxy` → `handler.Proxy = new WebProxy(url); handler.UseProxy = true;`.
|
||||
- [x] `IEgressPool` verteilt Requests round-robin/least-loaded über die aktiven Kanäle.
|
||||
Leere/❑ Kanalliste = heutiges Verhalten (ein Default-Ausgang).
|
||||
- [x] Umschalten „nur eigene IPs" ↔ „nur Proxys" ↔ „Mix" = Config ändern, kein Deploy-Umbau.
|
||||
|
||||
### 2b. Rate-Limiter **pro Kanal** (Generalisierung des globalen Limiters)
|
||||
- [x] Limiter-Schlüssel wird `{platform}-{endpointGroup}-{channelId}`. Jeder Kanal hält sein
|
||||
eigenes Budget → N Kanäle ≈ N× Durchsatz, jeder Kanal bleibt unter dem Per-Route-Limit.
|
||||
- [x] 429 sperrt **nur den betroffenen Kanal** kurz, nicht alle.
|
||||
|
||||
### 2c. Betrieb
|
||||
- [ ] Bei eigenen IPs prüfen: sind es echte getrennte Egress-IPs (multi-homed), nicht NAT hinter einer.
|
||||
- [ ] Health/Statistik je Kanal (Erfolg, 429-Rate, Latenz), damit tote Proxys automatisch pausiert werden.
|
||||
|
||||
**Aufwand:** moderat und **einmalig** — durch die gemeinsame Abstraktion kostet „beides
|
||||
umschaltbar" kaum mehr als „nur IPs".
|
||||
|
||||
---
|
||||
|
||||
## 3. Proxys & Header-Rotation — Einordnung (korrigiert)
|
||||
|
||||
**Klarstellung (Korrektur einer früheren Fassung):** Predictalytics betreibt **kein Trading und
|
||||
keine Wallet** — es ist reine Analysesoftware, die ausschließlich **öffentliche** Marktdaten liest.
|
||||
Das frühere „Ban gefährdet die Trading-Wallet"-Argument gehört zu **PolyTrader** (getrenntes
|
||||
Projekt) und trifft hier **nicht** zu. Damit ist die Proxy-Wahl eine reine Engineering-Entscheidung
|
||||
des Nutzers — freie Proxys eingeschlossen.
|
||||
|
||||
Was real bleibt (ehrliche Hinweise, keine Blocker — Entscheidung liegt beim Nutzer):
|
||||
- **ToS-Grauzone:** Rate-Limit-Umgehung per Routen-/Header-Rotation widerspricht vermutlich
|
||||
Polymarkets Nutzungsbedingungen. Realistische Konsequenz **hier**: einzelne IPs/Proxys werden
|
||||
geblockt und müssen ersetzt werden — mehr nicht (kein Kapital, keine Wallet betroffen).
|
||||
- **Datenintegrität (der eigentlich relevante Punkt):** Ein kaputter/bösartiger (v. a. gratis)
|
||||
Proxy kann Antworten **verfälschen** → korrupte Analyse („garbage in, garbage out"). Da die
|
||||
gesamte Auswertung darauf aufbaut, lohnt sich eine **Plausibilitätsprüfung** der Antworten
|
||||
(Feldtypen, Wertebereiche, z. B. Preise ∈ [0,1]) und — wo Genauigkeit zählt — das Bevorzugen
|
||||
kontrollierter Proxys/eigener IPs. Rein informativ, kein Zwang.
|
||||
- **DB-Verbindung NIE über Proxy** (ausdrücklicher Nutzer-Wille): Die MySQL-Verbindung läuft
|
||||
**immer direkt**. Der Egress-Pool gilt **ausschließlich** für ausgehende Polymarket-HTTP-Calls,
|
||||
niemals für die DB.
|
||||
|
||||
### 3a. Header-Rotation (separat aktivierbar, standardmäßig AUS)
|
||||
Auf Wunsch integriert, bewusst opt-in:
|
||||
- [ ] Config `Egress.HeaderRotation.Enabled` — **Default `false`**. Bei `false` wird ein einziger,
|
||||
konsistenter Standard-Header-Satz gesendet (heutiges Verhalten).
|
||||
- [ ] Rotiert **kohärente Header-SETS**, nicht nur den User-Agent isoliert: je Eintrag ein
|
||||
zusammenpassendes Bündel (`User-Agent` + `Accept` + `Accept-Language` + `Sec-CH-UA`…), damit
|
||||
die Kombination realistisch bleibt — ein moderner UA mit widersprüchlichen Accept-Headern
|
||||
fällt eher auf als gar keine Rotation. Set-Pool aus Config ladbar.
|
||||
- [ ] Auswahl pro Kanal **oder** pro Request (konfigurierbar).
|
||||
- [ ] Greift nur für Polymarket-Read-Calls; unabhängig vom Kanaltyp (IP oder Proxy) nutzbar.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reihenfolge
|
||||
1. **1b + 1d** (Caching/Dedup) — sofort, klein, spürbar.
|
||||
2. **2a–2c** (Egress-Kanäle + Per-Kanal-Limiter) — moderat, liefert „eigene IPs UND Proxys umschaltbar".
|
||||
3. **1c** (Tiering) — hängt an FIXPLAN D3.
|
||||
4. **1a** (Blockchain/Subgraph-Bulk) — größter struktureller Hebel; eigener Rechercheschritt
|
||||
(welcher Subgraph deckt Fills sauber ab?), dann als `IHistoricalTradeSource`.
|
||||
|
||||
## 5. Tests / Abnahme
|
||||
- [x] Per-Kanal-Limiter: unabhängige Budgets (kein globales Blocken); 429 sperrt nur einen Kanal.
|
||||
- [x] Egress-Binding: Smoke gegen einen Dienst, der die Quell-IP zurückgibt → Round-Robin nutzt
|
||||
wirklich verschiedene IPs; Proxy-Kanal geht über den Proxy.
|
||||
- [x] Kanal-Health: toter Proxy wird automatisch pausiert, Pool weicht aus.
|
||||
- [ ] Blockchain-Quelle: Stichproben-Abgleich Bulk-Historie ↔ REST-`/activity` eines Wallets,
|
||||
bevor sie produktiv wird.
|
||||
- [ ] **Header-Rotation:** bei `Enabled=false` wird genau ein konsistenter Standard-Satz gesendet
|
||||
(kein Rotieren); bei `Enabled=true` stammt jeder gesendete Header-Satz **unverändert** aus dem
|
||||
Pool (kohärent, keine zusammengewürfelten Felder).
|
||||
- [ ] **DB-Guard:** die MySQL-Verbindung nutzt nie den Egress-Pool/Proxy (Regressionsschutz).
|
||||
- [ ] **Response-Plausibilität:** grob unplausible Provider-Antworten (z. B. Preis außerhalb
|
||||
[0,1]) werden erkannt und verworfen statt in die DB zu wandern.
|
||||
@@ -0,0 +1,766 @@
|
||||
# Implementierungsplan: Predictalytics auf Windows + Linux
|
||||
|
||||
**Stand:** 2026-08-06 · Basis: [ANALYSE-Linux-Portierung.md](ANALYSE-Linux-Portierung.md)
|
||||
**Ziel:** Ein Code-Stand, der unter Windows und Linux identisch läuft — Bedienhülle in
|
||||
Avalonia, gesamte Solution auf .NET 10.
|
||||
|
||||
---
|
||||
|
||||
## 0. Drei Entscheidungen vorab — **alle getroffen (2026-08-06)**
|
||||
|
||||
| # | Entscheidung | Ergebnis |
|
||||
|---|---|---|
|
||||
| 0.1 | EF Core | **Weg A** — TFM `net10.0`, EF Core `9.0.18` + Pomelo `9.0.0` |
|
||||
| 0.2 | UI-Framework | **Avalonia 12.1.1**, kein LiveCharts2 |
|
||||
| 0.3 | Linux-Ziel | **Beides** — Avalonia-GUI *und* Headless-Modus (AP 3.4 verbindlich) |
|
||||
|
||||
### 0.1 ✅ EF Core: Version 9 behalten, nicht auf 10 gehen — **entschieden: Weg A**
|
||||
|
||||
Das ist der einzige Punkt, an dem „alles auf .NET 10" nicht sauber aufgeht.
|
||||
|
||||
**Befund (verifiziert über nuget.org am 2026-08-06):**
|
||||
|
||||
```
|
||||
Pomelo.EntityFrameworkCore.MySql → neueste stabile Version: 9.0.0 (2025-08-17)
|
||||
nuspec: targetFramework net8.0
|
||||
Microsoft.EntityFrameworkCore.Relational [9.0.0, 9.0.999] ← harte Obergrenze
|
||||
```
|
||||
|
||||
**Es gibt keinen Pomelo-Provider für EF Core 10.** Die Versionsspanne ist nach oben
|
||||
geschlossen; ein Upgrade auf EF Core 10 ist mit Pomelo technisch unmöglich.
|
||||
|
||||
| Weg | Konsequenz |
|
||||
|---|---|
|
||||
| **A — empfohlen: TFM `net10.0`, EF Core `9.0.18`, Pomelo `9.0.0`** | Alle Projekte laufen auf .NET 10. EF-Core-9-Pakete (net8.0) laufen problemlos auf der .NET-10-Runtime. **Kein Provider-Wechsel, kein DB-Risiko.** Nur +1 EF-Major statt +2. |
|
||||
| **B — EF Core `10.0.10` + `MySql.EntityFrameworkCore 10.0.9` (Oracle)** | Erzwingt den Wechsel des Datenbankproviders. Andere SQL-Generierung, andere Migrations-Historie, abweichendes Verhalten bei Typ-Mapping und Bulk-Operationen — **gegen die Produktiv-DB.** Realistisch +3–5 PT plus Regressionsrisiko in 94 Infrastructure-Dateien und 30+ Migrationen. |
|
||||
|
||||
**→ Gewählt: Weg A.** „Auf .NET 10 gleichbleiben" ist das eigentliche Ziel und wird
|
||||
vollständig erreicht — das Zielframework aller Projekte ist `net10.0`. Dass die
|
||||
EF-Core-*Bibliothek* bei 9.0.18 bleibt, ist ein Implementierungsdetail ohne Auswirkung auf
|
||||
Runtime, Sprachfeatures oder Toolchain. Sobald Pomelo einen EF-Core-10-Provider
|
||||
veröffentlicht, ist der Nachzug ein Einzeiler.
|
||||
|
||||
Weg B entfällt; ein Providerwechsel gegen die Produktiv-DB findet nicht statt.
|
||||
|
||||
### 0.2 Avalonia 12.1.1 — entschieden ✅
|
||||
|
||||
```
|
||||
Avalonia 12.1.1 (2026-07-29), lib-TFMs: net8.0, net10.0 ← natives .NET 10
|
||||
Avalonia.Desktop 12.1.1 · Avalonia.Themes.Fluent 12.1.1 · Avalonia.Controls.DataGrid 12.1.2
|
||||
```
|
||||
|
||||
Eine Einschränkung: **`Avalonia.Diagnostics` liegt noch bei 11.3.18**, es gibt keine
|
||||
12.x-Version. Das ist das DevTools-Inspektorfenster (F12) — reines Entwicklerwerkzeug, für
|
||||
die Laufzeit irrelevant. Falls beim Layouten vermisst: temporär eine 11.3.18-Referenz nur
|
||||
in der Debug-Konfiguration, oder ohne auskommen.
|
||||
|
||||
**LiveCharts2 wird nicht benötigt.** Die Bedienhülle enthält kein einziges Diagramm; alle
|
||||
Charts liegen in der Web-UI (Chart.js). Keine Chart-Bibliothek im Shell-Projekt.
|
||||
|
||||
### 0.3 ✅ Wo läuft die Linux-Instanz? — **entschieden: beides**
|
||||
|
||||
Eine Avalonia-GUI braucht unter Linux eine Desktop-Umgebung: X11 (bzw. Wayland über XWayland),
|
||||
dazu `libx11`, `libice`, `libsm`, `libfontconfig1` und installierte Fonts. Auf einem
|
||||
**headless Server ohne Desktop startet sie nicht.**
|
||||
|
||||
**→ Gewählt: beides in einem Binary.** Die Avalonia-Shell für Desktop-Betrieb (Windows und
|
||||
Linux), zusätzlich ein Schalter `--headless` für Server ohne Desktop-Session.
|
||||
|
||||
Damit ist **AP 3.4 verbindlicher Bestandteil**, nicht optional. Nach der
|
||||
Hosting-Extraktion (Phase 2) kostet er ca. 0,5 PT, weil der gesamte Kern dann bereits
|
||||
GUI-frei ist. Konsequenzen, die dadurch fix werden:
|
||||
|
||||
* Der Lizenzschlüssel muss **auch ohne Dialog** beziehbar sein (Konfiguration /
|
||||
Umgebungsvariable) — koppelt an die laufende HardwareID-Arbeit.
|
||||
* Das Deployment liefert zwei Betriebsarten: Desktop-Start und systemd/Container.
|
||||
* Phase 6 verifiziert beide Modi.
|
||||
|
||||
---
|
||||
|
||||
## 1. Zielarchitektur
|
||||
|
||||
Der entscheidende Umbau ist nicht die GUI, sondern das **Herausziehen eines
|
||||
plattformneutralen Hosting-Kerns**. Heute liegen ~870 portable Zeilen (EmbeddedWebServer,
|
||||
Watchdog, Serilog-Setup, Settings) im windows-gebundenen Projekt fest. Danach ist die
|
||||
GUI nur noch eine dünne Schale darüber — und der Headless-Modus fällt fast von selbst ab.
|
||||
|
||||
```
|
||||
src/
|
||||
Predictalytics.Domain net10.0 unverändert
|
||||
Predictalytics.Application net10.0 unverändert
|
||||
Predictalytics.Infrastructure net10.0 unverändert
|
||||
Predictalytics.Api net10.0 unverändert
|
||||
Predictalytics.Worker net10.0 unverändert
|
||||
Predictalytics.Hosting NEU net10.0 ← plattformneutraler Kern
|
||||
Predictalytics.Shell NEU net10.0 ← Avalonia-Bedienhülle (Win + Linux)
|
||||
Predictalytics.Application.Tests net10.0 net10.0 statt bisher net10.0 (bleibt)
|
||||
Predictalytics.WinFormsHost net10.0-windows ← Übergang, s. 6.2
|
||||
```
|
||||
|
||||
**`Predictalytics.Hosting`** (neu, GUI-frei) nimmt auf:
|
||||
|
||||
| Herkunft | Ziel |
|
||||
|---|---|
|
||||
| `WinFormsHost/Services/EmbeddedWebServer.cs` (365 Z.) | `Hosting/PredictalyticsHost.cs` — unverändert bis auf Binding + wwwroot |
|
||||
| `WinFormsHost/AppSettings.cs` (137 Z.) | `Hosting/PredictalyticsOptions.cs` — ohne `[Category]`/`[Editor]`-Attribute |
|
||||
| `WinFormsHost/Services/WatchdogHeartbeatService.cs` (141 Z.) | 1:1, ist bereits portabel |
|
||||
| `WinFormsHost/Program.cs:33-148` (Serilog-Setup) | `Hosting/LoggingSetup.cs` — 1:1, ist bereits portabel |
|
||||
| `WinFormsHost/Services/LicenseGuard.cs` (102 Z.) | `Hosting/LicenseGuard.cs` — ohne WinForms-Timer/Dialog |
|
||||
| `Infrastructure/Logging/RichTextBoxSink.cs` | umbenennen → `DelegateSink.cs` (ist schon portabel) |
|
||||
|
||||
**`Predictalytics.Shell`** (neu, Avalonia) enthält nur noch View + ViewModel.
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 0 — Vorarbeiten ✅ **erledigt (2026-08-06)**
|
||||
|
||||
| # | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| 0.1 | `InvariantCulture` ergänzen | ✅ `LimitlessProvider.cs:270` (Volume) |
|
||||
| 0.1b | **Nachtrag:** 13 × lokalzeit-verzerrendes `DateTime.TryParse` | ✅ neuer Helfer `Infrastructure/Helpers/DateParsing.cs`, alle Fundstellen umgestellt |
|
||||
| 0.2 | Eingecheckte Logdatei entfernen | ✅ nicht nötig — Datei ist **nicht** getrackt, `.gitignore:14 logs/` greift bereits |
|
||||
| 0.3 | `j:\`-Absolutpfad-Fallback entfernen | ✅ `EmbeddedWebServer.cs` |
|
||||
| 0.4 | Branch anlegen | ✅ `feature/linux-net10` |
|
||||
|
||||
**Abnahme erreicht:** `dotnet build` → 0 Fehler, 10 Warnungen (alle vorbestehend).
|
||||
`dotnet test` → **100 bestanden, 0 Fehler, 1 übersprungen**.
|
||||
|
||||
### Nachtrag 0.1b — Umfang und offener Punkt
|
||||
|
||||
`DateTime.TryParse("2026-08-06T12:00:00Z")` ohne `DateTimeStyles` liefert `Kind=Local` und
|
||||
rechnet in Lokalzeit um. Betroffen waren in beiden Providern `StartDate`, `EndDate`,
|
||||
`CreatedAt`, `ClosedAt`, `ExecutedAt`, `ExpirationDate` — während der Fallback derselben
|
||||
Zuweisungen (`DateTime.UtcNow`) korrektes UTC schrieb. Auf der Berliner Maschine also
|
||||
1–2 h Versatz, auf einem Linux-Server mit `TZ=UTC` hingegen nicht: derselbe Code hätte
|
||||
plattformabhängig unterschiedliche Werte erzeugt.
|
||||
|
||||
✅ **Altdaten-Backfill: entfällt (Entscheidung 2026-08-06).** Der Fix wirkt nur für neue
|
||||
Schreibvorgänge; bereits gespeicherte Zeilen tragen den 1–2-h-Versatz weiter. Bewusst
|
||||
akzeptiert — die Daten verlieren mit steigendem Alter ohnehin an Relevanz, und die
|
||||
betroffenen Felder werden bei jedem Markt-Sync überschrieben.
|
||||
|
||||
**Konsequenz für Phase 6:** Beim Vergleich Linux-Lauf ↔ Windows-Referenz (Prüfung 8.3)
|
||||
dürfen historische Zeilen nicht als Referenz dienen — nur frisch geschriebene Datensätze
|
||||
sind aussagekräftig.
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 1 — .NET 10 ✅ **erledigt (2026-08-06)**
|
||||
|
||||
**Abnahme erreicht:** `dotnet build` → 0 Fehler, 8 Warnungen (alle vorbestehend, 2 weniger
|
||||
als vorher). `dotnet test` → **100 bestanden, 0 Fehler, 1 übersprungen**, Testhost
|
||||
`net10.0`. Alle Projekte laufen auf `net10.0`, WinFormsHost auf `net10.0-windows`.
|
||||
|
||||
Ergebnis gegenüber der Planung:
|
||||
|
||||
| Erwartung | Tatsächlich |
|
||||
|---|---|
|
||||
| Swashbuckle 6.9 → 10.x „wahrscheinlichster Reibungspunkt", ggf. Wechsel auf `AddOpenApi()` | **Keine Bruchstelle.** Der genutzte API-Ausschnitt (`AddSwaggerGen`/`SwaggerDoc`/`UseSwagger`/`UseSwaggerUI`) ist stabil geblieben. Kein Umbau nötig. |
|
||||
| EF Core 8 → 9 „überschaubar" | Bestätigt, keine Anpassung nötig |
|
||||
| SQLite-Sicherheitswarnung „löst sich mit dem EF-Bump von selbst" | **Falsch** — EF Core 9.0.18 zieht SQLitePCLRaw 2.1.10, weiterhin von GHSA-2m69-gcr7-jv3q betroffen. Behoben über einen expliziten transitiven Pin auf 2.1.12. |
|
||||
|
||||
Bewusst **vor** dem Avalonia-Umbau erledigt: eine Baustelle nach der anderen, und das
|
||||
Shell-Projekt entsteht direkt auf dem Zielframework.
|
||||
|
||||
### AP 1.1 — Zentrale Frameworkverwaltung ✅
|
||||
`Directory.Build.props` setzt `TargetFramework=net10.0` zentral; `<TargetFramework>` aus
|
||||
allen `.csproj` entfernt (Ausnahme: WinFormsHost → `net10.0-windows`). Zusätzlich zentral:
|
||||
`SatelliteResourceLanguages=en`, das bisher in vier Projekten einzeln stand.
|
||||
|
||||
`Directory.Packages.props` mit `ManagePackageVersionsCentrally` eingeführt — 21 Pakete,
|
||||
Versionen aus allen `.csproj` entfernt. Zusätzlich
|
||||
`CentralPackageTransitivePinningEnabled`, um Sicherheits-Pins auf transitive
|
||||
Abhängigkeiten setzen zu können.
|
||||
|
||||
**Nicht angefasst:** `Spike/Spike.csproj` — leeres Restprojekt ohne Quelldateien, nicht in
|
||||
der Solution, baut auch im Ist-Zustand nicht (`CS5001: keine Main-Methode`). Kandidat zum
|
||||
Löschen, aber nicht Teil dieser Portierung.
|
||||
|
||||
### AP 1.2 — Paketmatrix
|
||||
|
||||
| Paket | heute | Ziel | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| `Microsoft.EntityFrameworkCore(.Relational/.Design)` | 8.0.11 | **9.0.18** | Obergrenze durch Pomelo |
|
||||
| `Pomelo.EntityFrameworkCore.MySql` | 8.0.2 | **9.0.0** | zieht MySqlConnector 2.4.0 |
|
||||
| `Microsoft.EntityFrameworkCore.InMemory/.Sqlite` (Tests) | 8.0.11 | **9.0.18** | muss zu EF Core passen |
|
||||
| `Swashbuckle.AspNetCore` | 6.9.0 | **10.2.3** | ⚠️ 6.x ist nicht ASP.NET-Core-10-tauglich |
|
||||
| `Serilog` | 4.2.0 | 4.4.0 | |
|
||||
| `Serilog.AspNetCore` | 8.0.3 | 10.0.0 | |
|
||||
| `Serilog.Sinks.File` | 6.0.0 | 7.0.0 | |
|
||||
| `Serilog.Extensions.Logging` / `.Sinks.Console` / `.Formatting.Compact` | 8.0.0 / 6.0.0 / 3.0.0 | jeweils aktuell | |
|
||||
| `Microsoft.Extensions.Http` / `.Hosting.Abstractions` / `.Logging.Abstractions` | 8.0.x | 10.0.x | |
|
||||
| `Microsoft.NET.Test.Sdk` / `xunit` / `Moq` / `coverlet` | aktuell | prüfen | |
|
||||
|
||||
Tatsächlich aufgelöste Versionen (verifiziert über `dotnet list package`): EF Core 9.0.18,
|
||||
Pomelo 9.0.0, Serilog 4.4.0 / Extensions.Logging 10.0.0 / Sinks.File 7.0.0 /
|
||||
Sinks.Console 6.1.1, Swashbuckle 10.2.3, Test-SDK 18.8.1, coverlet 10.0.1.
|
||||
Sicherheits-Pin: SQLitePCLRaw 2.1.12 (statt transitiv 2.1.10).
|
||||
|
||||
### AP 1.3 — Erwartete Bruchstellen *(Rückblick: keine eingetreten)*
|
||||
* **Swashbuckle 6.9 → 10.x** ist der wahrscheinlichste Reibungspunkt: geänderte API-Oberfläche,
|
||||
und ASP.NET Core 10 bringt mit `Microsoft.AspNetCore.OpenApi` eine eigene
|
||||
OpenAPI-Generierung mit. Betrifft `Api/ApiConfiguration.cs:19-20` und
|
||||
`WinFormsHost/Services/EmbeddedWebServer.cs:143-144,154-155`.
|
||||
*Alternative:* auf das eingebaute `AddOpenApi()`/`MapOpenApi()` umsteigen und Swashbuckle
|
||||
ganz ablösen — sauberer, aber die Swagger-UI müsste ersetzt werden (z. B. Scalar).
|
||||
* **EF Core 8 → 9:** überschaubar. Zu prüfen sind Migrationsverhalten und die
|
||||
`ExecuteSqlRawAsync`-Aufrufe in `EmbeddedWebServer.cs:56-100`.
|
||||
* `Predictalytics.Application.Tests` steht bereits auf `net10.0` — hier wird die
|
||||
Inkonsistenz gerade aufgelöst statt vergrößert.
|
||||
|
||||
**Abnahme:** `dotnet build` + `dotnet test` grün; WinFormsHost startet, Worker laufen,
|
||||
Web-UI erreichbar; **Migrationen gegen die Dev-DB (`bergisnu_db0`) verifiziert, nicht gegen Prod.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 2 — Hosting-Kern extrahieren ✅ **erledigt (2026-08-06)**
|
||||
|
||||
**Abnahme erreicht:** `dotnet build` → 0 Fehler. `dotnet test` → **100 bestanden, 0 Fehler,
|
||||
1 übersprungen**. `Predictalytics.Hosting` baut ohne Windows-Bezug; `WinFormsHost` enthält
|
||||
nur noch UI-Code (`MainForm`, `LicenseDialog`, `TerminalHelper`, `Program`) und referenziert
|
||||
Api/Worker/Infrastructure/LicenseLabrador nur noch transitiv über Hosting.
|
||||
|
||||
### ⚠️ Betrieblicher Hinweis: `settings.json` ging verloren
|
||||
|
||||
Beim Aufräumen der Build-Ausgabe in Phase 1 wurde `bin/` gelöscht — und dort lag die zur
|
||||
Laufzeit erzeugte `settings.json` mit DB-Zugangsdaten und Watchdog-API-Key. Die Datei war
|
||||
nie versioniert (`.gitignore:11`) und ist nicht wiederherstellbar.
|
||||
|
||||
**Beim nächsten Start neu einzutragen:** DB Server/Database/User/Password, Watchdog API Key,
|
||||
ggf. Egress-Kanäle.
|
||||
|
||||
Der Fehler kann sich nicht wiederholen: durch AP 2.3 liegen die Einstellungen jetzt unter
|
||||
`%APPDATA%\Predictalytics\settings.json` bzw. `~/.config/Predictalytics/settings.json` —
|
||||
außerhalb des Build-Verzeichnisses. `dotnet clean` oder ein Löschen von `bin/` sind damit
|
||||
folgenlos.
|
||||
|
||||
### Abweichungen von der Planung
|
||||
|
||||
| Geplant | Tatsächlich |
|
||||
|---|---|
|
||||
| Hosting als `Microsoft.NET.Sdk.Web` mit `OutputType=Library` (analog Api) | **`Microsoft.NET.Sdk` + `<FrameworkReference Include="Microsoft.AspNetCore.App" />`.** Der Web-SDK globbt `wwwroot/**` automatisch als Static Web Asset und kollidierte mit dem Content-Eintrag aus AP 2.5 (`DiscoverPrecompressedAssets`: doppelter Schlüssel). Gilt auch für den Host: WinFormsHost ist jetzt ebenfalls normaler SDK. Für die Avalonia-Shell gleich so anlegen. |
|
||||
| `Process.Start("explorer.exe", …)` erst in AP 3.3 ersetzen | Vorgezogen — `MainForm` wurde ohnehin umgeschrieben. Jetzt `ProcessStartInfo { UseShellExecute = true }`, funktioniert unter Windows und Linux. |
|
||||
| `LicenseGuard` komplett nach Hosting | Aufgeteilt: der plattformneutrale Teil liegt in `Hosting/LicenseGuard.cs`, der interaktive Dialogaufruf in `WinFormsHost/Services/LicenseGate.cs`. Genau die Naht, an der Phase 3 den Avalonia-Dialog bzw. den Headless-Pfad einhängt. |
|
||||
| `[Editor]`-Attribut für den mehrzeiligen Egress-Editor erhalten | **Entfallen.** Es verwies auf die .NET-Framework-Assembly `System.Design`, die es in .NET (Core) nicht gibt — der Editor dürfte schon vorher nicht gegriffen haben. Die Avalonia-Settings-View bekommt ein echtes mehrzeiliges Textfeld. Alle übrigen `System.ComponentModel`-Attribute sind plattformneutral und bleiben, das PropertyGrid behält Gruppen und Beschreibungen. |
|
||||
|
||||
### Zusätzlich mitgenommen
|
||||
|
||||
* `PredictalyticsOptions.WebserverHost` — Bind-Adresse konfigurierbar (Vorbereitung AP 4.1),
|
||||
Default `localhost`, also verhaltensgleich.
|
||||
* `PredictalyticsOptions.DbSslMode` — Default bewusst `None`, damit sich das Verbindungs-
|
||||
verhalten in einem reinen Refactoring nicht ändert. Umstellung auf `Preferred` ist
|
||||
jetzt eine reine Konfigurationsfrage.
|
||||
* `PredictalyticsHost.GetDatabaseSizeMbAsync()` — die DB-Abfrage lag vorher direkt im
|
||||
`MainForm`.
|
||||
* `RichTextBoxSink` → `DelegateSink` umbenannt (war nie WinForms-abhängig).
|
||||
|
||||
**Reiner Verschiebe- und Entkopplungsvorgang, keine neue Fachlogik.**
|
||||
|
||||
### AP 2.1 — Projekt `Predictalytics.Hosting` anlegen
|
||||
`net10.0`, Referenzen auf `Api`, `Worker`, `Infrastructure`, `LicenseLabrador.Client`.
|
||||
|
||||
### AP 2.2 — Verschieben (Tabelle in Abschnitt 1)
|
||||
`EmbeddedWebServer` → `PredictalyticsHost`, Watchdog, Serilog-Setup, LicenseGuard.
|
||||
`RichTextBoxSink` → `DelegateSink` umbenennen (der Sink ist ein reiner
|
||||
`Action<string, LogEventLevel>`-Delegat und war nie WinForms-abhängig).
|
||||
|
||||
### AP 2.3 — Konfiguration entkoppeln
|
||||
`AppSettings` → `PredictalyticsOptions` ohne `System.ComponentModel`-Attribute.
|
||||
Ladereihenfolge: `appsettings.json` → `appsettings.{Environment}.json` →
|
||||
Umgebungsvariablen (`PREDICTALYTICS_`) → `settings.json` im Benutzerprofil (Kompatibilität).
|
||||
Ablageort plattformabhängig über `Environment.SpecialFolder.ApplicationData`
|
||||
(Windows: `%APPDATA%`, Linux: `~/.config`) statt neben der Executable — das
|
||||
Installationsverzeichnis ist unter Linux üblicherweise nicht beschreibbar.
|
||||
|
||||
⚠️ Beim Umbau ist `SslMode = MySqlSslMode.None` (`AppSettings.cs:100`) fest verdrahtet.
|
||||
Da die Zugangsdaten künftig auch über Netz zu einer entfernten DB gehen, sollte das
|
||||
konfigurierbar und standardmäßig aktiv werden.
|
||||
|
||||
### AP 2.4 — Lifecycle GUI-frei machen
|
||||
* `System.Windows.Forms.Timer` → `PeriodicTimer` bzw. `IHostedService`
|
||||
(`MainForm.cs:71` DB-Größe, `LicenseGuard.cs:70` Revalidierung)
|
||||
* `Application.Exit()` → `IHostApplicationLifetime.StopApplication()`
|
||||
* `PredictalyticsHost` bekommt Events/Observables (`StatusChanged`, `DbSizeChanged`),
|
||||
an die sich später sowohl das Avalonia-ViewModel als auch der Headless-Modus hängen.
|
||||
|
||||
### AP 2.5 — wwwroot sauber deployen
|
||||
`FindWwwrootPath()` (`EmbeddedWebServer.cs:308-335`) mit der Verzeichnis-Heuristik ersatzlos
|
||||
streichen. Stattdessen in den Host-`.csproj`:
|
||||
```xml
|
||||
<Content Include="..\Predictalytics.Api\wwwroot\**"
|
||||
Link="wwwroot\%(RecursiveDir)%(Filename)%(Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
```
|
||||
und im Code nur noch `Path.Combine(AppContext.BaseDirectory, "wwwroot")`.
|
||||
|
||||
**Abnahme:** WinFormsHost benutzt nur noch `Predictalytics.Hosting` und enthält
|
||||
ausschließlich UI-Code. `Predictalytics.Hosting` baut ohne Windows-Referenzen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 3 — Avalonia-Bedienhülle ✅ **erledigt (2026-08-07)**
|
||||
|
||||
**Abnahme:** `dotnet build` → 0 Fehler, 0 Warnungen. `dotnet test` → 100 bestanden.
|
||||
Beide Betriebsarten laufen an:
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---|---|
|
||||
| `Predictalytics.Shell.exe --headless` | Logging läuft, Lizenzprüfung greift, sauberer Abbruch mit **Exit-Code 2** und handlungsfähiger Meldung |
|
||||
| `Predictalytics.Shell.exe` (GUI) | startet, Lizenzfenster erscheint, kein Absturz |
|
||||
| Konsolen-Kodierung | UTF-8 explizit gesetzt — ohne das zeigt die Windows-Konsole nur Fragezeichen statt Rahmen und Emoji |
|
||||
|
||||
### Nachtrag 2026-08-07: Hauptfenster verifiziert ✅
|
||||
|
||||
Zunächst blockierte eine abgelaufene Lizenz die Prüfung. Nach der Umstellung auf das
|
||||
Deploymentcenter (Abschnitt 5a) startet die GUI vollständig durch — belegt durch die
|
||||
Logdateien des Laufs:
|
||||
|
||||
```
|
||||
[INF] Shell initialisiert. Bereit.
|
||||
[INF] 'Start Webserver' startet die WebUI auf http://localhost:5000
|
||||
[INF] Einstellungen: C:\Users\bergm\AppData\Roaming\Predictalytics\settings.json
|
||||
```
|
||||
|
||||
Damit sind Lizenzschranke, Serilog-Aufbau mit Terminal-Sink, `MainWindowViewModel.Start()`
|
||||
und der neue Einstellungspfad nachgewiesen. **Noch nicht durchgeklickt**: die einzelnen
|
||||
Wartungsaktionen und die Einstellungsansicht — dafür fehlen die in Phase 1 verlorenen
|
||||
DB-Zugangsdaten.
|
||||
|
||||
### Umsetzung
|
||||
|
||||
| Datei | Zweck |
|
||||
|---|---|
|
||||
| `Program.cs` | Einstiegspunkt. Argumentauswertung **vor** jeder Avalonia-Initialisierung — sonst stirbt der Prozess auf einem Server ohne X11, bevor `--headless` greift. |
|
||||
| `App.axaml(.cs)` | Lizenzschranke, dann Hauptfenster. `ShutdownMode.OnExplicitShutdown` während der Aktivierung, danach `OnMainWindowClose`. |
|
||||
| `ViewModels/MainWindowViewModel.cs` | Bindungsziel, `CommunityToolkit.Mvvm`-Quellgeneratoren, kapselt `PredictalyticsHost` |
|
||||
| `ViewModels/LogLine.cs` | Terminalzeile mit Einfärbung nach Loglevel (Farbschema aus WinForms übernommen) |
|
||||
| `Views/MainWindow.axaml` | Menü, Werkzeugleiste, Terminal-Tab, Einstellungen-Tab, Statusleiste |
|
||||
| `Views/LicenseWindow.axaml` | Aktivierungsfenster, ersetzt `LicenseDialog` |
|
||||
| `Views/Dialogs.cs` | Ersatz für `MessageBox.Show` — im Code aufgebaut, kein Drittanbieterpaket |
|
||||
| `Services/HeadlessRunner.cs` | AP 3.4: Betrieb ohne Oberfläche |
|
||||
| `Services/ConsoleAttach.cs` | Windows-Konsole anhängen + UTF-8 |
|
||||
|
||||
### Abweichungen und Entscheidungen
|
||||
|
||||
* **`PropertyGrid`-Ersatz handgeschrieben**, wie empfohlen. Vier Gruppen als
|
||||
`HeaderedContentControl`, Passwortfelder mit `PasswordChar`, **echtes mehrzeiliges
|
||||
Textfeld** für die Egress-Kanäle (besser als das in Phase 2 entfallene `[Editor]`-Attribut).
|
||||
SSL-Modus als `ComboBox`; bewusst nur `None`/`Preferred`/`Required` — `VerifyCA`/`VerifyFull`
|
||||
bräuchten hinterlegte Zertifikate und würden hier nur zu Fehlkonfiguration einladen.
|
||||
* **Speichern ist jetzt explizit** („Speichern & übernehmen") statt wie beim `PropertyGrid`
|
||||
bei jeder Einzeländerung. Klarer, und erspart `INotifyPropertyChanged` auf `PredictalyticsOptions`.
|
||||
* **Feste Fenstergröße aufgelöst** (vorher `1886×1088` fix): jetzt `MinWidth`/`MinHeight`
|
||||
mit skalierendem `DockPanel`/`Grid`.
|
||||
* **Schrift-Fallbackkette** `Cascadia Code, DejaVu Sans Mono, Liberation Mono, Consolas, monospace`
|
||||
fürs Terminal; `Avalonia.Fonts.Inter` als mitgelieferte UI-Schrift, weil Linux-Distributionen
|
||||
sehr unterschiedliche Standardfonts haben.
|
||||
* **Ringpuffer statt Leeren**: die WinForms-Fassung verwarf bei 500 Zeilen das gesamte
|
||||
Terminal (`rtb.Clear()`), jetzt fällt jeweils nur die älteste Zeile heraus.
|
||||
* **`Avalonia.Diagnostics` nicht referenziert** — gibt es nur bis 11.3.18, nicht für 12.x.
|
||||
* **`global::Avalonia.Application`**: innerhalb von `Predictalytics.*` löst der kurze Name
|
||||
`Application` auf den eigenen Namespace `Predictalytics.Application` auf. Betrifft jede
|
||||
künftige Avalonia-Klasse in diesem Projekt.
|
||||
* **`Microsoft.NET.Sdk` statt `Sdk.Web`**, wie in Phase 2 gelernt (Static-Web-Assets-Kollision
|
||||
mit dem `wwwroot`-Content-Eintrag).
|
||||
|
||||
### Headless-Betrieb
|
||||
|
||||
Lizenz über `PREDICTALYTICS_LICENSE_KEY`; ohne nutzbare Lizenz Abbruch mit Exit-Code 2
|
||||
statt eines Dialogs, den niemand sieht. `SIGTERM` und `SIGINT` werden über
|
||||
`PosixSignalRegistration` abgefangen, damit systemd sauber stoppen kann.
|
||||
|
||||
Exit-Codes: `0` planmäßig beendet · `2` keine nutzbare Lizenz · `3` Abbruch mit Fehler.
|
||||
|
||||
### AP 3.1 — Projekt `Predictalytics.Shell`
|
||||
```xml
|
||||
<PackageReference Include="Avalonia" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.1" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
```
|
||||
MVVM mit `CommunityToolkit.Mvvm` (Source-Generatoren) — bei ~15 Settings-Properties plus
|
||||
Status-Bindings spart das spürbar Boilerplate gegenüber handgeschriebenem `INotifyPropertyChanged`.
|
||||
|
||||
### AP 3.2 — Control-Mapping
|
||||
|
||||
| WinForms (heute) | Avalonia | Aufwand |
|
||||
|---|---|---|
|
||||
| `MenuStrip` + 3 Menüs | `Menu` / `NativeMenuBar` | klein |
|
||||
| `ToolStrip` + 2 Buttons | `StackPanel` + `Button` | klein |
|
||||
| `TabControl` / `TabPage` | `TabControl` / `TabItem` | klein |
|
||||
| `StatusStrip` + 3 Labels | `DockPanel` unten + `TextBlock` | klein |
|
||||
| `MessageBox.Show` (7 Aufrufe) | eigener Dialog oder `MsBox.Avalonia` | klein |
|
||||
| **`RichTextBox` (farbiges Log)** | `ListBox`/`ItemsControl` über `ObservableCollection<LogLine>`, `DataTemplate` mit `Foreground`-Binding | **mittel** |
|
||||
| **`PropertyGrid`** | **kein Äquivalent in Avalonia Core** | **mittel–groß** |
|
||||
|
||||
**Zum Log-View:** die bisherige Ringpuffer-Logik (`TerminalHelper.cs:26-31` — bei 500 Zeilen
|
||||
`rtb.Clear()`) wird zur `ObservableCollection` mit `RemoveAt(0)` — angenehmer, weil die
|
||||
Historie nicht mehr komplett verworfen wird. Marshalling über `Dispatcher.UIThread.Post`
|
||||
statt `Control.BeginInvoke`. Farbschema aus `TerminalHelper.cs:33-39` direkt übernehmbar.
|
||||
|
||||
**Zum PropertyGrid** — das ist der einzige echte UI-Entwurfsaufwand. Drei Optionen:
|
||||
|
||||
| Option | Bewertung |
|
||||
|---|---|
|
||||
| **Handgeschriebene Settings-View** (empfohlen) | 4 Gruppen / 15 Properties (Webserver 2, Egress 1, Watchdog 6, Database 4 + abgeleiteter ConnectionString). Als `Expander` je Gruppe mit `TextBox`/`CheckBox`/`NumericUpDown`. Volle Kontrolle über Passwortfelder (`PasswordChar`) und Mehrzeilen-Editor für Egress. ~1–1,5 PT. |
|
||||
| Drittanbieter `Avalonia.PropertyGrid` | Spart Zeit, bringt aber eine Abhängigkeit fragwürdiger Pflegelage und passt selten zum Fluent-Theme. |
|
||||
| Reflection-basierter Generator | Elegant, aber Overengineering für 15 Properties. |
|
||||
|
||||
### AP 3.3 — Plattformdetails
|
||||
* **Fixe Fenstergröße auflösen:** `MinimumSize`/`MaximumSize = 1886×1088`
|
||||
(`MainForm.Designer.cs:233-234`) ist bei abweichender DPI/Auflösung unter Linux
|
||||
untauglich. → skalierendes Grid-Layout, sinnvolles `MinWidth`/`MinHeight`.
|
||||
* **Schriftart:** `"Cascadia Code"` (`MainForm.cs:25`) existiert unter Linux meist nicht.
|
||||
→ Fallback-Kette `Cascadia Code, DejaVu Sans Mono, monospace`.
|
||||
* **`Process.Start("explorer.exe", …)`** (3×, `MainForm.cs:192,207,209`) →
|
||||
```csharp
|
||||
Process.Start(new ProcessStartInfo(target) { UseShellExecute = true });
|
||||
```
|
||||
funktioniert auf Windows und Linux (dort über `xdg-open`).
|
||||
* **Lizenzdialog** (`LicenseDialog.cs`, 118 Z.) in Avalonia nachbauen: Label, `TextBox`
|
||||
(Großschreibung — `CharacterCasing.Upper` hat kein direktes Pendant, per
|
||||
`TextInput`-Handler oder Converter lösen), Status-Label, zwei Buttons. Der
|
||||
Aktivierungsablauf (`ValidateAsync` + `VerifyChecksum`) bleibt unverändert.
|
||||
*Hinweis: greift auf die HardwareID-Arbeit zu, die separat läuft.*
|
||||
|
||||
### AP 3.4 — Headless-Modus *(verbindlich, 0,5 PT — Entscheidung 0.3)*
|
||||
Schalter `--headless` in `Program.cs` der Shell: statt `BuildAvaloniaApp().StartWithClassicDesktopLifetime()`
|
||||
direkt `PredictalyticsHost` starten und auf `IHostApplicationLifetime` warten, Logs nur auf
|
||||
Console. Nach Phase 2 ist das reine Verdrahtung, weil der Kern bereits GUI-frei ist.
|
||||
|
||||
Zwingend mitzudenken:
|
||||
* **Lizenz ohne Dialog:** Schlüssel aus `PREDICTALYTICS_LICENSE_KEY` bzw. Konfiguration;
|
||||
bei fehlender/ungültiger Lizenz sauberer Abbruch mit Exit-Code ≠ 0 und klarer Logzeile
|
||||
statt eines Dialogs, den niemand sieht.
|
||||
* **Keine Avalonia-Initialisierung** vor der Argumentauswertung — sonst versucht der
|
||||
Prozess auf einem Server ohne X11 zu starten und stirbt, bevor der Schalter greift.
|
||||
* SIGTERM sauber behandeln (`IHostApplicationLifetime`), damit systemd korrekt stoppt.
|
||||
|
||||
**Abnahme:** Shell startet unter Windows **und** Linux-Desktop mit GUI; derselbe Build
|
||||
startet mit `--headless` auf einem Server ohne X11. In beiden Modi: Worker laufen, Web-UI
|
||||
erreichbar, Settings persistieren; im GUI-Modus zusätzlich farbiger Log-View und
|
||||
Lizenzdialog.
|
||||
|
||||
---
|
||||
|
||||
## 5b. WinForms-Host abgelöst ✅ (2026-08-08)
|
||||
|
||||
`main` brachte eine vollständige Deployment-Center-Integration mit (Lizenz, Heartbeat mit
|
||||
DB-Health-Check, Fehler-Stream, UpdateService), die im WinForms-Host entstanden war. Diese
|
||||
wurde nach `feature/linux-net10` gemerged und dabei in den plattformneutralen Kern gezogen.
|
||||
**`Predictalytics.WinFormsHost` ist entfernt** — die Avalonia-Shell ist ab jetzt die einzige
|
||||
Bedienoberfläche.
|
||||
|
||||
### Was nach `Predictalytics.Hosting` wanderte
|
||||
|
||||
| Datei | Anmerkung |
|
||||
|---|---|
|
||||
| `DcConfig`, `DcApiClient`, `DcErrorSink`, `DcHeartbeatService`, `DcUpdateService` | unverändert, waren bereits plattformneutral |
|
||||
| `DcErrorReporter` | `Application.ThreadException` + `MessageBox` entfernt; stattdessen `ReportUiThreadException(Exception)`, das der Host aufruft. Avalonia hängt sich über `Dispatcher.UIThread.UnhandledException` ein. |
|
||||
| `LicenseGuard` + `LicenseSession` | Dialogaufruf und `System.Windows.Forms.Timer` entfernt. Neu: `TryUseCachedAsync()`, `ActivateAsync()` und `StartPeriodicRevalidation(session, onUnusable)` über `PeriodicTimer`. Die Unterscheidung transienter Fehler und die Gnadenfrist-Warnung sind unverändert übernommen. |
|
||||
| Dc-Einstellungen | aus `AppSettings` nach `PredictalyticsOptions`; die Watchdog-Einstellungen entfielen |
|
||||
| `DcErrorSink` in Serilog | in `LoggingSetup` aufgenommen, Startbanner nutzt jetzt `DcConfig.AppVersion` |
|
||||
|
||||
`Deploymentcenter.BuildInfo.targets` wird nun von `Predictalytics.Hosting` importiert;
|
||||
`BuildInfo` entsteht damit im Namensraum `Predictalytics.Hosting`.
|
||||
|
||||
### Was in der Avalonia-Shell nachgezogen wurde
|
||||
|
||||
* Menü **Deployment Center** mit „Nach Updates suchen" und „Lizenzstatus anzeigen"
|
||||
* Einstellungsgruppe **Watchdog → Deployment Center** (Token, Heartbeat, Fehler-Reporting,
|
||||
Update-Kanal als ComboBox; die Server-URL wird nur angezeigt, nicht editiert)
|
||||
* Heartbeat-Snapshot samt `SELECT 1`-DB-Probe, identisch zur WinForms-Fassung
|
||||
* Update-Prüfung beim Start (still) und über das Menü (interaktiv), inklusive
|
||||
`NotifyStopping()` vor dem Start des Update-Agenten
|
||||
* Lizenzfenster nutzt `IsTransient`: bei fehlender Serververbindung wird nicht behauptet,
|
||||
die Lizenz sei ungültig
|
||||
|
||||
### Verifiziert
|
||||
|
||||
```
|
||||
🔑 Lizenz geprüft: valid (HWID 2:win:a765bd47…, Quelle machine-guid)
|
||||
Shell initialisiert. Bereit.
|
||||
🐕 Heartbeat ist aktiviert, aber es fehlt das Deployment-Center-Token
|
||||
Update-Prüfung: v1.0.0 ist aktuell (Kanal prod)
|
||||
```
|
||||
|
||||
`--license-status` meldet gültig samt Offline-Gnadenfrist. Build 0 Fehler, 100 Tests grün.
|
||||
**Noch offen:** Heartbeat und Fehler-Stream laufen erst mit gesetztem DC-Token; die
|
||||
Wartungsaktionen brauchen DB-Zugangsdaten.
|
||||
|
||||
---
|
||||
|
||||
## 5a. Zusatz — Umstellung auf das Deploymentcenter-Lizenzsystem ✅ (2026-08-07)
|
||||
|
||||
> **Überholt durch 5b.** Der hier beschriebene eigene `LicenseGuard` gegen
|
||||
> `Deploymentcenter.Client` war ein Provisorium und wurde durch die reifere Fassung aus
|
||||
> `main` ersetzt (`LicenseSession`, `IsTransient`, `TryGetCachedKey`, Gnadenfrist-Warnung).
|
||||
> Die Begründungen zur Hardware-ID v2 und zum AES-GCM-Cache gelten unverändert.
|
||||
|
||||
**Nicht ursprünglich geplant.** Beim Aktivieren der neuen Lizenz stellte sich heraus, dass
|
||||
Predictalytics noch am alten Lizenzsystem hing (LicenseLabrador, `license.mhdf.de`),
|
||||
während die Lizenzen inzwischen im Deploymentcenter (`dc.mhdf.de`) gepflegt werden.
|
||||
|
||||
Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client abgesichert:
|
||||
Slug `predictalytics`, HWID v2 aus `machine-guid`, Server meldet `valid`.
|
||||
|
||||
### Was sich geändert hat
|
||||
|
||||
| | vorher (LicenseLabrador) | jetzt (Deploymentcenter) |
|
||||
|---|---|---|
|
||||
| Endpunkt | `license.mhdf.de/public/api/v1` | `dc.mhdf.de/api/license/v1` |
|
||||
| Client | `LicenseLabrador.Client` (netstandard2.0) | `Deploymentcenter.Client` (netstandard2.0;net8.0) |
|
||||
| Authentifizierung | Ed25519-Signaturprüfung + Basic Auth im Quelltext | serverseitig, kein Geheimnis im Client |
|
||||
| Hardware-ID | SHA-256 aus MachineGuid **+ Rechnername** | **v2**: `2:<plattform>:<hex>`, ohne Rechnername |
|
||||
| Linux-HWID | `/etc/machine-id`, sonst MAC | `/etc/machine-id` → dbus → DMI-UUID → MAC → Schlüsseldatei |
|
||||
| Container | nicht vorgesehen | `DEPLOYMENTCENTER_HWID` überschreibt |
|
||||
| Cache | DPAPI, **stiller Klartext-Rückfall unter Linux** | AES-256-GCM, HKDF aus HWID+Slug, **kein Klartext-Rückfall**, `0600` unter Linux |
|
||||
| Ablageort | `%APPDATA%` | zusätzlich `XDG_CONFIG_HOME` unter Linux |
|
||||
|
||||
**Damit erledigt sich Analyse-Punkt 3.2 Nr. 2 von selbst:** der stille Klartext-Rückfall
|
||||
des Lizenz-Caches unter Linux existiert im neuen Client nicht mehr. Der Rechnername steckt
|
||||
außerdem nicht mehr im Hash — Umbenennen verbraucht keinen Aktivierungsplatz.
|
||||
|
||||
### Neue CLI-Schalter (`Services/LicenseCli.cs`)
|
||||
|
||||
Aus dem Deploymentcenter-Integrationsleitfaden übernommen, für Server ohne Oberfläche:
|
||||
|
||||
```bash
|
||||
Predictalytics.Shell --license-status # HWID, Cache-Ablage, Serverprüfung
|
||||
Predictalytics.Shell --license-set-key <SCHLÜSSEL> # einmalig aktivieren
|
||||
Predictalytics.Shell --license-deactivate # Aktivierungsplatz freigeben
|
||||
```
|
||||
|
||||
Die Schalter werden **vor** jeder Avalonia-Initialisierung ausgewertet, wie `--headless`.
|
||||
Der Schlüssel wird in der Ausgabe maskiert (`LLAB3-*****-*****-*****-AABB2`).
|
||||
|
||||
### ✅ Nachgetragen: Watchdog ist migriert (Stand 2026-08-23)
|
||||
|
||||
Der hier notierte Rest — `WatchdogHeartbeatService` gegen `watchdog.mhdf.de` mit dem
|
||||
Header `X-Watchdog-Key` — ist erledigt. An seiner Stelle steht
|
||||
`Hosting/DcHeartbeatService.cs`, das gegen `POST /api/watchdog/v1/ping` am
|
||||
Deployment Center meldet (`:105,143`). Damit laufen Lizenz, Watchdog, UpdateService,
|
||||
Fehler-Stream und Bugtracker über dieselbe Anbindung; `watchdog.mhdf.de` wird
|
||||
nicht mehr angesprochen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 4 — Aufräumen und Absichern ✅ **erledigt (2026-08-08)**
|
||||
|
||||
**Abnahme:** Build 0 Fehler, 8 Warnungen (alle vorbestehend).
|
||||
`dotnet test` → **124 bestanden** (vorher 100; 24 neue Tests zur Absicherung).
|
||||
|
||||
### AP 4.1 — Steuerendpunkte werden jetzt tatsächlich geschützt ✅
|
||||
|
||||
Der Befund war schlimmer als in der Analyse notiert: `/api/capabilities` **meldete**
|
||||
`CanControl`/`AuthRequired` aus der Konfiguration, **erzwungen wurde nichts**.
|
||||
`MapPredictalyticsControlEndpoints()` mappte Jobs, Trader-Steuerung und die Dev-Endpunkte
|
||||
bedingungslos und ungeschützt.
|
||||
|
||||
Neu:
|
||||
|
||||
| Bindung | Token | Verhalten |
|
||||
|---|---|---|
|
||||
| localhost | — | Steuerendpunkte aktiv, ungeschützt (das Betriebssystem schützt) |
|
||||
| localhost | gesetzt | Steuerendpunkte aktiv, Token wird verlangt |
|
||||
| **extern** | **fehlt** | **Steuerendpunkte werden gar nicht gemappt**, dazu ein `Log.Error` |
|
||||
| extern | gesetzt | Steuerendpunkte verlangen `X-Predictalytics-Key` |
|
||||
|
||||
Bewusst fail-safe: eine unbedachte Umstellung der Bind-Adresse öffnet nichts stillschweigend.
|
||||
`ApiTokenFilter` vergleicht laufzeitkonstant (`CryptographicOperations.FixedTimeEquals`),
|
||||
damit die Antwortzeit nicht verrät, wie viele Zeichen stimmen.
|
||||
Swagger ist bei externer Bindung abgeschaltet — es legt die gesamte API-Oberfläche offen.
|
||||
`/api/capabilities` meldet nun den tatsächlichen Zustand statt einer Konfigurationsabsicht.
|
||||
|
||||
### AP 4.2 — Globalisierung ✅
|
||||
`RuntimeSetup.UseInvariantCulture()`, aufgerufen als Erstes in beiden Startpfaden.
|
||||
Bewusst **nicht** über `InvariantGlobalization` im Projekt: das änderte zusätzlich die
|
||||
Semantik von Zeichenkettenvergleichen und wäre ein größerer Eingriff als nötig.
|
||||
|
||||
### AP 4.3 — Log-Verzeichnis ✅
|
||||
Standard ist jetzt `%APPDATA%\Predictalytics\logs` bzw. `~/.config/Predictalytics/logs` —
|
||||
neben den Einstellungen statt neben der Programmdatei, die unter Linux üblicherweise nicht
|
||||
beschreibbar ist und beim Update überschrieben würde. Übersteuerbar über
|
||||
`PREDICTALYTICS_LOG_DIR`, etwa auf `/var/log/predictalytics` im systemd-Betrieb.
|
||||
|
||||
### AP 4.4 — Solution und Repo-Kopplung ✅
|
||||
Der WinFormsHost-Sonderfall entfällt, weil das Projekt entfernt ist — die Solution baut
|
||||
unter Linux ohne Bedingung durch. Die relative Referenz auf das Schwester-Repo bleibt
|
||||
(vier Ebenen aufwärts), scheitert jetzt aber mit einer verständlichen Meldung samt
|
||||
Klon-URL statt mit einem Fehler über eine fehlende Projektdatei.
|
||||
|
||||
**Offen:** Git-Submodul oder NuGet-Paket statt Pfadreferenz — für CI und Container relevant,
|
||||
aber eine Entscheidung über Repo-Grenzen hinweg.
|
||||
|
||||
### AP 4.5 — Egress `SourceIp` ✅ (dokumentiert)
|
||||
Code ist korrekt; der Unterschied ist zur Laufzeit. Linux verlangt, dass die Adresse
|
||||
tatsächlich auf einem Interface konfiguriert ist (sonst `EADDRNOTAVAIL`), Windows ist
|
||||
toleranter. Als Kommentar an der Bind-Stelle hinterlegt. Praktischer Test steht in Phase 6 aus.
|
||||
|
||||
### Neue Tests
|
||||
|
||||
* `PredictalyticsOptionsTests` — `IsPubliclyBound` für Loopback-Schreibweisen, Leerwerte
|
||||
und externe Adressen; `WebserverUrl`; SSL-Modus im Connection-String; und dass leere
|
||||
Eingaben bestehende DB-Zugangsdaten **nicht** überschreiben.
|
||||
* `ApiTokenFilterTests` — richtiger Token passiert, fehlender/falscher/zu kurzer/zu langer
|
||||
sowie abweichend geschriebener Token werden abgewiesen.
|
||||
|
||||
### ✅ Ende-zu-Ende verifiziert (2026-08-08, gegen die echte Datenbank)
|
||||
|
||||
Nachgeholt, sobald die DB-Zugangsdaten wieder vorlagen. Geprüft wurde mit einem
|
||||
Wegwerf-Host, der **ausschließlich den Webserver** startet — die Worker blieben aus, weil
|
||||
parallel eine ältere Instanz auf derselben Datenbank läuft. Zusätzlich mit
|
||||
`ApiSettings__ReadOnlyDatabase=true`, sodass weder Migrationen noch Seeding liefen
|
||||
(im Log bestätigt).
|
||||
|
||||
Als „externe" Bindung diente `127.0.0.2`: die Adresse gilt der Prüfung als öffentlich,
|
||||
bleibt aber Loopback — der Test öffnet also nichts im Netz.
|
||||
|
||||
| Fall | `/api/capabilities` | `/api/jobs` | Swagger |
|
||||
|---|---|---|---|
|
||||
| localhost, kein Token | `canControl:true, authRequired:false` | 200 | 200 |
|
||||
| **extern, kein Token** | `canControl:false` | **404** | **404** |
|
||||
| extern, mit Token | `canControl:true, authRequired:true, tokenHeader:"X-Predictalytics-Key"` | ohne Header **401**, falscher Token **401**, richtiger **200** | **404** |
|
||||
|
||||
Damit ist belegt: eine externe Bindung ohne Token stellt die Steuerendpunkte gar nicht
|
||||
erst bereit, und Swagger ist in beiden externen Fällen abgeschaltet.
|
||||
|
||||
**Gleichzeitig verifiziert** (Vorgriff auf Phase 6):
|
||||
|
||||
* Datenbankverbindung steht — 37.803 MB, `SELECT`-Pfad über `GetDatabaseSizeMbAsync`
|
||||
* Web-UI und Assets werden ausgeliefert: `/`, `/css/style.css`, `/js/app.js` je 200
|
||||
* `/api/dashboard` liefert echte Daten: 151.094 Trader, 38.187.946 Trades
|
||||
|
||||
---
|
||||
|
||||
## 6a. Phase 4 — ursprüngliche Planung
|
||||
|
||||
### AP 4.1 — Kestrel-Binding und Absicherung ⚠️
|
||||
`UseUrls($"http://localhost:{port}")` (`EmbeddedWebServer.cs:122`) konfigurierbar machen
|
||||
(`ASPNETCORE_URLS` bzw. Options), CORS-`AllowedOrigins` (heute fest `http://localhost:5000`)
|
||||
mitziehen.
|
||||
|
||||
**Sobald nicht mehr nur auf `localhost` gelauscht wird, entfällt die bisher implizite
|
||||
Absicherung.** `MapPredictalyticsControlEndpoints()` (schreibende Operationen) und Swagger
|
||||
wären dann offen erreichbar. Das vorhandene `CanControl`/`AuthRequired`-Konzept
|
||||
(`Api/ApiConfiguration.cs:66`) muss davor scharf geschaltet werden.
|
||||
|
||||
### AP 4.2 — Globalisierung
|
||||
Im Host-Bootstrap `CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture`
|
||||
setzen, damit Zahlenformatierung unabhängig von der Systemlocale ist.
|
||||
`InvariantGlobalization=true` (spart `libicu` im Container) erst später erwägen — es ändert
|
||||
String-Vergleichssemantik und sollte nicht im selben Sprung passieren.
|
||||
|
||||
### AP 4.3 — Log-Verzeichnis
|
||||
`Path.Combine(AppContext.BaseDirectory, "logs")` (`Program.cs:33`) konfigurierbar machen.
|
||||
Default: Windows `%APPDATA%`, Linux `~/.local/share` bzw. `/var/log/predictalytics`.
|
||||
|
||||
### AP 4.4 — Solution und Repo-Kopplung
|
||||
* `Predictalytics.slnx`: WinFormsHost mit
|
||||
`Condition="$([MSBuild]::IsOSPlatform('Windows'))"` versehen, damit
|
||||
`dotnet build` der Gesamtsolution unter Linux durchläuft.
|
||||
* Die `..\..\..\..\LicenseLabrador\…`-Projektreferenz (4 Ebenen aufwärts) ist für CI und
|
||||
Container fragil → Git-Submodul oder NuGet-Paket.
|
||||
|
||||
### AP 4.5 — Egress-Kanäle prüfen ⚠️
|
||||
`EgressPoolService.cs:70-88` bindet Sockets an eine Quell-IP. Unter Linux muss die IP
|
||||
tatsächlich auf einem Interface konfiguriert sein (sonst `EADDRNOTAVAIL`); Windows ist
|
||||
toleranter. Für nicht-lokale IPs bräuchte es `IP_FREEBIND`/`CAP_NET_ADMIN`.
|
||||
Nur relevant, falls Egress-Kanäle produktiv genutzt werden. Proxy-Kanäle sind unkritisch.
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 5 — Deployment und CI (1–2 PT)
|
||||
|
||||
* `dotnet publish -r linux-x64` (und `win-x64`); bei self-contained an
|
||||
`--self-contained` + Trimming-Verhalten von Avalonia denken.
|
||||
* **Systemabhängigkeiten Linux (GUI):** `libx11-6`, `libice6`, `libsm6`, `libfontconfig1`,
|
||||
`libharfbuzz0b`, Monospace-Fonts (z. B. `fonts-dejavu`). **(GUI-Variante braucht eine
|
||||
Desktop-Session — siehe 0.3.)**
|
||||
* systemd-Unit bzw. Dockerfile (für den Headless-Modus); es existiert bereits ein
|
||||
`J:\Softwareprojekte\dockerCompose` — vermutlich anknüpfbar.
|
||||
* Reverse Proxy (nginx/Caddy) mit TLS vor Kestrel.
|
||||
* **Secrets:** DB-Passwort und Deployment-Center-Token liegen im Klartext in
|
||||
`settings.json` (nicht versioniert). Unter Linux → EnvironmentFile mit `0600` oder
|
||||
Secret-Store.
|
||||
*Erledigt:* Die früher hier genannten Basic-Auth-Zugangsdaten in `LicenseGuard.cs`
|
||||
gibt es nicht mehr — die Datei läuft vollständig über den `Deploymentcenter.Client`.
|
||||
*Offen und dringlicher:* In `src/Predictalytics.Api/appsettings.json` steht ein
|
||||
**echter OpenRouter-API-Key im Klartext**, und diese Datei **ist** versioniert.
|
||||
Schlüssel zurückziehen, neu ausstellen, künftig über `OpenRouter__ApiKey` setzen.
|
||||
* CI: Build + Test auf Linux **und** Windows.
|
||||
* `docs/BETRIEB-Deploymentcenter.md` um den Linux-Betrieb ergänzen.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase 6 — Verifikation (2 PT)
|
||||
|
||||
| # | Prüfung |
|
||||
|---|---|
|
||||
| 8.1 | Worker-Dauerlauf ≥ 24 h unter Linux gegen die **Dev-DB**, Logvergleich mit einem Windows-Lauf |
|
||||
| 8.2 | EF-Migrationen von Null gegen Dev-DB (Weg A: nur EF-9-Regression) |
|
||||
| 8.3 | Zahlenformatierung: Lauf unter `LANG=de_DE.UTF-8` — Volumina/Preise/PnL gegen Windows-Referenz |
|
||||
| 8.4 | Web-UI vollständig durchklicken (case-sensitive Assets, alle Endpunkte) |
|
||||
| 8.5 | Lizenz: Erstaktivierung, Neustart aus Cache, Offline-Gnadenfrist, Revalidierung |
|
||||
| 8.6 | Watchdog-Heartbeats erscheinen unter `watchdog.mhdf.de` |
|
||||
| 8.7 | Egress-Kanäle (falls genutzt) |
|
||||
| 8.8 | Shell unter Windows **und** Linux-Desktop: alle Buttons, Menüs, Settings-Persistenz |
|
||||
| 8.9 | Derselbe Build mit `--headless` auf einem Server ohne X11: Start, Lizenz aus Konfiguration, SIGTERM-Stopp |
|
||||
|
||||
---
|
||||
|
||||
## 9. Zeitplan
|
||||
|
||||
| Phase | Inhalt | PT |
|
||||
|---|---|---|
|
||||
| 0 | Vorarbeiten | 0,5 |
|
||||
| 1 | .NET 10 + Paketmatrix | 1,5–2,5 |
|
||||
| 2 | Hosting-Kern extrahieren | 1,5–2 |
|
||||
| 3 | Avalonia-Shell **inkl. Headless-Modus (AP 3.4)** | 4,5–6,5 |
|
||||
| 4 | Aufräumen, Binding, Absicherung | 1–1,5 |
|
||||
| 5 | Deployment + CI (Desktop **und** systemd/Container) | 1,5–2,5 |
|
||||
| 6 | Verifikation (beide Betriebsarten) | 2 |
|
||||
| | **Summe** | **12,5–17,5 PT** |
|
||||
|
||||
**≈ 2,5–3,5 Wochen fokussiert.**
|
||||
|
||||
Phasen 0–2 sind risikoarm und liefern bereits Wert (aufgeräumte Architektur, aktuelles
|
||||
Framework). Phase 3 ist der Hauptaufwand. Die Phasen sind strikt sequenziell — jede baut
|
||||
auf der vorigen auf; einzig AP 5 (Deployment) kann parallel zu Phase 3 vorbereitet werden.
|
||||
|
||||
---
|
||||
|
||||
## 10. Risiken
|
||||
|
||||
| Risiko | Eintritt | Wirkung | Gegenmaßnahme |
|
||||
|---|---|---|---|
|
||||
| **Pomelo hat keinen EF-Core-10-Provider** | **sicher** | blockiert EF Core 10 | ✅ gelöst durch Weg A: EF Core 9 auf net10.0 |
|
||||
| Swashbuckle 6.9 → 10.x mit Brüchen | wahrscheinlich | halber bis ganzer Tag | ggf. auf eingebautes `AddOpenApi()` wechseln |
|
||||
| PropertyGrid-Ersatz wird aufwendiger als geschätzt | mittel | +1 PT | handgeschriebene View, 15 Properties sind überschaubar |
|
||||
| Avalonia-GUI läuft auf dem Linux-Ziel nicht (kein Desktop) | sicher auf Servern | Shell unbrauchbar | ✅ abgedeckt durch AP 3.4 (verbindlich) |
|
||||
| `Avalonia.Diagnostics` fehlt für 12.x | sicher | nur Komfort | ohne DevTools arbeiten oder 11.3.18 in Debug |
|
||||
| **Lizenzaktivierung headless** | **sicher (AP 3.4)** | mittel | koppelt an laufende HardwareID-Arbeit — früh abstimmen |
|
||||
| Egress `SourceIp` unter Linux | niedrig | mittel | AP 4.5, früh testen |
|
||||
| Regression in der Datenschicht | niedrig (Weg A) | hoch | ausschließlich Dev-DB, nie Prod |
|
||||
|
||||
---
|
||||
|
||||
## 11. Nächster Schritt
|
||||
|
||||
**Stand 2026-08-23: Phase 0 bis 4 sind abgeschlossen** (jeweils oben mit ✅ vermerkt),
|
||||
ebenso der Zusatz 5a (Deploymentcenter-Lizenzsystem) und 5b (WinForms-Host abgelöst).
|
||||
Der ursprüngliche Text an dieser Stelle empfahl noch, mit Phase 0 zu beginnen — das ist
|
||||
überholt und deshalb ersetzt.
|
||||
|
||||
**Offen sind Phase 5 und Phase 6:**
|
||||
|
||||
1. **Phase 5 — Deployment und CI** (Abschnitt 7). Nichts davon ist bisher angefasst: kein
|
||||
`dotnet publish -r linux-x64`, keine systemd-Unit, kein Dockerfile, kein Reverse Proxy,
|
||||
keine CI. Das ist der eigentliche verbleibende Arbeitsblock.
|
||||
2. **Phase 6 — Verifikation** (Abschnitt 8, Punkte 8.1–8.9). Der Dauerlauf unter Linux
|
||||
(8.1) und die Prüfung unter `LANG=de_DE.UTF-8` (8.3) stehen aus. **Bisher wurde die
|
||||
Anwendung noch auf keinem Linux-System ausgeführt** — die Portierung ist zwar
|
||||
vollständig gebaut, aber unter Linux unbewiesen.
|
||||
3. Unabhängig davon und vorgelagert: den **OpenRouter-API-Key** aus
|
||||
`src/Predictalytics.Api/appsettings.json` zurückziehen und neu ausstellen (siehe
|
||||
Abschnitt 7, Secrets).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Archiv — abgelöste Plandokumente
|
||||
|
||||
Diese Dokumente wurden am **2026-08-23** hierher verschoben. Sie werden **nicht mehr
|
||||
gepflegt**. Was von ihnen noch offen war, steht gebündelt in [`ROADMAP.md`](../../ROADMAP.md);
|
||||
der Ist-Stand des Projekts in [`STATUS.md`](../../STATUS.md).
|
||||
|
||||
**Wozu sie trotzdem da sind:** Die Roadmap nennt jeden offenen Punkt knapp. Die Herleitung —
|
||||
warum etwas so entschieden wurde, welche Messungen dahinterstehen, welche Alternativen
|
||||
verworfen wurden — steht hier. Wer einen Roadmap-Punkt umsetzt, findet hier den Kontext.
|
||||
|
||||
**Vorsicht bei der Lektüre:** Der Text unterhalb des jeweiligen Statusblocks beschreibt den
|
||||
Stand seiner Entstehungszeit. Test-Baselines, Dateipfade und „so ist es heute"-Aussagen sind
|
||||
teils überholt. Immer gegen den Code prüfen, nie direkt übernehmen.
|
||||
|
||||
| Dokument | Stand | Was drinsteht |
|
||||
|---|---|---|
|
||||
| [`UMSETZUNGSPLAN.md`](UMSETZUNGSPLAN.md) | 2026-07-01, abgehakt 2026-08-23 | Phase 1 (Kernanalytik: PnL-Engine, Win-Rate, Deep-Dive, Copytrading-Score) und Phase 2 (Migrationen, Scoring-Entkopplung, Bugliste, Tests) sowie das Speicherplatz-Konzept. Bis auf A4 und B2 umgesetzt. |
|
||||
| [`FIXPLAN-DONE.md`](FIXPLAN-DONE.md) | 2026-07-09 | Vorgängerfassung des Fix-Plans, vollständig abgearbeitet. Reine Historie. |
|
||||
| [`FIXPLAN-TODO.md`](FIXPLAN-TODO.md) | 2026-07-09/11/13 | Teil D (Merkmals-Tags, HF-Trader-Tiering mit `IngestMode`) und Teil F (Kategorie-Erkennung: kanonische Tags, Rausch-Filter, Backfill). Umgesetzt bis auf F5 und zwei Tests. |
|
||||
| [`FIXPLAN-G-Speicher.md`](FIXPLAN-G-Speicher.md) | 2026-07-17 | Speicher-Governor, Retention für aggregierte Trader, `UsdcSize`/`OutcomeIndex`, Per-Kategorie-Edge. Vollständig umgesetzt; der Anhang mit den einmaligen SQL-Optimierungen ist offen. |
|
||||
| [`FIXPLAN-UI-Ranglisten.md`](FIXPLAN-UI-Ranglisten.md) | 2026-08-03 | Web-UI: Showcase-Sektionen, serverseitige Sortierung und Filterung. Vollständig umgesetzt bis auf die Sortierpfeile. |
|
||||
| [`PLAN-Linux-Portierung.md`](PLAN-Linux-Portierung.md) | 2026-08-06 bis -08 | Der Portierungsplan mit allen Entscheidungen und Abnahmevermerken. **Die ausführlichste Quelle für Roadmap-Stufe 2** — Abschnitt 7 (Deployment) und 8 (Verifikationsmatrix) sind dort detaillierter als in der Roadmap. |
|
||||
| [`ANALYSE-Linux-Portierung.md`](ANALYSE-Linux-Portierung.md) | 2026-08-06 | Die Bestandsaufnahme, die dem Portierungsplan vorausging: was am WinForms-Host hing, welche Pakete windows-spezifisch waren. Historisch. |
|
||||
| [`PLAN-Architektur-WebUI-Backend.md`](PLAN-Architektur-WebUI-Backend.md) | 2026-07 | Drei-Schichten-Konzept Core/Lokal/Public als Vorbereitung einer Monetarisierung. Phase 0–2 sind im Zuge der Portierung erledigt; Phase 3–4 sind Roadmap-Stufe 6. |
|
||||
| [`PLAN-DatenIngest-Skalierung.md`](PLAN-DatenIngest-Skalierung.md) | 2026-07 | Rate-Limits, Egress-Kanäle, Blockchain als Bulk-Quelle. Egress-Kanäle und Ingest-Tiering sind umgesetzt; der Rest ist Roadmap-Stufe 5. |
|
||||
@@ -0,0 +1,236 @@
|
||||
# Predictalytics – Umsetzungsplan (Phase 1 & 2 + Storage-Optimierung)
|
||||
|
||||
> ## Stand 2026-08-23 — Phase 1 und 2 sind bis auf zwei Punkte umgesetzt
|
||||
>
|
||||
> Die Häkchen waren bis dahin nie nachgezogen worden: 85 Punkte standen auf offen,
|
||||
> obwohl der Code sie längst enthielt. Am 2026-08-23 abschnittsweise gegen den Code
|
||||
> geprüft und gesetzt. **Belege:**
|
||||
>
|
||||
> | | Belegt durch |
|
||||
> |---|---|
|
||||
> | **A1** PnL-Engine | `Infrastructure/Services/PositionPnLEngine.cs` — alle `TradeSide`-Zweige einzeln behandelt (`:157-217`), `Split`/`Merge` neutral, unrealisiert aus `CurrentPrice − AvgCost` (`:266`), Gesamt-PnL `:290`. Entity `Domain/Entities/TraderPosition.cs`. Tests: `PositionPnLEngineTests.cs` |
|
||||
> | **A2** Win-Rate | `CalculateMarketWinRates` (`PositionPnLEngine.cs:381`) — marktbasiert, kein `return 0` mehr |
|
||||
> | **A3** Deep-Dive | `MarketOutcomePriceSnapshot` existiert und wird ausgewertet (`AnalyticsService.cs:88,390`). Die `50` ist jetzt **Rückfallwert bei fehlender Preishistorie** (`:456,457,474`), kein fester Neutralwert mehr |
|
||||
> | **A5** Copytrading-Score | `Infrastructure/Services/CopytradingEstimator.cs` — **abweichend umgesetzt und besser als geplant**: statt geschätztem Liquiditäts-Fit misst der Score den echten Alpha-Verfall gegen das Markt-Tape (Slippage bei 10 s Folgelatenz, 70 %) plus Sizing-Konsistenz (30 %). Frequenz/Konzentration deckt stattdessen `IngestMode` + Trait `not_copyable_hf` ab. **Track-Record-Länge fließt nicht in den Score ein** |
|
||||
> | **B1/B4** | 32 EF-Migrationen; Testprojekt existiert, **126 grün + 1 Skip** |
|
||||
> | **B3** | `ScoringService.RecalculateAllScoresAsync:127` rechnet nur bei `CalculatedAt < LastTradesUpdatedAt` neu, Rank-Update gebatcht; eigener `ScoringAndAlertsWorker` mit 15-Minuten-Takt |
|
||||
> | **B5** | `LoggingSetup.cs:90-133` — `retainedFileCountLimit` und `fileSizeLimitBytes` gesetzt |
|
||||
> | **B6** | Alle fünf Bugs behoben: geschlossene Märkte nur noch **1×/Tag** (`MarketSyncWorker.cs:52`), Watchlist von der Löschung ausgenommen (`TraderRepository.cs:137`), `GetKnownPlatformTradeIdsAsync` filtert auf die Batch-IDs (`TradeRepository.cs:158`), `_syncSemaphore` in `AddOrUpdateAsync` (`MarketRepository.cs:31`), `break` durch einen Filter ersetzt (`TradeHistoryWorker.cs:200`) |
|
||||
> | **C1/C2** | `TradeRetentionWorker` + `Application/Services/StorageGovernor.cs` (budgetabhängiges Fenster), Burst-Kompaktierung ab `TradeRetentionWorker.cs:148` |
|
||||
>
|
||||
> ### Was wirklich offen ist
|
||||
>
|
||||
> 1. **A4 — Strategie-Klassifikation** (Zeilen 47–54). Der einzige größere unerledigte
|
||||
> Block aus Phase 1. `StrategyType` entsteht weiterhin aus genau zwei Signalen
|
||||
> (`AnalyticsService.cs:418` und, **dupliziert**, `ScoringService.cs:89`):
|
||||
> `avgSize > 10000 → Whale`, `hedgingRate > 30 → Hedger`, sonst `Bot`/`Unknown`.
|
||||
> Genau der Zustand, den A4 beheben sollte — der Großteil der Trader landet
|
||||
> strukturell auf `Unknown`. Die Bausteine dafür liegen bereits fertig herum
|
||||
> (`StrategyMetricsCalculator`, `TraderTraitCalculator`, `FingerprintSnapshotService`),
|
||||
> sie speisen die Klassifikation nur nicht. Die Doppelung gehört beim Anfassen
|
||||
> zusammengeführt.
|
||||
> 2. **B2 — Secrets** (Zeilen 84–86). Nicht mehr „kein akuter Risikofall": in
|
||||
> `src/Predictalytics.Api/appsettings.json` steht ein **echter OpenRouter-API-Key im
|
||||
> Klartext**, versioniert seit Commit `7045002`. Siehe Warnung unten.
|
||||
> 3. **C3** (148–153) und **D1/D2** (165–173) sind DB-Arbeiten auf SQL-Ebene bzw. die
|
||||
> Entscheidung über den Datenbank-Neustart — beides liegt beim Nutzer, nicht im Code.
|
||||
> 4. **C1**, Zeile 137: Stichprobentest, wie weit `/activity` zurückreicht. Eine manuelle
|
||||
> Prüfung, die sich im Code nicht belegen lässt — bewusst offen gelassen.
|
||||
>
|
||||
> ### ⚠️ Sicherheit — Handlungsbedarf
|
||||
>
|
||||
> `src/Predictalytics.Api/appsettings.json` enthält den OpenRouter-API-Key im Klartext
|
||||
> und liegt so im Git-Verlauf und auf dem Gitea-Server. **Den Schlüssel bei OpenRouter
|
||||
> zurückziehen und neu ausstellen** — ihn nur aus der Datei zu löschen hilft nicht, die
|
||||
> Historie behält ihn. Danach über die Umgebungsvariable `OpenRouter__ApiKey` setzen;
|
||||
> `WebApplication.CreateBuilder` liest Umgebungsvariablen bereits mit Vorrang vor
|
||||
> `appsettings.json`, dafür ist keine Codeänderung nötig.
|
||||
|
||||
> **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
|
||||
- [x] 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)
|
||||
- [x] Neue Domain-Struktur `TraderPosition` (TraderId, MarketOutcomeId, SharesHeld, AvgCost, RealizedPnl, zuletzt aktualisiert) — inkrementell fortschreibbar statt bei jeder Berechnung die komplette Trade-Historie neu zu scannen
|
||||
- [x] Buchungslogik je `TradeSide` sauber definieren:
|
||||
- [x] `Buy`: Shares += Size, AvgCost neu gewichten
|
||||
- [x] `Sell`: RealizedPnl += Size × (Price − AvgCost), Shares −= Size
|
||||
- [x] `Redeem` (Marktauflösung): RealizedPnl += verbleibende Shares × (1 oder 0 je nach Gewinn-Outcome − AvgCost), Shares = 0
|
||||
- [x] `Split` / `Merge`: als neutrale Positionsumwandlung behandeln (kein PnL-Effekt), nicht wie `Sell`
|
||||
- [x] `AddLiquidity` / `RemoveLiquidity`: getrennt von Trading-PnL betrachten (eigene Kategorie, fließt nicht in "Trading-Skill"-Bewertung ein)
|
||||
- [x] Unrealisierten PnL für offene Positionen berechnen: `Shares × (MarketOutcome.CurrentPrice − AvgCost)`
|
||||
- [x] Gesamt-PnL = realisiert + unrealisiert (ersetzt `OverallPnL`, `PnL30d/7d/24h` Felder in `TraderAnalytics`)
|
||||
- [x] 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)
|
||||
- [x] `CalculatePnL`-Bug aus Abschnitt 0 im Zuge dessen mit erledigen
|
||||
|
||||
### A2. Win-Rate korrekt berechnen
|
||||
- [x] 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
|
||||
- [x] `WinRate = Anzahl gewonnener Märkte / Anzahl abgeschlossener Märkte` (offene Positionen zählen nicht mit)
|
||||
- [x] Placeholder `return 0;` in `TraderAnalyticsWorker.CalculateWinRate` ersetzen
|
||||
|
||||
### A3. Deep-Dive-Kennzahlen mit echten Werten füllen
|
||||
- [x] **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.
|
||||
- [x] Prüfen, ob Polymarkets CLOB-API einen Preishistorie-Endpoint (`/prices-history`) hergibt, den wir zum Backfill nutzen können
|
||||
- [x] Falls ja: neue Tabelle `MarketOutcomePriceSnapshot` (MarketOutcomeId, Timestamp, Price) einführen, periodisch befüllt (z.B. durch bestehenden `MarketSyncWorker`/`MarketHistoryWorker` erweitern)
|
||||
- [x] `AvgHoldDurationHours` echt berechnen: gewichtete Haltedauer zwischen Einstieg (Buy-Zeitpunkte, gewichtet nach Größe) und Ausstieg (Sell/Redeem) pro Position
|
||||
- [x] `EntryQuality`: Einstiegspreis im Vergleich zur nachfolgenden Preisentwicklung (z.B. Perzentil des Einstiegspreises innerhalb eines Zeitfensters danach)
|
||||
- [x] `ExitQuality`: analog für Ausstiegspreis
|
||||
- [x] `TimingAccuracy`: z.B. Anteil der Trades, die kurz vor einer für den Trader günstigen Preisbewegung platziert wurden
|
||||
- [x] 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)
|
||||
- [x] Neue Metrik-Dimension definieren, unabhängig vom bestehenden `PriorityScore`:
|
||||
- [x] **Liquiditäts-Fit**: durchschnittliche Positionsgröße im Verhältnis zur Marktliquidität/zum Volumen zum Handelszeitpunkt (Slippage-Risiko für Nachahmer)
|
||||
- [x] **Reaktionsfenster**: wie viel Zeit bliebe einem Copytrader realistisch zum Nachziehen (Trader mit Sekunden/Millisekunden-Kadenz sind nicht kopierbar)
|
||||
- [x] **Frequenz/Konzentration**: sehr hochfrequente/bot-artige Trader senken den Score automatisch
|
||||
- [x] **Track-Record-Länge & Konsistenz**: mehr abgeschlossene Märkte mit konsistent positivem PnL = höheres Vertrauen
|
||||
- [x] Kombinierten `CopytradingScore` (0–100) berechnen und persistieren (neues Feld auf `TraderScore` oder eigene Entity `TraderCopytradingProfile`)
|
||||
- [x] 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
|
||||
- [x] Erledigt sich durch A1, hier nur als Häkchen zur Nachverfolgung
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] `PollingWorker` soll **nicht** bei jedem 60-Sekunden-Zyklus alle Trader neu bewerten
|
||||
- [x] Nur Trader neu bewerten, die seit letzter Berechnung neue Trades bekommen haben (Dirty-Flag oder Vergleich `LastTradesUpdatedAt` vs. `TraderScore.CalculatedAt`)
|
||||
- [x] N+1-Datenbankzugriffe in `ScoringService.RecalculateAllScoresAsync` durch Batch-Queries ersetzen
|
||||
- [x] Eigenes Intervall für volle Neuberechnung/Ranking (z.B. alle 15 Min) getrennt vom reinen Trade-Polling (60s)
|
||||
|
||||
### B6. Weitere gefundene Bugs/Performance-Probleme (Review-Pass 2026-07-01)
|
||||
- [x] **`MarketSyncWorker` re-synct alle 30 Minuten ALLE Märkte, inkl. `includeClosed=true`, von Offset 0** ([MarketSyncWorker.cs](../../src/Predictalytics.Worker/Services/MarketSyncWorker.cs)) — holt damit bei jedem Zyklus jeden jemals geschlossenen Polymarket-Markt erneut komplett durch. Wächst unbegrenzt mit der Zeit, echtes Risiko für Rate-Limiting/Sperrung. Sollte auf: aktive Märkte häufig, geschlossene Märkte selten/inkrementell (z.B. nur kürzlich geschlossene, nicht der komplette Bestand) umgestellt werden
|
||||
- [x] **Watchlisted Trader nicht von Auto-Löschung ausgeschlossen** (`TraderRepository.GetTradersForCleanupAsync` / `TraderCleanupWorker`) — da `Trades`/`TraderScore`/`WatchlistEntries` per Cascade am Trader hängen, könnte ein manuell beobachteter Trader nach 1 Jahr Inaktivität (oder bei kurzzeitigem API-Fehler) unbemerkt komplett gelöscht werden. Watchlist-Einträge sollten von der Cleanup-Query ausgenommen werden
|
||||
- [x] **`TradeRepository.GetKnownPlatformTradeIdsAsync` lädt die komplette Trade-ID-Historie eines Traders ins RAM**, nur um eine kleine neu geholte Charge (~100-1000 Trades) zu deduplizieren — aufgerufen alle 60s (`PollingWorker`) bzw. alle 12h (`TradeHistoryWorker`) pro Trader. Wird mit wachsender Trade-Zahl immer teurer. Fix: nur `WHERE PlatformTradeId IN (<geholte Batch-IDs>)` abfragen statt der gesamten Historie
|
||||
- [x] **Race Condition in `MarketRepository.AddOrUpdateAsync`** (im Gegensatz zu `AddOrUpdateRangeAsync` ohne Locking) — `TradeHistoryWorker` verarbeitet bis zu 5 Trader parallel (`Parallel.ForEachAsync`); referenzieren zwei gleichzeitig denselben noch unbekannten Markt, prüfen beide unabhängig "existiert nicht" und einer crasht beim `Add` mit Unique-Constraint-Verletzung (wird geloggt, Trader-Sync für den Zyklus bricht ab, nächster Zyklus heilt es meist). Fix: gleiches Locking-Muster wie `AddOrUpdateRangeAsync` verwenden, oder Insert-Konflikt sauber abfangen/retry
|
||||
- [x] **`TradeHistoryWorker` bricht die Trade-Verarbeitung beim ersten bekannten Trade ab** (`if (!isInitial) break;`), verlässt sich also darauf, dass die API immer streng neueste-zuerst liefert. `PollingWorker`s äquivalente Schleife macht das NICHT. Sollte angeglichen werden — der Performance-Gewinn ist gering gegenüber dem Risiko einer stillen Datenlücke, falls die Annahme mal nicht stimmt
|
||||
|
||||
### B4. Testabdeckung für die kritische Logik
|
||||
- [x] Neues Testprojekt (z.B. `Predictalytics.Application.Tests`) anlegen — aktuell existiert **kein einziges** Testprojekt
|
||||
- [x] Unit-Tests für die neue PnL-Engine (A1) — insbesondere Grenzfälle: nur offene Position, nur geschlossene Position, Split/Merge, Redeem-Verlust vs. -Gewinn
|
||||
- [x] Unit-Tests für Win-Rate (A2)
|
||||
- [x] Unit-Tests für Deep-Dive-Kennzahlen (A3) und Copytrading-Score (A5)
|
||||
- [x] Diese Tests idealerweise **parallel zu A1–A5** schreiben, nicht erst am Ende nachziehen
|
||||
|
||||
### B5. Kleine verwandte Aufräumarbeit
|
||||
- [x] 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")
|
||||
- [x] Konfigurierbares Retention-Fenster einführen (Default-Vorschlag: 3–6 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
|
||||
- [x] 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
|
||||
- [x] 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)
|
||||
- [x] 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
|
||||
- [x] 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")
|
||||
- [x] 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"
|
||||
- [x] Mindestlänge für Kompaktierung festlegen (z.B. erst ab 20+ Trades im Burst lohnt sich das)
|
||||
- [x] Bestehende Bot-Heuristik (`intervals.Average() < 10` in `AnalyticsService`) als Ausgangspunkt wiederverwenden/verallgemeinern statt eine zweite, unabhängige Definition einzuführen
|
||||
- [x] Aggregat-Datensatz pro Burst: Anzahl Trades, Summe Size, Summe Amount, Min-Preis, Max-Preis, **VWAP** (nicht einfacher Durchschnitt)
|
||||
- [x] 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`
|
||||
- [x] **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. 30–50% 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
|
||||
- [x] 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.)
|
||||
Reference in New Issue
Block a user