using System; using System.Linq; using System.Threading.Tasks; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.ResolutionFarming.Models; using PolyTrader.Modules.ResolutionFarming.Persistence; using PolyTraderSharp.Models; using Xunit; namespace PolyTrader.Tests.WebUi { /// /// CopyTrading- und ResolutionFarming-Endpunkte ueber echte HTTP-Aufrufe (#568). /// /// Die Datei gleichen Namens enthielt vorher acht Tests, die keine einzige Zeile Produktionscode /// aufriefen: Sie summierten Listen im Test und verglichen das Ergebnis mit sich selbst. Die /// Absichten sind dieselben geblieben, geprueft wird jetzt die Antwort der Route. /// public class CopyTradingAndResolutionFarmingEndpointsTests { private static Task StartCopyTradingAsync() => WebUiTestHost.StartAsync(WebUiTestHost.AddCopyTradingStore); private static Task StartResolutionFarmingAsync() => WebUiTestHost.StartAsync(WebUiTestHost.AddResolutionFarmingStore); // ---- CopyTrading ----------------------------------------------------- [Fact] public async Task CopyTrading_Trader_anlegen_und_wieder_lesen() { await using var web = await StartCopyTradingAsync(); var (status, created) = await web.PostJsonAsync("/api/modules/copytrading/traders", new { id = 0, displayName = "Whale Alpha", walletAddress = " 0x1234567890abcdef1234567890abcdef12345678 ", category = " ", description = "Top Polymarket Trader", reasoning = "Winrate ueber 70 Prozent", isActive = true, isHidden = false, autoPauseEnabled = true, makerEntry = true, assignedAccountIds = new[] { 1, 2 } }); Assert.Equal(200, status); var trader = created.GetProperty("trader"); Assert.Equal(1, trader.GetProperty("id").GetInt32()); Assert.Equal("NEW_BIG_BET", trader.GetProperty("category").GetString()); // leere Kategorie -> Vorgabe Assert.Equal("0x1234567890abcdef1234567890abcdef12345678", trader.GetProperty("walletAddress").GetString()); var all = await web.GetJsonAsync("/api/modules/copytrading/traders"); var stored = Assert.Single(all.EnumerateArray()); Assert.Equal("Whale Alpha", stored.GetProperty("displayName").GetString()); Assert.True(stored.GetProperty("makerEntry").GetBoolean()); Assert.Equal(new[] { 1, 2 }, stored.GetProperty("assignedAccountIds").EnumerateArray().Select(e => e.GetInt32()).OrderBy(x => x).ToArray()); } [Fact] public async Task CopyTrading_offene_Trades_rechnen_Buchgewinn_je_Position_und_gesamt() { await using var web = await StartCopyTradingAsync(); var acc1 = new AccountState { AccountId = 1, Name = "Konto 1" }; acc1.OpenPositions["tok-1"] = new Position { AccountId = 1, TokenId = "tok-1", SourceTraderId = 1, SourceTraderName = "Whale 1", MarketQuestion = "Will Trump win in 2024?", Outcome = "Yes", EntryPrice = 0.50m, CurrentPrice = 0.70m, Size = 100m, AmountUsd = 50m, CurrentValueUsd = 70m }; var acc2 = new AccountState { AccountId = 2, Name = "Konto 2" }; acc2.OpenPositions["tok-2"] = new Position { AccountId = 2, TokenId = "tok-2", SourceTraderId = 2, SourceTraderName = "Whale 2", MarketQuestion = "Will Fed cut rates?", Outcome = "Yes", EntryPrice = 0.80m, CurrentPrice = 0.60m, Size = 50m, AmountUsd = 40m, CurrentValueUsd = 30m }; web.State.Accounts[1] = acc1; web.State.Accounts[2] = acc2; var open = await web.GetJsonAsync("/api/modules/copytrading/open-trades"); Assert.Equal(2, open.GetProperty("count").GetInt32()); Assert.Equal(90m, open.GetProperty("totalAmount").GetDecimal()); Assert.Equal(10m, open.GetProperty("totalUnrealized").GetDecimal()); // (70-50) + (30-40) var byMarket = open.GetProperty("trades").EnumerateArray() .ToDictionary(t => t.GetProperty("market").GetString()!); Assert.Equal(20m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPnl").GetDecimal()); Assert.Equal(40m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPct").GetDecimal()); Assert.Equal(-10m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPnl").GetDecimal()); Assert.Equal(-25m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPct").GetDecimal()); } [Fact] public async Task CopyTrading_geschlossene_Trades_filtern_nach_Trader_Ergebnis_und_Suche() { await using var web = await StartCopyTradingAsync(); var log = web.Get(); log.Insert(new ClosedTrade { TradeId = 1, AccountId = 1, SourceTraderId = 10, TokenId = "a", MarketQuestion = "Bitcoin reaches 100k?", Outcome = "Yes", RealizedPnl = 35m, ClosedAt = DateTime.UtcNow.AddHours(-2) }); log.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, SourceTraderId = 10, TokenId = "b", MarketQuestion = "Solana reaches 300?", Outcome = "No", RealizedPnl = -12m, ClosedAt = DateTime.UtcNow.AddHours(-5) }); log.Insert(new ClosedTrade { TradeId = 3, AccountId = 2, SourceTraderId = 20, TokenId = "c", MarketQuestion = "Ethereum reaches 5k?", Outcome = "Yes", RealizedPnl = 50m, ClosedAt = DateTime.UtcNow.AddHours(-1) }); var byTrader = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?traderId=10"); Assert.Equal(2, byTrader.GetProperty("totalCount").GetInt32()); var winners = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?result=Gewinner"); Assert.Equal(2, winners.GetProperty("totalCount").GetInt32()); Assert.Equal(85m, winners.GetProperty("summary").GetProperty("totalPnl").GetDecimal()); var search = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?search=bitcoin"); var hit = Assert.Single(search.GetProperty("items").EnumerateArray()); Assert.Equal(1, hit.GetProperty("tradeId").GetInt32()); } [Fact] public async Task CopyTrading_Kontoeinstellungen_ohne_Eintrag_liefern_die_Vorgaben() { await using var web = await StartCopyTradingAsync(); var settings = await web.GetJsonAsync("/api/modules/copytrading/settings/42"); Assert.Equal(42, settings.GetProperty("accountId").GetInt32()); Assert.Equal(5.0m, settings.GetProperty("perMarketLimit").GetDecimal()); Assert.Equal(10.0m, settings.GetProperty("perMasterLimit").GetDecimal()); Assert.Equal(0.98m, settings.GetProperty("maxBuyPrice").GetDecimal()); Assert.Equal(15.0m, settings.GetProperty("sellFloorPct").GetDecimal()); } // ---- ResolutionFarming ----------------------------------------------- [Fact] public async Task ResolutionFarming_Kandidaten_liefern_Annahme_und_Ablehnungsgrund() { await using var web = await StartResolutionFarmingAsync(); web.State.Accounts[1] = new AccountState { AccountId = 1, Name = "Demo", IsDemo = true }; var repo = web.Get(); repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t1", ScannedAt = DateTime.UtcNow, MarketQuestion = "Will candidate A win election?", Outcome = "Yes", Category = "Politics", Ask = 0.94m, NetEdgePct = 2.5m, Accepted = true, RejectReason = "" }); repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t2", ScannedAt = DateTime.UtcNow, MarketQuestion = "Will crypto token reach 100?", Outcome = "Yes", Category = "Crypto", Ask = 0.91m, NetEdgePct = 0.5m, Accepted = false, RejectReason = "Fee too high for edge" }); var result = await web.GetJsonAsync("/api/modules/resolutionfarming/candidates?accountId=1"); Assert.Equal(2, result.GetProperty("count").GetInt32()); var items = result.GetProperty("candidates").EnumerateArray().ToList(); var accepted = Assert.Single(items, c => c.GetProperty("accepted").GetBoolean()); Assert.Equal("Politics", accepted.GetProperty("category").GetString()); var rejected = Assert.Single(items, c => !c.GetProperty("accepted").GetBoolean()); Assert.Equal("Fee too high for edge", rejected.GetProperty("rejectReason").GetString()); } [Fact] public async Task ResolutionFarming_Positionen_summieren_den_Einsatz() { await using var web = await StartResolutionFarmingAsync(); var repo = web.Get(); repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-1", MarketQuestion = "Match 1", EntryPrice = 0.95m, Size = 100m, AmountUsd = 95m, Status = "Open", OpenedAt = DateTime.UtcNow }); repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-2", MarketQuestion = "Match 2", EntryPrice = 0.92m, Size = 50m, AmountUsd = 46m, Status = "Open", OpenedAt = DateTime.UtcNow }); var result = await web.GetJsonAsync("/api/modules/resolutionfarming/positions?accountId=1"); Assert.Equal(2, result.GetProperty("count").GetInt32()); Assert.Equal(141m, result.GetProperty("totalAmount").GetDecimal()); } [Fact] public async Task ResolutionFarming_Historie_rechnet_PnL_und_Trefferquote() { await using var web = await StartResolutionFarmingAsync(); var repo = web.Get(); repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "a", MarketQuestion = "Match A", RealizedPnl = 4.50m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow }); repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "b", MarketQuestion = "Match B", RealizedPnl = 3.80m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow }); repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "c", MarketQuestion = "Match C", RealizedPnl = -20.00m, RedeemStatus = "None", ClosedAt = DateTime.UtcNow }); var result = await web.GetJsonAsync("/api/modules/resolutionfarming/history?accountId=1"); var summary = result.GetProperty("summary"); Assert.Equal(3, summary.GetProperty("count").GetInt32()); Assert.Equal(-11.70m, summary.GetProperty("pnl").GetDecimal()); Assert.Equal(2, summary.GetProperty("wins").GetInt32()); Assert.Equal(66.7m, Math.Round(summary.GetProperty("winRate").GetDecimal(), 1)); } [Fact] public async Task ResolutionFarming_Einstellungen_ohne_Eintrag_liefern_die_Vorgaben() { await using var web = await StartResolutionFarmingAsync(); var settings = await web.GetJsonAsync("/api/modules/resolutionfarming/settings/5"); Assert.Equal(5, settings.GetProperty("accountId").GetInt32()); Assert.False(settings.GetProperty("enabled").GetBoolean()); Assert.Equal(0.90m, settings.GetProperty("minPrice").GetDecimal()); Assert.Equal(0.98m, settings.GetProperty("maxPrice").GetDecimal()); Assert.Equal(48, settings.GetProperty("maxHoursToResolution").GetInt32()); Assert.Contains("Sports", settings.GetProperty("categoryWhitelistCsv").GetString()); } } }