using System.Globalization; using FluentAssertions; using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Persistence.Entities; using IBKRTrader.Core.Trading; namespace IBKRTrader.Tests.Trading; /// /// Zahlen und Zeitpunkte, die in Logs, Dossiers oder in die Datenbank gehen, dürfen nicht an der /// Kultur des Hosts hängen. Wir betreiben Instanzen in EU und US und vergleichen deren Ausgaben /// miteinander – "373,67" auf der einen und "373.67" auf der anderen wäre dasselbe Ereignis in /// zwei Schreibweisen. /// /// Die Tests setzen bewusst eine Kultur mit Dezimalkomma und prüfen, dass sich davon nichts /// beeindrucken lässt. Der laufende Prozess stellt zusätzlich die invariante Kultur ein /// (AppHostBuilder), aber genau darauf soll sich der einzelne Codepfad nicht verlassen. /// [Trait("cat", "unit")] public class CultureIndependenceTests { /// Führt die Prüfung unter einer Kultur mit Dezimalkomma aus. private static void UnderGermanCulture(Action assert) { var previous = CultureInfo.CurrentCulture; try { CultureInfo.CurrentCulture = new CultureInfo("de-DE"); assert(); } finally { CultureInfo.CurrentCulture = previous; } } private static BrokerPosition Position() => new() { Symbol = "GOOG", SecType = "STK", Currency = "USD", ConId = 208813720, Quantity = 10, AvgCost = 373.67m }; [Fact] public void BrokerPosition_ToString_UsesInvariantNumbers() { UnderGermanCulture(() => Position().ToString().Should().Be("GOOG (STK) 10 @ 373.67 USD")); } [Fact] public void BrokerExecution_ToString_UsesInvariantNumbers() { var execution = new BrokerExecution { ExecId = "0001", Time = new DateTime(2026, 9, 3, 18, 26, 13, DateTimeKind.Utc), Symbol = "AAPL", SecType = "STK", Side = TradeSide.Buy, Quantity = 5, Price = 327.27m }; UnderGermanCulture(() => execution.ToString().Should().Be("2026-09-03 18:26:13 Buy 5x AAPL @ 327.27")); } [Fact] public void Dossier_WritesInvariantNumbers() { var dossier = new TradeDossier( SignalId: "sig-1", Decisions: [], OrderEvents: [], Trades: [ new CoreTrade { Module = "CT", Symbol = "AAPL", Action = "BUY", Quantity = 3, Price = 327.27m, TotalValue = 981.81m, Status = "Filled", TradedAt = new DateTime(2026, 9, 3, 18, 26, 13, DateTimeKind.Utc) } ], LogLines: []); UnderGermanCulture(() => { var markdown = DossierBuilder.ToMarkdown(dossier); markdown.Should().Contain("327.27").And.Contain("981.81"); markdown.Should().NotContain("327,27"); }); } // ─── Dateinamen (#489) ──────────────────────────────────────────────── /// /// Führt die Prüfung unter einer Kultur mit anderem Kalender aus. Das Dezimalkomma ist /// nur die halbe Gefahr: fa-IR rechnet im Sonnen-Hidschra-Kalender, th-TH im buddhistischen. /// private static void UnderCulture(string name, Action assert) { var previous = CultureInfo.CurrentCulture; try { CultureInfo.CurrentCulture = new CultureInfo(name); assert(); } finally { CultureInfo.CurrentCulture = previous; } } [Theory] [InlineData("fa-IR")] // Sonnen-Hidschra: Jahr 1405 statt 2026 [InlineData("th-TH")] // buddhistisch: Jahr 2569 statt 2026 [InlineData("de-DE")] public void LogFileNames_DoNotDependOnTheHostCalendar(string culture) { // Am 2026-09-06 lagen auf der Test-VM neben 2026-09-06.jsonl auch 1405-06-12.jsonl und // 2569-09-03.jsonl. Eine interpolierte Zeichenkette formatiert mit CurrentCulture, und die // bringt ihren Kalender mit: Der Supervisor sucht dann eine Datei, die es nicht gibt, und // die Tagesrotation legt jeden Tag eine neue Reihe an. var day = new DateTime(2026, 9, 6, 14, 3, 0, DateTimeKind.Utc); UnderCulture(culture, () => { FormattableString.Invariant($"{day:yyyy-MM-dd}.jsonl").Should().Be("2026-09-06.jsonl"); FormattableString.Invariant($"Info-{day:dd-MM-yy}.txt").Should().Be("Info-06-09-26.txt"); }); } [Fact] public void TheNaiveInterpolation_IsWhatWentWrong() { // Der Gegenbeweis, damit dieser Test nicht versehentlich trivial wird: Ohne den // ausdruecklichen Formatanbieter geht es tatsaechlich schief. Faellt diese Zusicherung // eines Tages, ist die Gefahr weg - dann darf der Test verschwinden, nicht vorher. var day = new DateTime(2026, 9, 6, 0, 0, 0, DateTimeKind.Utc); UnderCulture("fa-IR", () => $"{day:yyyy-MM-dd}".Should().NotBe("2026-09-06")); } }