using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using PolyTrader.Modules.ResolutionFarming.Models; using PolyTrader.Modules.ResolutionFarming.Persistence.Ef; using PolyTrader.Modules.ResolutionFarming.Services; using PolyTrader.Tests.TestSupport; using PolyTraderSharp.Services; using Xunit; namespace PolyTrader.Tests { /// /// Tests der Scanner-Bewertung (pure Filterkette inkl. Reject-Gründe) und der Orchestrierung /// (Marktquelle → Bewertung → Persistenz) über eine Fake-Marktquelle + reales InMemory-Repo. /// public class MarketScannerServiceTests { private static RfSettings DefaultSettings() => new() { AccountId = 1, MinPrice = 0.90m, MaxPrice = 0.98m, MaxHoursToResolution = 48, CategoryWhitelistCsv = "Sports,Politics,Geopolitics", BlacklistCsv = "", MinEdgePct = 1.5m }; private static ScannedMarket Market(decimal ask, string category = "Sports", double hours = 24, string slug = "match-1", string tags = "", int feeBps = 0) => new() { TokenId = "tok", MarketSlug = slug, EventSlug = "event-1", MarketQuestion = "Frage?", Outcome = "Yes", Category = category, Tags = tags, Ask = ask, FeeBps = feeBps, HoursToResolution = hours, EndDate = DateTime.UtcNow.AddHours(hours) }; // ----- Evaluate (pure) ----- [Fact] public void Evaluate_accepts_a_good_favorite() { var c = MarketScannerService.Evaluate(1, Market(0.95m), DefaultSettings()); Assert.True(c.Accepted); Assert.Equal("", c.RejectReason); Assert.Equal("event-1", c.ClusterKey); Assert.True(c.NetEdgePct > 0m); } [Fact] public void Evaluate_rejects_out_of_price_band() { var c = MarketScannerService.Evaluate(1, Market(0.85m), DefaultSettings()); Assert.False(c.Accepted); Assert.Contains("Band", c.RejectReason); } [Fact] public void Evaluate_rejects_category_not_whitelisted() { var c = MarketScannerService.Evaluate(1, Market(0.95m, category: "Crypto"), DefaultSettings()); Assert.False(c.Accepted); Assert.Contains("Whitelist", c.RejectReason); } [Fact] public void Evaluate_rejects_blacklisted_market() { var s = DefaultSettings(); s.BlacklistCsv = "dispute"; var c = MarketScannerService.Evaluate(1, Market(0.95m, slug: "x-dispute"), s); Assert.False(c.Accepted); Assert.Equal("Blacklist", c.RejectReason); } [Fact] public void Evaluate_rejects_outside_resolution_window() { var c = MarketScannerService.Evaluate(1, Market(0.95m, hours: 100), DefaultSettings()); Assert.False(c.Accepted); Assert.Contains("Auflösung", c.RejectReason); } [Fact] public void Evaluate_rejects_insufficient_net_edge() { // Ask 0.98 (im Band), Sports-Fee 75 bps -> Netto-Edge ~1.29% < 1.5%. var c = MarketScannerService.Evaluate(1, Market(0.98m), DefaultSettings()); Assert.False(c.Accepted); Assert.Contains("Netto-Edge", c.RejectReason); } // ----- Orchestrierung ----- private sealed class FakeSource : IFarmingMarketSource { private readonly List _markets; public FakeSource(params ScannedMarket[] markets) => _markets = new List(markets); public Task> GetNearResolutionFavoritesAsync(RfSettings s, CancellationToken ct) => Task.FromResult>(_markets); } [Fact] public async Task ScanAccount_persists_accepted_and_rejected_candidates() { var factory = new InMemoryContextFactory(o => new ResolutionFarmingDbContext(o)); var candRepo = new EfRfCandidateRepository(factory); var settingsRepo = new EfRfSettingsRepository(factory); var source = new FakeSource( Market(0.95m, slug: "good"), // akzeptiert Market(0.80m, slug: "too-cheap")); // abgelehnt (Preisband) var svc = new MarketScannerService(source, candRepo, settingsRepo, new TerminalLogger()); var result = await svc.ScanAccountAsync(1, DefaultSettings(), CancellationToken.None); Assert.Equal(2, result.Count); var stored = candRepo.GetRecent(1, 10); Assert.Equal(2, stored.Count); Assert.Contains(stored, c => c.Accepted && c.MarketSlug == "good"); Assert.Contains(stored, c => !c.Accepted && c.MarketSlug == "too-cheap"); } } }