Ein Kunde mit US-Wohnsitz soll ueber Polymarket US handeln - eine eigene, CFTC-regulierte Boerse, an der die heutige CLOB-Anbindung nicht verwendbar ist. Das Konzept macht Plattform zu einem Begriff im Core, laesst Module ihre Plattformen erklaeren und haelt Strategievarianten je Plattform im Modul. Begruendungen zu #622-#629 in docs/ROADMAP.md, Bauanleitung in docs/archiv/konzepte/KONZEPT-Multi-Plattform.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
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:
- Ein zweiter Plattform-Adapter für Polymarket US.
- Plattform als Konzept im Core, sodass ein dritter Handelsplatz (Kalshi) ein weiterer Adapter ist und kein weiterer Umbau.
- 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 |
tokenId, isNegRisk, actualFeeBps, Decimals-Overrides — CLOB-Begriffe im Vertrag |
PolymarketClobClient, PolymarketApiService |
direkt von Modulen aufgelöst; Registrierung von IClobClient sogar im CopyTrading-Modul |
AccountState |
WalletAddress, ApiPassphrase, PrivateKey — Global-Zugangsdaten als feste Felder |
Position, core_positions |
Primärschlüssel (AccountId, IsDemo, TokenId) |
MarketData, core_markets |
ConditionId, ClobTokenIds, NegRisk |
FeeModel |
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
- 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.
- Der Global-CLOB-Code wird umhüllt, nicht umgebaut. Signierung, Betragsrechnung und
Orderablauf von
PolymarketClobClientbleiben 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. - Module sprechen gegen Plattform-Verträge, nicht gegen
Polymarket*-Klassen. Das Muster existiert schon in ResolutionFarming und Accounting und wird verallgemeinert. - Logik fragt nach Fähigkeiten, nicht nach Namen.
if (caps.PublicTraderActivity)stattif (platform == PolymarketGlobal). Ein neuer Handelsplatz bringt seine Fähigkeiten mit, und bestehende Module entscheiden richtig, ohne ihn zu kennen. - 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.
- Strategievarianten leben im Modul, als Profil je Plattform — kein „ResolutionFarmingUS"-Modul. Die fachliche Idee bleibt eine; Gebühren, Schwellen und Marktquelle unterscheiden sich.
- Global bleibt die Vorgabe. Jede neue Spalte bekommt
polymarket-globalals Standardwert. Bestehende Installationen merken nach der Migration nichts.
5. Architektur
5.1 Plattform und Fähigkeiten
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> 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 inIPlatformAdapter(src/PolyTrader.Core/Platforms/). Die Streams,ISettlementHandlerundITraderActivitySourceentstehen 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.
public interface IPlatformRegistry
{
IReadOnlyList<PlatformId> Available { get; }
PlatformCapabilities Capabilities(PlatformId p);
T Get<T>(PlatformId p) where T : class; // wirft, wenn Plattform den Vertrag nicht hat
bool TryGet<T>(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
/// 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(Vorgabepolymarket-global). -
Zugangsdaten je Plattform, verschlüsselt wie F1 (
EncryptedStringConverter):- Global: wie heute (Wallet, Key, Secret, Passphrase, PrivateKey).
- US: Key-ID in
ApiKey, Ed25519-Secret inApiSecret.WalletAddress,PrivateKey,PayoutAddressleer. - 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,
abwärtskompatibel über Default-Implementierungen:
/// 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<PlatformId> SupportedPlatforms => new HashSet<PlatformId> { 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:
- 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".
- Handelspfad: Die Registry reicht jedem Modul ein
IOrderGateway, dasOriginModuleprüft und eine Order für eine nicht erklärte Plattform ablehnt und ins Journal schreibt. Das ist die zweite Sicherung nach dem Muster vonTradingGate(#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.csist leer. Backup trotzdem vor Beginn (Regel). - Migration auf einer Kopie der Server-Datenbank: Bestandspositionen haben danach
platform = polymarket-globalund denselbenOutcomeKeywie vorherTokenId. - 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 | — |