using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using PolyTrader.Modules.Accounting.Logic; using PolyTrader.Modules.Accounting.Models; using PolyTrader.Modules.Accounting.Services; using PolyTrader.Modules.Supervisor.Agent; using PolyTrader.Modules.Supervisor.Counterfactual; using PolyTrader.Modules.Supervisor.Persistence; using PolyTrader.Web.Endpoints; using Xunit; namespace PolyTrader.Tests.WebUi { public class SupervisorAndAccountingEndpointsTests { [Fact] public void Supervisor_Profiles_ContainsStandardProfiles() { var profiles = SupervisorProfiles.All; Assert.NotNull(profiles); Assert.NotEmpty(profiles); var allgemein = SupervisorProfiles.ByName("Allgemein"); var technik = SupervisorProfiles.ByName("Technik"); var copyTrading = SupervisorProfiles.ByName("CopyTrading"); var rf = SupervisorProfiles.ByName("ResolutionFarming"); Assert.Equal("Allgemein", allgemein.Name); Assert.Equal("Technik", technik.Name); Assert.Equal("CopyTrading", copyTrading.Name); Assert.Equal("ResolutionFarming", rf.Name); // Unknown fallback to Allgemein var fallback = SupervisorProfiles.ByName("UnknownRandomProfile"); Assert.Equal(SupervisorProfiles.Allgemein.Name, fallback.Name); } [Fact] public void Supervisor_AskRequest_RejectsEmptyQuestion() { var reqEmpty = new SupervisorEndpoints.SupervisorAskRequest("", "Allgemein", null); var reqWhitespace = new SupervisorEndpoints.SupervisorAskRequest(" ", "Technik", null); var reqValid = new SupervisorEndpoints.SupervisorAskRequest("Wie ist der Tages-PnL?", "Allgemein", "gpt-4o"); Assert.True(string.IsNullOrWhiteSpace(reqEmpty.Question)); Assert.True(string.IsNullOrWhiteSpace(reqWhitespace.Question)); Assert.False(string.IsNullOrWhiteSpace(reqValid.Question)); Assert.Equal("gpt-4o", reqValid.Model); } [Fact] public void Supervisor_Report_SerializesAndDeserializesToolCalls() { var toolCalls = new List { new { tool = "query_pnl", args = new { period = "today" } }, new { tool = "query_positions", args = new { minSize = 10 } } }; var json = JsonSerializer.Serialize(toolCalls); var report = new SupervisorReport { Id = 42, CreatedAt = DateTime.UtcNow, Profile = "Technik", Model = "gpt-4o-mini", Question = "Gibt es offene Fehlermuster?", Answer = "Keine kritischen Fehler gefunden.", ToolCallsJson = json, ToolCallCount = 2, PromptTokens = 450, CompletionTokens = 120 }; Assert.Equal(42, report.Id); Assert.Equal("Technik", report.Profile); Assert.Equal(2, report.ToolCallCount); Assert.Equal(570, report.PromptTokens + report.CompletionTokens); Assert.Contains("query_pnl", report.ToolCallsJson); Assert.Contains("query_positions", report.ToolCallsJson); } [Fact] public void Supervisor_CounterfactualRecord_MapsPropertiesAccurately() { var record = new CounterfactualRecord { Id = 1, CheckedAt = DateTime.UtcNow, DecisionId = 100, SignalId = "sig-abc-123", ModuleName = "CopyTrading", AccountId = 2, MarketSlug = "fed-rate-cut-2026", MarketQuestion = "Will Fed cut rates in March 2026?", Reason = "Price exceeds max buy price threshold", SignalPrice = 0.85m, IsWinner = true, HypotheticalPnlPerShare = 0.15m }; Assert.Equal("sig-abc-123", record.SignalId); Assert.Equal("CopyTrading", record.ModuleName); Assert.True(record.IsWinner); Assert.Equal(0.15m, record.HypotheticalPnlPerShare); Assert.Equal(0.85m, record.SignalPrice); } [Fact] public void Accounting_ResolveCurrency_ResolvesUSDCAndUSDAtParity() { var service = new AccountingReportService( ledger: null!, fx: null!); var usdc = service.ResolveCurrency("USDC", DateTime.UtcNow); var usd = service.ResolveCurrency("USD", DateTime.UtcNow); var eur = service.ResolveCurrency("EUR", DateTime.UtcNow); Assert.Equal("USDC", usdc.Code); Assert.Equal(1.0m, usdc.Factor); Assert.Equal("USD", usd.Code); Assert.Equal(1.0m, usd.Factor); Assert.Equal("EUR", eur.Code); // Ohne FX-Repo liefert EUR available = false und factor = 0 Assert.False(eur.Available); } [Fact] public void Accounting_PeriodStatement_CalculatesBalanceChangeAndTotals() { var statement = new PeriodStatement( AccountId: 1, From: new DateTime(2026, 3, 1), To: new DateTime(2026, 3, 31), OpeningBalanceUsdc: 1000m, ClosingBalanceUsdc: 1250m, Deposits: 200m, Withdrawals: 50m, TradeVolume: 5000m, Redeems: 107.5m, Rewards: 5.0m, Fees: 12.5m, NetTradingResultUsdc: 100m, TradeCount: 25, EntryCount: 60 ); decimal balanceChange = statement.BalanceChange; // Closing - Opening Assert.Equal(250m, balanceChange); // Factor scaling test (e.g. converting to EUR at factor 0.90) decimal factor = 0.90m; decimal eurNetResult = Math.Round(statement.NetTradingResultUsdc * factor, 2); decimal eurOpening = Math.Round(statement.OpeningBalanceUsdc * factor, 2); decimal eurClosing = Math.Round(statement.ClosingBalanceUsdc * factor, 2); Assert.Equal(90.00m, eurNetResult); Assert.Equal(900.00m, eurOpening); Assert.Equal(1125.00m, eurClosing); } [Fact] public void Accounting_LedgerQuery_SearchAndPaginationLogicWorks() { var entries = new List { new LedgerEntry { Id = 1, Timestamp = DateTime.UtcNow.AddMinutes(-30), AccountId = 1, EventType = LedgerEventType.TradeBuy, MarketSlug = "will-btc-hit-150k", Outcome = "Yes", Side = "BUY", Size = 100m, PriceUsdc = 0.55m, GrossUsdc = 55m, FeeUsdc = 0.55m, NetUsdc = -55.55m, TxHash = "0xaaa111" }, new LedgerEntry { Id = 2, Timestamp = DateTime.UtcNow.AddMinutes(-20), AccountId = 1, EventType = LedgerEventType.TradeSell, MarketSlug = "will-eth-hit-5k", Outcome = "Yes", Side = "SELL", Size = 50m, PriceUsdc = 0.80m, GrossUsdc = 40m, FeeUsdc = 0.40m, NetUsdc = 39.60m, TxHash = "0xbbb222" }, new LedgerEntry { Id = 3, Timestamp = DateTime.UtcNow.AddMinutes(-10), AccountId = 2, EventType = LedgerEventType.Redeem, MarketSlug = "will-solana-etf-pass", Outcome = "Yes", Side = "", Size = 200m, PriceUsdc = 1.00m, GrossUsdc = 200m, FeeUsdc = 0m, NetUsdc = 200m, TxHash = "0xccc333" } }; // 1. Search by market slug string search = "btc"; var filtered = entries.Where(e => (e.MarketSlug != null && e.MarketSlug.Contains(search, StringComparison.OrdinalIgnoreCase)) || (e.Outcome != null && e.Outcome.Contains(search, StringComparison.OrdinalIgnoreCase)) || (e.TxHash != null && e.TxHash.Contains(search, StringComparison.OrdinalIgnoreCase)) || e.EventType.ToString().Contains(search, StringComparison.OrdinalIgnoreCase) ).ToList(); Assert.Single(filtered); Assert.Equal("0xaaa111", filtered[0].TxHash); // 2. Search by event type search = "Redeem"; filtered = entries.Where(e => (e.MarketSlug != null && e.MarketSlug.Contains(search, StringComparison.OrdinalIgnoreCase)) || e.EventType.ToString().Contains(search, StringComparison.OrdinalIgnoreCase) ).ToList(); Assert.Single(filtered); Assert.Equal(3, filtered[0].Id); // 3. Pagination math int totalCount = entries.Count; int pageSize = 2; int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); Assert.Equal(2, totalPages); var page1 = entries.OrderByDescending(e => e.Timestamp).Take(pageSize).ToList(); Assert.Equal(2, page1.Count); Assert.Equal(3, page1[0].Id); Assert.Equal(2, page1[1].Id); } [Fact] public void Accounting_IngestRun_TracksExecutionStatus() { var run = new IngestRun { Id = 1, AccountId = 1, StartedAt = DateTime.UtcNow.AddMinutes(-5), FinishedAt = DateTime.UtcNow, Backfill = true, NewEntries = 15, DuplicateEntries = 42, BalanceDeltaUsdc = 120.50m, Success = true, Message = "Sync erfolgreich abgeschlossen" }; Assert.Equal(1, run.Id); Assert.True(run.Success); Assert.True(run.Backfill); Assert.Equal(15, run.NewEntries); Assert.Equal(42, run.DuplicateEntries); Assert.Equal("Sync erfolgreich abgeschlossen", run.Message); Assert.NotNull(run.FinishedAt); } } }