Phase 7: Mapping-Randfälle + Parser-/Repo-Randfälle (78 Tests)
- DbContextModelTests: feinkörnige Modell-Invarianten - Dezimal-Präzision (18,6) auf Geldfeldern (Core + Settings), MaxLength auf Strings, manuell gesetzte Keys (ValueGeneratedNever), AssignedAccountIds hat Value-Converter + text-Spalte, MarketData JSON-Spalten = text, Indizes (TradeRecord.ClosedAt, MasterTraderHistory TraderId/ClosedAt). (GetColumnType() wirft unter InMemory -> Spaltentyp via Relational:ColumnType- Annotation ausgelesen.) - MongoExportParserTests erweitert: numerische statt String-Dezimale, unbekannte Felder ignoriert, mehrere Dokumente in Reihenfolge, AssignedAccountIds als String-Zahlen, IsActive-Default. - RepositoryEdgeCaseTests: GetAll leer != null, Update-Alias, Factory-Isolation, History-Exists/GetByTraderSince inklusive an den Grenzen. Alle 78 gruen. (Copytrading-Service-Logik bewusst noch nicht getestet - wird parallel ueberarbeitet.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8d2c8094b
commit
92a88e8be8
@@ -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)
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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.)
|
||||||
@@ -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?
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class DbContextModelTests
|
||||||
|
{
|
||||||
|
private static CoreDbContext CoreCtx() =>
|
||||||
|
new InMemoryContextFactory<CoreDbContext>(o => new CoreDbContext(o)).CreateDbContext();
|
||||||
|
|
||||||
|
private static CopyTradingDbContext CtCtx() =>
|
||||||
|
new InMemoryContextFactory<CopyTradingDbContext>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,5 +116,51 @@ namespace PolyTrader.Tests
|
|||||||
Assert.Empty(MongoExportParser.ParseAccounts("[]"));
|
Assert.Empty(MongoExportParser.ParseAccounts("[]"));
|
||||||
Assert.Empty(MongoExportParser.ParseTraders("[]"));
|
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<int> { 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
|
||||||
|
|
||||||
|
private static EfMasterTraderHistoryRepository HistoryRepo() =>
|
||||||
|
new(new InMemoryContextFactory<CopyTradingDbContext>(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user