# 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) ```