using System; using System.Net.Http; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using PolyTrader.Tests.Fakes; using PolyTraderSharp; using PolyTraderSharp.Models; using PolyTraderSharp.Services; using Xunit; namespace PolyTrader.Tests { /// /// Verifiziert K1a (Fable-Fix): Der Stale-Order-Cleanup darf die ruhende Order einer aktiven /// SELL-Eskalationsleiter NICHT canceln – sonst steckt die Position am Floor fest (Floor-Deadlock). /// public class TraderMonitorServiceTests { private const string Tok = "tok-mon"; private static (TraderMonitorService svc, TradingState state, CopyTradingState copy, FakeClobClient clob) Build() { var state = new TradingState(); var copy = new CopyTradingState(); var clob = new FakeClobClient(); var logger = new TerminalLogger(); var api = new PolymarketApiService(logger, new HttpClient()); var posRepo = new FakePositionRepository(); var marketRepo = new FakeMarketRepository(); var tradeLog = new FakeCopyTradeLogRepository(); var threema = new ThreemaService(logger, new JobManager()); var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo); var signalCh = Channel.CreateUnbounded(); var closedCh = Channel.CreateUnbounded(); var svc = new TraderMonitorService(state, copy, api, clob, signalCh.Writer, closedCh.Writer, logger, posRepo, marketRepo, tradeLog, ladder); state.Accounts[1] = new AccountState { AccountId = 1, Name = "Live", IsDemo = false }; return (svc, state, copy, clob); } [Fact] public async Task Cleanup_does_not_cancel_orders_of_active_ladder() { var (svc, state, copy, clob) = Build(); // Stale (1h alt) getrackte Order + aktive Leiter für denselben Key. copy.PendingOrderTimestamps["1_" + Tok] = (DateTime.UtcNow.AddHours(-1), 7, "SELL"); copy.ExitLadders["1_" + Tok] = new ExitLadderState { AccountId = 1, TokenId = Tok, Floor = 0.40m, CurrentLimit = 0.40m }; clob.OpenOrdersByAsset[Tok] = new() { ("o1", "SELL", 0.40m) }; await svc.CleanupStaleOpenOrdersAsync(CancellationToken.None); Assert.Empty(clob.CanceledOrderIds); // Leiter-Order NICHT gecancelt } [Fact] public async Task Cleanup_cancels_stale_orders_without_ladder() { // Kontrast: ohne aktive Leiter wird die veraltete Order storniert. var (svc, state, copy, clob) = Build(); copy.PendingOrderTimestamps["1_" + Tok] = (DateTime.UtcNow.AddHours(-1), 7, "SELL"); clob.OpenOrdersByAsset[Tok] = new() { ("o1", "SELL", 0.40m) }; await svc.CleanupStaleOpenOrdersAsync(CancellationToken.None); Assert.Contains("o1", clob.CanceledOrderIds); } } }