From ad8f7b0d03b073e98ae989a1cb4d8669dbac2b16 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 9 Jul 2026 11:53:12 +0200 Subject: [PATCH] Slice 0 (Fable-Fixes): IClobClient-Seam fuer testbare CLOB-Interaktionen Verhaltensneutrale Testinfrastruktur als Grundlage fuer Slice 1/2: - IClobClient-Interface (Core) ueber die von Leiter/Reconciliation genutzten CLOB-Methoden (Place/CancelConflicting/GetOpenOrders/CancelOrder). - PolymarketClobClient implementiert IClobClient (Signaturen unveraendert). - DI-Seam im Modul: IClobClient -> PolymarketClobClient-Singleton. - SellLadderService haengt jetzt an IClobClient (statische CalculateExactOrderAmounts bleibt am konkreten Typ). - InternalsVisibleTo(PolyTrader.Tests) + FakeClobClient-Test-Double. Verifikation: Build 0 Fehler, 207 Tests gruen, --smoke-ui Container-Aufbau ok. Co-Authored-By: Claude Opus 4.8 --- src/PolyTrader.Core/Services/IClobClient.cs | 34 +++++++++++ .../Services/PolymarketClobClient.cs | 2 +- .../CopyTradingModule.cs | 4 ++ .../PolyTrader.Modules.CopyTrading.csproj | 7 +++ .../Services/SellLadderService.cs | 4 +- .../PolyTrader.Tests/Fakes/FakeClobClient.cs | 60 +++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/PolyTrader.Core/Services/IClobClient.cs create mode 100644 tests/PolyTrader.Tests/Fakes/FakeClobClient.cs diff --git a/src/PolyTrader.Core/Services/IClobClient.cs b/src/PolyTrader.Core/Services/IClobClient.cs new file mode 100644 index 0000000..6ce6b5c --- /dev/null +++ b/src/PolyTrader.Core/Services/IClobClient.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PolyTraderSharp.Models; + +namespace PolyTraderSharp.Services +{ + /// + /// Schmales Interface über die CLOB-Order-Operationen, die die SELL-Eskalationsleiter und die + /// Startup-Reconciliation nutzen. Zweck: Diese geldkritischen Service-Interaktionen (Leiter vs. + /// Cleanup, Neustart-Cancel) über einen gemockten Client integrationstestbar machen, ohne den + /// echten anzufassen. + /// + /// Bewusst minimal – nur die tatsächlich von den Modul-Services aufgerufenen Methoden. Die + /// Preis-/Mengenberechnung bleibt als statische, reine Methode + /// () außerhalb des Interfaces. + /// + public interface IClobClient + { + /// Platziert eine Order und liefert "OK" oder eine Fehlerbeschreibung. + Task PlaceOrderAsync( + AccountState account, string tokenId, string sideStr, decimal investAmountUsd, decimal limitPrice, + string orderType = "FOK", bool debugPayloadLog = false, bool isNegRisk = false, int actualFeeBps = 0, + decimal? overrideTickSize = null, int? overrideMakerDecimals = null, int? overrideTakerDecimals = null); + + /// Cancelt konfligierende offene Orders desselben Tokens vor dem Nachpreisen/Platzieren. + Task CancelConflictingOrdersAsync(AccountState acc, string assetId, decimal newPrice, string sideStr); + + /// Liefert die offenen Orders (Id, Side, Price) für ein Asset. + Task> GetOpenOrdersAsync(AccountState acc, string assetId); + + /// Cancelt eine einzelne Order per OrderId; true bei Erfolg. + Task CancelOrderAsync(AccountState acc, string orderId); + } +} diff --git a/src/PolyTrader.Core/Services/PolymarketClobClient.cs b/src/PolyTrader.Core/Services/PolymarketClobClient.cs index 67ca37f..7947422 100644 --- a/src/PolyTrader.Core/Services/PolymarketClobClient.cs +++ b/src/PolyTrader.Core/Services/PolymarketClobClient.cs @@ -100,7 +100,7 @@ namespace PolyTraderSharp.Services public byte SignatureType { get; set; } } - public class PolymarketClobClient + public class PolymarketClobClient : IClobClient { private readonly HttpClient _httpClient; private readonly TerminalLogger _logger; diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs index ea449f3..52c7b51 100644 --- a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs @@ -66,6 +66,10 @@ namespace PolyTrader.Modules.CopyTrading services.AddHostedService(); services.AddHostedService(); + // IClobClient-Seam: Leiter/Reconciliation hängen am Interface (mockbar für Integrationstests); + // die Live-Instanz ist der eine PolymarketClobClient-Singleton aus dem App-Container. + services.AddSingleton(sp => sp.GetRequiredService()); + // Phase 0.1: SELL-Eskalationsleiter (preist offene Exit-Limits stufenweise nach). // Singleton + Hosted, damit Engine und TraderMonitor StartLadderAsync aufrufen können. services.AddSingleton(); diff --git a/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj b/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj index b691065..78298e6 100644 --- a/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj +++ b/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj @@ -4,6 +4,13 @@ + + + + <_Parameter1>PolyTrader.Tests + + + diff --git a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs index 86e2053..cf362a6 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs @@ -27,7 +27,7 @@ namespace PolyTraderSharp.Services private readonly CopyTradingState _copyState; private readonly TradingState _state; - private readonly PolymarketClobClient _clob; + private readonly IClobClient _clob; private readonly TerminalLogger _logger; private readonly ThreemaService _threema; private readonly IPositionRepository _positionRepo; @@ -35,7 +35,7 @@ namespace PolyTraderSharp.Services public SellLadderService( CopyTradingState copyState, TradingState state, - PolymarketClobClient clob, + IClobClient clob, TerminalLogger logger, ThreemaService threema, IPositionRepository positionRepo) diff --git a/tests/PolyTrader.Tests/Fakes/FakeClobClient.cs b/tests/PolyTrader.Tests/Fakes/FakeClobClient.cs new file mode 100644 index 0000000..9efba56 --- /dev/null +++ b/tests/PolyTrader.Tests/Fakes/FakeClobClient.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PolyTraderSharp.Models; +using PolyTraderSharp.Services; + +namespace PolyTrader.Tests.Fakes +{ + /// + /// In-Memory-Stub für . Zeichnet Aufrufe auf und liefert steuerbare + /// Antworten, damit die Service-Interaktionen (SELL-Leiter, Startup-Reconciliation) ohne echten + /// CLOB integrationstestbar sind. Bewusst simpel und synchron (Task.FromResult). + /// + public sealed class FakeClobClient : IClobClient + { + public sealed record PlacedOrder(string TokenId, string Side, decimal Usdc, decimal Price, string OrderType); + + // Aufzeichnungen + public List Placed { get; } = new(); + public List CanceledOrderIds { get; } = new(); + public List<(string AssetId, decimal NewPrice, string Side)> ConflictCancels { get; } = new(); + + // Steuerbare Antworten + /// Ergebnis von PlaceOrderAsync (Default "OK"). Queue hat Vorrang, sonst dieser Wert. + public string PlaceResult { get; set; } = "OK"; + public Queue PlaceResults { get; } = new(); + public bool CancelResult { get; set; } = true; + + /// Offene Orders je Asset, die GetOpenOrdersAsync zurückgibt. + public Dictionary> OpenOrdersByAsset { get; } = new(); + + public Task PlaceOrderAsync( + AccountState account, string tokenId, string sideStr, decimal investAmountUsd, decimal limitPrice, + string orderType = "FOK", bool debugPayloadLog = false, bool isNegRisk = false, int actualFeeBps = 0, + decimal? overrideTickSize = null, int? overrideMakerDecimals = null, int? overrideTakerDecimals = null) + { + Placed.Add(new PlacedOrder(tokenId, sideStr, investAmountUsd, limitPrice, orderType)); + string result = PlaceResults.Count > 0 ? PlaceResults.Dequeue() : PlaceResult; + return Task.FromResult(result); + } + + public Task CancelConflictingOrdersAsync(AccountState acc, string assetId, decimal newPrice, string sideStr) + { + ConflictCancels.Add((assetId, newPrice, sideStr)); + return Task.CompletedTask; + } + + public Task> GetOpenOrdersAsync(AccountState acc, string assetId) + { + if (OpenOrdersByAsset.TryGetValue(assetId, out var list)) + return Task.FromResult(new List<(string, string, decimal)>(list)); + return Task.FromResult(new List<(string, string, decimal)>()); + } + + public Task CancelOrderAsync(AccountState acc, string orderId) + { + CanceledOrderIds.Add(orderId); + return Task.FromResult(CancelResult); + } + } +}