# Konzept: Mehrere Handelsplattformen (Polymarket US, später Kalshi) **Angelegt am 16.09.2026.** Planungsstand und Reihenfolge stehen im Deploymentcenter (Items #622–#629), nicht hier. Diese Datei ist die Bauanleitung und die Begründung. --- ## 1. Anlass und Ziel Ein Kunde mit Wohnsitz in den USA soll PolyTrader nutzen. Er darf polymarket.com nicht verwenden (US-IPs gesperrt), wohl aber **Polymarket US** — eine eigene, CFTC-regulierte Börse (QCX LLC als Designated Contract Market, QC Clearing LLC als Clearingstelle) mit KYC. Der Kunde kann KYC machen. Polymarket US ist technisch **ein anderer Handelsplatz**, nicht eine Variante desselben. Keine Zeile der heutigen CLOB-Anbindung ist dort verwendbar (siehe Abschnitt 2). Daraus folgen drei Ziele: 1. **Ein zweiter Plattform-Adapter** für Polymarket US. 2. **Plattform als Konzept im Core**, sodass ein dritter Handelsplatz (Kalshi) ein weiterer Adapter ist und kein weiterer Umbau. 3. **Module erklären, welche Plattformen sie bedienen können** — sichtbar in Desktop und WebUI und wirksam im Handelspfad. Strategien dürfen sich je Plattform unterscheiden. --- ## 2. Was Polymarket US anders macht Quelle: offizielle Doku `https://docs.polymarket.us` (Stand 16.09.2026). Vor dem Bau erneut prüfen — die Plattform ist jung und ändert sich (Gebühren wurden am 16.09.2026 angehoben). | | Polymarket Global (heute) | Polymarket US | |---|---|---| | Endpunkte | CLOB, Gamma, Data-API getrennt | `api.polymarket.us` (authentifiziert), `gateway.polymarket.us` (öffentlich) | | Echtzeit | CLOB-WSS, Alchemy/Polygon | `wss://api.polymarket.us/v1/ws/private` (Orders, Positionen, Saldo), `/v1/ws/markets` (Buch, Trades; max. 10 Märkte je Verbindung) | | Anmeldung | L1 EIP-712 mit Wallet-Key → L2 HMAC (apiKey/secret/passphrase) | Key-ID + Ed25519-Secret; Header `X-PM-Access-Key`, `X-PM-Timestamp` (ms, ±30 s), `X-PM-Signature` = Base64(Ed25519(`timestamp + method + path`)) | | Konto | Wallet / Proxy-Wallet, kein KYC | KYC-Person (iOS-App), Schlüssel unter `polymarket.us/developer`, Secret wird **nur einmal** angezeigt | | Markt-Identität | conditionId + tokenId je Ausgang | `marketSlug`; Ausgang über `intent` bzw. `outcomeSide` YES/NO | | Order | signierte Order (EIP-712), Preis als Zahl, FOK/GTC/GTD | `POST /v1/orders`: `intent` (`BUY_LONG`, `SELL_LONG`, `BUY_SHORT`, `SELL_SHORT`), `price {value, currency}`, `quantity` oder `cashOrderQty`, TIF `DAY/GTC/GTD/IOC/FOK`, `participateDontInitiate` (Post-only), `manualOrderIndicator`, `synchronousExecution` | | Weitere Order-Wege | — | Modify, Preview, Close-Position, Sammel-Orders, Cancel-All | | Geld | USDC auf Polygon | USD per Karte, ACH, Wire | | Abwicklung | On-Chain-Redeem nötig (#146) | automatisch zu 1 $ / 0 $, kein Redeem | | Gebühr | Taker in bps je Kategorie (`FeeModel`) | `Θ · C · p · (1−p)`; Taker Θ = 0,0695 (ab 16.09.2026), Maker-Rabatt Θ = −0,0125; Banker's Rounding auf Cent | | Limits | eigene je Endpunkt | 20 Anfragen/s je Schlüssel bzw. je IP; 429 → Backoff. „Global Rate Limit Exceeded" nach 5 s ist Latenzschutz, **kein** Limit | | Testumgebung | — | Retail-API: keine dokumentierte Sandbox. Preprod nur für die Exchange-API (OAuth), Zugang über onboarding@polymarket.us | | Liquidität, Marktangebot | globaler Pool, voll | eigener US-Pool, kleineres Angebot | **Zwei Folgen, die das Design bestimmen:** - Die **Gebührenformel** ist eine andere *Form*, nicht nur ein anderer Satz. Ein „Satz in bps" reicht als Modell nicht. Nahe 0,95 ist die US-Gebühr winzig, bei 0,50 am höchsten. - **Fremde Wallets sind nicht sichtbar.** Alles, was CopyTrading heute beobachtet (Polygon-Logs, Data-API-Activity), gibt es für den US-Pool nicht. --- ## 3. Ist-Zustand im Code PolyTrader ist modular (Core + Module), aber **Polymarket Global ist überall stillschweigend vorausgesetzt**: | Stelle | Bindung | |---|---| | [`IClobClient`](../../../src/PolyTrader.Core/Services/IClobClient.cs) | `tokenId`, `isNegRisk`, `actualFeeBps`, Decimals-Overrides — CLOB-Begriffe im Vertrag | | [`PolymarketClobClient`](../../../src/PolyTrader.Core/Services/PolymarketClobClient.cs), [`PolymarketApiService`](../../../src/PolyTrader.Core/Services/PolymarketApiService.cs) | direkt von Modulen aufgelöst; Registrierung von `IClobClient` sogar im CopyTrading-Modul | | [`AccountState`](../../../src/PolyTrader.Core/Models/AccountState.cs) | `WalletAddress`, `ApiPassphrase`, `PrivateKey` — Global-Zugangsdaten als feste Felder | | [`Position`](../../../src/PolyTrader.Core/Models/Position.cs), `core_positions` | Primärschlüssel `(AccountId, IsDemo, TokenId)` | | [`MarketData`](../../../src/PolyTrader.Core/Models/MarketData.cs), `core_markets` | `ConditionId`, `ClobTokenIds`, `NegRisk` | | [`FeeModel`](../../../src/PolyTrader.Core/Trading/FeeModel.cs) | statisch, bps je Kategorie | | `Streaming/AlchemyWssClient`, `BlockchainWss` | Polygon | | CopyTrading | 10 Dateien gegen CLOB/API/Chain | | ResolutionFarming | hat schon `IFarmingMarketSource`, `IMarketResolutionSource` — **gutes Vorbild** | | Accounting | hat schon `IActivitySource`, `ITransferSource`, `IBalanceAnchorSource` — ebenso | | Supervisor | nur `CounterfactualJob` liest über die API | `TokenId` kommt in 73 Dateien vor. Ein Umbau in einem Schritt ist ausgeschlossen. --- ## 4. Leitentscheidungen 1. **Die Plattform gehört zum Konto, nicht zur Installation.** Eine Installation kann Konten mehrerer Plattformen führen. Ob der Netzweg das zulässt (US-Standort gegen US-IP-Sperre von polymarket.com), ist eine Betriebsfrage (#622), keine Architekturfrage. 2. **Der Global-CLOB-Code wird umhüllt, nicht umgebaut.** Signierung, Betragsrechnung und Orderablauf von `PolymarketClobClient` bleiben Zeile für Zeile, wie sie sind. Der Global-Adapter übersetzt nur zwischen neutralem Vertrag und bestehendem Client. Das ist die direkte Anwendung von `.agents/rules/clob.md`. 3. **Module sprechen gegen Plattform-Verträge**, nicht gegen `Polymarket*`-Klassen. Das Muster existiert schon in ResolutionFarming und Accounting und wird verallgemeinert. 4. **Logik fragt nach Fähigkeiten, nicht nach Namen.** `if (caps.PublicTraderActivity)` statt `if (platform == PolymarketGlobal)`. Ein neuer Handelsplatz bringt seine Fähigkeiten mit, und bestehende Module entscheiden richtig, ohne ihn zu kennen. 5. **Module erklären ihre Plattformen ausdrücklich.** Fähigkeiten allein reichen nicht: Dass eine Plattform alles Nötige *hat*, heißt nicht, dass die Strategie dort *geprüft* ist. Die Erklärung ist eine Freigabe, die Fähigkeiten sind eine Plausibilitätsprüfung. 6. **Strategievarianten leben im Modul**, als Profil je Plattform — kein „ResolutionFarmingUS"-Modul. Die fachliche Idee bleibt eine; Gebühren, Schwellen und Marktquelle unterscheiden sich. 7. **Global bleibt die Vorgabe.** Jede neue Spalte bekommt `polymarket-global` als Standardwert. Bestehende Installationen merken nach der Migration nichts. --- ## 5. Architektur ### 5.1 Plattform und Fähigkeiten ```csharp namespace PolyTrader.Core.Platforms; /// Stabile Kennung, wird in der Datenbank gespeichert – nie umbenennen. public readonly record struct PlatformId(string Value) { public static readonly PlatformId PolymarketGlobal = new("polymarket-global"); public static readonly PlatformId PolymarketUs = new("polymarket-us"); public static readonly PlatformId Kalshi = new("kalshi"); } public sealed record PlatformCapabilities( bool PublicTraderActivity, // fremde Trades beobachtbar (CopyTrading) bool OnChainSettlement, // Redeem nötig, Chain-Transfers als Buchungsquelle bool PostOnlyOrders, bool OrderModify, // Preis ändern ohne Cancel+Neu bool MakerRebate, bool FractionalQuantity, bool NativeDemoEnvironment, // echte Testumgebung der Plattform IReadOnlySet TimeInForce, string SettlementCurrency); // "USDC", "USD" ``` ### 5.2 Verträge Neutral formuliert, je Plattform einmal implementiert. Aufgelöst über eine Registry, nie direkt. | Vertrag | Aufgabe | Global-Adapter hüllt | US-Adapter spricht | |---|---|---|---| | `IMarketCatalog` | Märkte suchen, nach Enddatum/Kategorie filtern, Auflösungsstatus | Gamma-API, `MarketSyncService` | `gateway` Markets/Events/Settlement | | `IOrderBookSource` | Buch, BBO | CLOB `/book` | `gateway` Book/BBO | | `IOrderGateway` | Platzieren, Ändern, Canceln, offene Orders | `PolymarketClobClient` | `POST/…/v1/orders` | | `IPortfolioSource` | Positionen, Saldo, Aktivität | Data-API, Chain | Portfolio, Account-Balances | | `IMarketStream` / `IAccountStream` | Echtzeit | CLOB-WSS, Alchemy | `/v1/ws/markets`, `/v1/ws/private` | | `IFeeModel` | Gebühr für (Preis, Menge, Rolle) | heutiges `FeeModel` | `Θ·C·p·(1−p)` | | `ISettlementHandler` | Was nach Auflösung zu tun ist | Redeem (#146) | nichts, nur Abgleich | | `ITraderActivitySource` | fremde Trades | Polygon, Data-API | **nicht vorhanden** | > **Stand nach #623 (18.09.2026):** Gebaut sind `IMarketCatalog`, `IOrderBookSource`, `IOrderGateway`, > `IPortfolioSource`, `IFeeModel`, gebündelt in `IPlatformAdapter` (`src/PolyTrader.Core/Platforms/`). > Die Streams, `ISettlementHandler` und `ITraderActivitySource` entstehen **mit ihrem ersten zweiten > Nutzer**, nicht auf Vorrat: Streams mit #625, Abwicklung mit #146 bzw. #625, Trader-Aktivität mit #628. > Die Registry löst den ganzen Adapter auf (`registry.ForAccount(account).Orders`), nicht einzelne > Verträge per Typ. ```csharp public interface IPlatformRegistry { IReadOnlyList Available { get; } PlatformCapabilities Capabilities(PlatformId p); T Get(PlatformId p) where T : class; // wirft, wenn Plattform den Vertrag nicht hat bool TryGet(PlatformId p, out T? service) where T : class; } ``` `IClobClient` bleibt bestehen, bis CopyTrading umgestellt ist; `IOrderGateway` für Global delegiert darauf. Es gibt keinen Tag, an dem beides gleichzeitig umgeschrieben wird. ### 5.3 Instrument und Order ```csharp /// Ein handelbarer Ausgang auf einer Plattform. Key ist innerhalb der Plattform eindeutig. /// Global: Key = tokenId, MarketKey = conditionId /// US: Key = "{marketSlug}:YES", MarketKey = marketSlug /// Kalshi: Key = "{ticker}:YES", MarketKey = ticker /// Gleichheit nur über (Platform, Key). public sealed record InstrumentRef(PlatformId Platform, string Key, string MarketKey = "", string Outcome = ""); public sealed record OrderRequest( InstrumentRef Instrument, OrderSide Side, // Buy / Sell (US: → intent BUY_LONG usw.) decimal LimitPrice, // immer Wahrscheinlichkeit 0..1 – Adapter rechnet um (Kalshi: Cent) decimal? Quantity, // Kontrakte/Shares decimal? NotionalUsd, // alternativ Betrag TimeInForce Tif, bool PostOnly, string OriginModule, // für die Plattform-Sperre (5.6) und das Journal string CorrelationId); ``` Plattformspezifisches (Global: `negRisk`, Tick-Size, Decimals; US: `manualOrderIndicator`, `synchronousExecution`) bleibt **im Adapter**. Der US-Adapter setzt `manualOrderIndicator` fest auf `AUTOMATIC` — ein Bot, der sich als manuell ausgibt, ist ein Compliance-Fehler, keine Einstellung. ### 5.4 Konten und Zugangsdaten - `core_accounts.platform` (Vorgabe `polymarket-global`). - Zugangsdaten je Plattform, verschlüsselt wie F1 (`EncryptedStringConverter`): - Global: wie heute (Wallet, Key, Secret, Passphrase, PrivateKey). - US: Key-ID in `ApiKey`, Ed25519-Secret in `ApiSecret`. `WalletAddress`, `PrivateKey`, `PayoutAddress` leer. - Kalshi: Key-ID + privater Schlüssel. Die bestehenden Felder reichen für US und Kalshi. Eine eigene Tabelle je Plattform wäre sauberer, kostet aber eine zweite Migration der F1-Verschlüsselung. **Entscheidung: vorhandene Felder wiederverwenden**, die Kontenansicht zeigt je Plattform nur die passenden Felder mit passender Beschriftung. Neu prüfen, falls eine Plattform mehr als vier Geheimnisse braucht. - Die Passwortsperre vor Konten und Geheimnissen (#567) gilt unverändert; Geheimnisse bleiben Desktop-only (WebUI-Parität). ### 5.5 Datenbank | Tabelle | Änderung | |---|---| | `core_accounts` | `+ platform` | | `core_positions` | **keine Änderung** (Entscheidung in #623): Eine Position gehört zu einem Konto, das Konto zu genau einer Plattform. `(AccountId, IsDemo, TokenId)` bleibt eindeutig, solange `TokenId` den plattformeindeutigen `InstrumentRef.Key` hält. Eine eigene Spalte wäre eine zweite Wahrheit, die auseinanderlaufen kann | | `core_markets` | `+ platform`; Schlüssel `(Platform, Id)` | | Modultabellen mit Token-Bezug (`ct_`, `rf_`, `acc_`) | `+ platform` mit Vorgabe, je Modul im jeweiligen Schritt | `Position.TokenId` bleibt, wie es ist, und hält `InstrumentRef.Key`. Die 73 Fundstellen ziehen schrittweise um, statt in einem Commit. Die lesenden Methoden von `IMarketRepository` (`GetById`, `FindByTokenId`, `GetActive`) liefern seit #623 nur Global-Märkte — ihre Aufrufer arbeiten mit Gamma-IDs und Token-IDs. **Migration `AddPlatformColumns`:** Es gibt keine automatischen Migrationen beim Start. Vor dem ersten Start einer Fassung mit #623 muss auf dem Server `dotnet ef database update --project src/PolyTrader.Core` laufen, sonst scheitert jede Abfrage an der fehlenden Spalte `Platform`. ### 5.6 Module erklären ihre Plattformen („Auslöser") Erweiterung von [`IPolyTraderModule`](../../../src/PolyTrader.Core/Modularity/IPolyTraderModule.cs), abwärtskompatibel über Default-Implementierungen: ```csharp /// Plattformen, für die dieses Modul geprüft und freigegeben ist. /// Vorgabe: nur Polymarket Global – ein Modul, das nichts erklärt, verhält sich wie heute. IReadOnlySet SupportedPlatforms => new HashSet { PlatformId.PolymarketGlobal }; /// Grund, warum das Modul eine erklärte Plattform in dieser Installation trotzdem nicht bedienen kann /// (fehlende Fähigkeit, fehlender Schlüssel). null = bedienbar. string? GetPlatformBlocker(PlatformId platform, PlatformCapabilities caps) => null; ``` **Eine Stelle für die Regeln.** Wie `ModuleActivationView` seit #560 liegt die Auswertung im Core (`ModulePlatformView`), und Desktop und WebUI zeigen dasselbe: | Wo | Was man sieht | |---|---| | Modul-Tab (Desktop, WebUI) | je Modul Plattform-Kennzeichen: ✓ bedienbar, ⚠ erklärt aber blockiert (mit Grund), — nicht erklärt | | Kontenansicht | je Konto die Module, die es bedienen | | Dashboard-Warnung | aktives Konto, das kein aktives Modul bedient; aktives Modul ohne passendes Konto | **Wirksam an zwei Stellen:** 1. **Zuteilung:** Ein Modul bekommt nur Konten, deren Plattform es erklärt und nicht blockiert. Module filtern nicht selbst — sie fragen den Core nach „meinen Konten". 2. **Handelspfad:** Die Registry reicht jedem Modul ein `IOrderGateway`, das `OriginModule` prüft und eine Order für eine nicht erklärte Plattform **ablehnt und ins Journal schreibt**. Das ist die zweite Sicherung nach dem Muster von `TradingGate` (#570): Eine Sperre, die nur in der Oberfläche existiert, hält keinen Programmierfehler auf. **Erste Erklärungen:** | Modul | Global | US | Begründung | |---|---|---|---| | CopyTrading | ✓ | — | braucht `PublicTraderActivity`; plattformübergreifend siehe #628 | | ResolutionFarming | ✓ | ✓ (ab #626) | nur eigene Marktdaten nötig, US-Gebühr nahe 1 sehr klein | | Accounting | ✓ | ✓ (ab #627) | Quellen je Plattform | | Supervisor | ✓ | ✓ | liest Journal und Dossiers, handelt nicht; `CounterfactualJob` über `IMarketCatalog` | ### 5.7 Strategieprofile je Plattform Ein Modul, das mehrere Plattformen erklärt, hält Einstellungen **je Plattform** (`rf_settings.platform`). Das Profil legt fest: - Gebührenmodell (aus der Registry, nie hart kodiert) — betrifft jeden Netto-Edge-Check. - Schwellen (Einstiegspreis, Mindest-Edge, Mindestliquidität). Bei getrenntem, kleinerem US-Pool sind Global-Werte **nicht** übertragbar; Startwerte stammen aus der Messphase. - Order-Stil: auf US lohnt Post-only wegen des Maker-Rabatts; auf Global zahlt der Maker 0. - Abwicklung: Global mit Redeem, US ohne. Die fachliche Logik (Kandidatenbewertung, Sizing, Risikogrenzen) bleibt gemeinsam und testbar ohne Plattform. ### 5.8 Demo-Modus Demo bleibt eine Eigenschaft des Kontos (`IsDemo`). Demo-Fills rechnen gegen **das Buch der Plattform des Kontos**, mit deren Gebührenmodell. Da die US-Retail-API keine Sandbox hat, ist Demo gegen das echte US-Buch der einzige Weg zur Messphase — und damit Pflicht vor dem Livegang. --- ## 6. Umsetzungsschritte | Item | Schritt | Voraussetzung | Ergebnis | |---|---|---|---| | #622 | **PL-0** Voraussetzungen US-Kunde | — | KYC, API-Schlüssel, Serverstandort, Rolle geklärt | | #623 | **PL-1** Plattform-Fundament | — | Verträge, Registry, Global-Adapter, DB-Spalten; **Verhalten unverändert** | | #624 | **PL-2** Plattform-Erklärung der Module | #623 | Anzeige + Zuteilung + Sperre im Gateway | | #625 | **PL-3** Adapter Polymarket US | #623; für echte Orders #622 | erst lesend, dann Demo, dann Orders | | #626 | **PL-4** ResolutionFarming für US | #624, #625 | Profil je Plattform, Messphase | | #627 | **PL-5** Accounting für US | #625 | US-Quellen statt Data-API/Chain | | #628 | **PL-6** CopyTrading plattformübergreifend | #625 + Entscheidung | nicht beschlossen | | #629 | **PL-7** Adapter Kalshi | #623 + konkreter Bedarf | nicht beschlossen | ### Akzeptanzkriterien **PL-1** - Alle bestehenden Tests grün, ohne Anpassung ihrer Erwartungen. - Diff an `PolymarketClobClient.cs` ist leer. Backup trotzdem vor Beginn (Regel). - Migration auf einer Kopie der Server-Datenbank: Bestandspositionen haben danach `platform = polymarket-global` und denselben `OutcomeKey` wie vorher `TokenId`. - Neue Tests: Registry löst je Plattform auf; Global-Adapter reicht Aufrufe 1:1 an den Client durch. **PL-2** - Ein Modul ohne Erklärung verhält sich exakt wie heute. - Test: Order über das Gateway mit nicht erklärter Plattform → abgelehnt, Journal-Eintrag, keine HTTP-Anfrage. - Desktop und WebUI zeigen für dieselbe Konfiguration dieselben Kennzeichen (ein gemeinsamer Test gegen `ModulePlatformView`). **PL-3** - Signatur gegen die Beispielwerte der Doku geprüft (Einheitentest mit festem Schlüssel und Zeitstempel). - Lesend gegen Produktion: Märkte, Buch, Positionen, Saldo des Kundenkontos. - 429 → Backoff; „Global Rate Limit Exceeded" → **kein** Drosseln (siehe Abschnitt 2). - Secret nie im Log, nie im WebUI, verschlüsselt at-rest. - Erste echte Order erst nach PL-4-Messphase und mit Freigabe durch Richard. **PL-4** - Netto-Edge-Check nutzt das US-Gebührenmodell (Test mit p = 0,50 und p = 0,97). - Demo-Messphase mit definiertem Zeitraum und Auswertung, bevor Live freigegeben wird. --- ## 7. Ausblick Kalshi Kalshi ist der Prüfstein für PL-1: Lässt sich der Adapter bauen, **ohne ein Modul anzufassen**, trägt die Abstraktion. Bekannte Unterschiede (vor dem Bau gegen die aktuelle Kalshi-Doku prüfen): eigene Anmeldung mit signierten Anfragen und privatem Schlüssel, Märkte über Ticker, Preise in Cent, eigene Gebührenformel, eigene Demo-Umgebung. Genau deshalb ist `LimitPrice` im Vertrag eine Wahrscheinlichkeit 0..1 und die Umrechnung Sache des Adapters. --- ## 8. Offene Fragen | Frage | Wer | Item | |---|---|---| | Gibt es für die Retail-API eine Testumgebung? | Anfrage an Polymarket US | #622 | | Wo läuft die Installation des US-Kunden, und darf sie zugleich Global-Konten führen? | Richard | #622 | | Rechtliche Einordnung unserer Rolle bei automatisiertem Handel auf einem fremden KYC-Konto | Richard / Rechtsberatung | #622 | | Lohnt CopyTrading mit Global-Signal und US-Ausführung trotz anderer Kurse? | Entscheidung | #628 | | Reihenfolge gegenüber Themenbereich A (Abnahme, #621 Betriebsfreigabe) | Richard | — |