diff --git a/UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md b/UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md new file mode 100644 index 0000000..d16f254 --- /dev/null +++ b/UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md @@ -0,0 +1,314 @@ +# Umsetzungsplan: Copytrading-Modul — Rentabilitäts-Verbesserungen + +> Stand: 2026-07-06 +> Ziel: Bekannte Verlustquellen im Copytrading-Modul beseitigen und die +> Rentabilität durch datengetriebene Trader-Auswahl, echte Fill-Daten und +> besseres SELL-Handling steigern. +> Reihenfolge: **Dieser Plan zuerst.** Phase 1 (Marktdaten-Fundament) ist +> Voraussetzung für die Strategiemodule MarketMaking und BundleArbitrage. + +--- + +## 0. Kontext & Hintergrund (für die Umsetzung ohne Vorwissen) + +Das Copytrading-Modul (`src/PolyTrader.Modules.CopyTrading/`) kopiert Trades von +Master-Tradern auf Polymarket. Signalkette: + +1. `AlchemyWebsocketService` erkennt On-Chain-Events der Master-Wallets (WSS). +2. `TraderMonitorService.TriggerFastBlockchainPoll()` parst die Transaktion direkt + (Fast Track, ~3–4 s hinter dem Master) oder fällt auf Data-API-Polling zurück. +3. Signale (`CopySignal`) laufen über einen Channel in die `CopyTradingEngine`. +4. Die Engine prüft Risiko-Limits (`CopyTradingAccountSettings`: PerMarketLimit, + PerMasterLimit, Zeitfenster-Limits, MaxBuyPrice) und platziert CLOB-Orders + über `PolymarketClobClient` (Core). + +**Historischer Kontext (wichtig!):** Eine Verlustanalyse im April 2026 +(`agentspace/prompts/AnalyzingOvernightTradingLosses.md`) hat als Hauptursache +für Overnight-Verluste identifiziert, dass der Bot SELLs der Master mit +Market-Orders ins leergeräumte Orderbuch kopiert und so zur „Exit-Liquidity" +wird (Beispiel: Entry 0.51, Master-Exit 0.99, unser Exit 0.49). Der damalige +Fix (GTD-Limit-Sells) ist **im aktuellen Modul-Code nicht mehr vorhanden** — +vermutlich bei der Modularisierung verloren gegangen. + +**Seit März 2026 erhebt Polymarket Taker-Fees** (Sports ~0,75 %, Politik/Finanzen +~1,0 %, Krypto ~1,8 %, am 50-¢-Preis am höchsten, Richtung 1 ¢/99 ¢ abnehmend; +Maker zahlen nichts und erhalten Rebates). Der Bot ist heute fast immer Taker. +Quelle: https://docs.polymarket.com/trading/fees — **bei Umsetzung aktuellen +Stand verifizieren.** + +### Leitplanken (gelten für alle Phasen) + +1. `.agents/rules/clob.md` beachten: Änderungen an der CLOB-Integration sind + hochkritisch. Vor jeder Änderung Backup/Commit der alten Version, jede + Änderung mehrfach prüfen. +2. Jede Phase lässt die App baubar und lauffähig zurück (Debug-Build grün). +3. Entscheidungslogik als testbare, pure Funktionen extrahieren und in + `PolyTrader.Tests` (xUnit, existiert bereits) abdecken. +4. Kein Livegang einer Phase ohne mehrtägige Beobachtung auf dem Server. + +--- + +## Phase 0 — Sofortmaßnahmen: Blutung stoppen + +### 0.1 🔴 SELL-Exit-Liquidity-Regression beheben (höchste Priorität) + +**Befund:** In `CopyTradingEngine.cs` (Live-SELL-Pfad, aktuell ~Zeile 694–735) +werden SELLs als `"MARKET"` mit Fallback-Limit `0.01m` gesendet: + +```csharp +decimal sellLimit = 0.01m; // Market Order Fallback Limit +... +var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side, + expectedUsdc, sellLimit, "MARKET", _state.DebugOrderPayloadLog, isNegRisk); +``` + +Das ist exakt das Verhalten, das die April-Verluste verursacht hat. + +**Ziel-Design: Eskalationsleiter statt Market-Order** + +1. Referenzpreis = `signal.Price` (Exit-Preis des Masters). +2. Erste Order: GTD-Limit. + - HF-Trader (`trader.Category == "HF"`): `signal.Price - 0.005m`. + - Sonst: `signal.Price * (1 - settings.MaxPriceDifference / 100m)`. +3. Neuer Setting-Wert `SellFloorPct` in `CopyTradingAccountSettings` + (Default z. B. 15 %): absolute Untergrenze = `signal.Price * (1 - SellFloorPct/100)`. +4. Hintergrund-Loop (Erweiterung von `CleanupStaleOpenOrdersAsync` in + `TraderMonitorService` oder eigener Loop): Order nach T Sekunden ohne Fill + (HF: ~20 s, sonst: ~120 s) canceln und eine Stufe tiefer neu platzieren + (Schrittweite z. B. 2 ¢ oder 3 % relativ), bis zum Floor. +5. Floor erreicht und kein Fill → Position halten, **Threema-Benachrichtigung** + senden (`ThreemaService` im Core existiert) und Position als „ExitPending" + markieren. +6. Position darf **nicht mehr optimistisch** aus `account.OpenPositions` + entfernt werden. Stattdessen Flag `ExitPending` (neues Property auf + `Position` oder Tracking-Dictionary im `CopyTradingState`), damit Limits + weiterhin korrekt rechnen und kein Doppel-SELL entsteht. Entfernen erst, + wenn der Fill über Sync/User-Channel (Phase 1) bestätigt ist. + +**Preis-/Stufenlogik als pure statische Funktion** implementieren (z. B. +`SellLadder.NextPrice(referencePrice, step, floor, attempt)`) und mit +Unit-Tests abdecken. + +**Akzeptanzkriterien:** +- Kein Code-Pfad sendet mehr `"MARKET"`-SELLs mit 0.01-Limit. +- Unit-Tests für Ladder-Preise (HF/normal, Floor-Clamping, 0.01/0.99-Grenzen). +- Log zeigt pro SELL: Referenzpreis, gewähltes Limit, Stufe. + +### 0.2 Fee-Modell einführen + +1. Fee-Rate je Markt beschaffen: Die CLOB-/Gamma-API liefert Fee-Informationen + am Markt-Objekt (Feldname bei Umsetzung anhand + https://docs.polymarket.com/trading/fees verifizieren, z. B. `fee_rate_bps`). + Fallback: statische Kategorie-Tabelle (Sports 0.75 %, Politics/Finance 1.0 %, + Crypto 1.8 %, Geopolitics 0 %). +2. `MarketData` (Core) um `TakerFeeBps` erweitern (EF-Migration Core), + Befüllung über `MarketSyncService` bzw. beim Markt-Fetch. +3. Risk-Check in `CopyTradingEngine`: erwartete Fee vom verfügbaren Edge + abziehen; Mikro-Trades, deren Fee den erwartbaren Gewinn frisst, verwerfen + (Logging mit Begründung wie bei den bestehenden Checks). +4. PnL-Berechnung (Demo **und** Live-Anzeige) um Fees korrigieren. + +**Akzeptanz:** Fee erscheint im TradeReasoning-Log jedes BUY; Demo-PnL weist +Fees aus. + +### 0.3 `ProfitTarget` implementieren oder entfernen + +`CopyTradingAccountSettings.ProfitTarget` (Default 50.0) existiert in Settings, +DB und UI, wird aber **nirgends ausgewertet** (toter Knopf). + +**Empfehlung: implementieren** als optionaler Take-Profit: +- Semantik: `0` = deaktiviert; sonst Prozent-Gewinnschwelle. +- Prüfung im 30-s-Live-Sync (`PollLiveAccountsAsync`): wenn + `CurrentPrice >= EntryPrice * (1 + ProfitTarget/100)` → Verkauf über die + Eskalationsleiter aus 0.1 (Startlimit = CurrentPrice), ExitReason + `"Profit Target"`. +- Zusammenspiel mit `PreRedeemLimit` beachten (beide können feuern — + PreRedeem hat Vorrang, da näher an 1.00). + +### 0.4 Kleinere Konsistenz-Fixes + +1. **20-Sekunden-Spam-Blockade** (`PendingOrderTimestamps`-Check am Anfang des + SELL-Pfads): blockiert aktuell auch legitime SELLs, wenn der Master < 20 s + nach dem Kauf aussteigt. Fix: Blockade nur für gleichgerichtete Orders + (BUY nach BUY), SELL nach BUY zulassen. +2. **`_state.GlobalPnl`**: wird in `PollLiveAccountsAsync`-Close-Pfaden addiert, + in `PollClosedAccountsAsync` nicht → Anzeige driftet. Vereinheitlichen. +3. **Demo-`ClosedTrade` ohne `TokenId`**: Im Demo-SELL-Pfad wird `TokenId` nicht + gesetzt (Preload von `_processedClosures` filtert auf `TokenId`). Setzen. + +--- + +## Phase 1 — Marktdaten-Fundament (Core-Infrastruktur) + +> Diese Phase gehört in **PolyTrader.Core** (`src/PolyTrader.Core/Streaming/`), +> nicht ins Modul — MarketMaking- und BundleArbitrage-Modul (separate Pläne) +> setzen sie voraus. + +### 1.1 CLOB User-Channel (echte Fills in Echtzeit) + +Polymarket bietet einen authentifizierten WSS-User-Channel, der Order-Events +(Platzierung, Teil-/Voll-Fill, Cancel) der eigenen Accounts pusht. +Endpoint/Protokoll bei Umsetzung verifizieren: +https://docs.polymarket.com (CLOB WSS, `user` channel; Auth via API-Key/ +Secret/Passphrase — liegen je Account in `AccountState`). + +Neuer Core-Service `ClobUserChannelService : BackgroundService`: +- Verbindet pro Live-Account, Auto-Reconnect mit Backoff (Muster von + `AlchemyWssClient` übernehmen). +- Publiziert Fill-Events intern (Event oder Channel), z. B. + `record OrderFillEvent(int AccountId, string TokenId, string OrderId, string Side, decimal Price, decimal Size, DateTime Ts)`. + +Konsumenten im Copytrading-Modul: +- `Position.EntryPrice`/`Size` mit **echten Fill-Daten** aktualisieren + (heute: Limit-Preis als EntryPrice, Korrektur erst im 30-s-REST-Sync). +- SELL-Eskalationsleiter (Phase 0.1): Fill-Bestätigung beendet die Leiter. +- Neue Tabelle `ct_fill_log` (EF-Migration im Modul): SignalPrice, OrderPrice, + FillPrice, Latenz (Signal→Fill in ms), TraderId, AccountId, TokenId, Side. + → Grundlage für Slippage-Statistik in Phase 3. + +### 1.2 CLOB Market-Channel (Orderbücher live) + +Neuer Core-Service `ClobMarketDataService`: +- Abonniert den öffentlichen `market`-Channel für eine dynamische Token-Liste + (Subscribe/Unsubscribe zur Laufzeit). +- Hält `OrderBookCache` (Best-Bid/Ask, Tiefe der obersten N Level, Timestamp). +- Interface für Konsumenten: `IOrderBookProvider.TryGetBook(tokenId, maxAgeMs)`. +- REST-Fallback `GET /book` über `PolymarketClobClient`, wenn kein Stream aktiv. + +**Hinweis:** Im Modul existiert bereits ein `PolymarketWssClient` (Auto-Redeem). +Nicht verschieben/umbauen (Regression-Risiko), sondern den neuen Core-Service +parallel aufbauen; spätere Konsolidierung als separater Schritt. + +### 1.3 Pre-Trade-Orderbuch-Check in der Engine + +Vor jedem Live-BUY in `CopyTradingEngine.ProcessAccountOrderAsync`: +1. Buch holen (`IOrderBookProvider`, Fallback REST, Timeout ~150 ms — + bei Timeout Verhalten wie heute, nicht blockieren). +2. Checks (neue Settings in `CopyTradingAccountSettings`): + - `MaxSpreadPct` (Default z. B. 5 %): Spread größer → Skip mit Log. + - Tiefen-Check: liegt an unserem Limit-Preis genug Ask-Size für + `exactShares`? Wenn nein → Skip („Sniping-Verdacht: Liquidität bereits + konsumiert") statt teuer ins dünne Buch zu laufen. + +**Akzeptanz Phase 1:** Fill-Log füllt sich mit echten Fills; TradeReasoning +zeigt Spread/Tiefe-Entscheidungen; kein messbarer Latenz-Nachteil im Hot-Path +(> 200 ms Zusatz wäre Regression). + +--- + +## Phase 2 — SELL-Verfeinerung: Proportionalität + +Heute (Proportionalitätsfilter in `CopyTradingEngine`, SELL-Pre-Flight): +verkauft der Master < 30 % seines Bestands → ignorieren; ≥ 30 % → **wir +verkaufen alles**. Information über gestaffelte Exits geht verloren. + +**Ziel:** Verkaufsquote spiegeln. +1. Beim Öffnen einer Position den Master-Bestand zum Einstiegszeitpunkt + festhalten (`MasterSharesAtEntry`, im `CopyTradingState.MasterTraderPositions` + bzw. auf der Position persistieren). +2. Bei SELL-Signal: `sellRatio = signal.Size / masterSharesVorVerkauf` (wie + heute berechnet). Statt Voll-Exit: `sharesToSell = ourShares * sellRatio`. +3. Untergrenzen beachten: bleibt danach < Polymarket-Minimum (5–6 Shares) übrig + → Voll-Exit statt Rest-Dust. +4. Kleiner Teilverkauf (< 10 %) weiterhin ignorieren (Rauschen von Day-Tradern), + Schwelle konfigurierbar (`MinSellRatioPct`). +5. Verkauf läuft immer über die Eskalationsleiter aus Phase 0.1. + +Akzeptanz: Unit-Tests für die Ratio-Logik inkl. Dust-Grenzen; Logs zeigen +„Teilverkauf x % gespiegelt". + +--- + +## Phase 3 — Trader-Intelligence (Auswahl automatisieren) + +> Beim Copytrading entscheidet die Master-Auswahl über den Großteil des +> Ergebnisses. Diese Phase macht sie messbar und selbstkorrigierend. + +### 3.1 Copy-PnL-Score („Kopierbarkeit") + +Der `MasterTraderAnalyticsJob` misst heute den PnL des **Masters**. Relevanter +ist, was **wir** mit ihm verdient haben — inkl. unserer Slippage und Fees. + +1. Neue Kennzahlen je Master aus `ct_`-Closed-Trades (`ICopyTradeLogRepository`, + Filter `SourceTraderId`, letzte 30 Tage): + - `CopyPnl30d`, `CopyProfitFactor` (Bruttogewinn/Bruttoverlust), + `CopyAvgPnlPerTrade`, `CopyTradeCount30d`. + - `AvgSlippagePct` aus `ct_fill_log` (Phase 1.1): Ø(FillPrice−SignalPrice)/SignalPrice. +2. Felder auf `TrackedTrader` ergänzen (+ EF-Migration `mod_copytrading_trackers`), + Berechnung im `MasterTraderAnalyticsJob`, Anzeige in `MastersTradersView`. +3. **Achtung Metrik-Falle:** Winrate allein ist irreführend (Favoriten-Käufer + haben 95 % Winrate und können trotzdem negativ sein). Profit-Faktor und + Ø-PnL/Trade als primäre Sortierung in der UI. + +### 3.2 Sniper-/Verhaltens-Metriken in den Analytics-Job + +Portierung der Logik aus `analyze_snipers.py` (liegt im Projektroot) nach C# +in den `MasterTraderAnalyticsJob`: +1. Data-API-Activity je Master über volle 3 Tage paginieren (das Skript zeigt + das Pagination-Muster; API-Limit je Request beachten). +2. Kennzahlen: `MedianHoldMinutes`, `SellWithin5MinPct` (Anteil SELLs < 5 min + nach zugehörigem BUY), `SellCount3d`. +3. Schwellen (konfigurierbar): `SellWithin5MinPct > 50 %` → Master als Sniper + flaggen: Warn-Status in UI + Threema-Hinweis. Optional Auto-Pause (siehe 3.3). + +### 3.3 Automatischer Kill-Switch je Master + +Neue Modul-Settings (global, z. B. in `CopyTradingState` + Persistenz): +`AutoPauseEnabled`, `AutoPauseMinTrades` (z. B. 10), `AutoPauseDrawdownUsd` +oder `-Pct`. + +Regel im Analytics-Job (läuft 2×/Tag — zusätzlich stündlicher Light-Check +sinnvoll): Copy-PnL der letzten N Trades unter Schwelle → `IsActive = false`, +`Reasoning` mit Begründung + Zeitstempel befüllen, Threema-Notification. +Reaktivierung bewusst nur manuell. + +**Akzeptanz Phase 3:** UI zeigt Copy-Score-Spalten; ein simulierter +Verlust-Master wird automatisch pausiert (Test mit Demo-Daten). + +--- + +## Phase 4 — Maker-Mode & Demo-Realismus + +### 4.1 Maker-Einstieg für langsame Master + +Für Master mit Haltedauern von Stunden/Tagen (SwissTony/RN1-Typ) ist der +3-Sekunden-Taker-Fill unnötig teuer (Fees + Spread). Neues Verhalten +(Flag je Trader, z. B. `Category == "HOLDER"` oder eigenes Bool `MakerEntry`): +1. BUY als GTC-Limit **auf** Best-Bid (oder Mid − 1 Tick) statt über dem Ask. +2. Kein Fill nach T Minuten (konfigurierbar, z. B. 10) und Signal-Markt noch + im Preisband → auf Taker-Verhalten eskalieren oder verwerfen (Setting). +3. Fees: Maker zahlt 0 und sammelt ggf. Rebates — im Fee-Modell (0.2) abbilden. + +### 4.2 Demo-Modus realistisch machen + +Demo füllt heute zum Signalpreis ohne Slippage/Fees → Demo-Ergebnisse sind +systematisch geschönt und als Validierung neuer Master unbrauchbar. + +Fill-Modell im Demo-Pfad der Engine: +`FillPreis = Signalpreis + halber Spread (aus IOrderBookProvider, Fallback ++1 ¢) `, Fee der Marktkategorie abziehen, beides im `ClosedTrade` ausweisen. + +**Akzeptanz:** Demo- und Live-PnL desselben Masters weichen über 2 Wochen um +< 20 % relativ ab (grobe Plausibilität statt heutiger Systematik-Lücke). + +--- + +## Offene Entscheidungen (vor Umsetzung mit Richard klären) + +1. `SellFloorPct`-Default und Stufen-Timing der Eskalationsleiter (0.1). +2. `ProfitTarget`: implementieren (Empfehlung) oder Feld entfernen? +3. Auto-Pause: nur benachrichtigen oder hart deaktivieren? (Empfehlung: hart, + nachts passiert sonst genau das Falsche.) +4. Maker-Mode: als Trader-Flag oder automatisch aus `MedianHoldMinutes` + ableiten? (Empfehlung: automatisch ab z. B. Median > 60 min, manuell + überschreibbar.) + +## Reihenfolge & Abhängigkeiten + +``` +Phase 0 (sofort, unabhängig) + └── Phase 1 (Core-Infra; parallel zu 0 möglich, Livegang nach 0) + ├── Phase 2 (braucht 0.1-Leiter) + ├── Phase 3 (braucht 1.1-Fill-Log für Slippage; Rest unabhängig) + └── Phase 4 (braucht 1.2-Orderbuch) +``` diff --git a/UMSETZUNGSPLAN-Modul-BundleArbitrage.md b/UMSETZUNGSPLAN-Modul-BundleArbitrage.md new file mode 100644 index 0000000..4c6aa11 --- /dev/null +++ b/UMSETZUNGSPLAN-Modul-BundleArbitrage.md @@ -0,0 +1,187 @@ +# Umsetzungsplan: Modul „BundleArbitrage" (Intra-Market- & NegRisk-Arbitrage) + +> Stand: 2026-07-06 +> Ziel: Neues Strategiemodul, das Preissummen-Anomalien innerhalb von +> Polymarket erkennt und handelt: YES + NO < $1.00 (binäre Märkte) und +> Summen-Verletzungen in NegRisk-Multi-Outcome-Märkten. +> Reihenfolge: Nach/parallel zu MarketMaking — nutzt dieselbe Orderbuch- +> Infrastruktur. **Harte Voraussetzung:** Phase 1 (Marktdaten-Fundament) aus +> `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`. +> **Wichtig:** Dieses Modul startet bewusst als reines Mess-Modul +> (Detection-only). Ob Execution gebaut wird, entscheidet die Messphase. + +--- + +## 0. Strategie-Hintergrund & ehrliche Einordnung + +**Mechanik:** +- **Binär:** Kostet YES + NO zusammen < $1.00 (beide zum Ask kaufbar), + ist der Kauf beider Seiten ein garantierter Gewinn: Das Paar zahlt bei + Resolution sicher $1.00 aus — oder kann on-chain sofort zu $1.00 USDC + zusammengelegt werden (CTF `mergePositions`). +- **NegRisk (Multi-Outcome, genau ein Gewinner):** Summe aller YES-Asks < $1.00 + → alle YES kaufen (eines zahlt aus). Komplementär: Überteuerte Summen über + die NO-Seite bzw. NegRisk-Konvertierungen handeln. + +**Ehrliche Einordnung (Stand 2026):** Auf den großen Märkten ist das ein +HFT-Spiel — Fenster von Sekunden, dominiert von spezialisierten Bots; die +Taker-Fees seit März 2026 haben viele kleine Anomalien zusätzlich unprofitabel +gemacht. **Die Chance liegt im Long Tail** (kleine/neue Märkte, auf die die +großen Bots nicht schauen) und als **Beifang** der ohnehin laufenden +Orderbuch-Streams des MarketMaking-Moduls. Deshalb: erst messen, dann bauen. + +**Fee-Beachtung:** Als Taker fallen je Leg Fees an (kategorieabhängig, +0–1,8 %). Ein Bundle mit 2 ¢ Brutto-Marge kann nach Fees negativ sein. +Die Profitrechnung muss Fees je Leg von Anfang an enthalten. Maker-seitige +Ausführung (ein Leg ruht als Limit) ist fee-frei, aber nicht atomar. + +--- + +## 1. Architektur-Einbettung + +Neues Projekt `src/PolyTrader.Modules.BundleArbitrage/` als `IPolyTraderModule` +(`Name = "BundleArbitrage"`, `DbPrefix = "ba_"`), Registrierung in `Program.cs`. + +**Eigener Polymarket-Account** (gleiche Begründung wie in den anderen +Modul-Plänen; kann sich in v1 den Account mit MarketMaking teilen, sofern +die Inventar-Buchführung getrennt bleibt — Empfehlung: eigener Account, +sobald Execution live geht). + +### Persistenz + +| Tabelle | Inhalt | +|---|---| +| `ba_opportunities` | Jede erkannte Anomalie: Zeitpunkt, Markt/Event, Legs mit Preisen & ausführbarer Size, Brutto-/Netto-Marge (nach Fees), Lebensdauer (wann verschwunden) | +| `ba_executions` | Ausgeführte Bundles: Legs, Fills, Slippage, Ergebnis | +| `ba_settings` | Schwellen, Size-Limits, Modus (Detect/Execute) | + +Die Lebensdauer-Messung („wie lange war die Anomalie ausführbar?") ist der +wichtigste Datenpunkt der Messphase — sie entscheidet, ob unsere +Ausführungslatenz überhaupt konkurrenzfähig ist. + +--- + +## 2. Komponenten + +### 2.1 `ArbScannerService : BackgroundService` — Detection + +Zwei Datenpfade: +1. **Hot Set (WSS):** Für die vom `ClobMarketDataService` (Core) ohnehin + gestreamten Bücher (MarketMaking-Märkte + Top-Volumen-Märkte) wird bei + jedem Book-Update die Summenprüfung getriggert (< 1 ms, pure Funktion). +2. **Long-Tail-Sweep (REST):** Zyklischer Scan über aktive Märkte + (Gamma-API-Liste, dann CLOB `GET /book` bzw. Batch-Preis-Endpoints — + verfügbare Batch-Endpoints bei Umsetzung in der Doku prüfen). + Rate-Limits respektieren (Batching + Delays wie im + `TraderMonitorService`-Muster); Sweep-Frequenz Setting (z. B. alle 60 s + für 500 Märkte, priorisiert nach Volumen/Neuheit). + +**Prüf-Logik (pure, getestete Klasse `BundleMath`):** +- Binär: `bestAskYes + bestAskNo + FeeYes + FeeNo < 1.00 − MinMarginPct`. + Ausführbare Size = min(AskSize beider Seiten), ggf. über mehrere Book-Level + kumuliert (Level-2-Sweep-Rechnung). +- NegRisk: `Σ bestAskYes_i + Σ Fees < 1.00 − MinMarginPct` über alle Outcomes + eines NegRisk-Events (Event-Gruppierung über Gamma-API; `NegRisk`-Flag + existiert bereits in `MarketData`). +- Jede erkannte Anomalie → `ba_opportunities`; bei Verschwinden (nächstes + Update unterschreitet Schwelle) Lebensdauer nachtragen. + +### 2.2 Mess-Auswertung (Phase BA-1, entscheidungsrelevant) + +Report (UI-Tab + wöchentlicher Threema-Report): +- Anomalien/Tag nach Marge-Bucket (0,5–1 %, 1–2 %, > 2 % netto). +- Verteilung ausführbare Size und Lebensdauer. +- Erwarteter Monatsertrag bei angenommener Erfolgsquote X % = + Σ(Netto-Marge × min(Size, unser Limit)) über gefangene Fenster. + +**Go/No-Go-Kriterium für Execution:** erwarteter Ertrag > Entwicklungs- und +Kapitalkosten; realistisch fangbare Fenster (Lebensdauer > unsere Latenz, +konservativ ≥ 2–3 s). + +### 2.3 `ArbExecutionService` — nur nach Go-Entscheidung + +1. **Beide Legs gleichzeitig** als IOC-artige Orders senden (CLOB-Ordertypen + FOK/FAK bei Umsetzung in der Doku verifizieren; `PolymarketClobClient` + ggf. erweitern). Preis = erkannter Ask + kleiner Puffer, Size = min-Leg. +2. **Single-Leg-Risiko** (ein Leg füllt, das andere nicht) ist das + Kernproblem — Behandlungsreihenfolge: + a) Sofortiger Retry des offenen Legs (bis Preis `1.00 − Fees − MinMargin/2`). + b) Kein Fill → offenes Leg als GTC-Maker-Order zum Break-even-Preis stellen. + c) Timeout (Setting, z. B. 10 min) → Leg über Eskalationsleiter abbauen + (Muster aus Copytrading-Plan Phase 0.1) und Verlust in `ba_executions` + verbuchen. `MaxSingleLegLossUsd`-Tageslimit als Kill-Switch. +3. Size-Limits: `MaxUsdPerBundle` (Start 10–25), `MaxOpenBundles`, + Tagesbudget. +4. `.agents/rules/clob.md` beachten — jede CLOB-Client-Erweiterung mit + Backup/Commit und Mehrfach-Review. + +### 2.4 Kapital-Recycling: CTF `mergePositions` (Phase BA-4) + +Ohne Merge bindet jedes Bundle Kapital bis zur Resolution (bei kurzlaufenden +Märkten oft akzeptabel — Priorisierung im Scanner auf EndDate < 7 Tage +umgeht das Problem anfangs). + +On-Chain-Merge: YES + NO gleicher Size → $1.00 USDC sofort, via +ConditionalTokens `mergePositions(...)`; NegRisk-Sets über den +NegRisk-Adapter. Implementierung teilt sich Infrastruktur mit dem +Auto-Redeem des ResolutionFarming-Moduls (Phase RF-4) — **gemeinsamen +Core-Baustein `OnChainCtfService` bauen**, nicht zweimal implementieren. +Contract-Adressen/ABI aus https://docs.polymarket.com (Developer/CTF) +verifizieren; Gas (POL) -Handling und Balance-Warnung wie im RF-Plan. + +### 2.5 UI + +- Tab „Live-Anomalien": aktuelle Opportunities mit Netto-Marge/Size. +- Tab „Messung": Statistik-Report aus 2.2. +- Tab „Executions": Bundles, Single-Leg-Vorfälle, PnL. +- Tab „Settings": Schwellen, Modus-Schalter Detect/Execute (Default: Detect). + +--- + +## 3. Phasen & Akzeptanzkriterien + +### Phase BA-1: Detection-only (2–4 Wochen Messung) +- Scanner (Hot Set + Long-Tail-Sweep), `BundleMath` mit Unit-Tests + (inkl. Fee-Rechnung, Level-2-Kumulation, NegRisk-Summen), + `ba_opportunities`-Logging, Mess-Report. +- Akzeptanz: App baut & läuft; Report nach 2 Wochen vollständig; + dokumentierte Go/No-Go-Empfehlung. + +### Phase BA-2: Execution klein (nur bei Go) +- IOC-Doppel-Leg, Single-Leg-Behandlung, Size-Limits, Kill-Switch. +- Zunächst nur binäre Märkte (NegRisk-Execution ist komplexer → BA-3). +- Akzeptanz: ≥ 20 Bundles ausgeführt; Single-Leg-Quote < 20 %; + Netto-PnL nach Fees > 0. + +### Phase BA-3: NegRisk-Execution +- Multi-Leg-Bundles (N Outcomes), strengere Size-/Slippage-Grenzen + (mehr Legs = mehr Single-Leg-Risiko). + +### Phase BA-4: `OnChainCtfService` (Merge) — Kapital-Recycling +- Gemeinsam mit ResolutionFarming RF-4 (Redeem) als ein Core-Baustein. +- Testmarkt/Kleinstbetrag zuerst; Akzeptanz: Bundle → USDC ohne manuellen + Eingriff, USDC-Delta verifiziert. + +--- + +## 4. Risiken & Gegenmaßnahmen + +| Risiko | Gegenmaßnahme | +|---|---| +| Anomalien existieren, sind aber in < 1 s weg | Messphase BA-1 entscheidet VOR Entwicklungsaufwand für Execution | +| Single-Leg-Exposure | IOC-Orders, Retry-Kaskade, Tages-Verlustlimit, kleine Bundles | +| Fees fressen Marge | Netto-Rechnung inkl. Fees je Leg von Anfang an; `MinMarginPct` konservativ (Start ≥ 1 %) | +| Rate-Limits durch Long-Tail-Sweep | Batching, Priorisierung, Sweep-Frequenz drosseln; API-Fehlerquote überwachen | +| Stale-Book-Falsch-Signale | Max-Age-Check auf Book-Daten (`TryGetBook(maxAgeMs)`); Anomalie erst nach 2 aufeinanderfolgenden Bestätigungen | +| On-Chain-Merge-Fehler | Separater Baustein, Testmarkt, clob.md-Regeln, Balance-Verifikation | + +## 5. Offene Entscheidungen + +1. `MinMarginPct` (netto, nach Fees) für Detection-Logging (Empfehlung 0,5 %) + vs. Execution (Empfehlung ≥ 1 %). +2. Long-Tail-Sweep-Umfang (alle aktiven Märkte vs. Top-N + Neue) — abhängig + von beobachteten Rate-Limits. +3. Account-Frage: mit MarketMaking teilen oder eigener (Empfehlung: eigener, + sobald BA-2 startet). +4. Priorität von BA-4 (Merge): Bei Fokus auf kurzlaufende Märkte zunächst + verzichtbar — Kapitalbindung von Tagen ist bei kleinen Größen tragbar. diff --git a/UMSETZUNGSPLAN-Modul-MarketMaking.md b/UMSETZUNGSPLAN-Modul-MarketMaking.md new file mode 100644 index 0000000..edc3e16 --- /dev/null +++ b/UMSETZUNGSPLAN-Modul-MarketMaking.md @@ -0,0 +1,200 @@ +# Umsetzungsplan: Modul „MarketMaking" (Liquidity Rewards + Spread) + +> Stand: 2026-07-06 +> Ziel: Neues Strategiemodul, das beidseitige Limit-Orders in belohnungs- +> berechtigten Polymarket-Märkten stellt und drei Ertragsquellen kombiniert: +> tägliche Liquidity Rewards (USDC), Maker-Rebates und den Spread selbst. +> Reihenfolge: Nach ResolutionFarming. **Harte Voraussetzung:** Phase 1 +> (Marktdaten-Fundament: `ClobMarketDataService`, `ClobUserChannelService`) +> aus `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`. + +--- + +## 0. Strategie-Hintergrund + +Polymarket zahlt täglich (00:00 UTC) USDC-Rewards an Wallets, die kompetitive +Resting-Limit-Orders in berechtigten Märkten stellen. Der Reward-Pool liegt +2026 bei > $5 M/Monat (Sport-Peaks ~$8 M). Die Formel belohnt: Nähe zum +Midpoint (innerhalb eines markt-spezifischen Max-Spreads), Ordergröße +(Mindestgröße je Markt) und beidseitige Tiefe (einseitige Orders scoren +reduziert). Seit den Taker-Fees (März 2026) gibt es zusätzlich ein +**Maker-Rebate-Programm** (Anteil der Taker-Fees wird täglich an Maker +ausgeschüttet). Maker zahlen selbst keine Fees. + +**Referenzen (bei Umsetzung Formel/Parameter aktuell verifizieren):** +- https://docs.polymarket.com/market-makers/liquidity-rewards +- https://docs.polymarket.com/trading/fees (Maker-Rebates) +- Reward-Parameter je Markt (Max-Spread, Min-Size, Tages-Pool) kommen aus der + Gamma-/CLOB-API am Markt-Objekt. + +**Warum dieses Modul strategisch wertvoll ist:** Es ist die einzige Strategie, +bei der wir nicht gegen schnellere Bots um denselben Trade konkurrieren — +Anwesenheit wird bezahlt. Ertrag ist stetig statt direktional. + +**Hauptrisiko: Adverse Selection.** Unsere Quotes werden bevorzugt dann +gefüllt, wenn jemand mit besserer Information (News-Bot, Live-Sport-Feed) +gegen uns handelt. Gegenmaßnahmen: Marktauswahl (ruhige, langlaufende Märkte; +anfangs KEINE Live-Sport- und KEINE Krypto-Kurzfrist-Märkte), Inventar-Limits, +Volatilitäts-Pause. + +--- + +## 1. Architektur-Einbettung + +Neues Projekt `src/PolyTrader.Modules.MarketMaking/` als `IPolyTraderModule` +(`Name = "MarketMaking"`, `DbPrefix = "mm_"`), Registrierung in `Program.cs`. + +**Eigener Polymarket-Account zwingend** (gleiche Begründung wie im +ResolutionFarming-Plan, hier noch kritischer: Der Copytrading- +`TraderMonitorService` würde MM-Inventar als Positionen adoptieren und der +Copytrading-`CancelConflictingOrdersAsync`-Mechanismus würde unsere +Resting-Quotes canceln!). + +### Persistenz + +| Tabelle | Inhalt | +|---|---| +| `mm_settings` | Globale + je-Markt-Settings (Size, Spread-Ziel, Limits) | +| `mm_markets` | Kuratierte/gescorte Märkte (Reward-Parameter, Status) | +| `mm_quotes_log` | Quote-Historie (Preis, Size, Dauer, Cancel-Grund) — für Reward-Optimierung | +| `mm_fills` | Fills mit Seite, Preis, Inventar danach | +| `mm_daily_pnl` | Tagesabrechnung: Rewards, Rebates, Spread-PnL, Inventar-PnL | + +--- + +## 2. Komponenten + +### 2.1 `MarketSelectorJob` — Marktauswahl & Scoring + +Täglich + manuell triggerbar: +1. Reward-berechtigte Märkte über Gamma-/CLOB-API listen (Felder: Reward-Pool/ + Rate, `rewardsMaxSpread`, `rewardsMinSize` — Feldnamen verifizieren). +2. Score je Markt: `erwarteter Reward pro gequoteter $ ÷ Risiko-Proxy`. + - Reward-Schätzung: Tages-Pool des Markts ÷ beobachtete konkurrierende + Maker-Liquidität innerhalb des Max-Spreads (aus Orderbuch-Snapshots). + - Risiko-Proxy: realisierte Midpoint-Volatilität (Stddev der Mid-Bewegungen + über 24 h aus `ClobMarketDataService`-Daten), Zeit bis Resolution + (je näher, desto gefährlicher), Kategorie. +3. Harte Ausschlüsse (erste Ausbaustufe): Live-Sport (in-play), Krypto- + Kurzfrist-Märkte (15 min/1 h), Märkte < 7 Tage vor EndDate, Midpoint + außerhalb 0.10–0.90 (Extrempreise = asymmetrisches Inventarrisiko). +4. Output: Ranking in `mm_markets` + UI; Betreiber aktiviert Märkte manuell + (Whitelist-Prinzip — der Bot wählt in v1 nicht selbst). + +### 2.2 `QuotingEngine : BackgroundService` — Kern des Moduls + +Je aktivem Markt eine Quote-State-Machine: + +1. **Zielquote:** Bid und Ask symmetrisch um den Midpoint, Abstand + `QuoteSpreadTicks` (Setting), immer **innerhalb** des Reward-Max-Spreads; + Size ≥ Reward-Min-Size (Setting `QuoteSizeUsd`, initial klein). +2. **Requote-Trigger:** Midpoint-Bewegung > Schwelle (z. B. 1 Tick), eigene + Order gefüllt, Reward-Fenster verletzt. Requote = Cancel + neue Order über + `PolymarketClobClient`. +3. **Churn-Begrenzung:** Mindest-Ruhezeit zwischen Requotes (z. B. 3–5 s), + Hysterese (nicht bei jedem Tick nachziehen) — API-Rate-Limits und + Order-Spam vermeiden. +4. **Fill-Verarbeitung:** über `ClobUserChannelService` (Echtzeit). Nach Fill: + Inventar aktualisieren, Gegenquote anpassen (siehe 2.3). +5. Alle Quotes/Cancels in `mm_quotes_log` (Grundlage für Optimierung). + +Die Preis-/Requote-Logik als **pure, getestete Klasse** (`QuoteCalculator`) +implementieren — Input: Book-Snapshot, Inventar, Settings; Output: Ziel-Quotes. +Unit-Tests in `PolyTrader.Tests` (das ist die kritischste Logik des Moduls). + +### 2.3 `InventoryManager` — Risikosteuerung + +1. Inventar je Markt = Netto-Shares (YES-äquivalent) × Preis. +2. **Skew:** Bei wachsendem Inventar Quotes asymmetrisch verschieben + (Kaufseite weiter weg, Verkaufsseite näher/attraktiver), Faktor + proportional zu `Inventar / MaxInventoryUsd`. +3. **Limits (Settings je Markt + global):** + - `MaxInventoryUsd` je Markt (Default klein, z. B. 50). + - `MaxTotalInventoryUsd` über alle Märkte. + - Bei Limit-Bruch: Quoting nur noch auf der abbauenden Seite + („Reduce-Only-Modus") bis Inventar < 50 % des Limits. +4. **Exit vor Resolution:** Ab `ExitHoursBeforeEnd` (Default 48 h) Reduce-Only, + ab 24 h aktiver Abbau (Maker-seitig, notfalls Taker mit Verlust-Deckel). +5. **Volatilitäts-Pause:** Midpoint-Sprung > X % in Y Sekunden → alle Quotes + des Markts canceln, Cooldown Z Minuten (News-Schutz). Global-Kill-Switch + analog `GlobalTradingPaused`. + +### 2.4 `RewardTracker` + +1. Tägliche Reward-/Rebate-Eingänge erkennen (USDC-Transfers auf die Wallet + via Data-API/Alchemy) und `mm_daily_pnl` zuordnen. +2. Tagesabrechnung: `Rewards + Rebates + SpreadPnL + InventarPnL(mark-to-mid) + − Verluste = Netto`. Threema-Tagesreport. +3. Kennzahl je Markt: **Reward-ROI pro gequoteter $** → Feedback in den + `MarketSelectorJob` (schlechte Märkte deaktivieren). + +### 2.5 UI + +- Tab „Märkte": Kandidaten-Ranking, aktiv/inaktiv-Toggle, Reward-Parameter. +- Tab „Live": aktuelle Quotes, Inventar je Markt (Ampel), letzte Fills. +- Tab „Abrechnung": `mm_daily_pnl`-Historie, Reward-ROI je Markt. +- Tab „Settings": PropertyGrid. + +--- + +## 3. Phasen & Akzeptanzkriterien + +### Phase MM-1: Fundament-Verifikation + Selector (read-only) +- Voraussetzung prüfen: `ClobMarketDataService`/`ClobUserChannelService` + laufen stabil (mehrtägiger Soak-Test, Reconnect-Verhalten). +- `MarketSelectorJob` + UI-Ranking, keine Orders. +- Akzeptanz: Ranking plausibel; Orderbuch-Daten für Top-Märkte lückenlos + über 72 h (Basis für Volatilitäts-Proxy). + +### Phase MM-2: Paper-Quoting (Messung Adverse Selection) +- QuotingEngine läuft vollständig, sendet aber **keine** Orders; simulierte + Fills: Quote gilt als gefüllt, wenn der Marktpreis durch unser Quote-Level + handelt (aus Market-Channel-Trades ableitbar). +- 2 Wochen laufen lassen. Messen: simulierter Spread-PnL, Inventarverläufe, + Wie oft wären wir „überfahren" worden (Fill unmittelbar vor großer + Gegenbewegung)? +- Akzeptanz/Go-Kriterium: simuliertes Inventar bleibt innerhalb der Limits; + Spread-PnL ≥ 0 (Rewards kommen on top und sind der eigentliche Ertrag). +- **Hinweis:** Rewards selbst lassen sich nicht simulieren — sie erfordern + echte Resting-Orders. Paper-Phase misst nur die Risikoseite. + +### Phase MM-3: Live auf 1–2 ruhigen Märkten +- Eigener Account, kleines Kapital (z. B. 300–500 USDC), `QuoteSizeUsd` + knapp über Reward-Min-Size, 1–2 langlaufende Politik-/Geopolitik-Märkte. +- Akzeptanz nach 2–4 Wochen: tägliche Rewards fließen nachweislich + (`mm_daily_pnl`); Netto (Rewards + Spread − Inventarverluste) > 0; + keine Order-Leichen (Cancel-Fehler) im CLOB. + +### Phase MM-4: Skalierung + Skew-Feintuning +- Mehr Märkte (Selector-getrieben), Inventar-Skew-Parameter aus Fill-Daten + optimieren, Size je Markt anhand Reward-ROI erhöhen. + +### Phase MM-5 (optional): Reward-Optimierung +- Order-Laddering (mehrere Level innerhalb des Max-Spreads), dynamische + Spread-Wahl abhängig von Konkurrenz-Liquidität, Teilnahme an + Sponsor-/Sonder-Reward-Programmen (z. B. Sport-Events pre-game). + +--- + +## 4. Risiken & Gegenmaßnahmen + +| Risiko | Gegenmaßnahme | +|---|---| +| Adverse Selection durch News-/Latenz-Bots | Marktauswahl (keine Live-Events), Volatilitäts-Pause, kleine Size | +| Inventar läuft in Resolution | Exit-Regeln ab 48 h/24 h vor EndDate (2.3) | +| Order-Churn → Rate-Limits/Sperren | Requote-Hysterese, Mindest-Ruhezeit, Monitoring der API-Fehlerquote | +| Reward-Regeländerungen | Parameter täglich aus API lesen, nichts hartkodieren | +| WSS-Ausfall → blinde Quotes | Watchdog: keine Book-Updates > N s → alle Quotes canceln (Fail-Safe) | +| Konflikt mit Copytrading | Eigener Account (Abschnitt 1) | + +Der Fail-Safe „bei Datenverlust alles canceln" ist Pflicht ab MM-3 und muss +getestet werden (WSS künstlich trennen). + +## 5. Offene Entscheidungen + +1. Startmärkte (Empfehlung: 1–2 langlaufende Politik-/Geopolitik-Märkte mit + mittlerem Volumen — genug Reward-Pool, wenig Newsflow). +2. `QuoteSizeUsd`/Kapital für MM-3. +3. Beidseitig quoten von Anfang an (voller Reward-Score) oder zunächst + einseitig konservativ? (Empfehlung: beidseitig, dafür kleine Size — + einseitig scored schlechter und halbiert den Lerneffekt.) diff --git a/UMSETZUNGSPLAN-Modul-ResolutionFarming.md b/UMSETZUNGSPLAN-Modul-ResolutionFarming.md new file mode 100644 index 0000000..573935d --- /dev/null +++ b/UMSETZUNGSPLAN-Modul-ResolutionFarming.md @@ -0,0 +1,212 @@ +# Umsetzungsplan: Modul „ResolutionFarming" (Favoriten nahe Auflösung) + +> Stand: 2026-07-06 +> Ziel: Neues Strategiemodul, das systematisch unterbewertete Favoriten +> (~90–98 ¢) in bald auflösenden Märkten kauft, bis zur Resolution hält und +> automatisch redeemt. +> Reihenfolge: **Erstes neues Strategiemodul** (geringster Infrastrukturbedarf, +> validiert die Modul-Architektur über Copytrading hinaus). +> Voraussetzung: Phase 0 + 0.2 (Fee-Modell) aus +> `UMSETZUNGSPLAN-CopyTrading-Verbesserungen.md`. Phase 1 (Orderbuch) ist +> hilfreich, aber nicht zwingend für den Start. + +--- + +## 0. Strategie-Hintergrund (Warum das funktioniert) + +Auswertungen der Polymarket-Handelsdaten zeigen ein **Favorite-Longshot- +Reversal**: Outcomes mit hoher Wahrscheinlichkeit sind systematisch +*unterbewertet* (Retail überschätzt Longshots und drückt damit den Favoriten- +Preis). Ein 95-¢-Favorit gewinnt im Schnitt öfter als in 95 % der Fälle. +2–5 % Marge in 24–48 h ergibt hohe annualisierte Renditen — **sofern das +Tail-Risiko diszipliniert gemanagt wird**: Ein verlorener 95-¢-Trade +vernichtet ~19 gewonnene. Das Risikomodell IST die Strategie. + +Interner Kontext: Die profitabelsten kopierten Master (Typ „SwissTony"/„RN1") +machen genau das — hunderte BUYs, nie SELLs, Auflösung abwarten. Dieses Modul +internalisiert die Strategie und eliminiert die Copy-Latenz und die +Fremdbestimmung der Marktauswahl. + +**Fees (seit März 2026):** Taker-Fees je Kategorie (Sports ~0,75 %, Politik +~1,0 %, Krypto ~1,8 %, Geopolitik 0 %) — bei 2–5 % Brutto-Marge ist die +Kategorie-Wahl entscheidend. Maker-Einstieg (Limit ins Buch) zahlt 0 Fees. + +--- + +## 1. Architektur-Einbettung + +Neues Projekt `src/PolyTrader.Modules.ResolutionFarming/` (Class Library, +net8.0-windows), Registrierung als `IPolyTraderModule` analog +`CopyTradingModule` (`Name = "ResolutionFarming"`, `DbPrefix = "rf_"`), +Einbindung in `Program.cs` der App. + +### ⚠️ Grundsatzentscheidung: Eigener Polymarket-Account je Strategiemodul + +**Dringende Empfehlung:** Das Modul handelt über einen **eigenen Account** +(Multi-Account-Support existiert im Core / `AccountState`). + +Begründung: Der `TraderMonitorService` des Copytrading-Moduls synct **alle** +Wallet-Positionen eines Accounts in `account.OpenPositions`, würde +ResolutionFarming-Positionen „adoptieren" (Master-Zuordnungs-Fallbacks), +in seine Limits (PerMarket/PerMaster/Zeitfenster) einrechnen und ggf. +Auto-Redeem-/Cleanup-Logik darauf anwenden. Saubere Trennung über getrennte +Wallets vermeidet diese gesamte Konfliktklasse zur Laufzeit **und** +buchhalterisch (PnL je Strategie sauber messbar). + +In der UI/Settings des Moduls: Zuordnung `AccountId ↔ Modul` mit Warnung, +wenn derselbe Account auch im Copytrading aktiv ist. + +### Persistenz (EF Core / Pomelo / MySQL, eigener DbContext analog `CopyTradingDbContext`) + +| Tabelle | Inhalt | +|---|---| +| `rf_settings` | Modul-Settings je Account (Preisband, Budgets, Limits) | +| `rf_candidates` | Scanner-Ergebnisse (Markt, Preis, Score, Filtergründe) — auch abgelehnte, für spätere Kalibrierung | +| `rf_positions` | Offene Farming-Positionen (TokenId, Entry, Size, EndDate, ClusterKey, Status) | +| `rf_closed_trades` | Abgeschlossene Trades inkl. Fees, Redeem-Infos | + +Zusätzlich schreibt das Modul in den generischen Core-Trade-Log +(modulübergreifendes Dashboard). + +--- + +## 2. Komponenten + +### 2.1 `MarketScannerJob : BackgroundService` + +Alle 10–15 Minuten (JobManager-Registrierung wie `MasterTraderAnalyticsJob`, +manuell triggerbar): + +1. Gamma-API: aktive Märkte mit `endDate < now + MaxHoursToResolution` + (Default 48 h), nicht closed. Bestehenden `PolymarketApiService` erweitern + (Query-Parameter für endDate-Fenster; Endpoint-Details bei Umsetzung aus + https://docs.polymarket.com verifizieren). +2. Je Markt den Favoriten bestimmen (Outcome mit höchstem Preis). Preisquelle: + CLOB Midpoint/Book (REST `GET /book` bzw. `IOrderBookProvider`, falls + Phase 1 des Copytrading-Plans schon umgesetzt). +3. Filterkette (jeder Reject wird mit Grund in `rf_candidates` geloggt): + - Preisband: `MinPrice ≤ ask ≤ MaxPrice` (Default 0.90–0.98). + - Liquidität: Ask-Tiefe am Zielpreis ≥ geplante Ordergröße × Faktor; + zusätzlich Markt-Volumen/Liquiditätsfelder der Gamma-API als Grobfilter. + - Kategorie-Whitelist (Default: Sports, Geopolitik, Politik; **Krypto + ausschließen** — 1,8 % Fee frisst die Marge; keine 15-Min-/Stunden-Märkte). + - Netto-Edge-Check: `(1 − ask) − Fee(ask, Kategorie) ≥ MinEdgePct` + (Default z. B. 1,5 %). + - Blacklist-Mechanismus (Slugs/Tags), z. B. für Marktarten mit + Resolution-Streitigkeiten (UMA-Disputes). +4. Kandidaten mit Score in `rf_candidates` schreiben; Anzeige in der Modul-UI. + +### 2.2 Risiko-Engine (pure, testbare Klasse `FarmingRiskEngine`) + +Settings je Account (`rf_settings`): + +| Setting | Default | Bedeutung | +|---|---|---| +| `MaxPerMarketUsd` | 25 | Max. Einsatz je Markt | +| `MaxPerClusterPct` | 10 % | Max. Anteil der Bankroll je **Ereignis-Cluster** | +| `MaxTotalExposurePct` | 60 % | Max. Gesamteinsatz in offenen Positionen | +| `MaxNewPositionsPerDay` | 20 | Drosselung | +| `MinEdgePct` | 1.5 % | Netto-Edge nach Fees | +| `DailyLossKillSwitchUsd` | konfig. | Tagesverlust → Modul pausiert + Threema | + +**Cluster-Definition (kritisch!):** 20 Fußballspiele desselben Spieltags sind +keine 20 unabhängigen Wetten. ClusterKey ableiten aus Event-/Series-Slug der +Gamma-API (z. B. Liga+Datum, Turnier, Wahl-Event). Korrelierte Favoriten +(z. B. „Kandidat X gewinnt" + „Partei von X gewinnt") teilen einen Cluster. +Erste Version: Heuristik über Event-Slug; Verfeinerung später. + +### 2.3 `FarmingExecutionService` + +1. Einstieg **Maker-first**: GTC-Limit auf Best-Bid bzw. Mid − 1 Tick + (0 Fees). Kein Fill nach T Minuten (Default 15) → Entscheidung per Setting: + Taker-Fill (wenn Netto-Edge auch mit Fee noch ≥ MinEdge) oder verwerfen. +2. Order-Verwaltung über `PolymarketClobClient` (Core), Tracking analog + `PendingOrderTimestamps`-Muster des Copytrading-Moduls. +3. Positionen in `rf_positions` + Core-Positions-Sync gegen die Wallet + (Data-API `positions` — Muster aus `TraderMonitorService.PollLiveAccountsAsync` + übernehmen, aber schlanker: kein Master-Mapping nötig). + +### 2.4 `ResolutionMonitorJob` + `AutoRedeemService` + +1. Monitor: prüft offene `rf_positions` gegen Resolution + (`PolymarketApiService.CheckMarketResolutionAsync` existiert bereits). +2. **On-Chain-Auto-Redeem** (heute im gesamten Projekt nur manuell — dieser + Baustein nützt auch dem Copytrading): + - Gewinner-Shares einlösen via ConditionalTokens-Contract auf Polygon + (`redeemPositions(...)`); für NegRisk-Märkte über den NegRisk-Adapter. + **Contract-Adressen und Aufrufparameter bei Umsetzung zwingend aus der + offiziellen Doku verifizieren** (https://docs.polymarket.com, + Developer-Sektion CTF/NegRisk). + - Implementierung: Nethereum-Paket ODER Raw-RPC über die vorhandene + Alchemy-Anbindung; Signing mit dem Account-PrivateKey (liegt in + `AccountState`). + - Gas: Wallet braucht POL; Balance-Check + Threema-Warnung bei Unterdeckung. + - Retry mit Backoff; Erfolg = USDC-Balance-Delta verifiziert. + - `.agents/rules/clob.md` gilt hier besonders: On-Chain-Signing ist + hochkritisch — zuerst mit Kleinstbetrag auf einem Testmarkt verifizieren. +3. Übergangslösung bis 2.4 fertig: PreRedeem-artiger Verkauf (GTC-Limit 0.99+) + oder manueller Redeem — das Modul funktioniert auch ohne On-Chain-Teil, + bindet dann nur Kapital länger. + +### 2.5 UI (`RegisterUi`, ein Fenster mit Tabs analog `CopyTradingMainForm`) + +- Tab „Kandidaten": aktueller Scan mit Filtergründen (auch Rejects). +- Tab „Positionen": offene Farming-Positionen, Cluster-Auslastung, Countdown. +- Tab „Historie/Statistik": Winrate je Preisband (Kalibrierung!), Netto-PnL, + Fees, Redeem-Status. +- Tab „Settings": PropertyGrid auf `rf_settings` (Muster `AccountSettingsView`). + +--- + +## 3. Phasen & Akzeptanzkriterien + +### Phase RF-1: Modul-Skelett + Scanner (read-only) +- Projekt, Modul-Registrierung, DbContext + Migration, Scanner-Job, UI-Tab + „Kandidaten". **Keine Order-Platzierung.** +- Akzeptanz: App baut & startet mit Modul; Scanner liefert plausible + Kandidaten; Rejects nachvollziehbar geloggt; 1 Woche Kandidaten-Sammlung. + +### Phase RF-2: Demo-Betrieb (4 Wochen) +- Execution im Demo-Modus (realistisches Fill-Modell: Ask-Preis + Fee, + siehe Copytrading-Plan Phase 4.2). Risiko-Engine aktiv. +- Akzeptanz/Go-Kriterium für Live: Kalibrierungstabelle zeigt + `realisierte Winrate je Preisband > Preisband-Mitte` und + Netto-Edge nach Fees > 0 über ≥ 100 Demo-Trades. + +### Phase RF-3: Live klein +- Eigener Account, kleines Budget (z. B. 200–500 USDC), `MaxPerMarketUsd` 5–10. +- Kill-Switch + Threema-Reporting (Tageszusammenfassung) aktiv. +- Akzeptanz: 2 Wochen Live ohne Ausführungsfehler; Live-Ergebnis im Rahmen + der Demo-Erwartung. + +### Phase RF-4: Auto-Redeem on-chain +- Wie 2.4; zuerst Testmarkt/Kleinstbetrag, dann aktivieren. +- Akzeptanz: Gewinner-Position wird ohne manuellen Eingriff zu USDC. + +### Phase RF-5: Kalibrierung & Skalierung +- Scoring von Heuristik auf Daten umstellen: historische Winrate je + Preisband × Kategorie aus `rf_candidates`/`rf_closed_trades` (+ optional + öffentliche Polymarket-Historien-Datensätze) → nur Bänder/Kategorien mit + nachgewiesenem Edge handeln. Budget stufenweise erhöhen. + +--- + +## 4. Risiken & Gegenmaßnahmen + +| Risiko | Gegenmaßnahme | +|---|---| +| Tail-Event (Favorit verliert) | Cluster-Limits, MaxPerMarket, Diversifikation über Kategorien | +| Korrelierte Cluster falsch geschnitten | Konservative Cluster-Heuristik, Review der Cluster in der UI | +| Resolution-Disputes (UMA) | Blacklist strittiger Marktarten; nur klare, objektiv auflösbare Märkte | +| Fee-Änderungen | Fee je Trade persistieren, MinEdge-Check dynamisch | +| Konflikt mit Copytrading-Sync | Eigener Account (Abschnitt 1) | +| On-Chain-Redeem-Fehler | Separate Phase, Testmarkt zuerst, Balance-Verifikation, clob.md-Regeln | + +## 5. Offene Entscheidungen + +1. Eigener Account: neuer Polymarket-Account nötig — wer legt ihn an, wie viel + Startkapital? +2. Preisband-Default (0.90–0.98) und `MinEdgePct` — mit Demo-Daten validieren. +3. Nethereum vs. Raw-RPC für On-Chain-Calls (Empfehlung: Nethereum, weniger + Fehlerfläche beim ABI-Encoding). +4. Taker-Fallback beim Einstieg erlauben oder strikt Maker-only? diff --git a/tests/PolyTrader.Tests/DbContextModelTests.cs b/tests/PolyTrader.Tests/DbContextModelTests.cs new file mode 100644 index 0000000..4f2c7e1 --- /dev/null +++ b/tests/PolyTrader.Tests/DbContextModelTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using PolyTrader.Core.Persistence.Ef; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; +using PolyTrader.Tests.TestSupport; +using PolyTraderSharp.Models; +using Xunit; + +namespace PolyTrader.Tests +{ + /// + /// Feinkörnige Modell-Invarianten (Randfälle des Mappings): Dezimal-Präzision auf + /// Geldfeldern, MaxLength auf Strings, manuell gesetzte Keys (ValueGeneratedNever), + /// Value-Converter/Spaltentyp der JSON-Felder, Indizes. + /// + public class DbContextModelTests + { + private static CoreDbContext CoreCtx() => + new InMemoryContextFactory(o => new CoreDbContext(o)).CreateDbContext(); + + private static CopyTradingDbContext CtCtx() => + new InMemoryContextFactory(o => new CopyTradingDbContext(o)).CreateDbContext(); + + private static IProperty Prop(DbContext ctx, Type clr, string name) => + ctx.Model.FindEntityType(clr)!.FindProperty(name)!; + + // GetColumnType() wirft unter dem InMemory-Provider (kein RelationalTypeMapping); + // die HasColumnType-Annotation lässt sich aber providerunabhängig auslesen. + private static string? ColumnType(IProperty p) => + p.FindAnnotation("Relational:ColumnType")?.Value as string; + + [Theory] + [InlineData(typeof(AccountState), nameof(AccountState.TotalBalance))] + [InlineData(typeof(AccountState), nameof(AccountState.AvailableBalance))] + [InlineData(typeof(Position), nameof(Position.EntryPrice))] + [InlineData(typeof(TradeRecord), nameof(TradeRecord.RealizedPnl))] + public void Core_money_fields_have_precision_18_6(Type clr, string name) + { + using var ctx = CoreCtx(); + var p = Prop(ctx, clr, name); + Assert.Equal(18, p.GetPrecision()); + Assert.Equal(6, p.GetScale()); + } + + [Theory] + [InlineData(nameof(CopyTradingAccountSettings.PerMarketLimit))] + [InlineData(nameof(CopyTradingAccountSettings.PerMasterLimit))] + [InlineData(nameof(CopyTradingAccountSettings.perMaxTimeNone))] + public void Settings_money_fields_have_precision_18_6(string name) + { + using var ctx = CtCtx(); + var p = Prop(ctx, typeof(CopyTradingAccountSettings), name); + Assert.Equal(18, p.GetPrecision()); + Assert.Equal(6, p.GetScale()); + } + + [Theory] + [InlineData(typeof(AccountState), nameof(AccountState.WalletAddress), 128)] + [InlineData(typeof(AccountState), nameof(AccountState.Name), 200)] + [InlineData(typeof(MarketData), nameof(MarketData.Id), 120)] + public void Core_string_fields_have_expected_max_length(Type clr, string name, int expected) + { + using var ctx = CoreCtx(); + Assert.Equal(expected, Prop(ctx, clr, name).GetMaxLength()); + } + + [Fact] + public void TrackedTrader_category_has_max_length_64() + { + using var ctx = CtCtx(); + Assert.Equal(64, Prop(ctx, typeof(TrackedTrader), nameof(TrackedTrader.Category)).GetMaxLength()); + } + + [Theory] + [InlineData(typeof(AccountState), nameof(AccountState.AccountId))] + public void Core_manual_keys_are_not_store_generated(Type clr, string name) + { + using var ctx = CoreCtx(); + Assert.Equal(ValueGenerated.Never, Prop(ctx, clr, name).ValueGenerated); + } + + [Theory] + [InlineData(typeof(TrackedTrader), nameof(TrackedTrader.Id))] + [InlineData(typeof(ClosedTrade), nameof(ClosedTrade.TradeId))] + [InlineData(typeof(CopyTradingAccountSettings), nameof(CopyTradingAccountSettings.AccountId))] + public void Module_manual_keys_are_not_store_generated(Type clr, string name) + { + using var ctx = CtCtx(); + Assert.Equal(ValueGenerated.Never, Prop(ctx, clr, name).ValueGenerated); + } + + [Fact] + public void AssignedAccountIds_has_value_converter_and_text_column() + { + using var ctx = CtCtx(); + var p = Prop(ctx, typeof(TrackedTrader), nameof(TrackedTrader.AssignedAccountIds)); + Assert.NotNull(p.GetValueConverter()); + Assert.Equal("text", ColumnType(p)); + } + + [Theory] + [InlineData(nameof(MarketData.ClobTokenIds))] + [InlineData(nameof(MarketData.Outcomes))] + public void Market_json_columns_are_text(string name) + { + using var ctx = CoreCtx(); + Assert.Equal("text", ColumnType(Prop(ctx, typeof(MarketData), name))); + } + + [Fact] + public void TradeRecord_is_indexed_on_closed_at() + { + using var ctx = CoreCtx(); + var indexed = ctx.Model.FindEntityType(typeof(TradeRecord))! + .GetIndexes() + .SelectMany(i => i.Properties.Select(p => p.Name)); + Assert.Contains(nameof(TradeRecord.ClosedAt), indexed); + } + + [Fact] + public void MasterTraderHistory_is_indexed_on_trader_and_closed_at() + { + using var ctx = CtCtx(); + var indexed = ctx.Model.FindEntityType(typeof(MasterTraderHistoryRecord))! + .GetIndexes() + .SelectMany(i => i.Properties.Select(p => p.Name)) + .ToList(); + Assert.Contains(nameof(MasterTraderHistoryRecord.TraderId), indexed); + Assert.Contains(nameof(MasterTraderHistoryRecord.ClosedAt), indexed); + } + } +} diff --git a/tests/PolyTrader.Tests/MongoExportParserTests.cs b/tests/PolyTrader.Tests/MongoExportParserTests.cs index 703394b..4813b80 100644 --- a/tests/PolyTrader.Tests/MongoExportParserTests.cs +++ b/tests/PolyTrader.Tests/MongoExportParserTests.cs @@ -116,5 +116,51 @@ namespace PolyTrader.Tests Assert.Empty(MongoExportParser.ParseAccounts("[]")); Assert.Empty(MongoExportParser.ParseTraders("[]")); } + + [Fact] + public void ParseAccounts_accepts_numeric_decimals_not_only_strings() + { + const string json = """[{ "_id": 1, "TotalBalance": 12.5, "PerMarketLimit": 3 }]"""; + + var imp = Assert.Single(MongoExportParser.ParseAccounts(json)); + Assert.Equal(12.5m, imp.Account.TotalBalance); + Assert.Equal(3m, imp.Settings.PerMarketLimit); + } + + [Fact] + public void ParseAccounts_ignores_unknown_extra_fields() + { + const string json = """[{ "_id": 1, "Name": "X", "SomethingUnknown": {"a":1}, "Extra": [1,2,3] }]"""; + + var imp = Assert.Single(MongoExportParser.ParseAccounts(json)); + Assert.Equal("X", imp.Account.Name); + } + + [Fact] + public void ParseAccounts_reads_multiple_documents_in_order() + { + const string json = """[{ "_id": 1 }, { "_id": 2 }, { "_id": 3 }]"""; + + var ids = MongoExportParser.ParseAccounts(json).Select(x => x.Account.AccountId).ToArray(); + Assert.Equal(new[] { 1, 2, 3 }, ids); + } + + [Fact] + public void ParseTraders_accepts_assigned_account_ids_as_string_numbers() + { + const string json = """[{ "_id": 9, "AssignedAccountIds": ["1", "3"] }]"""; + + var t = Assert.Single(MongoExportParser.ParseTraders(json)); + Assert.Equal(new HashSet { 1, 3 }, t.AssignedAccountIds); + } + + [Fact] + public void ParseTraders_missing_isactive_defaults_true() + { + const string json = """[{ "_id": 9 }]"""; + + var t = Assert.Single(MongoExportParser.ParseTraders(json)); + Assert.True(t.IsActive); + } } } diff --git a/tests/PolyTrader.Tests/RepositoryEdgeCaseTests.cs b/tests/PolyTrader.Tests/RepositoryEdgeCaseTests.cs new file mode 100644 index 0000000..e2a3c1c --- /dev/null +++ b/tests/PolyTrader.Tests/RepositoryEdgeCaseTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; +using PolyTrader.Tests.TestSupport; +using PolyTraderSharp.Models; +using Xunit; + +namespace PolyTrader.Tests +{ + public class RepositoryEdgeCaseTests + { + private static EfTrackedTraderRepository TraderRepo() => + new(new InMemoryContextFactory(o => new CopyTradingDbContext(o))); + + private static EfMasterTraderHistoryRepository HistoryRepo() => + new(new InMemoryContextFactory(o => new CopyTradingDbContext(o))); + + [Fact] + public void GetAll_on_empty_repo_returns_empty_not_null() + { + var repo = TraderRepo(); + var all = repo.GetAll(); + Assert.NotNull(all); + Assert.Empty(all); + } + + [Fact] + public void Update_alias_behaves_like_upsert_for_existing() + { + var repo = TraderRepo(); + repo.Upsert(new TrackedTrader { Id = 1, DisplayName = "A" }); + repo.Update(new TrackedTrader { Id = 1, DisplayName = "B" }); + + Assert.Equal("B", Assert.Single(repo.GetAll()).DisplayName); + } + + [Fact] + public void Two_factories_are_isolated_from_each_other() + { + var a = TraderRepo(); + var b = TraderRepo(); + a.Upsert(new TrackedTrader { Id = 1 }); + + Assert.Single(a.GetAll()); + Assert.Empty(b.GetAll()); + } + + [Fact] + public void History_Exists_is_inclusive_on_window_boundaries() + { + var repo = HistoryRepo(); + var t = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc); + repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, TokenId = "tok", ClosedAt = t }); + + // Fenster endet exakt auf dem Zeitpunkt (untere Grenze) + Assert.True(repo.Exists(1, "tok", t, t.AddSeconds(2))); + // Fenster beginnt exakt auf dem Zeitpunkt (obere Grenze) + Assert.True(repo.Exists(1, "tok", t.AddSeconds(-2), t)); + } + + [Fact] + public void GetByTraderSince_is_inclusive_on_cutoff() + { + var repo = HistoryRepo(); + var cutoff = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc); + repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = cutoff }); // genau am Cutoff -> drin + repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = cutoff.AddTicks(-1) }); // knapp davor -> raus + + Assert.Single(repo.GetByTraderSince(1, cutoff)); + } + } +}