Files
IBKRTrader/docs/konzepte/KONZEPT-Linux-Portierung.md
T
RichardandClaude Opus 5 27577ef1b2 @ Doku: Analyse Linux-Portierung (Avalonia + net10, Aufwand, Fundstellen)
Bestandsaufnahme gegen b96a207, kein Code geaendert. Kernbefunde:

- Der nicht-UI-Teil ist portabel: kein DllImport, keine Registry, kein WMI,
  kein DPAPI, keine Windows-Zeitzonen-IDs. WinForms-Kopplung im Core sind
  genau 3 Dateien, je Modul genau eine Form-Datei.
- Daraus die Empfehlung: erst headless (~6,5 PT), dann Avalonia (~12 PT).
  LiveCharts2 ist Feature-Neubau, kein Migrationsposten (kein Diagramm im
  Bestand).
- Kritisch: ParseExecutionTime verwirft die Zeitzone (Kind=Unspecified) und
  DailyReportService nutzt DateTimeKind.Local - auf einem UTC-Host liefert
  derselbe Code lautlos andere Werte.
- NuGet.config ist bereits heute kaputt: PDFsharp-MigraDoc,
  Microsoft.EntityFrameworkCore (Glob matcht nicht ohne Suffix) und
  Microsoft.CodeAnalysis.CSharp.Workspaces fehlen im packageSourceMapping.
  Gegen leeren Paket-Ordner verifiziert (NU1100).
- Weiter: mysqldump.exe + PATH-Split(';') im BackupWorker, "Segoe UI" und
  kulturabhaengige Betragsformatierung im PdfExporter, Schreibzugriff neben
  der Binaerdatei (FHS), Fundstellenverzeichnis mit Datei+Zeile im Anhang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:26:49 +02:00

36 KiB
Raw Blame History

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 19 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 57 Personentage
B. Desktop-Linux (zusätzlich die komplette UI auf Avalonia) ~1.300 LOC WinForms neu bauen +1014 Personentage
C. LiveCharts2 (heute existiert kein einziges Diagramm) Neubau, keine Migration +13 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 <clear/>. 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:

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<LogEntry>? 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)

public System.Drawing.Image? Icon { get; set; }
public Func<Form> CreateForm { get; init; } = () => new Form();

Umbau: Der Contract muss toolkit-neutral werden. Zwei Wege:

  • Pragmatisch: Func<object> 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

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

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):

/// 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 12 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

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: 12 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

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/<pid>/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

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: 11,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 102465535 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.0002.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<T>) wird zu ObservableCollection<T>.
  • 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: 1014 Personentage für jemanden, der Avalonia kennt. Ohne Vorerfahrung realistisch +35 Tage Einarbeitung. LiveCharts2 zusätzlich 13 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: 12 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äß 12 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: 13 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 13
I Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) 13
Gesamt ~2125 Personentage

Ohne Avalonia-Vorerfahrung auf Block G realistisch +35 Tage aufschlagen.

Zum Vergleich in Wochen: headless in gut einer Woche, komplett mit Desktop-UI in etwa 45 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,53 Wochen)

Ziel: IBKRTrader.Desktop ersetzt IBKRTrader.App und läuft auf beiden Plattformen.

  1. Shell + Launcher + Core-Views.
  2. Die drei Modul-Fenster.
  3. 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 12 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, 100138 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<Form> B
Core/Modularity/WindowMenu.cs gesamt vollständig WinForms, wird ersetzt B
Core/Trading/Ibkr/IbkrMapping.cs 99113 verwirft Zeitzone, Kind = Unspecified D
Core/IBKR/IBKRGatewayService.cs 247 .ToLower() kulturabhängig, geht in URL C
Core/Workers/BuiltIn/BackupWorker.cs 95118 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, 270320 [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