Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27577ef1b2 |
@@ -0,0 +1,689 @@
|
|||||||
|
# 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 `<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`:
|
||||||
|
|
||||||
|
```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<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)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`
|
||||||
|
|
||||||
|
```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/<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`
|
||||||
|
|
||||||
|
```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<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: 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<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 |
|
||||||
Reference in New Issue
Block a user