# Analyse: Linux-Fähigkeit des IBKRTrader > Stand: 2026-08-06. **Reine Analyse – es wurde kein Code geändert.** > Grundlage ist der Commit `b96a207` (main): 154 C#-Dateien, ~16.200 LOC, 6 Projekte, 165 Tests. > Alle Aussagen in Abschnitt 1–9 sind am Quelltext bzw. an einem Probe-Restore verifiziert; > Stellen, die nur plausibel und ungeprüft sind, stehen ausdrücklich als solche gekennzeichnet. --- ## 0. Kurzfassung **Die gute Nachricht:** Der portabilitätskritische Teil der Anwendung ist bereits sauber. Kein einziges `DllImport`, keine Registry, kein WMI, kein DPAPI, kein `TimeZoneInfo.FindSystemTimeZoneById`. Die Verschlüsselung (`AesGcm`), die Datenbank (Pomelo/EF Core) und der TWS-Adapter laufen ohne Änderung auf Linux. Die WinForms-Kopplung im **Core** beschränkt sich auf **drei Dateien**, und jedes Modul trägt **genau eine** Form-Datei. **Die eigentliche Arbeit** liegt an zwei Stellen und sie sind unterschiedlich groß: | | Umfang | Aufwand | |---|---|---| | **A. Headless-Linux** (Worker, Trading, Accounting, Supervisor, REST/MCP – ohne UI) | ~15 Fundstellen, 3 Core-Dateien entkoppeln | **5–7 Personentage** | | **B. Desktop-Linux** (zusätzlich die komplette UI auf Avalonia) | ~1.300 LOC WinForms neu bauen | **+10–14 Personentage** | | **C. LiveCharts2** (heute existiert **kein einziges** Diagramm) | Neubau, keine Migration | **+1–3 Personentage** | **Empfehlung:** Die beiden Schritte trennen. Eine headless Linux-Variante ist mit ~1 Woche erreichbar und liefert sofort den größten praktischen Nutzen (Dauerbetrieb auf einem Server statt auf dem Windows-Desktop). Der Avalonia-Umbau ist danach eine unabhängige Etappe, die man ohne Zeitdruck und ohne laufenden Betrieb zu gefährden angehen kann. Details in Abschnitt 11. **Ein Punkt ist unabhängig von der UI der gefährlichste:** die Zeitzonen-Behandlung (Abschnitt 6). Der Code mischt heute `DateTime.Now` und `DateTime.UtcNow` und verwirft in `ParseExecutionTime` bewusst die Zeitzone. Auf einem Windows-Rechner mit `Europe/Berlin` und in einem Linux-Container mit `UTC` liefert **derselbe Code unterschiedliche Werte** – lautlos, ohne Fehler, in Buchungszeitstempeln. Das muss vor der Portierung geklärt werden, nicht danach. --- ## 1. Was **nicht** geändert werden muss Diese Prüfungen sind negativ ausgefallen – das sind ersparte Personentage: | Geprüft | Ergebnis | |---|---| | `DllImport` / `LibraryImport` / `Marshal` | **keine Fundstelle** | | Windows Registry, WMI, EventLog, `WindowsIdentity` | **keine Fundstelle** | | DPAPI / `ProtectedData` | **keine Fundstelle** – `SecretProtection` nutzt `AesGcm` + `SHA256`, voll portabel | | `TimeZoneInfo.FindSystemTimeZoneById` (Windows- vs. IANA-IDs) | **keine Fundstelle** | | Hartkodierte Laufwerksbuchstaben im Produktivpfad | nur in `BackupWorker` (s. 7.1) | | EF Core / Pomelo / MySqlConnector | voll portabel, Migrationen unberührt | | WinForms-Designer-Dateien (`*.Designer.cs`, Layout-`.resx`) | **keine** – die gesamte UI ist handgeschriebener Code-Behind. Das erspart die übliche Designer-Konvertierung vollständig. | ### 1.1 Die TWS-API läuft auf Linux (geprüft) Das Paket `IB.TWS.CSharpApi 9.76.1` liefert `lib/net45/CSharpAPI.dll` – deshalb steht heute `NoWarn="NU1701"` im Core-csproj. Die Assembly-Referenzen wurden ausgelesen: ``` mscorlib 4.0.0.0 System 4.0.0.0 System.Core 4.0.0.0 ``` Nur diese drei, alle auf .NET 10 vollständig typweitergeleitet. Es gibt keine Abhängigkeit auf `System.Configuration`, `System.Web` oder sonst etwas Windows-Gebundenes. Die DLL wird auf Linux laufen. **Verbleibendes Restrisiko: gering, aber ungeprüft** – ein Verbindungs-Smoke-Test gegen das Gateway von einem Linux-Host aus gehört in die erste Etappe. > **Sauberere Alternative:** IBKR liefert im offiziellen TWS-API-Download den C#-Quelltext mit. > Den als eigenes `netstandard2.0`-Projekt in `src/` aufzunehmen, ersetzt das NuGet-Mirror-Paket, > beseitigt `NU1701` und macht die Herkunft nachvollziehbar. Halber Tag, optional. --- ## 2. Sofort-Blocker: der Restore schlägt heute auf **jedem** frischen Rechner fehl Das ist keine Linux-Besonderheit, aber es ist das Erste, worüber man auf einer neuen Maschine stolpert – und deshalb Teil dieser Analyse. `NuGet.config` nutzt `packageSourceMapping` als Allowlist mit ``. Drei Pakete haben kein passendes Muster. Verifiziert mit einem Restore gegen einen leeren Paket-Ordner: ``` error NU1100: "PDFsharp-MigraDoc (>= 6.2.4)" kann für "net10.0-windows" nicht aufgelöst werden. error NU1100: "Microsoft.EntityFrameworkCore (>= 8.0.13)" kann für "net10.0-windows" nicht aufgelöst werden. error NU1100: "Microsoft.CodeAnalysis.CSharp.Workspaces (>= 4.5.0)" kann für "net10.0-windows" nicht aufgelöst werden. ``` Auf dem Entwicklungsrechner fällt das nicht auf, weil alle drei längst im globalen Paket-Cache liegen. Die Ursachen: - `PDFsharp-MigraDoc` – gar kein Muster vorhanden. - `Microsoft.EntityFrameworkCore` – das Muster lautet `Microsoft.EntityFrameworkCore.*`; der Glob matcht das Paket **ohne** Suffix nicht. - `Microsoft.CodeAnalysis.CSharp.Workspaces` – transitiv über `EntityFrameworkCore.Design`, kein Muster. **Aufwand: 10 Minuten.** Muss vor allem anderen erledigt sein, sonst startet der erste Linux-Build nicht. --- ## 3. Block A – Projekt- und Build-Ebene **Alle sechs Projekte** stehen heute auf `net10.0-windows` mit `UseWindowsForms=true` – auch der Core, alle drei Module und das Testprojekt. | Projekt | heute | Ziel | |---|---|---| | `IBKRTrader.Core` | `net10.0-windows`, WinForms | `net10.0`, **keine** UI-Abhängigkeit | | `Modules.CongressTrading` | `net10.0-windows`, WinForms | `net10.0` | | `Modules.Accounting` | `net10.0-windows`, WinForms | `net10.0` | | `Modules.Supervisor` | `net10.0-windows`, WinForms | `net10.0` | | `IBKRTrader.Tests` | `net10.0-windows`, WinForms | `net10.0` | | `IBKRTrader.App` | `WinExe`, `net10.0-windows` | neu: `IBKRTrader.Desktop` (Avalonia) + `IBKRTrader.Daemon` (Konsole) | Zwei Nebenwirkungen, die man kennen muss: 1. **`ImplicitUsings` + `UseWindowsForms` fügt `System.Windows.Forms` und `System.Drawing` als globale Usings hinzu.** Sobald das wegfällt, brechen Dateien, die unbemerkt `Point`, `Size`, `Color`, `Font` oder `Padding` benutzt haben. Das ist *nützlich* – der Compiler findet die Arbeit für uns – aber es erklärt, warum die erste Umstellung mehr Fehler wirft als die 3 bekannten Core-Dateien vermuten lassen. 2. `ApplicationHighDpiMode`, `Properties/Resources.resx` (24 `System.Drawing.Bitmap`-Icons) und `ApplicationConfiguration.Initialize()` verschwinden mit dem App-Projekt. Die 24 PNGs unter `Resources/` bleiben brauchbar, werden aber in Avalonia über `AvaloniaResource` + `Bitmap` eingebunden statt über den `ResourceManager`. **Aufwand Block A: 0,5 Tage.** --- ## 4. Block B – den Core von WinForms lösen Das ist der architektonisch wichtigste Schritt und erfreulich klein. **Genau drei Dateien** im Core kennen WinForms: ### 4.1 `Core/Logging/LoggingService.cs` (139 LOC, davon ~40 betroffen) Hält direkt ein `RichTextBox?`, benutzt `System.Drawing.Color` und marshallt selbst per `InvokeRequired`/`BeginInvoke`: ```csharp private RichTextBox? _rtb; public void AttachRichTextBox(RichTextBox rtb) => _rtb = rtb; private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150); ``` **Umbau:** `AttachRichTextBox` durch ein `event Action? EntryWritten` bzw. ein `ILogSink`-Interface ersetzen. Das Einfärben und das Thread-Marshalling wandern in die UI-Schicht (Avalonia: `Dispatcher.UIThread.Post`). Der `LogEntry`-Record ist bereits sauber und braucht keine Änderung. Nebenbei zu bereinigen: `e.Level.ToString().ToUpper()` in Zeile 115 ist kulturabhängig (s. Abschnitt 5). ### 4.2 `Core/Modularity/ModuleView.cs` (57 LOC) ```csharp public System.Drawing.Image? Icon { get; set; } public Func
CreateForm { get; init; } = () => new Form(); ``` **Umbau:** Der Contract muss toolkit-neutral werden. Zwei Wege: - **Pragmatisch:** `Func CreateWindow` + `string IconKey` (Ressourcenname statt Bitmap). Der Core kennt dann keine UI-Typen mehr, die Shell castet. - **Sauber:** ein `IModuleWindow`-Marker-Interface, das die Desktop-Schicht auf `Window` abbildet. Das `IModuleUiHost`-Interface selbst (`RegisterView`, `IsOpen`, `OpenView`, `ActivateMain`, `RequestShutdown`, `OpenStateChanged`) ist **bereits toolkit-neutral** und kann unverändert bleiben. ### 4.3 `Core/Modularity/WindowMenu.cs` (79 LOC) Vollständig WinForms (`MenuStrip`, `ToolStripMenuItem`, `Font`, `FontStyle`). Wird ersatzlos gelöscht und in der Avalonia-Schicht neu gebaut. Die *Logik* dahinter (Launcher + alle Views + kontextabhängige rechte Aktion) ist trivial und in ~60 Zeilen XAML/C# nachgebaut. ### 4.4 Die Module **Je Modul genau eine betroffene Datei:** - `Modules.Accounting/Ui/AccountingMainForm.cs` (302 LOC) - `Modules.CongressTrading/UI/CongressTradingForm.cs` (129 LOC) - `Modules.Supervisor/Ui/SupervisorMainForm.cs` (188 LOC) `Modules.Accounting/Logic/PdfExporter.cs` sieht in einer naiven Suche nach WinForms aus, ist es aber nicht – `Font` und `Colors` stammen dort aus `MigraDoc.DocumentObjectModel`. (Der PdfExporter hat ein *anderes* Linux-Problem, s. 7.2.) **Das heißt:** Zieht man diese drei Dateien heraus, sind Core und alle Module sofort headless-fähig. Genau darauf baut die Empfehlung in Abschnitt 11 auf. **Aufwand Block B: 1 Tag.** --- ## 5. Block C – Kultur und Stringformatierung Der Bestand ist überwiegend gut: `StringComparison.Ordinal`/`OrdinalIgnoreCase` wird konsequent benutzt, der `IbkrMapping` und der `CsvExporter` arbeiten korrekt mit `CultureInfo.InvariantCulture`. Es gibt aber fünf konkrete Fundstellen. ### 5.1 `PdfExporter` formatiert Geldbeträge kulturabhängig — **relevant, weil Finanzdokument** `src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:25` ```csharp string M(decimal baseAmount) => V(baseAmount).ToString("N2") + " " + currencyCode; ``` Ebenso Zeile 86: `e.Quantity.ToString("0.###")`, `e.PriceNative.ToString("0.###")`. Alle ohne `IFormatProvider`, also **CurrentCulture**. Auf dem heutigen deutschen Windows kommt `1.234,56` heraus. In einem Linux-Container mit `LANG=C` oder mit `InvariantGlobalization=true` wird daraus `1,234.56` – dieselbe Zahl, andere Bedeutung für einen Leser, und der PDF-Export ist ausdrücklich als *prüfbare Aufstellung* gedacht. Auffällig: der `CsvExporter` im selben Modul macht es richtig (`CultureInfo.InvariantCulture`). Der PDF-Export sollte bewusst festgelegt werden – entweder fest `de-DE` (Leserfreundlichkeit) oder fest invariant (Maschinenlesbarkeit), aber nicht "was der Host gerade meint". ### 5.2 `CapitolTradesScraper.ParseDate` parst kulturabhängig `src/IBKRTrader.Modules.CongressTrading/Scraper/CapitolTradesScraper.cs:333` ```csharp return DateOnly.TryParse(datePart, out var d) ? d : null; ``` Quelle ist capitoltrades.com mit ISO-Datum, das unter den meisten Kulturen durchgeht – aber `TryParse` ohne `IFormatProvider` ist bei einem externen, unkontrollierten Eingabeformat die falsche Wahl. `TryParseExact("yyyy-MM-dd", InvariantCulture)` ist hier auch fachlich richtiger: ein Formatwechsel bei der Quelle soll *auffallen*, nicht stillschweigend zu einem falschen Datum werden. Gleiches gilt für `int.Parse(match.Groups[1].Value)` in Zeile 124. ### 5.3 Kulturabhängiges `ToUpper()` / `ToLower()` - `Core/Logging/LoggingService.cs:115` – `e.Level.ToString().ToUpper()` - `Core/IBKR/IBKRGatewayService.cs:247` – `outsideRth.ToString().ToLower()`, fließt in eine URL Beide sind das klassische Türkisch-I-Problem und beide mit `…Invariant()` in einer Minute erledigt. Der zweite ist der unangenehmere, weil er in einen HTTP-Query-String geht. ### 5.4 Die Grundsatzentscheidung: ICU oder Invariant? Auf Linux kommt die Kulturdatenbank aus **ICU** (`libicu`). Das muss entschieden und im csproj festgeschrieben werden: - **Mit ICU** (`libicu` im Image installieren): Kulturen verhalten sich weitgehend wie auf Windows ab .NET 5, das ebenfalls ICU benutzt. Größeres Image. - **`InvariantGlobalization=true`**: schlankes Image, keine ICU-Abhängigkeit – aber *jede* kulturabhängige Formatierung wird lautlos invariant. Genau dann schlagen 5.1 und 5.2 durch. Solange 5.1/5.2 nicht behoben sind, ist die Wahl sicherheitsrelevant. Danach ist sie beliebig. Empfehlung: Fundstellen explizit machen, dann `InvariantGlobalization=true` (schlank und deterministisch). **Aufwand Block C: 0,5 Tage.** --- ## 6. Block D – Zeit und Zeitzonen ⚠ **der kritischste Punkt** Hier liegt das einzige Risiko, das *stillschweigend falsche Daten* erzeugt statt einen Fehler. ### 6.1 Die Ausgangslage Der Code mischt drei Konventionen: | Konvention | Fundstellen (Auswahl) | |---|---| | `DateTime.UtcNow` – korrekt für Persistenz | ~25 Stellen: alle EF-Entitäten, `ExecutionService`, `PortfolioService`, `TradeHistoryService`, `AccountingIngestService`, `BudgetService` | | `DateTime.Now` – Ortszeit des Hosts | `LoggingService:51`, `BackupWorker:34`, `WorkerBase:114/118/146`, `DailyReportService:44`, drei UI-Statuszeilen, `PdfExporter:51` | | `Kind = Unspecified` – weder noch | `IbkrMapping.ParseExecutionTime` | ### 6.2 `ParseExecutionTime` verwirft die Zeitzone – bewusst, aber jetzt folgenreich `src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs:99` (aus R10, gerade committet): ```csharp /// Die Zeitzone wird verworfen – der Wert bleibt Ortszeit der Börse, wie ihn TWS meldet. ``` TWS liefert je nach Aufruf `20260804 17:52:56` oder `20260804 17:52:56 Europe/Berlin`. Der Suffix wird abgeschnitten, es entsteht ein `DateTime` mit `Kind = Unspecified`. Dieser Wert landet in `BrokerExecution.Time` und von dort perspektivisch in der Buchführung – wo er neben `DateTime.UtcNow`-Feldern liegt. Solange alles auf **einem** Windows-Rechner mit `Europe/Berlin` läuft, ist das konsistent genug, um nicht aufzufallen. Auf einem Linux-Container mit `TZ=UTC` bedeutet derselbe abgeschnittene Zeitstempel plötzlich etwas anderes als vorher – **ohne dass sich eine Zeile Code ändert.** Es gibt keine Exception, keinen Log-Eintrag, nur um 1–2 Stunden verschobene Ausführungszeiten. **Das ist zu klären, bevor die erste Zeile portiert wird.** Der saubere Weg: die von TWS gemeldete Zeitzone *nicht* verwerfen, sondern über `TimeZoneInfo` (IANA-IDs, die TWS liefert bereits `Europe/Berlin`-Format) nach UTC konvertieren und als `DateTimeOffset` führen. Das ist auch unabhängig von Linux die bessere Lösung, weil Ausführungen an US-Börsen sonst Berliner Ortszeit tragen. > **Nebenbefund:** IANA-IDs (`Europe/Berlin`) funktionieren mit `TimeZoneInfo` auf .NET 6+ auch > auf Windows. Es braucht also keine ID-Übersetzung – ein Problem, das man bei solchen > Portierungen sonst regelmäßig hat, entfällt hier. ### 6.3 `DailyReportService` feuert zur falschen Uhrzeit `src/IBKRTrader.Modules.Supervisor/Services/DailyReportService.cs:56` ```csharp var candidate = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0, DateTimeKind.Local); ``` `IBKRTRADER_SUPERVISOR_DAILY=18` heißt heute "18 Uhr deutscher Zeit". Auf einem UTC-Container heißt es "20 Uhr deutscher Zeit" (Sommerzeit). Fachlich meint man aber eine Börsen- bzw. Ortszeit. Lösung: eine explizite Report-Zeitzone konfigurierbar machen statt `Local` zu benutzen. Der Test `NextRun` existiert bereits und lässt sich mitziehen. ### 6.4 Logdatei- und Backup-Namen `LoggingService` benennt Dateien nach `DateTime.Now` (`{Level}-dd-MM-yy.txt` bzw. `{yyyy-MM-dd}.jsonl`), `BackupWorker` nach `DateTime.Now` (`yyyy-MM-dd_HH-mm`). Beim Umzug auf UTC entsteht ein einmaliger Bruch in der Dateibenennung: der Tageswechsel liegt woanders, es kann für einen Tag zwei Teil-Dateien geben. Nicht kritisch, aber der `SupervisorTools`-Zugriff `Logs/{date}.jsonl` und `DossierService` lesen genau diese Namen – man sollte es wissen und bewusst umstellen, statt es zu entdecken. ### 6.5 Datenbank MariaDB speichert `DATETIME` ohne Offset. Solange die App UTC schreibt und UTC liest, ist der Server-`time_zone` egal. Wandert die DB später auch, ist das der Punkt, an dem man `SET time_zone` prüfen muss. **Ungeprüft** – für den reinen App-Umzug nicht relevant. **Aufwand Block D: 1–2 Tage**, davon der größere Teil Audit und Tests, nicht Code. --- ## 7. Block E – Dateisystem, Pfade, Prozesse ### 7.1 `BackupWorker` ist der einzige echt Windows-gebundene Codeteil `src/IBKRTrader.Core/Workers/BuiltIn/BackupWorker.cs:95-118` ```csharp var candidates = new[] { "mysqldump.exe", @"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe", @"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysqldump.exe", @"C:\xampp\mysql\bin\mysqldump.exe" }; foreach (var dir in pathVar.Split(';')) var full = Path.Combine(dir.Trim(), "mysqldump.exe"); ``` Drei Fehler auf einmal: `.exe`-Endung, Windows-Installationspfade, und `PATH` mit `;` getrennt – Linux nutzt `:`. Letzteres ist `Path.PathSeparator`. Lösung: Kandidatenliste und Endung über `OperatingSystem.IsWindows()` verzweigen, `mariadb-dump` als Kandidat aufnehmen (heißt auf aktuellen MariaDB-Versionen so), Trennzeichen aus `Path.PathSeparator`. > **Zusätzlicher Sicherheitsbefund, der erst auf Linux entsteht:** Zeile 66 übergibt das > DB-Passwort als Kommandozeilenargument (`--password={db.Password}`). Unter Linux ist > `/proc//cmdline` für **jeden lokalen Nutzer lesbar** – das Passwort steht damit für die > Dauer des Dumps offen im Prozessbaum. Auf Windows ist das weniger exponiert. Beim Umzug also > gleich auf `MYSQL_PWD` (Umgebungsvariable) oder eine temporäre Options-Datei mit `chmod 600` > umstellen. Halber Tag, und unabhängig von Linux ohnehin die bessere Lösung. ### 7.2 PDF-Export findet auf Linux keine Schriftart `src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:30` ```csharp style.Font.Name = "Segoe UI"; ``` "Segoe UI" ist eine Windows-Schrift und auf Linux nicht vorhanden. PDFsharp 6.x löst Schriften auf Nicht-Windows-Plattformen nicht automatisch auf – es braucht einen eigenen `GlobalFontSettings.FontResolver` (`IFontResolver`), der die Schriftdatei liefert. Ohne ihn scheitert der Export zur Laufzeit. Lösung: eine freie Schrift (z. B. DejaVu Sans oder Inter) als `EmbeddedResource` ins Accounting-Modul legen und einen kleinen `IFontResolver` implementieren. Das macht den PDF-Export gleichzeitig **plattformunabhängig reproduzierbar** – dasselbe Dokument sieht auf jedem Host identisch aus, was für ein prüfbares Finanzdokument ein Gewinn ist. Rund ein halber Tag. ### 7.3 Schreibzugriff neben der Binärdatei `Logs/`, `Backups/`, `settings.json`, `master.key` und `openrouter.key` liegen alle unter `AppDomain.CurrentDomain.BaseDirectory` bzw. `AppContext.BaseDirectory`. Auf Windows neben der `.exe` üblich. Auf Linux liegt eine Anwendung typischerweise unter `/opt/…` oder `/usr/local/…` und der Dienstbenutzer hat dort **keinen Schreibzugriff**. Erwartet werden `/var/log/ibkrtrader`, `/var/lib/ibkrtrader`, `/etc/ibkrtrader`. Das ist kein Einzeiler, sondern eine kleine Entwurfsentscheidung: die vier Pfade sollten aus einer zentralen `IAppPaths`-Abstraktion kommen, die unter Windows das heutige Verhalten beibehält und unter Linux den FHS-Konventionen folgt (oder per Umgebungsvariable überschreibbar ist). Betroffen sind `LoggingService:18`, `SettingsService:13`, `BackupWorker:35/126`, `Program.cs:227`, `OpenRouterClient:39`, `SupervisorTools:30`, `DossierService:29`. Dazu: `master.key` und `openrouter.key` brauchen auf Linux `chmod 600`. Ein Startup-Check, der zu weite Rechte meldet, wäre angemessen – Windows-ACLs übertragen sich nicht. ### 7.4 Groß-/Kleinschreibung Linux-Dateisysteme sind case-sensitiv. Zwei Stellen sind zu beachten: - Der Ordner heißt bei CongressTrading `UI/`, bei Accounting und Supervisor `Ui/`. MSBuild-Globbing stört das nicht, aber es ist eine Inkonsistenz, die man bei der Gelegenheit begradigen sollte. - `LoggingService:63` baut Log-Verzeichnisse aus dem `Module`-String: `Logs/Core`, `Logs/IBKR`, `Logs/CT`, `Logs/Supervisor`, `Logs/Accounting`, `Logs/AI`. Diese Strings sind über den Code verstreut (~90 Aufrufstellen). Auf Windows wären `Logs/CT` und `Logs/ct` dasselbe Verzeichnis, auf Linux zwei. Die Schreibweisen sind heute konsistent – aber es ist eine Fußangel, die eine Konstantenklasse (`LogModules.Core` etc.) endgültig entschärfen würde. ### 7.5 Kleinigkeiten - `LoggingService:69/72` schreibt hartkodiert `"\r\n"` in die `.txt`-Logs. Auf Linux kosmetisch störend; `Environment.NewLine` wäre richtig. (Das JSONL nutzt korrekt `"\n"`.) - `.gitattributes` ist vorhanden und korrekt konfiguriert (`* text=auto`, Binärdateien ausgenommen). **Kein Handlungsbedarf** – gemischte Zeilenenden werden beim Arbeiten von Linux aus nicht churnen. - `scripts/provision-db.ps1` ist PowerShell. `pwsh` gibt es auf Linux, aber ein `.sh`-Pendant wäre freundlicher. Optional, 1 Stunde. - Der Code mischt `AppDomain.CurrentDomain.BaseDirectory` (ältere Dateien) und `AppContext.BaseDirectory` (neuere). Identischer Wert, rein kosmetisch – erledigt sich mit 7.3. **Aufwand Block E: 1–1,5 Tage.** --- ## 8. Block F – Netzwerk und Dienste Drei `HttpListener`-basierte Dienste: | Datei | Prefix | |---|---| | `Core/Workers/BuiltIn/WebserverService.cs:38` | `http://localhost:{port}/` | | `Core/Workers/BuiltIn/WebApiService.cs:46` | `http://localhost:{port}/api/` | | `Modules.Supervisor/Mcp/McpLightServer.cs:44` | `http://127.0.0.1:{port}/mcp/` | `HttpListener` **funktioniert auf Linux** (dort als verwaltete Socket-Implementierung statt über `http.sys`). Zu beachten: - Ports unter 1024 brauchen root. Genutzt werden 5001 und ein per Env gesetzter MCP-Port, der bereits auf `1024–65535` geprüft wird – **passt**. - Kein HTTPS ohne Zusatzarbeit. Alle drei binden auf localhost, also derzeit kein Thema. - Auf Linux entfällt die `netsh urlacl`-Registrierung – eine Erleichterung, kein Problem. `IBKRGatewayService:39` setzt `ServerCertificateCustomValidationCallback = (_,_,_,_) => true`, akzeptiert also jedes Zertifikat. Das ist für den lokalen Client-Portal-Gateway mit Selbstsignat gedacht und funktioniert auf Linux identisch. Es ist unabhängig von dieser Portierung eine Stelle, die man irgendwann auf Pinning des Gateway-Zertifikats einengen sollte – hier nur der Vollständigkeit halber. **Aufwand Block F: 0 Tage** (nur Verifikation). Langfristig wäre ein Umstieg von `HttpListener` auf Kestrel/Minimal-API sauberer – `HttpListener` gilt als Altlast –, das ist aber **nicht** Voraussetzung für Linux. --- ## 9. Block G – Die UI: Avalonia + LiveCharts2 ### 9.1 Umfang | Datei | LOC | |---|---| | `LauncherForm.cs` | 162 | | `UI/ShellUiHost.cs` | 107 | | `UI/WorkerListBindingSource.cs` | 77 | | `UI/LogPanelController.cs` | 33 | | `UI/Views/DashboardView.cs` | 98 | | `UI/Views/{Logs,Settings,Workers}View.cs` | 64 | | `Core/Modularity/WindowMenu.cs` + `ModuleView.cs` | 136 | | `Modules.Accounting/Ui/AccountingMainForm.cs` | 302 | | `Modules.Supervisor/Ui/SupervisorMainForm.cs` | 188 | | `Modules.CongressTrading/UI/CongressTradingForm.cs` | 129 | | **Summe** | **~1.300** | Erfahrungsgemäß wächst das bei einer Umsetzung mit XAML + ViewModels auf 2.000–2.500 LOC, weil MVVM Struktur kostet, die im Code-Behind heute implizit ist. **Kein einziges Diagramm im Bestand.** Die Suche nach `Chart`/`Series`/`Plot` liefert null Treffer. LiveCharts2 ist damit **kein Migrationsposten, sondern ein Feature-Neubau** – und sollte auch so geplant und geschätzt werden, getrennt vom Rest. ### 9.2 Was gut übertragbar ist Der Shell-Entwurf passt bemerkenswert gut auf Avalonia: - `ShellUiHost` (Dictionary offener Fenster, eine Instanz je View, `OpenStateChanged`-Event) ist **fast vollständig toolkit-neutral**. Nur der Typ `Form` und `MessageBox.Show` müssen getauscht werden. Die 107 LOC überleben zu ~80 %. - Mehrere gleichrangige Top-Level-Fenster + Launcher entsprechen exakt Avalonias `IClassicDesktopStyleApplicationLifetime` mit mehreren `Window`-Instanzen. - `WorkerInfo` implementiert bereits `INotifyPropertyChanged` – das ist genau das, was Avalonias Binding erwartet. `WorkerListBindingSource` (`BindingList`) wird zu `ObservableCollection`. - Das UI-Thread-Marshalling (`InvokeRequired`/`BeginInvoke`, 6 Stellen) wird zu `Dispatcher.UIThread.Post` – ein mechanischer 1:1-Ersatz. ### 9.3 Was echte Arbeit ist | WinForms | Avalonia | Anmerkung | |---|---|---| | `DataGridView` (4×) | `DataGrid` | **eigenes Paket** `Avalonia.Controls.DataGrid` + Theme-Include in `App.axaml`. `AccountingMainForm:187` bindet heute anonyme Typen – die müssen zu echten Record-ViewModels werden. | | `MessageBox.Show` (`ShellUiHost:46`) | – | Avalonia hat **keine** eingebaute MessageBox. Eigener Dialog oder Zusatzpaket. | | `SaveFileDialog` (2×) | `IStorageProvider.SaveFilePickerAsync` | asynchron, anderer API-Zuschnitt | | `RichTextBox` mit `SelectionColor` | `ItemsControl`/`SelectableTextBlock` | Das farbige Log-Panel muss anders gebaut werden (eingefärbte Items statt Selection-Färbung) – im Ergebnis sauberer. | | `ToolStrip`/`MenuStrip`/`StatusStrip` | `Menu` + Panel-Layout | Direkte Entsprechungen fehlen; wird handgebaut. | | `DateTimePicker`, `ComboBox` | `DatePicker`, `ComboBox` | unkritisch | | `SystemColors.GrayText` etc. | Theme-Ressourcen | Fluent-Theme, gleichzeitig Hell/Dunkel möglich | | `Dock`/`Anchor`, absolute `Point`-Positionen | Grid/DockPanel/StackPanel | Layout muss neu gedacht, nicht übersetzt werden | Die Steuerelement-Inventur über alle UI-Dateien: 12 `Label`, 10 `Button`, 9 `Panel`, 4 `FlowLayoutPanel`, 4 `DataGridView`, 2 `ToolStrip`, 2 `TabControl`, 2 `SaveFileDialog`, 2 `MenuStrip`, 1 `StatusStrip`, 1 `RichTextBox`, 1 `BindingSource`. Überschaubar – es gibt keine exotischen Controls und kein Custom-Drawing. ### 9.4 Lizenz und Laufzeitabhängigkeiten - **Avalonia ist MIT-lizenziert**, ohne Kosten und ohne Umsatzschwelle. "Avalonia Accelerate" ist nur ein optionales kommerzielles Support-/Tooling-Paket. Für das Projekt entstehen keine Lizenzkosten. (Das passt zur bereits getroffenen Linie – PDFsharp wurde ausdrücklich statt QuestPDF gewählt, um Umsatzschwellen zu vermeiden.) - **LiveCharts2** (`LiveChartsCore.SkiaSharpView.Avalonia`) ist ebenfalls MIT. - Beide bringen **SkiaSharp** mit. Auf Linux braucht das `libfontconfig1` und (für die Desktop-UI) X11- oder Wayland-Bibliotheken im Image. Bei einer headless Variante entfällt das komplett – ein weiteres Argument für die Trennung in Abschnitt 11. - Zusätzliche `packageSourceMapping`-Muster: `Avalonia*`, `LiveChartsCore*`, `SkiaSharp*`, `HarfBuzzSharp*`, `Tmds.DBus*`, `MicroCom*`. **Aufwand Block G: 10–14 Personentage** für jemanden, der Avalonia kennt. Ohne Vorerfahrung realistisch +3–5 Tage Einarbeitung. LiveCharts2 zusätzlich 1–3 Tage je nach gewünschtem Umfang. --- ## 10. Block H – Tests, Smoke-Check und CI 165 Tests, 2.302 LOC. Das Testprojekt steht auf `net10.0-windows` + WinForms – **wegen genau einer Datei**: `UiConstructionTests.cs` (87 LOC), die die Modul-Fenster auf einem STA-Thread konstruiert. Dieselbe Idee steckt hinter dem `--smoke-ui`-Schalter in `Program.cs:270`. Nach der Portierung: - Die restlichen ~2.200 LOC Tests laufen **unverändert** auf `net10.0`/Linux. Sie testen Mapping, Risiko, Portfolio, Accounting, Supervisor, Krypto, Logging – alles portabel, mit EF-InMemory statt echter DB. - `UiConstructionTests` und `--smoke-ui` werden auf `Avalonia.Headless` umgestellt. **Das ist ein Gewinn, kein Verlust:** WinForms lässt sich in CI ohne Desktop-Session nicht sinnvoll instanziieren, Avalonia.Headless ist genau dafür gebaut. Der Smoke-Check wird damit CI-fähig, was er heute nicht ist. - Eine Test-Fixture hängt an einem Pfad: `tests/…csproj` bindet `..\..\ct_raw.html` mit Backslashes ein. MSBuild normalisiert das – **kein Problem**. Empfehlung für die CI: eine GitHub-Actions-/Gitea-Actions-Matrix `ubuntu-latest` + `windows-latest` einrichten, sobald Etappe 1 steht. Das hält die Portabilität dauerhaft und fängt Rückfälle (neues `DateTime.Now`, neues `.ToString("N2")`) sofort ab. **Aufwand Block H: 1–2 Tage.** --- ## 11. Betrieb: der Punkt, den man leicht übersieht **Die App auf Linux zu bringen, macht das IB Gateway nicht Linux-tauglich.** Das ist eine getrennte Baustelle: - TWS bzw. IB Gateway ist eine Java-Anwendung und läuft auf Linux – aber mit GUI. Für den Dauerbetrieb ohne Bildschirm braucht es **IBC** (IBController) plus **Xvfb** als virtuellen X-Server, dazu die Behandlung des täglichen Auto-Restarts und des 2FA-Handlings. - Das ist erfahrungsgemäß **1–2 Tage** eigener Arbeit und hat mit dem C#-Code nichts zu tun. - Alternative: Gateway bleibt auf dem Windows-Rechner, die Linux-App verbindet sich über das Netz auf Port 4002. Dann muss in der TWS-Konfiguration die erlaubte Client-IP eingetragen werden (heute steht in `settings.example.json` `127.0.0.1`) – und der TWS-API-Verkehr ist unverschlüsselt, gehört also nicht über ein unvertrautes Netz. Weitere Betriebspunkte: `systemd`-Unit statt Autostart, Log-Rotation über `logrotate` statt `BackupWorker`-Kopien, Dienstbenutzer ohne Login-Shell. **Aufwand Block I: 1–3 Tage**, je nachdem ob das Gateway mitwandert. --- ## 12. Aufwandsübersicht | Block | Inhalt | Tage | |---|---|---:| | **0** | `NuGet.config` reparieren (blockiert alles andere) | 0,1 | | **A** | Zielframeworks, csproj-Aufteilung, Projektstruktur | 0,5 | | **B** | Core von WinForms lösen (3 Dateien + 3 Modul-Forms herauslösen) | 1,0 | | **C** | Kultur/Formatierung (5 Fundstellen + Globalisierungsentscheidung) | 0,5 | | **D** | Zeit/Zeitzonen ⚠ (Audit, `ParseExecutionTime`, `DailyReportService`, Tests) | 1,5 | | **E** | Dateisystem (`BackupWorker`, PDF-Fonts, `IAppPaths`, Secrets-Rechte) | 1,5 | | **F** | Netzwerk/Dienste (nur Verifikation) | 0,0 | | **H** | Tests auf `net10.0`, Smoke-Check headless, CI-Matrix | 1,5 | | | **Zwischensumme: headless Linux lauffähig** | **~6,5** | | **G1** | Avalonia: Shell, Launcher, 4 Core-Views | 5,0 | | **G2** | Avalonia: 3 Modul-Fenster (Accounting ist das größte) | 5,0 | | **G3** | Theming, Feinschliff, Dialoge, Icons | 2,0 | | | **Zwischensumme: Desktop-Linux** | **~12** | | **G4** | LiveCharts2 – Neubau, kein Bestand vorhanden | 1–3 | | **I** | Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) | 1–3 | | | **Gesamt** | **~21–25 Personentage** | Ohne Avalonia-Vorerfahrung auf Block G realistisch **+3–5 Tage** aufschlagen. Zum Vergleich in Wochen: **headless in gut einer Woche**, **komplett mit Desktop-UI in etwa 4–5 Wochen** Vollzeit. --- ## 13. Vorschlag: in zwei Etappen, nicht in einem Zug ### Etappe 1 – Headless Linux (~1,5 Wochen) Ziel: Core + alle drei Module + Worker + REST/MCP laufen als `systemd`-Dienst auf Linux. Die Windows-UI bleibt **unverändert bestehen** und läuft weiter. 1. `NuGet.config` reparieren. 2. Die drei Core-Dateien und die drei Modul-Forms herauslösen; Core und Module auf `net10.0`. 3. Neues Projekt `IBKRTrader.Daemon` (Konsole, `net10.0`) – nimmt `Program.cs` ab Zeile 51 (`Host.CreateDefaultBuilder`) fast unverändert auf. Der Generic Host und die `IHostedService`-Worker sind dafür bereits die richtige Grundlage; das wurde in R4 gelegt. 4. Blöcke C, D, E abarbeiten. 5. Testprojekt auf `net10.0`, CI-Matrix Linux + Windows. Danach ist die App auf Linux **im Dauerbetrieb einsatzfähig** – ohne dass ein einziges Fenster angefasst wurde. Für ein System, das rund um die Uhr Marktdaten zieht und Signale verarbeitet, ist das der eigentliche Gewinn. ### Etappe 2 – Avalonia-Desktop (~2,5–3 Wochen) Ziel: `IBKRTrader.Desktop` ersetzt `IBKRTrader.App` und läuft auf beiden Plattformen. 6. Shell + Launcher + Core-Views. 7. Die drei Modul-Fenster. 8. Erst danach LiveCharts2 – als eigenständiges Feature mit eigener Anforderung, nicht als Nebenprodukt der Portierung. **Warum diese Reihenfolge:** Etappe 1 bringt den vollen Betriebsnutzen bei einem Sechstel des Aufwands, und sie ist reversibel – wenn Etappe 2 liegen bleibt, steht trotzdem ein funktionierendes System da. Umgekehrt (erst UI) hätte man nach drei Wochen eine schöne Oberfläche und immer noch keinen Linux-Betrieb. --- ## 14. Was du entscheiden musst Vier Punkte, die nicht aus dem Code ableitbar sind: 1. **Zeitzonen-Konvention.** Alles UTC in der Persistenz und nur an der Oberfläche umrechnen? Oder eine feste "Betriebszeitzone"? Das entscheidet den Zuschnitt von Block D – und es entscheidet, wie `ParseExecutionTime` künftig aussieht. **Das ist der einzige Punkt, der vor dem ersten Handgriff geklärt sein muss.** 2. **Wandert das IB Gateway mit auf Linux** (IBC + Xvfb, eigene 1–2 Tage) oder bleibt es auf dem Windows-Rechner und die Linux-App verbindet sich über das lokale Netz? 3. **Soll die Windows-Desktop-Version erhalten bleiben?** Avalonia läuft auf beiden Plattformen – die Frage ist nur, ob Windows weiter *getestet* werden muss (CI-Matrix) oder ob Linux das alleinige Ziel wird. 4. **Zahlenformat im PDF-Export:** fest deutsch oder fest invariant? Für ein prüfbares Finanzdokument sollte es festgelegt und nicht vom Host abhängig sein (5.1). --- ## Anhang: Fundstellenverzeichnis Kurzliste aller konkret zu ändernden Stellen außerhalb der UI, nach Datei sortiert: | Datei | Zeile | Befund | Block | |---|---|---|---| | `NuGet.config` | – | 3 fehlende `packageSourceMapping`-Muster → Restore schlägt fehl | 0 | | alle 6 `*.csproj` | – | `net10.0-windows` + `UseWindowsForms` | A | | `Core/Logging/LoggingService.cs` | 12, 22, 100–138 | `RichTextBox`, `System.Drawing.Color` | B | | `Core/Logging/LoggingService.cs` | 115 | `.ToUpper()` kulturabhängig | C | | `Core/Logging/LoggingService.cs` | 18, 63, 66, 88 | Pfade unter `BaseDirectory`, Modulnamen als Verzeichnisse | E | | `Core/Logging/LoggingService.cs` | 69, 72 | hartkodiertes `\r\n` | E | | `Core/Modularity/ModuleView.cs` | 27, 30 | `System.Drawing.Image`, `Func` | B | | `Core/Modularity/WindowMenu.cs` | gesamt | vollständig WinForms, wird ersetzt | B | | `Core/Trading/Ibkr/IbkrMapping.cs` | 99–113 | verwirft Zeitzone, `Kind = Unspecified` ⚠ | D | | `Core/IBKR/IBKRGatewayService.cs` | 247 | `.ToLower()` kulturabhängig, geht in URL | C | | `Core/Workers/BuiltIn/BackupWorker.cs` | 95–118 | `mysqldump.exe`, `C:\`-Pfade, `PATH.Split(';')` | E | | `Core/Workers/BuiltIn/BackupWorker.cs` | 66 | DB-Passwort in der Kommandozeile (auf Linux exponiert) | E | | `Core/Workers/BuiltIn/BackupWorker.cs` | 34, 35, 126 | `DateTime.Now`, `BaseDirectory` | D/E | | `Core/Workers/WorkerBase.cs` | 114, 118, 146 | `DateTime.Now` in der Ablaufsteuerung | D | | `Core/Settings/SettingsService.cs` | 13 | `settings.json` neben der Binärdatei | E | | `Modules.Accounting/Logic/PdfExporter.cs` | 30 | `"Segoe UI"` – auf Linux nicht vorhanden | E | | `Modules.Accounting/Logic/PdfExporter.cs` | 25, 86 | `ToString("N2")`/`("0.###")` kulturabhängig | C | | `Modules.Accounting/Ui/AccountingMainForm.cs` | gesamt (302) | WinForms, 3 `DataGridView`, 2 `SaveFileDialog` | G | | `Modules.CongressTrading/Scraper/CapitolTradesScraper.cs` | 124, 333 | `int.Parse`/`DateOnly.TryParse` ohne `IFormatProvider` | C | | `Modules.CongressTrading/UI/CongressTradingForm.cs` | gesamt (129) | WinForms | G | | `Modules.Supervisor/Services/DailyReportService.cs` | 44, 56 | `DateTime.Now` + `DateTimeKind.Local` → falsche Uhrzeit auf UTC-Host | D | | `Modules.Supervisor/Agent/OpenRouterClient.cs` | 39 | `openrouter.key` neben der Binärdatei, Dateirechte | E | | `Modules.Supervisor/Ui/SupervisorMainForm.cs` | gesamt (188) | WinForms, `RichTextBox` | G | | `Program.cs` | 31, 48, 227, 270–320 | `[STAThread]`, `ApplicationConfiguration`, `master.key`, Smoke-UI | A/B/E | | `LauncherForm.cs`, `UI/**` | gesamt (541) | WinForms-Shell | G | | `tests/…csproj` + `UiConstructionTests.cs` | – | `net10.0-windows` nur wegen einer Datei | H |