From d86a08343889dce9caba1cee24d743ca574a8f12 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 7 Aug 2026 10:35:01 +0200 Subject: [PATCH] L5: WinForms vollstaendig entfernt - das Projekt ist plattformneutral Die Avalonia-Shell hatte in L4 Funktionsgleichheit erreicht, damit ist die WinForms-Fassung ueberfluessig. Entfernt: IBKRTrader.App.csproj, Program.cs, LauncherForm.cs, UI/ (14 Dateien) und Properties/Resources.* - zusammen rund 1.900 Zeilen. Rueckfallpunkt bleibt der Tag winforms-final. Resources/ mit den 24 PNG-Symbolen bleibt: die Avalonia-Shell bindet sie ueber AvaloniaResource ein und nutzt dieselben Symbolschluessel. Kein Projekt traegt mehr eine Plattformbindung. Verifiziert: alle acht Projekte bauen einzeln fuer linux-x64, und die gesamte Projektmappe baut ohne Fehler und ohne Warnung. Die CI kann damit vereinfacht werden: der Behelf aus L4 - unter Linux die Projekte einzeln statt der Mappe zu bauen, weil IBKRTrader.App dort nicht baubar war - entfaellt. Beide Plattformen bauen und testen jetzt dieselbe Mappe. Doku nachgezogen: README (Architektur-Uebersicht, Start- und Pruefbefehle), ARCHITECTURE (Ziel-Architektur auf den Ist-Stand, neuer Abschnitt L0-L5 in der Phasen-Checkliste, AppTimeZone ergaenzt). Der Hinweis "UI-Contract liegt im Core" war nach L1a falsch - WindowMenu ist dort raus, der Contract ist jetzt toolkit-neutral. Verifiziert: Build 0 Fehler/0 Warnungen, 193 Tests gruen, Smoke-UI konstruiert alle 7 Ansichten + Launcher + Dialog, Daemon-Prueflauf OK. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/build.yml | 24 +- IBKRTrader.App.csproj | 65 ---- IBKRTrader.slnx | 1 - LauncherForm.cs | 163 ---------- Program.cs | 133 -------- Properties/Resources.Designer.cs | 303 ------------------ Properties/Resources.resx | 133 -------- README.md | 24 +- UI/LogPanelController.cs | 79 ----- UI/ModuleViews.cs | 71 ---- UI/ShellUiHost.cs | 109 ------- UI/ViewIcons.cs | 52 --- UI/Views/DashboardView.cs | 99 ------ UI/Views/LogsView.cs | 21 -- UI/Views/Modules/AccountingMainForm.cs | 303 ------------------ UI/Views/Modules/CongressTradingForm.cs | 131 -------- UI/Views/Modules/SupervisorMainForm.cs | 188 ----------- UI/Views/SettingsView.cs | 23 -- UI/Views/WorkersView.cs | 20 -- UI/WindowMenu.cs | 85 ----- UI/WorkerListBindingSource.cs | 77 ----- docs/ARCHITECTURE.md | 40 ++- .../IBKRTrader.App.Avalonia.csproj | 6 +- 23 files changed, 68 insertions(+), 2082 deletions(-) delete mode 100644 IBKRTrader.App.csproj delete mode 100644 LauncherForm.cs delete mode 100644 Program.cs delete mode 100644 Properties/Resources.Designer.cs delete mode 100644 Properties/Resources.resx delete mode 100644 UI/LogPanelController.cs delete mode 100644 UI/ModuleViews.cs delete mode 100644 UI/ShellUiHost.cs delete mode 100644 UI/ViewIcons.cs delete mode 100644 UI/Views/DashboardView.cs delete mode 100644 UI/Views/LogsView.cs delete mode 100644 UI/Views/Modules/AccountingMainForm.cs delete mode 100644 UI/Views/Modules/CongressTradingForm.cs delete mode 100644 UI/Views/Modules/SupervisorMainForm.cs delete mode 100644 UI/Views/SettingsView.cs delete mode 100644 UI/Views/WorkersView.cs delete mode 100644 UI/WindowMenu.cs delete mode 100644 UI/WorkerListBindingSource.cs diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 53a8e6b..d1a8241 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -30,22 +30,16 @@ jobs: with: dotnet-version: '10.0.x' - # Windows: die gesamte Projektmappe, inklusive der alten WinForms-Shell. - - name: Build (Windows, gesamte Mappe) - if: matrix.os == 'windows-latest' - run: | - dotnet build IBKRTrader.slnx -c Release - dotnet test IBKRTrader.slnx --no-build -c Release --logger "trx;LogFileName=test-results.trx" + # Die gesamte Projektmappe baut auf beiden Plattformen – seit die WinForms-Shell + # entfernt ist, gibt es kein Projekt mehr mit Windows-Bindung. + - name: Restore + run: dotnet restore IBKRTrader.slnx - # Linux: alles AUSSER IBKRTrader.App (WinForms, net10.0-windows) – das lässt sich hier - # naturgemäß nicht bauen. Diese Aufzählung entfällt, sobald die WinForms-Shell aus dem - # Build genommen wird; dann baut auch hier wieder die ganze Mappe. - - name: Build (Linux, plattformneutrale Projekte) - if: matrix.os == 'ubuntu-latest' - run: | - dotnet build src/IBKRTrader.App.Avalonia -c Release - dotnet build src/IBKRTrader.Daemon -c Release - dotnet test tests/IBKRTrader.Tests -c Release --logger "trx;LogFileName=test-results.trx" + - name: Build + run: dotnet build IBKRTrader.slnx --no-restore -c Release + + - name: Test + run: dotnet test IBKRTrader.slnx --no-build -c Release --logger "trx;LogFileName=test-results.trx" # Die Konstruktionsprüfung der Oberfläche braucht mit Avalonia KEIN Anzeigegerät mehr # (SetupWithoutStarting). Mit WinForms war das auf einem Build-Server nicht möglich. diff --git a/IBKRTrader.App.csproj b/IBKRTrader.App.csproj deleted file mode 100644 index 172ccd5..0000000 --- a/IBKRTrader.App.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - WinExe - net10.0-windows - enable - true - enable - en - PerMonitorV2 - IBKRTrader.App - IBKRTrader - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - diff --git a/IBKRTrader.slnx b/IBKRTrader.slnx index 3a81a0c..d274988 100644 --- a/IBKRTrader.slnx +++ b/IBKRTrader.slnx @@ -1,5 +1,4 @@ - diff --git a/LauncherForm.cs b/LauncherForm.cs deleted file mode 100644 index 6af25b7..0000000 --- a/LauncherForm.cs +++ /dev/null @@ -1,163 +0,0 @@ -using IBKRTrader.Core.Time; -using IBKRTrader.Core.Logging; -using IBKRTrader.Core.Modularity; -using IBKRTrader.Core.Settings; -using IBKRTrader.Core.Workers; -using IBKRTrader.UI; -using Microsoft.Extensions.DependencyInjection; - -namespace IBKRTrader; - -/// -/// Launcher – das Basis-Fenster (Shell). Zeigt je registrierter View einen Button, führt beim Start -/// Migrationen aus, startet Module und die WorkerEngine, und trägt das gemeinsame Fenster-Menü. -/// Die inhaltlichen Ansichten (Logs, Settings, Workers, Module) sind eigenständige Fenster. -/// -public sealed class LauncherForm : Form -{ - private readonly ShellUiHost _uiHost; - private readonly IServiceProvider _services; - private readonly LoggingService _logger; - private readonly WorkerEngine _workerEngine; - private readonly IReadOnlyList _modules; - - private readonly Dictionary _viewButtons = new(StringComparer.OrdinalIgnoreCase); - private readonly ToolStripStatusLabel _status = new("Start..."); - - public LauncherForm(ShellUiHost uiHost, IServiceProvider services) - { - _uiHost = uiHost; - _services = services; - _logger = services.GetRequiredService(); - _workerEngine = services.GetRequiredService(); - _modules = services.GetServices().ToList(); - - Text = "IBKRTrader — Launcher"; - Width = 720; - Height = 540; - StartPosition = FormStartPosition.CenterScreen; - - _uiHost.SetMainWindow(this); - - BuildUi(); - _uiHost.OpenStateChanged += UpdateButtonStates; - } - - private void BuildUi() - { - var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new Size(24, 24) }; - WindowMenu.Wire(menu, _uiHost, null); - - // Fenster-Buttons in einem ToolStrip (Icon über Text) – wie im PolytraderSharp-Launcher. - var toolstrip = new ToolStrip - { - Dock = DockStyle.Top, - GripStyle = ToolStripGripStyle.Hidden, - ImageScalingSize = new Size(32, 32), - AutoSize = true, - Padding = new Padding(4) - }; - - foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title)) - { - var id = view.Id; - var btn = new ToolStripButton(view.Title, ViewIcons.Resolve(view.IconKey)) - { - DisplayStyle = ToolStripItemDisplayStyle.ImageAndText, - ImageScaling = ToolStripItemImageScaling.None, - TextImageRelation = TextImageRelation.ImageAboveText, - AutoSize = true, - Padding = new Padding(6, 2, 6, 2) - }; - btn.Click += (_, _) => _uiHost.OpenView(id); - _viewButtons[id] = btn; - toolstrip.Items.Add(btn); - } - - var content = new Panel { Dock = DockStyle.Fill, BackColor = SystemColors.ControlLightLight }; - content.Controls.Add(new Label - { - Text = "IBKRTrader — Launcher\nFenster über die Leiste oben öffnen.", - Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleCenter, - ForeColor = SystemColors.GrayText, Font = new Font(Font.FontFamily, 11f) - }); - - var statusStrip = new StatusStrip(); - statusStrip.Items.Add(_status); - - // Dock-Stacking: zuletzt hinzugefügtes Top-Control liegt oben → Menü über ToolStrip. - Controls.Add(content); - Controls.Add(statusStrip); - Controls.Add(toolstrip); - Controls.Add(menu); - MainMenuStrip = menu; - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - _ = StartupAsync(); - } - - protected override void OnFormClosing(FormClosingEventArgs e) - { - // Auch das Schließen-X läuft über die Sicherheitsabfrage. - if (!_uiHost.ShutdownConfirmed) - { - e.Cancel = true; - BeginInvoke((Action)(() => _uiHost.RequestShutdown())); - return; - } - - _uiHost.CloseAllViews(); - try - { - foreach (var module in _modules) - module.StopAsync(default).GetAwaiter().GetResult(); - } - catch { /* Shutdown darf nicht am Modul scheitern */ } - // Worker/Services stoppt der Host in Program.Main via AppHost.StopAsync() nach Application.Run. - - base.OnFormClosing(e); - } - - private async Task StartupAsync() - { - _logger.Info("Core", "=== IBKRTrader startet ==="); - _logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}"); - - // Log-Level aus Settings. - var levelStr = _services.GetRequiredService().Settings.Logging.Level; - if (Enum.TryParse(levelStr, true, out var level)) - _logger.SetMinLevel(level); - - // Das gesamte Schema (core_ + ct_) läuft über EF-Migrationen, extern via - // `dotnet ef database update` angewendet – keine Laufzeit-Migration mehr. - - // Worker/Services laufen bereits (Generic Host, AppHost.Start()). Hier nur noch Modul-Start. - foreach (var module in _modules) - { - try { await module.StartAsync(default); } - catch (Exception ex) { _logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); } - } - - _logger.Info("Core", "IBKRTrader bereit."); - SetStatus("Bereit"); - UpdateButtonStates(); - } - - private void UpdateButtonStates() - { - if (IsDisposed) return; - if (InvokeRequired) { BeginInvoke((Action)UpdateButtonStates); return; } - foreach (var (id, btn) in _viewButtons) - btn.Checked = _uiHost.IsOpen(id); - } - - private void SetStatus(string text) - { - if (IsDisposed) return; - if (InvokeRequired) { BeginInvoke((Action)(() => SetStatus(text))); return; } - _status.Text = $"Status: {text} | {AppTimeZone.Now:HH:mm:ss}"; - } -} diff --git a/Program.cs b/Program.cs deleted file mode 100644 index c710707..0000000 --- a/Program.cs +++ /dev/null @@ -1,133 +0,0 @@ -using IBKRTrader.Core.Logging; -using IBKRTrader.Core.Modularity; -using IBKRTrader.Core.Settings; -using IBKRTrader.Core.Trading; -using IBKRTrader.Core.Workers; -using IBKRTrader.Hosting; -using IBKRTrader.UI; -using IBKRTrader.UI.Views; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace IBKRTrader; - -/// -/// Einstiegspunkt der WinForms-Shell (Windows). -/// -/// Der Host selbst kommt aus und ist plattformneutral – hier -/// wird er nur um Fensterverwaltung und Launcher ergänzt. Derselbe Host trägt den kopflosen -/// Linux-Dienst IBKRTrader.Daemon; so kann keine der beiden Varianten eine Registrierung -/// vergessen. -/// -internal static class Program -{ - public static IHost? AppHost { get; private set; } - - [STAThread] - static void Main(string[] args) - { - // Headless-Smoke-Test der UI (konstruiert jede View + Launcher, ohne Message-Loop). - if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase)) - { - Environment.ExitCode = RunSmokeUi(); - return; - } - - ApplicationConfiguration.Initialize(); - - var modules = AppHostBuilder.CreateModules(); - AppHost = AppHostBuilder.Build(modules, RegisterShellServices); - - AppHostBuilder.RunStartupChecks(AppHost.Services); - - // Host starten → alle Worker/Services (IHostedService) laufen an. - AppHost.Start(); - - // Views registrieren (Core + Module), dann Launcher starten. - var uiHost = AppHost.Services.GetRequiredService(); - RegisterCoreViews(uiHost, AppHost.Services); - foreach (var module in modules) - module.RegisterUi(uiHost, AppHost.Services); - ModuleViews.Register(uiHost, AppHost.Services); - ViewIcons.AssignDefaults(uiHost); - - Application.Run(AppHost.Services.GetRequiredService()); - AppHost.StopAsync().GetAwaiter().GetResult(); - } - - /// Nur die Dienste, die es ohne Oberfläche nicht gibt. - private static void RegisterShellServices(IServiceCollection services, IConfiguration configuration) - { - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - } - - /// Registriert die Core-Views (Dashboard, Workers, Logs, Settings) bei der Shell. - private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp) - { - uiHost.RegisterView(new ModuleView - { - Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5, - CreateView = () => new DashboardView( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetServices(), - sp.GetRequiredService(), - sp.GetServices()) - }); - uiHost.RegisterView(new ModuleView - { - Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10, - CreateView = () => new WorkersView(sp.GetRequiredService()) - }); - uiHost.RegisterView(new ModuleView - { - Id = "core.logs", Title = "Logs", Group = "Core", Order = 20, - CreateView = () => new LogsView(sp.GetRequiredService()) - }); - uiHost.RegisterView(new ModuleView - { - Id = "core.settings", Title = "Settings", Group = "Core", Order = 30, - CreateView = () => new SettingsView(sp.GetRequiredService()) - }); - } - - /// - /// Headless-Smoke-Test: baut den Host, registriert Views + Modul-UI und konstruiert jede - /// registrierte View sowie den Launcher – ohne Message-Loop. Gibt die Fehleranzahl zurück. - /// Der Host wird bewusst NICHT gestartet: sonst liefen Worker und Broker-Verbindungen an, - /// was für eine reine Konstruktionsprüfung unerwünscht ist. - /// - private static int RunSmokeUi() - { - ApplicationConfiguration.Initialize(); - - var modules = AppHostBuilder.CreateModules(); - using var host = AppHostBuilder.Build(modules, RegisterShellServices); - - AppHostBuilder.RunStartupChecks(host.Services); - - var uiHost = host.Services.GetRequiredService(); - RegisterCoreViews(uiHost, host.Services); - foreach (var module in modules) - module.RegisterUi(uiHost, host.Services); - ModuleViews.Register(uiHost, host.Services); - ViewIcons.AssignDefaults(uiHost); - - var failures = 0; - Console.WriteLine("=== Smoke-UI: View-Konstruktion ==="); - foreach (var view in uiHost.Views) - { - try { using var form = (Form)view.CreateView(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); } - catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); } - } - - try { using var launcher = host.Services.GetRequiredService(); Console.WriteLine("[OK] LauncherForm konstruiert"); } - catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}"); } - - Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); - return failures; - } -} diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs deleted file mode 100644 index 5637a2e..0000000 --- a/Properties/Resources.Designer.cs +++ /dev/null @@ -1,303 +0,0 @@ -//------------------------------------------------------------------------------ -// -// Dieser Code wurde von einem Tool generiert. -// Laufzeitversion:4.0.30319.42000 -// -// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn -// der Code erneut generiert wird. -// -//------------------------------------------------------------------------------ - -namespace IBKRTrader.Properties { - using System; - - - /// - /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. - /// - // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert - // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. - // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen - // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IBKRTrader.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle - /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap accept_button { - get { - object obj = ResourceManager.GetObject("accept_button", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap add { - get { - object obj = ResourceManager.GetObject("add", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap cancel { - get { - object obj = ResourceManager.GetObject("cancel", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap coins_in_hand { - get { - object obj = ResourceManager.GetObject("coins_in_hand", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap cross_reference { - get { - object obj = ResourceManager.GetObject("cross_reference", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap dashboard { - get { - object obj = ResourceManager.GetObject("dashboard", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap delete { - get { - object obj = ResourceManager.GetObject("delete", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap diskette { - get { - object obj = ResourceManager.GetObject("diskette", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap emotion_batman { - get { - object obj = ResourceManager.GetObject("emotion_batman", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap error_log { - get { - object obj = ResourceManager.GetObject("error_log", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap file_start_workflow { - get { - object obj = ResourceManager.GetObject("file_start_workflow", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap money { - get { - object obj = ResourceManager.GetObject("money", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap money_add { - get { - object obj = ResourceManager.GetObject("money_add", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap money_delete { - get { - object obj = ResourceManager.GetObject("money_delete", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap money_dollar { - get { - object obj = ResourceManager.GetObject("money_dollar", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap refresh_all { - get { - object obj = ResourceManager.GetObject("refresh_all", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap setting_tools { - get { - object obj = ResourceManager.GetObject("setting_tools", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap stop { - get { - object obj = ResourceManager.GetObject("stop", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap system_time { - get { - object obj = ResourceManager.GetObject("system_time", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap token_quantifier { - get { - object obj = ResourceManager.GetObject("token_quantifier", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap traffic_lights_green { - get { - object obj = ResourceManager.GetObject("traffic_lights_green", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap traffic_lights_red { - get { - object obj = ResourceManager.GetObject("traffic_lights_red", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap traffic_lights_yellow { - get { - object obj = ResourceManager.GetObject("traffic_lights_yellow", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap warning { - get { - object obj = ResourceManager.GetObject("warning", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} diff --git a/Properties/Resources.resx b/Properties/Resources.resx deleted file mode 100644 index 18293c6..0000000 --- a/Properties/Resources.resx +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - ..\Resources\accept_button.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\cancel.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\coins_in_hand.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\cross_reference.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\dashboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\diskette.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\emotion_batman.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\error_log.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\file_start_workflow.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\money.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\money_add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\money_delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\money_dollar.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\refresh_all.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\setting_tools.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\stop.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\system_time.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\token_quantifier.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\traffic_lights_green.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\traffic_lights_red.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\traffic_lights_yellow.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\warning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - diff --git a/README.md b/README.md index e219f33..3f98174 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,20 @@ Modulares C#-Trading-Framework für Interactive-Brokers-Aktien. **Harter Core + Strategie-Module + Launcher**, der die Fenster der Module öffnet. Konzept nach dem Vorbild von PolytraderSharp (nur IBKR statt Polymarket). +**Läuft auf Windows und Linux** – wahlweise mit Oberfläche (Avalonia) oder kopflos als Dienst. + ## Architektur (Kurzform) ``` -IBKRTrader.App WinExe – Generic Host + Launcher/Shell (WinForms) +src/IBKRTrader.App.Avalonia Oberfläche (Avalonia, plattformneutral) +src/IBKRTrader.Daemon kopfloser Dienst (systemd) – dieselbe Anwendung ohne Fenster +src/IBKRTrader.Hosting Host-Zusammenstellung, von beiden Einstiegspunkten geteilt src/IBKRTrader.Core Contracts, EF-Persistenz, Trading-Kern, Worker, Security src/IBKRTrader.Modules.* je Modul ein eigenes Projekt (referenziert nur Core) tests/IBKRTrader.Tests xUnit (Unit + EF-InMemory) ``` +Alle Projekte sind `net10.0` ohne Plattformbindung. Der UI-Contract im Core ist toolkit-neutral +(`Func CreateView`, `IconKey` statt Bild), damit Core und Module auch kopflos laufen – +die Fenster registriert die Shell zentral in `Shell/ModuleViews.cs`. - **Generic Host** (`Host.CreateDefaultBuilder`), Worker/Services als `IHostedService`. - **Module** über `IModule` (RegisterServices/RegisterUi/Start/Stop); UI über `IModuleUiHost`/`ModuleView`. - **Persistenz**: EF Core (Pomelo/MariaDB), Migrationen **extern** angewendet (nicht zur Laufzeit). @@ -25,10 +32,21 @@ tests/IBKRTrader.Tests xUnit (Unit + EF-InMemory) ```bash dotnet build IBKRTrader.slnx dotnet test IBKRTrader.slnx -dotnet run --project IBKRTrader.App.csproj -- --smoke-ui # Headless-UI-Check -dotnet run --project IBKRTrader.App.csproj # App starten ``` +Anwendung starten: +```bash +dotnet run --project src/IBKRTrader.App.Avalonia +``` + +Prüfläufe – beide ohne Anzeigegerät und ohne laufende Dienste, also CI-tauglich: +```bash +dotnet run --project src/IBKRTrader.App.Avalonia -- --smoke-ui +dotnet run --project src/IBKRTrader.Daemon -- --check +``` + +Kopflos auf Linux (systemd): siehe [deploy/README.md](deploy/README.md). + ## Konfiguration - `appsettings.Local.json` (gitignored) hält den DB-Connection-String (`Database:MySqlConnectionString`). - `settings.json` (gitignored) – App-Settings (IBKR-Ports, Logging, Worker, Trading). diff --git a/UI/LogPanelController.cs b/UI/LogPanelController.cs deleted file mode 100644 index 4de92bb..0000000 --- a/UI/LogPanelController.cs +++ /dev/null @@ -1,79 +0,0 @@ -using IBKRTrader.Core.Logging; - -namespace IBKRTrader.UI; - -/// -/// Bindet eine an den und färbt die Zeilen -/// nach Log-Level ein. -/// -/// Einfärbung und Wechsel auf den UI-Thread liegen bewusst hier und nicht mehr im -/// : der Core trägt seit der Linux-Portierung keine UI-Abhängigkeit -/// (weder WinForms noch System.Drawing). Der Dienst meldet nur noch Einträge. -/// -public sealed class LogPanelController : IDisposable -{ - private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150); - private static readonly Color ColorWarn = Color.FromArgb(255, 190, 60); - private static readonly Color ColorError = Color.FromArgb(255, 80, 80); - - private readonly RichTextBox _rtb; - private readonly LoggingService _logger; - - public LogPanelController(RichTextBox rtb, LoggingService logger) - { - _rtb = rtb; - _logger = logger; - - // Dunkles Theme fürs Log-Panel. - _rtb.BackColor = Color.FromArgb(20, 20, 30); - _rtb.ForeColor = Color.FromArgb(200, 200, 200); - _rtb.Font = new Font("Consolas", 9f); - _rtb.ReadOnly = true; - _rtb.WordWrap = false; - - _logger.EntryWritten += OnEntryWritten; - _rtb.Disposed += (_, _) => Dispose(); - } - - public void Dispose() => _logger.EntryWritten -= OnEntryWritten; - - /// - /// Wird aus beliebigen Worker-Threads gerufen – deshalb der Wechsel auf den UI-Thread. Ein - /// Fehler hier darf den schreibenden Worker niemals mitreißen. - /// - private void OnEntryWritten(LogEntry e) - { - try - { - if (_rtb.IsDisposed) return; - - var color = e.Level switch - { - AppLogLevel.Warn => ColorWarn, - AppLogLevel.Error => ColorError, - _ => ColorInfo - }; - var text = LoggingService.Format(e) + Environment.NewLine; - - if (_rtb.InvokeRequired) _rtb.BeginInvoke(() => AppendColored(text, color)); - else AppendColored(text, color); - } - catch { /* Fenster wird gerade geschlossen */ } - } - - private void AppendColored(string text, Color color) - { - if (_rtb.IsDisposed) return; - _rtb.SelectionStart = _rtb.TextLength; - _rtb.SelectionLength = 0; - _rtb.SelectionColor = color; - _rtb.AppendText(text); - _rtb.SelectionColor = _rtb.ForeColor; - if (_rtb.TextLength > 0) - _rtb.ScrollToCaret(); - } - - public void Clear() => _rtb.Clear(); - - public void CopyAll() => Clipboard.SetText(_rtb.Text); -} diff --git a/UI/ModuleViews.cs b/UI/ModuleViews.cs deleted file mode 100644 index e14ad88..0000000 --- a/UI/ModuleViews.cs +++ /dev/null @@ -1,71 +0,0 @@ -using IBKRTrader.Core.Analytics; -using IBKRTrader.Core.Logging; -using IBKRTrader.Core.Modularity; -using IBKRTrader.Core.Trading; -using IBKRTrader.Core.Workers; -using IBKRTrader.Modules.Accounting.Persistence; -using IBKRTrader.Modules.Accounting.Services; -using IBKRTrader.Modules.CongressTrading.Database; -using IBKRTrader.Modules.Supervisor.Agent; -using IBKRTrader.Modules.Supervisor.Persistence; -using IBKRTrader.Modules.Supervisor.Services; -using IBKRTrader.UI.Views.Modules; -using Microsoft.Extensions.DependencyInjection; - -namespace IBKRTrader.UI; - -/// -/// Registriert die Fenster der Module bei der Shell. -/// -/// Warum hier und nicht im Modul? Ein Modul, das sein eigenes Fenster erzeugt, muss das -/// UI-Toolkit referenzieren – und wäre damit an WinForms bzw. Avalonia gebunden. Genau das verhindert -/// den kopflosen Linux-Betrieb. Die Modulprojekte bleiben deshalb frei von UI-Code; ihr -/// RegisterUi ist leer, und die Shell verdrahtet die Fenster zentral. Die Modul-Dienste -/// kommen unverändert aus dem DI-Container. -/// -/// Registriert wird nur, was auch geladen ist: fehlt ein Modul in dieser Sitzung, entfällt -/// sein Fenster, und der Launcher zeigt es gar nicht erst an. -/// -public static class ModuleViews -{ - public static void Register(IModuleUiHost host, IServiceProvider sp) - { - RegisterIfLoaded(sp, "CongressTrading", () => host.RegisterView(new ModuleView - { - Id = "congresstrading.main", Title = "Congress Trading", Group = "CongressTrading", Order = 100, - CreateView = () => new CongressTradingForm( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService()) - })); - - RegisterIfLoaded(sp, "Supervisor", () => host.RegisterView(new ModuleView - { - Id = "supervisor.main", Title = "Supervisor", Group = "Supervisor", Order = 300, - CreateView = () => new SupervisorMainForm( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService()) - })); - - RegisterIfLoaded(sp, "Accounting", () => host.RegisterView(new ModuleView - { - Id = "accounting.main", Title = "Accounting", Group = "Accounting", Order = 400, - CreateView = () => new AccountingMainForm( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService()) - })); - } - - /// Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist. - private static void RegisterIfLoaded(IServiceProvider sp, string moduleName, Action register) - { - if (sp.GetServices().Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase))) - register(); - } -} diff --git a/UI/ShellUiHost.cs b/UI/ShellUiHost.cs deleted file mode 100644 index 6a7d92b..0000000 --- a/UI/ShellUiHost.cs +++ /dev/null @@ -1,109 +0,0 @@ -using IBKRTrader.Core.Modularity; - -namespace IBKRTrader.UI; - -/// -/// Verwaltet die registrierten Fenster-Views: öffnet sie als eigenständige Forms, hält je View -/// höchstens eine Instanz offen und holt ein bereits offenes Fenster wieder in den Vordergrund. -/// Meldet Änderungen am Offen-Status (für die Button-Markierung im Launcher). -/// Vorbild: PolytraderSharp ShellUiHost. -/// -public sealed class ShellUiHost : IModuleUiHost -{ - private readonly List _views = new(); - private readonly Dictionary _open = new(StringComparer.OrdinalIgnoreCase); - private Form? _mainWindow; - - /// True, sobald das Beenden bestätigt wurde (der Launcher wertet das in FormClosing aus). - public bool ShutdownConfirmed { get; private set; } - - public event Action? OpenStateChanged; - - public IReadOnlyList Views => _views; - - public void RegisterView(ModuleView view) => _views.Add(view); - - /// Setzt das Hauptfenster (Launcher) – Ziel für . - public void SetMainWindow(Form main) => _mainWindow = main; - - public bool IsOpen(string viewId) => - _open.TryGetValue(viewId, out var form) && !form.IsDisposed; - - public void ActivateMain() - { - if (_mainWindow is null || _mainWindow.IsDisposed) return; - if (_mainWindow.WindowState == FormWindowState.Minimized) - _mainWindow.WindowState = FormWindowState.Normal; - _mainWindow.BringToFront(); - _mainWindow.Activate(); - } - - /// Sicherheitsabfrage; bei Bestätigung wird die Message-Loop beendet. - public void RequestShutdown() - { - if (ShutdownConfirmed) return; - var owner = _mainWindow is { IsDisposed: false } ? _mainWindow : null; - var result = MessageBox.Show(owner, - "IBKRTrader wirklich beenden? Laufende Worker/Services werden gestoppt.", - "Beenden", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); - if (result != DialogResult.OK) return; - - ShutdownConfirmed = true; - Application.Exit(); - } - - public void OpenView(string viewId) - { - var view = _views.FirstOrDefault(v => v.Id == viewId); - if (view is not null) OpenView(view); - } - - private void OpenView(ModuleView view) - { - if (_open.TryGetValue(view.Id, out var existing) && !existing.IsDisposed) - { - if (existing.WindowState == FormWindowState.Minimized) - existing.WindowState = FormWindowState.Normal; - existing.BringToFront(); - existing.Activate(); - return; - } - - // Der Core-Contract ist toolkit-neutral (CreateView liefert object). Ein anderer Typ als - // Form ist ein Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster geben. - var form = (Form)view.CreateView(); - if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name) - form.Text = view.Title; - - // Gemeinsames „Fenster"-Menü in jedes Fenster injizieren (identische Shell-Chrome). - if (form.MainMenuStrip is null) - AttachWindowMenu(form, view.Id); - - _open[view.Id] = form; - form.FormClosed += (_, _) => - { - _open.Remove(view.Id); - OpenStateChanged?.Invoke(); - }; - - form.Show(); - OpenStateChanged?.Invoke(); - } - - private void AttachWindowMenu(Form form, string currentViewId) - { - var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new Size(24, 24) }; - WindowMenu.Wire(menu, this, currentViewId); - form.Controls.Add(menu); - form.MainMenuStrip = menu; - } - - /// Schließt alle offenen View-Fenster (beim Herunterfahren). - public void CloseAllViews() - { - foreach (var form in _open.Values.ToList()) - if (!form.IsDisposed) - form.Close(); - _open.Clear(); - } -} diff --git a/UI/ViewIcons.cs b/UI/ViewIcons.cs deleted file mode 100644 index 9bc8373..0000000 --- a/UI/ViewIcons.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Drawing; -using IBKRTrader.Core.Modularity; - -namespace IBKRTrader.UI; - -/// -/// Löst die toolkit-neutralen -Schlüssel gegen die Bildressourcen -/// der Shell auf. Der Core kennt seit der Linux-Portierung keine Bilddaten mehr -/// (System.Drawing.Image ist seit .NET 7 Windows-only) – er liefert nur noch den Schlüssel, -/// die jeweilige Shell das Bild. -/// -/// Die Avalonia-Shell bekommt ein gleichnamiges Gegenstück mit denselben Schlüsseln -/// und denselben PNG-Dateien, sodass Core und Module unverändert bleiben. -/// -public static class ViewIcons -{ - /// Symbol-Schlüssel → Bildressource aus Resources/. - private static readonly Dictionary ImageByKey = new(StringComparer.OrdinalIgnoreCase) - { - ["dashboard"] = Properties.Resources.dashboard, - ["workers"] = Properties.Resources.system_time, - ["logs"] = Properties.Resources.error_log, - ["settings"] = Properties.Resources.setting_tools, - ["congresstrading"] = Properties.Resources.cross_reference, - ["accounting"] = Properties.Resources.coins_in_hand, - ["supervisor"] = Properties.Resources.token_quantifier, - }; - - /// Standard-Symbolschlüssel je View-ID – greift für Views, die keinen eigenen setzen. - private static readonly Dictionary DefaultKeyByViewId = new(StringComparer.OrdinalIgnoreCase) - { - ["core.dashboard"] = "dashboard", - ["core.workers"] = "workers", - ["core.logs"] = "logs", - ["core.settings"] = "settings", - ["congresstrading.main"] = "congresstrading", - ["accounting.main"] = "accounting", - ["supervisor.main"] = "supervisor", - }; - - /// Bild zum Schlüssel, oder null (kein Symbol / unbekannter Schlüssel). - public static Image? Resolve(string? iconKey) => - iconKey != null && ImageByKey.TryGetValue(iconKey, out var img) ? img : null; - - /// Setzt bei allen registrierten Views den Standard-Schlüssel, falls noch keiner gesetzt ist. - public static void AssignDefaults(IModuleUiHost host) - { - foreach (var view in host.Views) - if (view.IconKey is null && DefaultKeyByViewId.TryGetValue(view.Id, out var key)) - view.IconKey = key; - } -} diff --git a/UI/Views/DashboardView.cs b/UI/Views/DashboardView.cs deleted file mode 100644 index 908985b..0000000 --- a/UI/Views/DashboardView.cs +++ /dev/null @@ -1,99 +0,0 @@ -using IBKRTrader.Core.Time; -using IBKRTrader.Core.Modularity; -using IBKRTrader.Core.Settings; -using IBKRTrader.Core.Trading; -using IBKRTrader.Core.Workers; -using Microsoft.Extensions.Configuration; - -namespace IBKRTrader.UI.Views; - -/// Core-View: Gesamtüberblick (Trading-Modus, aggregierte Kennzahlen, geladene Module). -public sealed class DashboardView : Form -{ - private readonly DashboardService _dashboard; - private readonly SettingsService _settings; - private readonly IReadOnlyList _modules; - private readonly IConfiguration _config; - private readonly int _workerCount; - - private readonly Label _lblMode = new() { AutoSize = true, Location = new Point(20, 20), Font = new Font("Segoe UI", 13f, FontStyle.Bold) }; - private readonly Label _lblStats = new() { AutoSize = true, Location = new Point(20, 60) }; - private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 90), ForeColor = SystemColors.GrayText }; - private readonly DataGridView _modulesGrid = new() - { - Location = new Point(20, 130), - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - ReadOnly = true, - AllowUserToAddRows = false, - RowHeadersVisible = false, - AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill - }; - - public DashboardView( - DashboardService dashboard, - SettingsService settings, - IEnumerable modules, - IConfiguration config, - IEnumerable workers) - { - _dashboard = dashboard; - _settings = settings; - _modules = modules.ToList(); - _config = config; - _workerCount = workers.Count(); - - Text = "Dashboard"; - Width = 900; - Height = 560; - StartPosition = FormStartPosition.CenterScreen; - MinimumSize = new Size(600, 400); - - var modLabel = new Label { Text = "Geladene Module:", Location = new Point(20, 108), AutoSize = true }; - var refresh = new Button { Text = "Aktualisieren", Location = new Point(760, 18), Width = 110, Anchor = AnchorStyles.Top | AnchorStyles.Right }; - refresh.Click += async (_, _) => await RefreshAsync(); - - _modulesGrid.Size = new Size(ClientSize.Width - 40, ClientSize.Height - 150); - - Controls.Add(_lblMode); - Controls.Add(_lblStats); - Controls.Add(_lblStatus); - Controls.Add(modLabel); - Controls.Add(_modulesGrid); - Controls.Add(refresh); - } - - protected override async void OnShown(EventArgs e) - { - base.OnShown(e); - await RefreshAsync(); - } - - private async Task RefreshAsync() - { - var t = _settings.Settings.Trading; - _lblMode.Text = $"Trading: {t.Mode} – {(t.TradingEnabled ? "AKTIV" : "inaktiv")}"; - _lblMode.ForeColor = t.TradingEnabled ? Color.SeaGreen : SystemColors.GrayText; - - _modulesGrid.DataSource = _modules - .Select(m => new - { - Modul = m.Name, - Präfix = m.DbPrefix, - Status = m.GetActivationBlocker(_config) ?? "aktivierbar" - }) - .ToList(); - - try - { - var snap = await _dashboard.GetSnapshotAsync(); - _lblStats.Text = $"Offene Positionen: {snap.OpenPositions} | Exposure: {snap.TotalExposure:N2} | " + - $"Trades gesamt: {snap.TotalTrades} | Worker/Services: {_workerCount}"; - _lblStatus.Text = $"Aktualisiert: {AppTimeZone.Now:HH:mm:ss}"; - } - catch (Exception ex) - { - _lblStats.Text = $"Kennzahlen n/v | Worker/Services: {_workerCount}"; - _lblStatus.Text = $"DB nicht erreichbar: {ex.Message}"; - } - } -} diff --git a/UI/Views/LogsView.cs b/UI/Views/LogsView.cs deleted file mode 100644 index adb7f14..0000000 --- a/UI/Views/LogsView.cs +++ /dev/null @@ -1,21 +0,0 @@ -using IBKRTrader.Core.Logging; - -namespace IBKRTrader.UI.Views; - -/// Core-View: Live-Log (RichTextBox), an den LoggingService gebunden. -public sealed class LogsView : Form -{ - public LogsView(LoggingService logger) - { - Text = "Logs"; - Width = 1000; - Height = 650; - StartPosition = FormStartPosition.CenterScreen; - - var rtb = new RichTextBox { Dock = DockStyle.Fill }; - Controls.Add(rtb); - - // Verbindet den Logger mit der RichTextBox (Theme + AttachRichTextBox). - _ = new LogPanelController(rtb, logger); - } -} diff --git a/UI/Views/Modules/AccountingMainForm.cs b/UI/Views/Modules/AccountingMainForm.cs deleted file mode 100644 index 52b9721..0000000 --- a/UI/Views/Modules/AccountingMainForm.cs +++ /dev/null @@ -1,303 +0,0 @@ -using IBKRTrader.Core.Time; -using IBKRTrader.Core.Logging; -using IBKRTrader.Modules.Accounting.Logic; -using IBKRTrader.Modules.Accounting.Persistence; -using IBKRTrader.Modules.Accounting.Services; - -namespace IBKRTrader.UI.Views.Modules; - -/// -/// Fenster des Accounting-Moduls: Übersicht/BWA, Ledger, Steuer (Platzhalter), Abrechnung/Export, -/// Abruf/Status. Alle DB-Zugriffe laufen NUR auf Nutzer-Interaktion (nicht im Konstruktor) – so -/// konstruiert der Smoke-UI-Check das Fenster auch ohne DB fehlerfrei. -/// -public sealed class AccountingMainForm : Form -{ - private readonly ILedgerRepository _ledger; - private readonly IIngestRunRepository _runs; - private readonly AccountingReportService _report; - private readonly AccountingIngestService _ingest; - private readonly LoggingService _logger; - - private readonly DateTimePicker _from = new() { Format = DateTimePickerFormat.Short, Width = 110 }; - private readonly DateTimePicker _to = new() { Format = DateTimePickerFormat.Short, Width = 110 }; - private readonly ComboBox _account = new() { DropDownStyle = ComboBoxStyle.DropDown, Width = 140 }; - private readonly ComboBox _currency = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 80 }; - - private readonly Label _kpis = new() { AutoSize = true, Location = new Point(12, 8) }; - private readonly DataGridView _monthly = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; - private readonly DataGridView _ledgerGrid = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; - private readonly DataGridView _runsGrid = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; - private readonly Label _status = new() { AutoSize = true, ForeColor = SystemColors.GrayText, Location = new Point(12, 8) }; - - public AccountingMainForm( - ILedgerRepository ledger, IIngestRunRepository runs, AccountingReportService report, - AccountingIngestService ingest, LoggingService logger) - { - _ledger = ledger; - _runs = runs; - _report = report; - _ingest = ingest; - _logger = logger; - - Text = "Accounting"; - Width = 1000; - Height = 680; - StartPosition = FormStartPosition.CenterScreen; - MinimumSize = new Size(760, 480); - - _from.Value = DateTime.Today.AddMonths(-1); - _to.Value = DateTime.Today; - _currency.Items.AddRange(new object[] { "USD", "EUR" }); - _currency.SelectedIndex = 0; - - BuildLayout(); - } - - private void BuildLayout() - { - var tabs = new TabControl { Dock = DockStyle.Fill }; - - // ── gemeinsame Filterleiste ── - var filter = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 40, Padding = new Padding(8, 6, 8, 0) }; - filter.Controls.Add(new Label { Text = "Von", AutoSize = true, Margin = new Padding(0, 8, 4, 0) }); - filter.Controls.Add(_from); - filter.Controls.Add(new Label { Text = "Bis", AutoSize = true, Margin = new Padding(8, 8, 4, 0) }); - filter.Controls.Add(_to); - filter.Controls.Add(new Label { Text = "Konto", AutoSize = true, Margin = new Padding(8, 8, 4, 0) }); - filter.Controls.Add(_account); - filter.Controls.Add(new Label { Text = "Währung", AutoSize = true, Margin = new Padding(8, 8, 4, 0) }); - filter.Controls.Add(_currency); - var btnRefresh = new Button { Text = "Aktualisieren", Width = 120, Margin = new Padding(12, 3, 0, 0) }; - btnRefresh.Click += (_, _) => RefreshAll(); - filter.Controls.Add(btnRefresh); - - // ── Tab: Übersicht/BWA ── - var tabOverview = new TabPage("Übersicht / BWA"); - _monthly.Top = 90; - var overviewPanel = new Panel { Dock = DockStyle.Fill }; - overviewPanel.Controls.Add(_monthly); - var kpiPanel = new Panel { Dock = DockStyle.Top, Height = 84 }; - kpiPanel.Controls.Add(_kpis); - overviewPanel.Controls.Add(kpiPanel); - tabOverview.Controls.Add(overviewPanel); - - // ── Tab: Ledger ── - var tabLedger = new TabPage("Ledger"); - tabLedger.Controls.Add(_ledgerGrid); - - // ── Tab: Steuer (Platzhalter) ── - var tabTax = new TabPage("Steuer"); - tabTax.Controls.Add(new Label - { - Dock = DockStyle.Fill, Padding = new Padding(16), - Text = "Steuerliche Einordnung ist noch offen (Jurisdiktion nicht festgelegt).\n\n" + - "Der neutrale Ledger und die Periodenabrechnung sind davon unabhängig gültig.\n" + - "Eine konkrete Steuerschicht (z. B. DE-Kapitalertragsteuer oder US Form 8949 / Schedule D)\n" + - "wird hier später als klar dokumentierte, prüfbare Rechenschicht ergänzt.\n\n" + - "Hinweis: Dies ist keine Steuerberatung." - }); - - // ── Tab: Abrechnung / Export ── - var tabExport = new TabPage("Abrechnung / Export"); - var exportPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), FlowDirection = FlowDirection.TopDown }; - exportPanel.Controls.Add(new Label { AutoSize = true, Text = "Exportiert die aktuelle Auswahl (Zeitraum / Konto / Währung):" }); - var btnCsvLedger = new Button { Text = "Ledger als CSV…", Width = 180, Margin = new Padding(0, 8, 0, 0) }; - btnCsvLedger.Click += (_, _) => ExportCsvLedger(); - var btnCsvStmt = new Button { Text = "Abrechnung als CSV…", Width = 180, Margin = new Padding(0, 8, 0, 0) }; - btnCsvStmt.Click += (_, _) => ExportCsvStatement(); - var btnPdf = new Button { Text = "Abrechnung als PDF…", Width = 180, Margin = new Padding(0, 8, 0, 0) }; - btnPdf.Click += (_, _) => ExportPdf(); - exportPanel.Controls.Add(btnCsvLedger); - exportPanel.Controls.Add(btnCsvStmt); - exportPanel.Controls.Add(btnPdf); - tabExport.Controls.Add(exportPanel); - - // ── Tab: Abruf / Status ── - var tabIngest = new TabPage("Abruf / Status"); - var ingestButtons = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 40, Padding = new Padding(8, 6, 8, 0) }; - var btnIncr = new Button { Text = "Inkrementell abrufen", Width = 160 }; - btnIncr.Click += async (_, _) => await RunIngest(backfill: false); - var btnBackfill = new Button { Text = "Backfill (voll)", Width = 140, Margin = new Padding(8, 0, 0, 0) }; - btnBackfill.Click += async (_, _) => await RunIngest(backfill: true); - ingestButtons.Controls.Add(btnIncr); - ingestButtons.Controls.Add(btnBackfill); - var statusPanel = new Panel { Dock = DockStyle.Top, Height = 40 }; - statusPanel.Controls.Add(_status); - _status.Text = "Offline-Standard: keine Live-Quelle registriert → der Ingest bucht nichts (korrekt)."; - tabIngest.Controls.Add(_runsGrid); - tabIngest.Controls.Add(statusPanel); - tabIngest.Controls.Add(ingestButtons); - - tabs.TabPages.AddRange(new[] { tabOverview, tabLedger, tabTax, tabExport, tabIngest }); - - Controls.Add(tabs); - Controls.Add(filter); - } - - // ── Daten laden (nur auf Interaktion) ── - - private string? SelectedAccount() - { - var text = _account.Text?.Trim(); - return string.IsNullOrWhiteSpace(text) || text == "(alle)" ? null : text; - } - - private void RefreshAll() - { - try - { - LoadAccounts(); - LoadOverview(); - LoadLedger(); - LoadRuns(); - } - catch (Exception ex) - { - _logger.Error("Accounting", $"UI-Refresh fehlgeschlagen: {ex.Message}", ex); - MessageBox.Show(this, ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - } - - private void LoadAccounts() - { - var current = _account.Text; - _account.Items.Clear(); - _account.Items.Add("(alle)"); - foreach (var a in _ledger.DistinctAccounts()) _account.Items.Add(a); - _account.Text = string.IsNullOrEmpty(current) ? "(alle)" : current; - } - - private void LoadOverview() - { - DateTime from = _from.Value.Date, to = _to.Value.Date.AddDays(1).AddTicks(-1); - var stmt = _report.BuildStatement(SelectedAccount(), from, to); - var view = _report.GetCurrencyView(_currency.Text, to); - decimal C(decimal v) => Math.Round(v * view.Factor, 2); - - _kpis.Text = - $"Netto-Handelsergebnis: {C(stmt.NetTradingResult):N2} {view.Code} " + - $"Handelsvolumen: {C(stmt.TradeVolume):N2} Dividenden: {C(stmt.Dividends):N2} Fees: {C(stmt.Fees):N2}\n" + - $"Einzahlungen: {C(stmt.Deposits):N2} Auszahlungen: {C(stmt.Withdrawals):N2} " + - $"Endsaldo: {C(stmt.ClosingBalance):N2} Trades: {stmt.TradeCount} Buchungen: {stmt.EntryCount}\n" + - $"{view.Note}"; - - var monthly = _report.BuildMonthly(SelectedAccount(), from, to) - .Select(m => new - { - Monat = m.From.ToString("yyyy-MM"), - Anfang = C(m.OpeningBalance), - Einzahlungen = C(m.Deposits), - Auszahlungen = C(m.Withdrawals), - Volumen = C(m.TradeVolume), - Fees = C(m.Fees), - Ergebnis = C(m.NetTradingResult), - Endsaldo = C(m.ClosingBalance) - }).ToList(); - _monthly.DataSource = monthly; - } - - private void LoadLedger() - { - DateTime from = _from.Value.Date, to = _to.Value.Date.AddDays(1).AddTicks(-1); - var rows = _ledger.Query(SelectedAccount(), from, to, 2000) - .Select(e => new - { - Zeit = e.Timestamp, e.AccountId, Typ = e.EventType.ToString(), e.Side, e.Symbol, - e.Currency, e.Quantity, Preis = e.PriceNative, Brutto = e.GrossBase, Fee = e.FeeBase, - Netto = e.NetBase, e.TransactionId - }).ToList(); - _ledgerGrid.DataSource = rows; - } - - private void LoadRuns() - { - var rows = _runs.GetRecent(SelectedAccount(), 100) - .Select(r => new - { - r.AccountId, Start = r.StartedAt, Ende = r.FinishedAt, r.Backfill, - Neu = r.NewEntries, Duplikate = r.DuplicateEntries, r.Success, - Anker = r.BalanceAnchorBase, LedgerNetto = r.LedgerNetBase, Delta = r.BalanceDeltaBase, r.Message - }).ToList(); - _runsGrid.DataSource = rows; - } - - // ── Export ── - - private void ExportCsvLedger() - { - DateTime from = _from.Value.Date, to = _to.Value.Date.AddDays(1).AddTicks(-1); - var entries = _ledger.Query(SelectedAccount(), from, to, 100000); - SaveText("ledger.csv", "CSV|*.csv", CsvExporter.Ledger(entries)); - } - - private void ExportCsvStatement() - { - DateTime from = _from.Value.Date, to = _to.Value.Date.AddDays(1).AddTicks(-1); - var stmt = _report.BuildStatement(SelectedAccount(), from, to); - SaveText("abrechnung.csv", "CSV|*.csv", CsvExporter.Statement(stmt)); - } - - private void ExportPdf() - { - try - { - DateTime from = _from.Value.Date, to = _to.Value.Date.AddDays(1).AddTicks(-1); - var account = SelectedAccount(); - var stmt = _report.BuildStatement(account, from, to); - var monthly = _report.BuildMonthly(account, from, to); - var entries = _ledger.Query(account, from, to, 100000).OrderBy(e => e.Timestamp).ToList(); - var view = _report.GetCurrencyView(_currency.Text, to); - - byte[] pdf = PdfExporter.Render(stmt, monthly, entries, view.Code, view.Factor, view.Note); - - using var dlg = new SaveFileDialog { FileName = "abrechnung.pdf", Filter = "PDF|*.pdf" }; - if (dlg.ShowDialog(this) == DialogResult.OK) - { - File.WriteAllBytes(dlg.FileName, pdf); - _logger.Info("Accounting", $"PDF-Abrechnung geschrieben: {dlg.FileName}"); - } - } - catch (Exception ex) - { - _logger.Error("Accounting", $"PDF-Export fehlgeschlagen: {ex.Message}", ex); - MessageBox.Show(this, ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - } - - private void SaveText(string suggested, string filter, string content) - { - try - { - using var dlg = new SaveFileDialog { FileName = suggested, Filter = filter }; - if (dlg.ShowDialog(this) == DialogResult.OK) - { - File.WriteAllText(dlg.FileName, content); - _logger.Info("Accounting", $"Export geschrieben: {dlg.FileName}"); - } - } - catch (Exception ex) - { - _logger.Error("Accounting", $"Export fehlgeschlagen: {ex.Message}", ex); - MessageBox.Show(this, ex.Message, "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - } - - // ── Ingest ── - - private async Task RunIngest(bool backfill) - { - try - { - _status.Text = backfill ? "Backfill läuft…" : "Inkrementeller Abruf läuft…"; - await _ingest.IngestAllAsync(backfill, CancellationToken.None); - _status.Text = $"Abruf abgeschlossen ({AppTimeZone.Now:HH:mm:ss})."; - LoadRuns(); - } - catch (Exception ex) - { - _status.Text = $"Fehler: {ex.Message}"; - _logger.Error("Accounting", $"Manueller Ingest fehlgeschlagen: {ex.Message}", ex); - } - } -} diff --git a/UI/Views/Modules/CongressTradingForm.cs b/UI/Views/Modules/CongressTradingForm.cs deleted file mode 100644 index 7cf7487..0000000 --- a/UI/Views/Modules/CongressTradingForm.cs +++ /dev/null @@ -1,131 +0,0 @@ -using IBKRTrader.Core.Time; -using IBKRTrader.Core.Logging; -using IBKRTrader.Core.Trading; -using IBKRTrader.Core.Workers; -using IBKRTrader.Modules.CongressTrading; -using IBKRTrader.Modules.CongressTrading.Database; - -namespace IBKRTrader.UI.Views.Modules; - -/// -/// Eigenständiges Fenster des CongressTrading-Moduls: DB-Kennzahlen, manueller Scrape-Trigger -/// und die offenen Positionen des Moduls (aus dem Core-Portfolio). -/// -public sealed class CongressTradingForm : Form -{ - private const string ScrapeWorkerName = "CT-ScrapeWorker"; - - private readonly CongressRepository _repo; - private readonly WorkerEngine _engine; - private readonly IPortfolioService _portfolio; - private readonly LoggingService _logger; - - private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) }; - private readonly Label _lblMembers = new() { AutoSize = true, Location = new Point(20, 100) }; - private readonly Button _btnRefresh = new() { Text = "Aktualisieren", Location = new Point(20, 140), Width = 140 }; - private readonly Button _btnScrape = new() { Text = "Scrape jetzt", Location = new Point(170, 140), Width = 140 }; - private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText }; - - private readonly DataGridView _positions = new() - { - Location = new Point(20, 250), - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - ReadOnly = true, - AllowUserToAddRows = false, - RowHeadersVisible = false, - AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill - }; - - public CongressTradingForm(CongressRepository repo, WorkerEngine engine, IPortfolioService portfolio, LoggingService logger) - { - _repo = repo; - _engine = engine; - _portfolio = portfolio; - _logger = logger; - - Text = "Congress Trading [CT]"; - Width = 900; - Height = 600; - StartPosition = FormStartPosition.CenterScreen; - MinimumSize = new Size(600, 400); - BuildLayout(); - } - - private void BuildLayout() - { - var title = new Label - { - Text = "Congress Trading", - Font = new Font(Font.FontFamily, 14f, FontStyle.Bold), - Location = new Point(18, 20), - AutoSize = true - }; - - var posLabel = new Label - { - Text = "Offene Positionen (Modul CT):", - Location = new Point(20, 225), - AutoSize = true - }; - - _positions.Size = new Size(ClientSize.Width - 40, ClientSize.Height - 270); - - _btnRefresh.Click += async (_, _) => await RefreshStatsAsync(); - _btnScrape.Click += async (_, _) => await TriggerScrapeAsync(); - - Controls.Add(title); - Controls.Add(_lblTrades); - Controls.Add(_lblMembers); - Controls.Add(_btnRefresh); - Controls.Add(_btnScrape); - Controls.Add(_lblStatus); - Controls.Add(posLabel); - Controls.Add(_positions); - } - - protected override async void OnShown(EventArgs e) - { - base.OnShown(e); - await RefreshStatsAsync(); - } - - private async Task RefreshStatsAsync() - { - try - { - var trades = await _repo.GetTradeCountAsync(); - var members = await _repo.GetMemberCountAsync(); - _lblTrades.Text = $"Trades in DB: {trades:N0}"; - _lblMembers.Text = $"Mitglieder in DB: {members:N0}"; - - var positions = await _portfolio.GetPositionsAsync(CongressTradingModule.LogTag); - _positions.DataSource = positions - .Select(p => new { p.Symbol, Stück = p.Quantity, Ø_Kurs = p.AvgPrice, Wert = p.Notional }) - .ToList(); - - _lblStatus.Text = $"Aktualisiert: {AppTimeZone.Now:HH:mm:ss}"; - } - catch (Exception ex) - { - _lblTrades.Text = "Trades in DB: n/v"; - _lblMembers.Text = "Mitglieder in DB: n/v"; - _lblStatus.Text = $"DB nicht erreichbar: {ex.Message}"; - _logger.Warn(CongressTradingModule.LogTag, $"Kennzahlen konnten nicht geladen werden: {ex.Message}"); - } - } - - private async Task TriggerScrapeAsync() - { - try - { - _lblStatus.Text = "Scrape angestoßen..."; - await _engine.TriggerWorkerAsync(ScrapeWorkerName); - _logger.Info(CongressTradingModule.LogTag, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster)."); - } - catch (Exception ex) - { - _lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}"; - _logger.Error(CongressTradingModule.LogTag, "Manueller Scrape-Trigger fehlgeschlagen.", ex); - } - } -} diff --git a/UI/Views/Modules/SupervisorMainForm.cs b/UI/Views/Modules/SupervisorMainForm.cs deleted file mode 100644 index 36a95fc..0000000 --- a/UI/Views/Modules/SupervisorMainForm.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Text; -using System.Text.Json; -using IBKRTrader.Core.Analytics; -using IBKRTrader.Core.Logging; -using IBKRTrader.Modules.Supervisor.Agent; -using IBKRTrader.Modules.Supervisor.Persistence; -using IBKRTrader.Modules.Supervisor.Services; - -namespace IBKRTrader.UI.Views.Modules; - -/// -/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser, -/// Berichte, Settings. Read-only. DB-/Agent-Zugriffe laufen NUR auf Nutzer-Interaktion (Smoke-UI-sicher). -/// -public sealed class SupervisorMainForm : Form -{ - private readonly SupervisorAgent _agent; - private readonly DossierService _dossiers; - private readonly ISupervisorReportRepository _reports; - private readonly LoggingService _logger; - - private readonly ComboBox _profile = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 160 }; - private readonly TextBox _question = new() { Dock = DockStyle.Fill, Multiline = true, Height = 60 }; - private readonly RichTextBox _answer = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) }; - private readonly Button _ask = new() { Text = "Fragen", Width = 100 }; - - private readonly DataGridView _signals = new() { Dock = DockStyle.Left, Width = 360, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; - private readonly RichTextBox _dossier = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) }; - private readonly DataGridView _reportsGrid = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; - - public SupervisorMainForm( - SupervisorAgent agent, DossierService dossiers, ISupervisorReportRepository reports, LoggingService logger) - { - _agent = agent; - _dossiers = dossiers; - _reports = reports; - _logger = logger; - - Text = "Supervisor"; - Width = 1080; - Height = 720; - StartPosition = FormStartPosition.CenterScreen; - MinimumSize = new Size(800, 520); - - foreach (var p in SupervisorProfiles.All) _profile.Items.Add(p.Name); - _profile.SelectedIndex = 0; - - BuildLayout(); - } - - private void BuildLayout() - { - var tabs = new TabControl { Dock = DockStyle.Fill }; - - // ── Tab: Analyse ── - var tabChat = new TabPage("Analyse"); - var top = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 36, Padding = new Padding(8, 6, 8, 0) }; - top.Controls.Add(new Label { Text = "Profil", AutoSize = true, Margin = new Padding(0, 8, 4, 0) }); - top.Controls.Add(_profile); - _ask.Click += async (_, _) => await AskAsync(); - var qPanel = new Panel { Dock = DockStyle.Top, Height = 70, Padding = new Padding(8, 2, 8, 4) }; - qPanel.Controls.Add(_question); - var askPanel = new Panel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(8, 0, 8, 0) }; - askPanel.Controls.Add(_ask); - var answerPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) }; - answerPanel.Controls.Add(_answer); - tabChat.Controls.Add(answerPanel); - tabChat.Controls.Add(askPanel); - tabChat.Controls.Add(qPanel); - tabChat.Controls.Add(top); - - // ── Tab: Dossier-Browser ── - var tabDossier = new TabPage("Dossier-Browser"); - _signals.SelectionChanged += (_, _) => ShowSelectedDossier(); - var refreshSignals = new Button { Text = "Signale laden", Dock = DockStyle.Top, Height = 28 }; - refreshSignals.Click += (_, _) => LoadSignals(); - var left = new Panel { Dock = DockStyle.Left, Width = 360 }; - left.Controls.Add(_signals); - left.Controls.Add(refreshSignals); - var dossierPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) }; - dossierPanel.Controls.Add(_dossier); - tabDossier.Controls.Add(dossierPanel); - tabDossier.Controls.Add(left); - - // ── Tab: Berichte ── - var tabReports = new TabPage("Berichte"); - var refreshReports = new Button { Text = "Berichte laden", Dock = DockStyle.Top, Height = 28 }; - refreshReports.Click += (_, _) => LoadReports(); - tabReports.Controls.Add(_reportsGrid); - tabReports.Controls.Add(refreshReports); - - // ── Tab: Settings (Info) ── - var tabSettings = new TabPage("Settings"); - tabSettings.Controls.Add(new Label - { - Dock = DockStyle.Fill, Padding = new Padding(16), - Text = - "Supervisor – read-only Analyse/Forensik über alle Module.\n\n" + - "OpenRouter-Key: env IBKRTRADER_OPENROUTER_KEY oder Datei 'openrouter.key' (gitignored).\n" + - $" Status: {(string.IsNullOrEmpty(OpenRouterClient.DefaultApiKeyProvider()) ? "NICHT gesetzt – Chat nicht verfügbar" : "gesetzt")}\n\n" + - "Tagesbericht (opt-in): env IBKRTRADER_SUPERVISOR_DAILY = Stunde 0–23.\n" + - "MCP-Light (opt-in): env IBKRTRADER_MCP_PORT = Port (bindet nur 127.0.0.1).\n\n" + - "Sicherheit: OpenRouter ist ein bewusst freigegebener externer Datenempfänger. Es werden nur\n" + - "Analyse-Daten der Tools gesendet, niemals Secrets. Kein Tool kann handeln oder schreiben." - }); - - tabs.TabPages.AddRange(new[] { tabChat, tabDossier, tabReports, tabSettings }); - Controls.Add(tabs); - } - - // ── Analyse ── - - private async Task AskAsync() - { - var question = _question.Text.Trim(); - if (string.IsNullOrEmpty(question)) return; - - _ask.Enabled = false; - _answer.Clear(); - var profile = SupervisorProfiles.ByName(_profile.Text); - var progress = new Progress(s => AppendLine(s)); - - try - { - var result = await _agent.AskAsync(question, profile: profile, progress: progress); - AppendLine(""); - AppendLine("─── Antwort ───"); - AppendLine(result.Answer); - - _reports.Insert(new SupervisorReport - { - Profile = profile.Name, - Model = SupervisorAgent.DefaultModel, - Question = question, - Answer = result.Answer, - ToolCallsJson = JsonSerializer.Serialize(result.ToolInvocations.Select(i => new { i.Tool, i.Arguments })), - ToolCallCount = result.ToolInvocations.Count, - PromptTokens = result.PromptTokens, - CompletionTokens = result.CompletionTokens - }); - } - catch (Exception ex) - { - AppendLine(""); - AppendLine($"FEHLER: {ex.Message}"); - _logger.Warn("Supervisor", $"Analyse fehlgeschlagen: {ex.Message}"); - } - finally - { - _ask.Enabled = true; - } - } - - private void AppendLine(string text) - { - if (_answer.InvokeRequired) { _answer.BeginInvoke(() => AppendLine(text)); return; } - _answer.AppendText(text + "\n"); - _answer.ScrollToCaret(); - } - - // ── Dossier ── - - private void LoadSignals() - { - try { _signals.DataSource = _dossiers.RecentSignals(200); } - catch (Exception ex) { _logger.Warn("Supervisor", $"Signale laden fehlgeschlagen: {ex.Message}"); } - } - - private void ShowSelectedDossier() - { - if (_signals.CurrentRow?.DataBoundItem is not SignalSummary s) return; - try { _dossier.Text = DossierBuilder.ToMarkdown(_dossiers.BuildForSignal(s.SignalId)); } - catch (Exception ex) { _dossier.Text = $"FEHLER: {ex.Message}"; } - } - - // ── Berichte ── - - private void LoadReports() - { - try - { - _reportsGrid.DataSource = _reports.GetRecent(100) - .Select(r => new { r.CreatedAt, r.Profile, r.Model, r.Question, r.ToolCallCount, r.PromptTokens, r.CompletionTokens }) - .ToList(); - } - catch (Exception ex) { _logger.Warn("Supervisor", $"Berichte laden fehlgeschlagen: {ex.Message}"); } - } -} diff --git a/UI/Views/SettingsView.cs b/UI/Views/SettingsView.cs deleted file mode 100644 index 427dc7e..0000000 --- a/UI/Views/SettingsView.cs +++ /dev/null @@ -1,23 +0,0 @@ -using IBKRTrader.Core.Settings; - -namespace IBKRTrader.UI.Views; - -/// Core-View: Einstellungen (PropertyGrid auf AppSettings) mit Speichern-Button. -public sealed class SettingsView : Form -{ - public SettingsView(SettingsService settings) - { - Text = "Settings"; - Width = 820; - Height = 720; - StartPosition = FormStartPosition.CenterScreen; - - var grid = new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = settings.Settings }; - - var save = new Button { Text = "Speichern", Dock = DockStyle.Bottom, Height = 36 }; - save.Click += (_, _) => settings.Save(); - - Controls.Add(grid); - Controls.Add(save); - } -} diff --git a/UI/Views/WorkersView.cs b/UI/Views/WorkersView.cs deleted file mode 100644 index 1113c58..0000000 --- a/UI/Views/WorkersView.cs +++ /dev/null @@ -1,20 +0,0 @@ -using IBKRTrader.Core.Workers; - -namespace IBKRTrader.UI.Views; - -/// Core-View: Worker/Services-Übersicht (DataGridView, live an die WorkerEngine gebunden). -public sealed class WorkersView : Form -{ - public WorkersView(WorkerEngine engine) - { - Text = "Workers / Services"; - Width = 1200; - Height = 700; - StartPosition = FormStartPosition.CenterScreen; - - var dgv = new DataGridView { Dock = DockStyle.Fill }; - Controls.Add(dgv); - - WorkerListBindingSource.Setup(dgv, engine.WorkerInfos); - } -} diff --git a/UI/WindowMenu.cs b/UI/WindowMenu.cs deleted file mode 100644 index c06115d..0000000 --- a/UI/WindowMenu.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Drawing; -using System.Windows.Forms; -using IBKRTrader.Core.Modularity; - -namespace IBKRTrader.UI; - -/// -/// Baut das gemeinsame Fenster-Menü, das auf JEDEM Fenster erscheint und das Wechseln zwischen allen -/// Fenstern (Launcher + Core + Module) erlaubt. Es nutzt nur den Core-Contract -/// und funktioniert deshalb aus jedem Fenster. -/// -/// Lag früher im Core. Verschoben in die Shell, weil er reine WinForms-Umsetzung ist – der -/// Core soll keine UI-Abhängigkeit tragen (Linux-Portierung). Die Avalonia-Shell bekommt ein -/// eigenes Gegenstück gegen denselben Contract. -/// -public static class WindowMenu -{ - /// - /// Verdrahtet einen mit der Fensterliste: füllt ihn sofort und baut ihn bei - /// jeder Offen-Status-Änderung neu auf. Die Registrierung wird beim Entsorgen sauber gelöst. - /// - public static void Wire(MenuStrip menu, IModuleUiHost host, string? currentViewId) - { - void Refresh() - { - if (menu.IsDisposed) return; - if (menu.IsHandleCreated && menu.InvokeRequired) - { - try { menu.BeginInvoke((Action)(() => Populate(menu, host, currentViewId))); } - catch { /* Fenster wird gerade geschlossen */ } - return; - } - Populate(menu, host, currentViewId); - } - - Populate(menu, host, currentViewId); - host.OpenStateChanged += Refresh; - menu.Disposed += (_, _) => host.OpenStateChanged -= Refresh; - } - - /// Baut die Menüleiste komplett neu auf (Launcher, alle Views nebeneinander, Aktion rechts). - public static void Populate(MenuStrip menu, IModuleUiHost host, string? currentViewId) - { - menu.Items.Clear(); - - var launcher = new ToolStripMenuItem("Launcher") { Checked = currentViewId == null }; - if (currentViewId == null) launcher.Font = new Font(launcher.Font, FontStyle.Bold); - launcher.Click += (_, _) => host.ActivateMain(); - menu.Items.Add(launcher); - - foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title)) - { - bool isCurrent = view.Id == currentViewId; - var icon = ViewIcons.Resolve(view.IconKey); - var item = new ToolStripMenuItem(view.Title) - { - Image = icon, - ImageScaling = ToolStripItemImageScaling.SizeToFit, - DisplayStyle = icon != null - ? ToolStripItemDisplayStyle.ImageAndText - : ToolStripItemDisplayStyle.Text, - Checked = isCurrent || host.IsOpen(view.Id) - }; - if (isCurrent) item.Font = new Font(item.Font, FontStyle.Bold); - string id = view.Id; - item.Click += (_, _) => host.OpenView(id); - menu.Items.Add(item); - } - - // Kontextabhängige rechte Aktion: nur der Launcher darf die App beenden; jedes andere Fenster - // bietet nur „Fenster schließen" (kein App-Shutdown, Module laufen weiter). - if (currentViewId == null) - { - var exit = new ToolStripMenuItem("Beenden") { Alignment = ToolStripItemAlignment.Right }; - exit.Click += (_, _) => host.RequestShutdown(); - menu.Items.Add(exit); - } - else - { - var close = new ToolStripMenuItem("Fenster schließen") { Alignment = ToolStripItemAlignment.Right }; - close.Click += (_, _) => menu.FindForm()?.Close(); - menu.Items.Add(close); - } - } -} diff --git a/UI/WorkerListBindingSource.cs b/UI/WorkerListBindingSource.cs deleted file mode 100644 index 0b03d6b..0000000 --- a/UI/WorkerListBindingSource.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.ComponentModel; -using IBKRTrader.Core.Workers; - -namespace IBKRTrader.UI; - -/// -/// Richtet dgv_workerlist vollständig ein: -/// Spalten, Binding, Formatierung, Kontext-Menü. -/// -public static class WorkerListBindingSource -{ - public static void Setup(DataGridView dgv, BindingList source) - { - dgv.AutoGenerateColumns = false; - dgv.ReadOnly = false; - dgv.AllowUserToAddRows = false; - dgv.RowHeadersVisible = false; - dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; - dgv.MultiSelect = false; - - // ── Spalten definieren ─────────────────────────────────────────────── - dgv.Columns.Clear(); - - dgv.Columns.Add(new DataGridViewCheckBoxColumn - { - DataPropertyName = nameof(WorkerInfo.Active), - HeaderText = "Active", - Width = 65, - ReadOnly = false - }); - - dgv.Columns.Add(MakeTextColumn(nameof(WorkerInfo.Type), "Type", 80, false)); - dgv.Columns.Add(MakeTextColumn(nameof(WorkerInfo.Module), "Module", 90, false)); - dgv.Columns.Add(MakeTextColumn(nameof(WorkerInfo.WorkerName), "Worker Name", 180, false)); - - dgv.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = nameof(WorkerInfo.LastRuntime), - HeaderText = "Last Runtime", - Width = 150, - ReadOnly = true, - DefaultCellStyle = { Format = "dd.MM.yyyy HH:mm:ss", NullValue = "–" } - }); - - dgv.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = nameof(WorkerInfo.NextRuntime), - HeaderText = "Next Runtime", - Width = 150, - ReadOnly = true, - DefaultCellStyle = { Format = "dd.MM.yyyy HH:mm:ss", NullValue = "–" } - }); - - dgv.Columns.Add(MakeTextColumn(nameof(WorkerInfo.RunEvery), "Run Every", 90, false)); - dgv.Columns.Add(MakeTextColumn(nameof(WorkerInfo.Info), "Info", 400, false)); - - // ── Binding ────────────────────────────────────────────────────────── - var bs = new BindingSource { DataSource = source }; - dgv.DataSource = bs; - - // ── Styling ────────────────────────────────────────────────────────── - dgv.EnableHeadersVisualStyles = false; - dgv.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9f, FontStyle.Bold); - dgv.DefaultCellStyle.Font = new Font("Segoe UI", 9f); - dgv.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(240, 240, 255); - dgv.GridColor = Color.LightGray; - } - - private static DataGridViewTextBoxColumn MakeTextColumn( - string prop, string header, int width, bool readOnly) => new() - { - DataPropertyName = prop, - HeaderText = header, - Width = width, - ReadOnly = readOnly - }; -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c6789db..8565d47 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,14 +12,25 @@ nur dass statt Polymarket über IBKR gehandelt wird. ## 1. Ziel-Architektur (nach PolytraderSharp) +> **Stand seit der Linux-Portierung (2026-08-07):** Alle Projekte sind `net10.0` ohne +> Plattformbindung. Die WinForms-Shell ist entfernt; der letzte Stand liegt im Tag +> `winforms-final`. Einstiegspunkte sind jetzt `IBKRTrader.App.Avalonia` (mit Oberfläche) und +> `IBKRTrader.Daemon` (kopflos, systemd); beide bauen ihren Host über `IBKRTrader.Hosting`. +> Analyse und Vorgehen: [konzepte/KONZEPT-Linux-Portierung.md](konzepte/KONZEPT-Linux-Portierung.md). + ``` -IBKRTrader.App (WinExe, Root) – Generic Host + Shell (Launcher) + Core-Views -│ Program.cs: Host.CreateDefaultBuilder, IConfiguration, Module laden, ShellUiHost, Application.Run -│ Ui/: LauncherForm, ShellUiHost, Views/ (Dashboard, Terminal, Settings, Jobs) +src/IBKRTrader.App.Avalonia (WinExe, net10.0) – Oberfläche: Launcher, Shell, Core-Views, Modul-Fenster +│ Program.cs: Host bauen (Hosting), Startprüfungen, dann Avalonia; --smoke-ui ohne Anzeigegerät +│ Shell/: AvaloniaUiHost, CoreViews, ModuleViews, ViewIcons, WindowMenu +│ Views/: LauncherWindow, Dashboard/Workers/Logs/Settings, Views/Modules/ (3 Modul-Fenster) │ -├── src/IBKRTrader.Core (classlib, net10.0-windows, UseWindowsForms) +src/IBKRTrader.Daemon (Exe, net10.0) – kopfloser Dienst: --check, --db-version, SIGTERM +src/IBKRTrader.Hosting (classlib, net10.0) – AppHostBuilder + RunStartupChecks, von beiden geteilt +│ +├── src/IBKRTrader.Core (classlib, net10.0 – plattformneutral, kein UI-Toolkit) │ ├── Modularity/ IModule (Name, DbPrefix, RegisterServices, RegisterUi, Start/Stop, ActivationBlocker) -│ │ ModuleView, IModuleUiHost, WindowMenu ← UI-Contract liegt im Core +│ │ ModuleView, IModuleUiHost ← toolkit-neutraler UI-Contract (Func, IconKey) +│ ├── Time/ AppTimeZone (Betriebszeitzone der Instanz; Persistenz bleibt UTC) │ ├── Configuration/ DatabaseOptions, ServerVersion-Pinning │ ├── DependencyInjection/ AddCorePersistence(...) │ ├── Persistence/Ef/ CoreDbContext + Entities + EF-Repositories (hinter Interfaces) @@ -132,6 +143,25 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin - [ ] **Offen:** `PlaceOrderAsync` mit echter Ausführung verifizieren (Fill → Buchung); asynchrone Fill-Verfolgung (Orders ohne sofortige Ausführung) - [ ] IBKR-Account-Credentials mit `EncryptedStringConverter` speichern +### L0–L5 – Linux-Portierung: Avalonia statt WinForms ✅ (2026-08-07) +Analyse und Begründung: [konzepte/KONZEPT-Linux-Portierung.md](konzepte/KONZEPT-Linux-Portierung.md). +Rückfallpunkt für den letzten WinForms-Stand: Tag `winforms-final`. + +- [x] **L0** `NuGet.config` repariert – drei Pakete hatten kein `packageSourceMapping`-Muster; ein frischer Klon konnte nicht wiederherstellen (auf dem Entwicklungsrechner unsichtbar, weil gecacht) +- [x] **L1a** Core und Module von WinForms entkoppelt: `net10.0` statt `net10.0-windows`. UI-Contract toolkit-neutral (`Func CreateView`, `IconKey` statt `System.Drawing.Image` – letzteres ist seit .NET 7 Windows-only). `LoggingService` meldet über `EntryWritten` statt eine `RichTextBox` zu halten +- [x] **L1b** **Betriebszeitzone** (`AppTimeZone`, `Trading.ApplicationTimeZoneId`): EU- und US-Instanzen sauber getrennt, Persistenz bleibt UTC. `ParseExecutionTime` verwirft die von TWS gemeldete Börsenzeitzone nicht mehr, sondern rechnet gegen sie nach UTC. Kultur-Fixes (PDF-Beträge fest `de-DE`, Scraper-Datum `TryParseExact`), `BackupWorker` plattformunabhängig, DB-Passwort über `MYSQL_PWD` statt Kommandozeile +- [x] **L2** Kopfloser Dienst `IBKRTrader.Daemon` (systemd, `--check`, `--db-version`) + `IBKRTrader.Hosting` als geteilte Host-Zusammenstellung. `AppPaths` (Umgebungsvariable → Binärverzeichnis wenn beschreibbar → FHS) +- [x] **L3** Avalonia-Shell (11.3.19, DataGrid 11.3.13) + Core-Ansichten. `PropertyGrid` ersetzt durch eine aus den Attributen erzeugte Einstellungsmaske (11 Abschnitte, 41 Felder) +- [x] **L4** Die drei Modul-Fenster portiert; CI-Matrix ubuntu + windows +- [x] **L5** WinForms vollständig entfernt – `IBKRTrader.App`, `LauncherForm`, `UI/`, `Properties/Resources.*` + +**Ergebnis:** Alle Projekte `net10.0` ohne Plattformbindung. `publish -r linux-x64` liefert Daemon (11 MB) +und Oberfläche (32 MB) ohne eine einzige Windows-Abhängigkeit. Der Smoke-UI-Lauf braucht kein +Anzeigegerät mehr und ist damit erstmals Teil der CI. + +**Offen:** IB Gateway kopflos betreiben (IBC + Xvfb) – eigene Baustelle, unabhängig vom Code; +LiveCharts2 kommt mit den neuen Modulen (Avalonia deshalb auf der 11er-Linie gepinnt). + --- ## Kurskorrektur abgeschlossen (R1–R7) diff --git a/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj b/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj index ec9f17d..fb7e098 100644 --- a/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj +++ b/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj @@ -1,9 +1,9 @@ - + WinExe net10.0 enable