diff --git a/src/IBKRTrader.Core/Capital/ICapitalService.cs b/src/IBKRTrader.Core/Capital/ICapitalService.cs index 1bda16e..3c92a21 100644 --- a/src/IBKRTrader.Core/Capital/ICapitalService.cs +++ b/src/IBKRTrader.Core/Capital/ICapitalService.cs @@ -40,7 +40,7 @@ public interface ICapitalService /// /// Vermerkt, dass die Order beim Broker liegt – die Bindung bleibt bestehen. /// - /// Der Gegenspieler von . Bis #489 gab es zu + /// Der Gegenspieler von . Bis #488 gab es zu /// einer nicht ausgeführten Order nur das Freigeben. Eine Order, die beim Broker wartet, wurde /// damit wie eine gescheiterte behandelt: Das Geld war wieder frei, die Order lebte weiter. /// diff --git a/src/IBKRTrader.Core/Logging/LoggingService.cs b/src/IBKRTrader.Core/Logging/LoggingService.cs index e854982..a8b2032 100644 --- a/src/IBKRTrader.Core/Logging/LoggingService.cs +++ b/src/IBKRTrader.Core/Logging/LoggingService.cs @@ -1,5 +1,6 @@ using IBKRTrader.Core.Configuration; using IBKRTrader.Core.Time; +using System.Globalization; using System.Runtime.CompilerServices; namespace IBKRTrader.Core.Logging; @@ -92,8 +93,13 @@ public class LoggingService var dir = Path.Combine(LogBaseDir, e.Module); Directory.CreateDirectory(dir); - var file = Path.Combine(dir, $"{e.Level}-{e.Timestamp:dd-MM-yy}.txt"); - var line = $"[{e.Timestamp:HH:mm:ss}] {e.Message}"; + // InvariantCulture ist hier kein Formalismus, sondern der Unterschied zwischen einer + // Datei, die es gibt, und einer, die niemand findet: Eine interpolierte Zeichenkette + // formatiert mit CurrentCulture, und die bringt ihren Kalender mit. Unter fa-IR steht + // im Namen das Jahr 1405, unter th-TH 2569 - beobachtet am 2026-09-06 auf der Test-VM, + // wo neben 2026-09-06.jsonl auch 1405-06-12.jsonl und 2569-09-03.jsonl lagen. + var file = Path.Combine(dir, FormattableString.Invariant($"{e.Level}-{e.Timestamp:dd-MM-yy}.txt")); + var line = FormattableString.Invariant($"[{e.Timestamp:HH:mm:ss}] {e.Message}"); if (e.Exception != null) line += $"{Environment.NewLine} {e.Exception}"; @@ -115,7 +121,7 @@ public class LoggingService try { Directory.CreateDirectory(LogBaseDir); - var file = Path.Combine(LogBaseDir, $"{e.Timestamp:yyyy-MM-dd}.jsonl"); + var file = Path.Combine(LogBaseDir, FormattableString.Invariant($"{e.Timestamp:yyyy-MM-dd}.jsonl")); var message = e.Exception != null ? $"{e.Message} | {e.Exception.Message}" : e.Message; var json = LogJson.WriteLine(utc, e.Level, e.Module, message, cid); diff --git a/src/IBKRTrader.Core/Persistence/Entities/AnalysisEntities.cs b/src/IBKRTrader.Core/Persistence/Entities/AnalysisEntities.cs index 7f26b33..45aa657 100644 --- a/src/IBKRTrader.Core/Persistence/Entities/AnalysisEntities.cs +++ b/src/IBKRTrader.Core/Persistence/Entities/AnalysisEntities.cs @@ -9,7 +9,7 @@ public enum TradeDecision Failed, // versucht, aber fehlgeschlagen (z. B. Order-Fehler) /// - /// Abgesendet, Ausgang offen – die Order liegt beim Broker (#489). Bewusst weder "Executed" + /// Abgesendet, Ausgang offen – die Order liegt beim Broker (#488). Bewusst weder "Executed" /// noch "Failed": Beides wäre eine Behauptung über etwas, das noch nicht entschieden ist. /// Pending @@ -38,7 +38,7 @@ public enum DecisionReason // ----- Ausführung ----- OrderPlaced, // Order erfolgreich platziert OrderFailed, // Broker/Order-Fehler beim Platzieren - OrderWorking // Order liegt beim Broker, noch nicht ausgeführt (#489) + OrderWorking // Order liegt beim Broker, noch nicht ausgeführt (#488) } /// Art eines Order-Lifecycle-Ereignisses (als String persistiert – erweiterbar). diff --git a/src/IBKRTrader.Core/Trading/BrokerOrders.cs b/src/IBKRTrader.Core/Trading/BrokerOrders.cs index 787636e..3a8c3f7 100644 --- a/src/IBKRTrader.Core/Trading/BrokerOrders.cs +++ b/src/IBKRTrader.Core/Trading/BrokerOrders.cs @@ -3,7 +3,7 @@ namespace IBKRTrader.Core.Trading; /// /// Wie eine Order beim Broker ausgegangen ist – drei Ausgänge, nicht zwei. /// -/// Warum das der Kern von #489 ist. Bis dahin kannte nur +/// Warum das der Kern von #488 ist. Bis dahin kannte nur /// „erfolgreich" und „fehlgeschlagen". Damit ließ sich der häufigste Fall am Wochenende gar nicht /// ausdrücken: Die Order ist beim Broker angenommen und wartet auf die Börseneröffnung. /// Sie als Fehlschlag zu buchen, gibt das gebundene Kapital frei, während die Order weiterläuft – diff --git a/src/IBKRTrader.Core/Trading/ExecutionService.cs b/src/IBKRTrader.Core/Trading/ExecutionService.cs index 3b59efe..f0a065a 100644 --- a/src/IBKRTrader.Core/Trading/ExecutionService.cs +++ b/src/IBKRTrader.Core/Trading/ExecutionService.cs @@ -136,7 +136,7 @@ public sealed class ExecutionService : IExecutionService var result = await _broker.PlaceOrderAsync(order, ct); - // Drei Ausgänge, drei Wege – und der gefährliche ist der mittlere (#489). Eine Order, die + // Drei Ausgänge, drei Wege – und der gefährliche ist der mittlere (#488). Eine Order, die // beim Broker liegt, ist WEDER ausgeführt NOCH gescheitert. Sie wie einen Fehlschlag zu // behandeln, gibt das gebundene Kapital frei, während die Order weiterläuft: Dasselbe Geld // wird ein zweites Mal vergeben, und die spätere Ausführung erzeugt eine Position, die diff --git a/src/IBKRTrader.Core/Trading/Ibkr/IbkrConnection.cs b/src/IBKRTrader.Core/Trading/Ibkr/IbkrConnection.cs index 752477b..9937bbf 100644 --- a/src/IBKRTrader.Core/Trading/Ibkr/IbkrConnection.cs +++ b/src/IBKRTrader.Core/Trading/Ibkr/IbkrConnection.cs @@ -984,7 +984,7 @@ internal sealed class IbkrConnection : DefaultEWrapper, IDisposable // Ordermeldungen zuerst und getrennt: Ob eine Meldung eine Order beendet, entscheidet der // Code – nicht die Tatsache, dass sie über den Fehler-Callback kam. IBKR schickt über - // denselben Weg auch Hinweise zu angenommenen Orders (#489). + // denselben Weg auch Hinweise zu angenommenen Orders (#488). if (id >= 0 && _orders.TryGetValue(id, out var order)) { if (IbkrMapping.IsOrderNotice(errorCode)) diff --git a/src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs b/src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs index 1ab5e8e..18430b0 100644 --- a/src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs +++ b/src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs @@ -353,7 +353,7 @@ internal static class IbkrMapping /// /// Meldungen, die zu einer Order gehören, sie aber nicht ablehnen. /// - /// 399 ist der Grund, aus dem es diese Liste gibt (#489): IBKR schickt darunter + /// 399 ist der Grund, aus dem es diese Liste gibt (#488): IBKR schickt darunter /// Hinweise zu einer angenommenen Order – am Wochenende regelmäßig "Your order will not /// be placed at the exchange until Monday 09:00". Als Fehler gelesen, meldete der Adapter /// "Order abgelehnt", das Kapitalmodell gab die Bindung frei, und am Montag füllte eine Order, diff --git a/src/IBKRTrader.Core/Workers/BuiltIn/ReservationWatchdogWorker.cs b/src/IBKRTrader.Core/Workers/BuiltIn/ReservationWatchdogWorker.cs index 3ba2e39..10d315c 100644 --- a/src/IBKRTrader.Core/Workers/BuiltIn/ReservationWatchdogWorker.cs +++ b/src/IBKRTrader.Core/Workers/BuiltIn/ReservationWatchdogWorker.cs @@ -18,7 +18,7 @@ namespace IBKRTrader.Core.Workers.BuiltIn; /// globaler Kaufstopp – und niemals ein automatischer Verkauf zur Heilung. Eine Software, /// die in einen fallenden Markt hinein Positionen schließt, weil eine Kennzahl unterschritten /// ist, richtet mehr Schaden an als der Zustand, den sie beheben soll. -/// Die offenen Bindungen gegen das Orderbuch des Brokers halten (#489). Eine Bindung, zu der +/// Die offenen Bindungen gegen das Orderbuch des Brokers halten (#488). Eine Bindung, zu der /// keine Order mehr liegt, kommt in Quarantäne statt in die Freigabe – es gibt zwei Erklärungen /// dafür, und sie führen zu entgegengesetzten Handlungen. /// diff --git a/src/IBKRTrader.Daemon/OrderBookTool.cs b/src/IBKRTrader.Daemon/OrderBookTool.cs index e3ba2a9..f585733 100644 --- a/src/IBKRTrader.Daemon/OrderBookTool.cs +++ b/src/IBKRTrader.Daemon/OrderBookTool.cs @@ -10,7 +10,7 @@ namespace IBKRTrader.Daemon; /// /// Der Blick ins Orderbuch des Brokers – und der Abgleich mit dem, was wir selbst gebucht haben -/// (#489). +/// (#488). /// /// Wozu es das gibt. Am 2026-09-06 stellte sich heraus, dass vier Orders bei /// geschlossener Börse angenommen und für Montag vorgemerkt wurden, während der Adapter sie als diff --git a/src/IBKRTrader.Daemon/Program.cs b/src/IBKRTrader.Daemon/Program.cs index a15a20a..61ab583 100644 --- a/src/IBKRTrader.Daemon/Program.cs +++ b/src/IBKRTrader.Daemon/Program.cs @@ -56,7 +56,7 @@ internal static class Program // Optionskette und Bewertungsgrößen eines Basiswerts ansehen. Liest nur. var chainSymbol = ArgAfter(args, "--option-chain"); - // Was liegt tatsächlich beim Broker? Der Gegencheck zur eigenen Buchführung (#489). + // Was liegt tatsächlich beim Broker? Der Gegencheck zur eigenen Buchführung (#488). // Ohne ihn ist "die Order wurde abgelehnt" eine Vermutung, keine Feststellung. var openOrders = HasFlag(args, "--open-orders"); diff --git a/src/IBKRTrader.Hosting/AppHostBuilder.cs b/src/IBKRTrader.Hosting/AppHostBuilder.cs index 7b086fa..864eb18 100644 --- a/src/IBKRTrader.Hosting/AppHostBuilder.cs +++ b/src/IBKRTrader.Hosting/AppHostBuilder.cs @@ -180,7 +180,7 @@ public static class AppHostBuilder { // Hier wird nur GEMELDET, nicht entschieden. // - // Bis #489 lief an dieser Stelle RecoverAsync gegen eine leere Orderliste, weil offene + // Bis #488 lief an dieser Stelle RecoverAsync gegen eine leere Orderliste, weil offene // Broker-Orders nicht abrufbar waren - jede ueberlebende Bindung landete damit sofort in // Quarantaene. Das war vertretbar, solange nie eine Bindung den Zustand "Submitted" // erreichte. Seit eine wartende Order ihre Bindung behaelt, waere es falsch: Eine voellig diff --git a/src/IBKRTrader.Modules.Supervisor/Services/DossierService.cs b/src/IBKRTrader.Modules.Supervisor/Services/DossierService.cs index 157d953..3f557a1 100644 --- a/src/IBKRTrader.Modules.Supervisor/Services/DossierService.cs +++ b/src/IBKRTrader.Modules.Supervisor/Services/DossierService.cs @@ -75,7 +75,10 @@ public sealed class DossierService var to = times.Max().Date.AddDays(1); for (var day = from; day <= to; day = day.AddDays(1)) { - string path = Path.Combine(_logsDirectory, $"{day:yyyy-MM-dd}.jsonl"); + // Muss zeichengleich zu dem sein, was LoggingService schreibt - sonst sucht der + // Supervisor unter einem Namen, den es nicht gibt. + string path = Path.Combine( + _logsDirectory, FormattableString.Invariant($"{day:yyyy-MM-dd}.jsonl")); if (!File.Exists(path)) continue; foreach (var line in File.ReadLines(path)) { diff --git a/tests/IBKRTrader.Tests/Trading/CultureIndependenceTests.cs b/tests/IBKRTrader.Tests/Trading/CultureIndependenceTests.cs index 37a3ee5..e5338ba 100644 --- a/tests/IBKRTrader.Tests/Trading/CultureIndependenceTests.cs +++ b/tests/IBKRTrader.Tests/Trading/CultureIndependenceTests.cs @@ -100,4 +100,54 @@ public class CultureIndependenceTests 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")); + } } diff --git a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs index 0c4ea4c..c43733b 100644 --- a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs +++ b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs @@ -300,10 +300,10 @@ public class ExecutionServiceTests Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } - // ─── Die Order, die beim Broker liegt (#489) ───────────────────────────── + // ─── Die Order, die beim Broker liegt (#488) ───────────────────────────── /// - /// Der Test, um den es bei #489 geht. Am 2026-09-06 wurden 67 Orders bei geschlossener + /// Der Test, um den es bei #488 geht. Am 2026-09-06 wurden 67 Orders bei geschlossener /// Börse angenommen und für Montag vorgemerkt, während der Adapter sie als abgelehnt meldete und /// jede Bindung freigegeben wurde. Das Geld war wieder im Topf, die Orders lebten weiter. /// @@ -346,7 +346,7 @@ public class ExecutionServiceTests public async Task RejectedOrder_StillReleasesTheCapital() { // Die Gegenprobe: Bei einer beweisbar abgelehnten Order MUSS das Geld zurück. Sonst hätte - // der Fix aus #489 nur die eine Klemme gegen die andere getauscht. + // der Fix aus #488 nur die eine Klemme gegen die andere getauscht. ArrangeHappyPath(); _broker.PlaceOrderAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(OrderResult.Fail("Order abgelehnt"))); diff --git a/tests/IBKRTrader.Tests/Trading/IbkrMappingTests.cs b/tests/IBKRTrader.Tests/Trading/IbkrMappingTests.cs index bb007d5..e5a840c 100644 --- a/tests/IBKRTrader.Tests/Trading/IbkrMappingTests.cs +++ b/tests/IBKRTrader.Tests/Trading/IbkrMappingTests.cs @@ -397,7 +397,7 @@ public class IbkrMappingTests IbkrMapping.IsInformational(IbkrMapping.DelayedDataNotice).Should().BeTrue(); } - // ─── Ordermeldungen (#489) ───────────────────────────────────────── + // ─── Ordermeldungen (#488) ───────────────────────────────────────── /// /// Der Fehler, der 67 Orders unsichtbar gemacht hat: IBKR schickt unter Code 399 Hinweise zu