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 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-09 11:53:12 +02:00
co-authored by Claude Opus 4.8
parent bb929fa23a
commit ad8f7b0d03
6 changed files with 108 additions and 3 deletions
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.Tests.Fakes
{
/// <summary>
/// In-Memory-Stub für <see cref="IClobClient"/>. 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).
/// </summary>
public sealed class FakeClobClient : IClobClient
{
public sealed record PlacedOrder(string TokenId, string Side, decimal Usdc, decimal Price, string OrderType);
// Aufzeichnungen
public List<PlacedOrder> Placed { get; } = new();
public List<string> CanceledOrderIds { get; } = new();
public List<(string AssetId, decimal NewPrice, string Side)> ConflictCancels { get; } = new();
// Steuerbare Antworten
/// <summary>Ergebnis von PlaceOrderAsync (Default "OK"). Queue hat Vorrang, sonst dieser Wert.</summary>
public string PlaceResult { get; set; } = "OK";
public Queue<string> PlaceResults { get; } = new();
public bool CancelResult { get; set; } = true;
/// <summary>Offene Orders je Asset, die GetOpenOrdersAsync zurückgibt.</summary>
public Dictionary<string, List<(string Id, string Side, decimal Price)>> OpenOrdersByAsset { get; } = new();
public Task<string> 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<List<(string Id, string Side, decimal Price)>> 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<bool> CancelOrderAsync(AccountState acc, string orderId)
{
CanceledOrderIds.Add(orderId);
return Task.FromResult(CancelResult);
}
}
}