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);
+ }
+ }
+}