Das Suffix ".Avalonia" gab es nur, weil daneben ein WinForms-IBKRTrader.App
stand. Das ist seit L5 weg, also faellt auch das Suffix. Git erkennt alle
Dateien als Umbenennung; Assembly, Wurzel-Namensraum und die
avares://-Ressourcen-URI sind mitgezogen.
Nebeneffekt der Umbenennung: die global::Avalonia-Qualifizierungen entfallen.
Sie waren noetig, weil der Namensraum IBKRTrader.App.Avalonia das
Avalonia-Paket verdeckt hat - ein Ueberbleibsel genau der Namensgebung, die
jetzt weg ist.
Inhaltlich falsch gewordene Aussagen berichtigt - das waren die eigentlichen
Ueberbleibsel, nicht die Kommentare:
- .agents/rules/grundregeln.md schrieb weiterhin "C# .NET 10 WinForms",
RichTextBox-Logging, LauncherForm und PropertyGrid vor. Das ist die Regel,
nach der kuenftig gearbeitet wird - sie haette die Portierung Stueck fuer
Stueck rueckgaengig gemacht. Jetzt: Avalonia, keine Plattform-Suffixe, die
11er-Pinnung mit Begruendung, dazu die beiden Regeln, die uns in L1b am
meisten gekostet haben (UTC persistieren + AppTimeZone statt DateTime.Now;
jede Formatierung mit ausdruecklichem IFormatProvider).
- Core: LogEntry ("wird in RichTextBox geschrieben"), IWorker/WorkerEngine/
WorkerInfo ("DataGridView-Zeile"/"-Binding"), ModuleView ("die
WinForms-Shell castet auf Form").
- Doku: ARCHITECTURE (Modul-Ui-Ordner, "designbare Forms mit Initialize"),
KONZEPT-Modul-Accounting ("UI (WinForms, ein Fenster mit Tabs)").
BEWUSST STEHEN GEBLIEBEN sind die Kommentare, die WinForms nur als
Begruendung nennen - warum LoggingService ein Ereignis hat statt einer
RichTextBox, warum ModuleView Func<object> liefert, warum es benannte
Record-Zeilentypen gibt, warum die Einstellungsmaske aus Attributen entsteht.
Das ist die Herleitung des heutigen Entwurfs; ohne sie sieht spaeter jede
dieser Stellen nach Umstaendlichkeit ohne Grund aus.
KONZEPT-Linux-Portierung.md bekommt einen Statusvermerk: umgesetzt, die
Pfadangaben im Fundstellenverzeichnis beziehen sich auf den alten Aufbau.
Zwei Abweichungen von der Schaetzung sind dort festgehalten - der geringere
Aufwand dank der PolytraderSharp-Vorlage, und dass die dort empfohlene
InvariantGlobalization ein Fehler gewesen waere.
Verifiziert: Build 0 Fehler/0 Warnungen, 193 Tests gruen, Smoke-UI
konstruiert alle 7 Ansichten + Launcher + Dialog, Daemon-Prueflauf OK,
publish -r linux-x64 fuer beide Einstiegspunkte fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 KiB
Analyse: Linux-Fähigkeit des IBKRTrader
UMGESETZT am 2026-08-07 (L0–L5). Dieses Dokument ist die Analyse, die der Portierung vorausging, und bleibt als Begründung erhalten – es beschreibt den Stand vor dem Umbau. Was tatsächlich gebaut wurde, steht in der Phasen-Checkliste von ../ARCHITECTURE.md; die Pfadangaben im Fundstellenverzeichnis unten beziehen sich auf den alten Aufbau.
Zwei Punkte sind gegenüber der Schätzung anders gekommen:
- Der Aufwand lag deutlich unter den veranschlagten 21–25 Personentagen, weil PolytraderSharp dieselbe Portierung bereits durchlaufen hatte und als Vorlage diente (Avalonia-Pinnung, toolkit-neutraler Contract,
AppTimeZone).InvariantGlobalization=true– in der Analyse noch als Empfehlung für ein schlankes Image genannt – wäre ein Fehler gewesen: ohne ICU fällt die Auflösung von Windows-Zeitzonen-IDs aus und die festede-DE-Formatierung des PDF-Exports kippt auf invariant. Beides lautlos. Der Daemon setzt es deshalb ausdrücklich auffalse.
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 insrc/aufzunehmen, ersetzt das NuGet-Mirror-Paket, beseitigtNU1701und 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 lautetMicrosoft.EntityFrameworkCore.*; der Glob matcht das Paket ohne Suffix nicht.Microsoft.CodeAnalysis.CSharp.Workspaces– transitiv überEntityFrameworkCore.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:
ImplicitUsings+UseWindowsFormsfügtSystem.Windows.FormsundSystem.Drawingals globale Usings hinzu. Sobald das wegfällt, brechen Dateien, die unbemerktPoint,Size,Color,FontoderPaddingbenutzt 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.ApplicationHighDpiMode,Properties/Resources.resx(24System.Drawing.Bitmap-Icons) undApplicationConfiguration.Initialize()verschwinden mit dem App-Projekt. Die 24 PNGs unterResources/bleiben brauchbar, werden aber in Avalonia überAvaloniaResource+Bitmapeingebunden statt über denResourceManager.
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 aufWindowabbildet.
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 (
libicuim 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 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 mitTimeZoneInfoauf .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: 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
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>/cmdlinefü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 aufMYSQL_PWD(Umgebungsvariable) oder eine temporäre Options-Datei mitchmod 600umstellen. 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 SupervisorUi/. MSBuild-Globbing stört das nicht, aber es ist eine Inkonsistenz, die man bei der Gelegenheit begradigen sollte. LoggingService:63baut Log-Verzeichnisse aus demModule-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ärenLogs/CTundLogs/ctdasselbe Verzeichnis, auf Linux zwei. Die Schreibweisen sind heute konsistent – aber es ist eine Fußangel, die eine Konstantenklasse (LogModules.Coreetc.) endgültig entschärfen würde.
7.5 Kleinigkeiten
LoggingService:69/72schreibt hartkodiert"\r\n"in die.txt-Logs. Auf Linux kosmetisch störend;Environment.NewLinewäre richtig. (Das JSONL nutzt korrekt"\n".).gitattributesist vorhanden und korrekt konfiguriert (* text=auto, Binärdateien ausgenommen). Kein Handlungsbedarf – gemischte Zeilenenden werden beim Arbeiten von Linux aus nicht churnen.scripts/provision-db.ps1ist PowerShell.pwshgibt es auf Linux, aber ein.sh-Pendant wäre freundlicher. Optional, 1 Stunde.- Der Code mischt
AppDomain.CurrentDomain.BaseDirectory(ältere Dateien) undAppContext.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–65535geprü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 TypFormundMessageBox.Showmüssen getauscht werden. Die 107 LOC überleben zu ~80 %.- Mehrere gleichrangige Top-Level-Fenster + Launcher entsprechen exakt Avalonias
IClassicDesktopStyleApplicationLifetimemit mehrerenWindow-Instanzen. WorkerInfoimplementiert bereitsINotifyPropertyChanged– das ist genau das, was Avalonias Binding erwartet.WorkerListBindingSource(BindingList<T>) wird zuObservableCollection<T>.- Das UI-Thread-Marshalling (
InvokeRequired/BeginInvoke, 6 Stellen) wird zuDispatcher.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
libfontconfig1und (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. UiConstructionTestsund--smoke-uiwerden aufAvalonia.Headlessumgestellt. 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/…csprojbindet..\..\ct_raw.htmlmit 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.json127.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.
NuGet.configreparieren.- Die drei Core-Dateien und die drei Modul-Forms herauslösen; Core und Module auf
net10.0. - Neues Projekt
IBKRTrader.Daemon(Konsole,net10.0) – nimmtProgram.csab Zeile 51 (Host.CreateDefaultBuilder) fast unverändert auf. Der Generic Host und dieIHostedService-Worker sind dafür bereits die richtige Grundlage; das wurde in R4 gelegt. - Blöcke C, D, E abarbeiten.
- 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.
- Shell + Launcher + Core-Views.
- Die drei Modul-Fenster.
- 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:
- 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
ParseExecutionTimekünftig aussieht. Das ist der einzige Punkt, der vor dem ersten Handgriff geklärt sein muss. - 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?
- 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.
- 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<Form> |
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 |