using System; using System.Collections.Generic; using System.Linq; using PolyTrader.Modules.Accounting.Logic; using PolyTrader.Modules.Accounting.Models; using Xunit; namespace PolyTrader.Tests { /// Sicherheitsnetz für die neutrale Abrechnung (A-2): Periodenaggregation, FX, CSV. public class AccountingReportTests { private static readonly DateTime Jun = new(2026, 6, 15, 12, 0, 0, DateTimeKind.Utc); private static readonly DateTime Jul = new(2026, 7, 10, 12, 0, 0, DateTimeKind.Utc); private static LedgerEntry E(LedgerEventType type, decimal net, DateTime ts, decimal gross = 0m, decimal fee = 0m) => new() { AccountId = 1, EventType = type, Timestamp = ts, NetUsdc = net, GrossUsdc = gross == 0m ? Math.Abs(net) : gross, FeeUsdc = fee }; // ---------------- AccountingEngine ---------------- [Fact] public void Statement_aggregates_period_and_carries_opening_balance() { var entries = new List { // VOR dem Zeitraum → nur Anfangssaldo E(LedgerEventType.Deposit, 1000m, new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), gross: 1000m), // im Zeitraum (Juli) E(LedgerEventType.TradeBuy, -100m, Jul, gross: 100m, fee: 1m), E(LedgerEventType.TradeSell, 130m, Jul, gross: 130m, fee: 1m), E(LedgerEventType.Reward, 5m, Jul, gross: 5m), E(LedgerEventType.Deposit, 200m, Jul, gross: 200m), E(LedgerEventType.Withdrawal, -50m, Jul, gross: 50m), }; var from = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc); var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc); var s = AccountingEngine.BuildStatement(entries, from, to, 1); Assert.Equal(1000m, s.OpeningBalanceUsdc); Assert.Equal(200m, s.Deposits); Assert.Equal(50m, s.Withdrawals); Assert.Equal(230m, s.TradeVolume); // 100 + 130 Assert.Equal(5m, s.Rewards); Assert.Equal(2m, s.Fees); Assert.Equal(35m, s.NetTradingResultUsdc); // -100 +130 +5 (exkl. Ein-/Auszahlungen) Assert.Equal(2, s.TradeCount); // Invariante: Endsaldo − Anfangssaldo = Handelsergebnis + Einzahlungen − Auszahlungen Assert.Equal(1185m, s.ClosingBalanceUsdc); // 1000 + 35 + 200 - 50 Assert.Equal(s.NetTradingResultUsdc + s.Deposits - s.Withdrawals, s.BalanceChange); } [Fact] public void Monthly_breakdown_chains_opening_balances() { var entries = new List { E(LedgerEventType.Deposit, 100m, Jun, gross: 100m), // Juni E(LedgerEventType.TradeSell, 40m, Jul, gross: 40m), // Juli }; var from = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc); var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc); var months = AccountingEngine.BuildMonthlyBreakdown(entries, from, to, 1); Assert.Equal(2, months.Count); Assert.Equal(0m, months[0].OpeningBalanceUsdc); // Juni startet bei 0 Assert.Equal(100m, months[0].ClosingBalanceUsdc); Assert.Equal(100m, months[1].OpeningBalanceUsdc); // Juli erbt Juni-Endsaldo Assert.Equal(140m, months[1].ClosingBalanceUsdc); } // ---------------- FxConverter ---------------- [Fact] public void Fx_conversions_usdc_usd_eur() { Assert.Equal(100m, FxConverter.UsdcToUsd(100m, 1.0m)); Assert.Equal(92m, FxConverter.UsdToEur(100m, 0.92m)); Assert.Equal(92m, FxConverter.UsdcToEur(100m, 0.92m, 1.0m)); } [Fact] public void Fx_nearest_on_or_before_picks_latest_valid_rate() { var rates = new List { new() { Date = new DateTime(2026, 7, 3), UsdToEur = 0.90m }, new() { Date = new DateTime(2026, 7, 5), UsdToEur = 0.92m }, // Freitag }; // Sonntag 2026-07-05 gibt es keinen Kurs → letzter gültiger (Freitag). var pick = FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 6)); Assert.NotNull(pick); Assert.Equal(0.92m, pick!.UsdToEur); Assert.Null(FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 1))); // vor allen Kursen } // ---------------- CsvExporter ---------------- [Fact] public void Csv_ledger_has_header_and_escapes_commas() { var entries = new List { new() { AccountId = 1, EventType = LedgerEventType.TradeBuy, Timestamp = Jul, MarketSlug = "will-x-win, really?", Side = "BUY", Size = 10m, PriceUsdc = 0.5m, GrossUsdc = 5m, FeeUsdc = 0.1m, NetUsdc = -5.1m, TxHash = "0xabc", Source = "polymarket-activity" } }; string csv = CsvExporter.Ledger(entries); var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries); Assert.StartsWith("Timestamp,AccountId,EventType", lines[0]); Assert.Contains("\"will-x-win, really?\"", lines[1]); // Komma-Feld gequotet Assert.Contains("-5.1", lines[1]); // kulturinvariant (Punkt) } [Fact] public void Csv_statement_lists_key_figures() { var s = new PeriodStatement(1, Jul, Jul, 100m, 135m, 0m, 0m, 230m, 0m, 5m, 2m, 35m, 2, 3); string csv = CsvExporter.Statement(s); Assert.Contains("Endsaldo,135", csv); Assert.Contains("Netto-Handelsergebnis (Cash),35", csv); } } }