using System; using System.Collections.Generic; using System.Linq; using PolyTrader.Modules.ResolutionFarming.Models; using PolyTrader.Web.Endpoints; using PolyTraderSharp.Models; using Xunit; namespace PolyTrader.Tests.WebUi { public class CopyTradingAndResolutionFarmingEndpointsTests { [Fact] public void CopyTrading_TrackedTraderInput_MapsCorrectly() { var input = new CopyTradingEndpoints.TrackedTraderInput( Id: 0, DisplayName: "Whale Alpha", WalletAddress: "0x1234567890abcdef1234567890abcdef12345678", Category: "INSIDER", Description: "Top Polymarket Trader", Reasoning: "Consistent winrate above 70%", IsActive: true, IsHidden: false, AutoPauseEnabled: true, MakerEntry: true, AssignedAccountIds: new List { 1, 2 } ); int id = input.Id <= 0 ? 1 : input.Id; var trader = new TrackedTrader { Id = id, DisplayName = input.DisplayName ?? $"Trader #{id}", WalletAddress = input.WalletAddress?.Trim() ?? string.Empty, Category = string.IsNullOrWhiteSpace(input.Category) ? "NEW_BIG_BET" : input.Category.Trim(), Description = input.Description ?? string.Empty, Reasoning = input.Reasoning ?? string.Empty, IsActive = input.IsActive, IsHidden = input.IsHidden, AutoPauseEnabled = input.AutoPauseEnabled, MakerEntry = input.MakerEntry, AssignedAccountIds = new HashSet(input.AssignedAccountIds ?? new List()) }; Assert.Equal(1, trader.Id); Assert.Equal("Whale Alpha", trader.DisplayName); Assert.Equal("INSIDER", trader.Category); Assert.True(trader.IsActive); Assert.True(trader.MakerEntry); Assert.Equal(2, trader.AssignedAccountIds.Count); Assert.Contains(1, trader.AssignedAccountIds); Assert.Contains(2, trader.AssignedAccountIds); } [Fact] public void CopyTrading_OpenTrades_CalculatesUnrealizedMetricsProperly() { var positions = new List { new Position { AccountId = 1, SourceTraderId = 1, SourceTraderName = "Whale 1", MarketQuestion = "Will Trump win in 2024?", Outcome = "Yes", Side = "BUY", EntryPrice = 0.50m, CurrentPrice = 0.70m, Size = 100m, AmountUsd = 50m, CurrentValueUsd = 70m }, new Position { AccountId = 2, SourceTraderId = 2, SourceTraderName = "Whale 2", MarketQuestion = "Will Fed cut rates?", Outcome = "Yes", Side = "BUY", EntryPrice = 0.80m, CurrentPrice = 0.60m, Size = 50m, AmountUsd = 40m, CurrentValueUsd = 30m } }; decimal totalAmount = positions.Sum(p => p.AmountUsd); decimal totalUnrealized = positions.Sum(p => p.CurrentValueUsd - p.AmountUsd); Assert.Equal(90m, totalAmount); Assert.Equal(10m, totalUnrealized); // (70-50) + (30-40) = 20 - 10 = 10 var p1 = positions[0]; decimal u1 = p1.CurrentValueUsd - p1.AmountUsd; decimal pct1 = p1.AmountUsd > 0 ? 100m * u1 / p1.AmountUsd : 0m; Assert.Equal(20m, u1); Assert.Equal(40m, pct1); var p2 = positions[1]; decimal u2 = p2.CurrentValueUsd - p2.AmountUsd; decimal pct2 = p2.AmountUsd > 0 ? 100m * u2 / p2.AmountUsd : 0m; Assert.Equal(-10m, u2); Assert.Equal(-25m, pct2); } [Fact] public void CopyTrading_ClosedTrades_FiltersCorrectly() { var closed = new List { new ClosedTrade { TradeId = 1, AccountId = 1, SourceTraderId = 10, MarketQuestion = "Bitcoin reaches 100k?", Outcome = "Yes", RealizedPnl = 35m, ClosedAt = DateTime.UtcNow.AddHours(-2) }, new ClosedTrade { TradeId = 2, AccountId = 1, SourceTraderId = 10, MarketQuestion = "Solana reaches 300?", Outcome = "No", RealizedPnl = -12m, ClosedAt = DateTime.UtcNow.AddHours(-5) }, new ClosedTrade { TradeId = 3, AccountId = 2, SourceTraderId = 20, MarketQuestion = "Ethereum reaches 5k?", Outcome = "Yes", RealizedPnl = 50m, ClosedAt = DateTime.UtcNow.AddHours(-1) } }; // Filter by Trader var trader10Trades = closed.Where(t => t.SourceTraderId == 10).ToList(); Assert.Equal(2, trader10Trades.Count); // Filter by Result var winners = closed.Where(t => t.RealizedPnl > 0).ToList(); Assert.Equal(2, winners.Count); decimal totalWinPnl = winners.Sum(w => w.RealizedPnl); Assert.Equal(85m, totalWinPnl); // Search Filter string search = "Bitcoin"; var searchResults = closed.Where(t => t.MarketQuestion.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList(); var matched = Assert.Single(searchResults); Assert.Equal(1, matched.TradeId); } [Fact] public void CopyTrading_AccountSettings_DefaultValuesAreConsistent() { var settings = new CopyTradingAccountSettings { AccountId = 42 }; Assert.Equal(42, settings.AccountId); Assert.Equal(5.0m, settings.PerMarketLimit); Assert.Equal(10.0m, settings.PerMasterLimit); Assert.Equal(0.98m, settings.MaxBuyPrice); Assert.Equal(2.0m, settings.MaxPriceDifference); Assert.Equal(15.0m, settings.SellFloorPct); } [Fact] public void ResolutionFarming_Candidates_MappingAndEdgeFiltering() { var candidates = new List { new RfCandidate { Id = 1, AccountId = 1, MarketQuestion = "Will candidate A win election?", Outcome = "Yes", Category = "Politics", Ask = 0.94m, NetEdgePct = 2.5m, Accepted = true, RejectReason = "" }, new RfCandidate { Id = 2, AccountId = 1, 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 acceptedOnly = candidates.Where(c => c.Accepted).ToList(); var accepted = Assert.Single(acceptedOnly); Assert.Equal("Politics", accepted.Category); Assert.True(accepted.NetEdgePct >= 1.5m); var rejected = candidates.First(c => !c.Accepted); Assert.Equal("Fee too high for edge", rejected.RejectReason); } [Fact] public void ResolutionFarming_Positions_SummaryMetrics() { var positions = new List { new RfPosition { AccountId = 1, TokenId = "tok-1", MarketQuestion = "Match 1", EntryPrice = 0.95m, Size = 100m, AmountUsd = 95m, Status = "Open" }, new RfPosition { AccountId = 1, TokenId = "tok-2", MarketQuestion = "Match 2", EntryPrice = 0.92m, Size = 50m, AmountUsd = 46m, Status = "Open" } }; decimal totalInvested = positions.Sum(p => p.AmountUsd); Assert.Equal(141m, totalInvested); Assert.Equal(2, positions.Count); } [Fact] public void ResolutionFarming_History_CalculatesPnlAndWinRate() { var history = new List { new RfClosedTrade { Id = 101, AccountId = 1, MarketQuestion = "Match A", RealizedPnl = 4.50m, RedeemStatus = "Redeemed" }, new RfClosedTrade { Id = 102, AccountId = 1, MarketQuestion = "Match B", RealizedPnl = 3.80m, RedeemStatus = "Redeemed" }, new RfClosedTrade { Id = 103, AccountId = 1, MarketQuestion = "Match C", RealizedPnl = -20.00m, RedeemStatus = "None" } }; decimal totalPnl = history.Sum(t => t.RealizedPnl); int wins = history.Count(t => t.RealizedPnl > 0); decimal winrate = 100m * wins / history.Count; Assert.Equal(-11.70m, totalPnl); Assert.Equal(2, wins); Assert.Equal(66.7m, Math.Round(winrate, 1)); } [Fact] public void ResolutionFarming_Settings_DefaultValuesValid() { var settings = new RfSettings { AccountId = 5 }; Assert.Equal(5, settings.AccountId); Assert.False(settings.Enabled); Assert.Equal(0.90m, settings.MinPrice); Assert.Equal(0.98m, settings.MaxPrice); Assert.Equal(48, settings.MaxHoursToResolution); Assert.Equal(1.5m, settings.MinEdgePct); Assert.Equal(25m, settings.MaxPerMarketUsd); Assert.Contains("Sports", settings.CategoryWhitelistCsv); } } }