diff --git a/.gitignore b/.gitignore
index 456afe4..ac6f044 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@ $RECYCLE.BIN/
## Application data (do NOT commit secrets or runtime data)
settings.json
+appsettings.Local.json
Backups/
Logs/
*.log
diff --git a/IBKRTrader.App.csproj b/IBKRTrader.App.csproj
index 4af190a..3cae826 100644
--- a/IBKRTrader.App.csproj
+++ b/IBKRTrader.App.csproj
@@ -24,6 +24,7 @@
+
@@ -35,6 +36,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/LauncherForm.Designer.cs b/LauncherForm.Designer.cs
deleted file mode 100644
index a04fdb8..0000000
--- a/LauncherForm.Designer.cs
+++ /dev/null
@@ -1,274 +0,0 @@
-namespace IBKRTrader
-{
- partial class LauncherForm
- {
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Windows Form Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LauncherForm));
- menuStrip1 = new MenuStrip();
- toolStrip1 = new ToolStrip();
- statusStrip1 = new StatusStrip();
- tabControl1 = new TabControl();
- tabPage_dash = new TabPage();
- tabPage_sett = new TabPage();
- tabPage_modules = new TabPage();
- flp_modules = new FlowLayoutPanel();
- btn_activateTrading = new ToolStripButton();
- tabPage_history = new TabPage();
- tabPage_workers = new TabPage();
- toolStrip2 = new ToolStrip();
- dgv_workerlist = new DataGridView();
- pg_settings = new PropertyGrid();
- tabPage_logs = new TabPage();
- rtb_logs = new RichTextBox();
- toolStrip3 = new ToolStrip();
- toolStrip1.SuspendLayout();
- tabControl1.SuspendLayout();
- tabPage_sett.SuspendLayout();
- tabPage_workers.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)dgv_workerlist).BeginInit();
- tabPage_logs.SuspendLayout();
- SuspendLayout();
- //
- // menuStrip1
- //
- menuStrip1.ImageScalingSize = new Size(24, 24);
- menuStrip1.Location = new Point(0, 0);
- menuStrip1.Name = "menuStrip1";
- menuStrip1.Size = new Size(2441, 24);
- menuStrip1.TabIndex = 0;
- menuStrip1.Text = "menuStrip1";
- //
- // toolStrip1
- //
- toolStrip1.ImageScalingSize = new Size(24, 24);
- toolStrip1.Items.AddRange(new ToolStripItem[] { btn_activateTrading });
- toolStrip1.Location = new Point(0, 24);
- toolStrip1.Name = "toolStrip1";
- toolStrip1.Size = new Size(2441, 34);
- toolStrip1.TabIndex = 1;
- toolStrip1.Text = "toolStrip1";
- //
- // statusStrip1
- //
- statusStrip1.ImageScalingSize = new Size(24, 24);
- statusStrip1.Location = new Point(0, 1197);
- statusStrip1.Name = "statusStrip1";
- statusStrip1.Size = new Size(2441, 22);
- statusStrip1.TabIndex = 2;
- statusStrip1.Text = "statusStrip1";
- //
- // tabControl1
- //
- tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
- tabControl1.Controls.Add(tabPage_dash);
- tabControl1.Controls.Add(tabPage_history);
- tabControl1.Controls.Add(tabPage_sett);
- tabControl1.Controls.Add(tabPage_logs);
- tabControl1.Controls.Add(tabPage_workers);
- tabControl1.Controls.Add(tabPage_modules);
- tabControl1.Location = new Point(12, 52);
- tabControl1.Name = "tabControl1";
- tabControl1.SelectedIndex = 0;
- tabControl1.Size = new Size(2429, 1142);
- tabControl1.TabIndex = 3;
- //
- // tabPage_dash
- //
- tabPage_dash.Location = new Point(4, 34);
- tabPage_dash.Name = "tabPage_dash";
- tabPage_dash.Padding = new Padding(3);
- tabPage_dash.Size = new Size(2421, 1104);
- tabPage_dash.TabIndex = 0;
- tabPage_dash.Text = "Dashboard";
- tabPage_dash.UseVisualStyleBackColor = true;
- //
- // tabPage_sett
- //
- tabPage_sett.Controls.Add(pg_settings);
- tabPage_sett.Location = new Point(4, 34);
- tabPage_sett.Name = "tabPage_sett";
- tabPage_sett.Padding = new Padding(3);
- tabPage_sett.Size = new Size(2421, 1104);
- tabPage_sett.TabIndex = 1;
- tabPage_sett.Text = "Settings";
- tabPage_sett.UseVisualStyleBackColor = true;
- //
- // tabPage_modules
- //
- tabPage_modules.Controls.Add(flp_modules);
- tabPage_modules.Location = new Point(4, 34);
- tabPage_modules.Name = "tabPage_modules";
- tabPage_modules.Padding = new Padding(3);
- tabPage_modules.Size = new Size(2421, 1104);
- tabPage_modules.TabIndex = 2;
- tabPage_modules.Text = "Module";
- tabPage_modules.UseVisualStyleBackColor = true;
- //
- // flp_modules
- //
- flp_modules.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
- flp_modules.AutoScroll = true;
- flp_modules.Location = new Point(6, 6);
- flp_modules.Name = "flp_modules";
- flp_modules.Padding = new Padding(6);
- flp_modules.Size = new Size(2409, 1092);
- flp_modules.TabIndex = 0;
- //
- // btn_activateTrading
- //
- btn_activateTrading.Alignment = ToolStripItemAlignment.Right;
- btn_activateTrading.Image = (Image)resources.GetObject("btn_activateTrading.Image");
- btn_activateTrading.ImageTransparentColor = Color.Magenta;
- btn_activateTrading.Name = "btn_activateTrading";
- btn_activateTrading.Size = new Size(166, 29);
- btn_activateTrading.Text = "Activate Trading";
- //
- // tabPage_history
- //
- tabPage_history.Location = new Point(4, 34);
- tabPage_history.Name = "tabPage_history";
- tabPage_history.Padding = new Padding(3);
- tabPage_history.Size = new Size(2421, 1104);
- tabPage_history.TabIndex = 3;
- tabPage_history.Text = "Trade History";
- tabPage_history.UseVisualStyleBackColor = true;
- //
- // tabPage_workers
- //
- tabPage_workers.Controls.Add(dgv_workerlist);
- tabPage_workers.Controls.Add(toolStrip2);
- tabPage_workers.Location = new Point(4, 34);
- tabPage_workers.Name = "tabPage_workers";
- tabPage_workers.Padding = new Padding(3);
- tabPage_workers.Size = new Size(2421, 1104);
- tabPage_workers.TabIndex = 4;
- tabPage_workers.Text = "Workers / Services";
- tabPage_workers.UseVisualStyleBackColor = true;
- //
- // toolStrip2
- //
- toolStrip2.ImageScalingSize = new Size(24, 24);
- toolStrip2.Location = new Point(3, 3);
- toolStrip2.Name = "toolStrip2";
- toolStrip2.Size = new Size(2415, 25);
- toolStrip2.TabIndex = 0;
- toolStrip2.Text = "toolStrip2";
- //
- // dgv_workerlist
- //
- dgv_workerlist.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
- dgv_workerlist.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_workerlist.Location = new Point(6, 31);
- dgv_workerlist.Name = "dgv_workerlist";
- dgv_workerlist.RowHeadersWidth = 62;
- dgv_workerlist.Size = new Size(2409, 1067);
- dgv_workerlist.TabIndex = 1;
- //
- // pg_settings
- //
- pg_settings.BackColor = SystemColors.Control;
- pg_settings.Location = new Point(3, 6);
- pg_settings.Name = "pg_settings";
- pg_settings.Size = new Size(1101, 1075);
- pg_settings.TabIndex = 0;
- //
- // tabPage_logs
- //
- tabPage_logs.Controls.Add(toolStrip3);
- tabPage_logs.Controls.Add(rtb_logs);
- tabPage_logs.Location = new Point(4, 34);
- tabPage_logs.Name = "tabPage_logs";
- tabPage_logs.Padding = new Padding(3);
- tabPage_logs.Size = new Size(2421, 1104);
- tabPage_logs.TabIndex = 5;
- tabPage_logs.Text = "Logs";
- tabPage_logs.UseVisualStyleBackColor = true;
- //
- // rtb_logs
- //
- rtb_logs.Location = new Point(6, 31);
- rtb_logs.Name = "rtb_logs";
- rtb_logs.Size = new Size(1849, 811);
- rtb_logs.TabIndex = 0;
- rtb_logs.Text = "";
- //
- // toolStrip3
- //
- toolStrip3.ImageScalingSize = new Size(24, 24);
- toolStrip3.Location = new Point(3, 3);
- toolStrip3.Name = "toolStrip3";
- toolStrip3.Size = new Size(2415, 25);
- toolStrip3.TabIndex = 1;
- toolStrip3.Text = "toolStrip3";
- //
- // Form1
- //
- AutoScaleDimensions = new SizeF(10F, 25F);
- AutoScaleMode = AutoScaleMode.Font;
- ClientSize = new Size(2441, 1219);
- Controls.Add(tabControl1);
- Controls.Add(statusStrip1);
- Controls.Add(toolStrip1);
- Controls.Add(menuStrip1);
- MainMenuStrip = menuStrip1;
- Name = "LauncherForm";
- Text = "IBKRTrader — Launcher";
- toolStrip1.ResumeLayout(false);
- toolStrip1.PerformLayout();
- tabControl1.ResumeLayout(false);
- tabPage_sett.ResumeLayout(false);
- tabPage_workers.ResumeLayout(false);
- tabPage_workers.PerformLayout();
- ((System.ComponentModel.ISupportInitialize)dgv_workerlist).EndInit();
- tabPage_logs.ResumeLayout(false);
- tabPage_logs.PerformLayout();
- ResumeLayout(false);
- PerformLayout();
- }
-
- #endregion
-
- private MenuStrip menuStrip1;
- private ToolStrip toolStrip1;
- private ToolStripButton btn_activateTrading;
- private StatusStrip statusStrip1;
- private TabControl tabControl1;
- private TabPage tabPage_dash;
- private TabPage tabPage_sett;
- private TabPage tabPage_modules;
- private FlowLayoutPanel flp_modules;
- private TabPage tabPage_history;
- private TabPage tabPage_workers;
- private DataGridView dgv_workerlist;
- private ToolStrip toolStrip2;
- private PropertyGrid pg_settings;
- private TabPage tabPage_logs;
- private RichTextBox rtb_logs;
- private ToolStrip toolStrip3;
- }
-}
diff --git a/LauncherForm.cs b/LauncherForm.cs
index d3a0f2e..2136846 100644
--- a/LauncherForm.cs
+++ b/LauncherForm.cs
@@ -1,224 +1,155 @@
using IBKRTrader.Core.Database.Migrations;
using IBKRTrader.Core.Logging;
-using IBKRTrader.Core.Modules;
+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 der Anwendung.
-/// Enthält die Core-Panels (Workers, Logs, Settings) und eine Modul-Liste,
-/// aus der jedes Modul als eigenständiges Fenster geöffnet wird.
+/// 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 partial class LauncherForm : Form
+public sealed class LauncherForm : Form
{
- private readonly LoggingService _logger;
- private readonly WorkerEngine _workerEngine;
- private readonly SettingsService _settings;
- private readonly CoreMigrations _migrations;
- private readonly IBKRMigrations _ibkrMigrations;
- private readonly ModuleRegistry _modules;
- private readonly WindowManager _windows;
- private readonly IServiceProvider _services;
+ private readonly ShellUiHost _uiHost;
+ private readonly IServiceProvider _services;
+ private readonly LoggingService _logger;
+ private readonly WorkerEngine _workerEngine;
+ private readonly IReadOnlyList _modules;
- private LogPanelController? _logPanel;
-
- public LauncherForm(
- LoggingService logger,
- WorkerEngine workerEngine,
- SettingsService settings,
- CoreMigrations migrations,
- IBKRMigrations ibkrMigrations,
- ModuleRegistry modules,
- WindowManager windows,
- IServiceProvider services)
+ private readonly Dictionary _viewButtons = new(StringComparer.OrdinalIgnoreCase);
+ private readonly FlowLayoutPanel _buttonPanel = new()
{
- InitializeComponent();
+ Dock = DockStyle.Fill, Padding = new Padding(16), AutoScroll = true
+ };
+ private readonly ToolStripStatusLabel _status = new("Start...");
- _logger = logger;
- _workerEngine = workerEngine;
- _settings = settings;
- _migrations = migrations;
- _ibkrMigrations = ibkrMigrations;
- _modules = modules;
- _windows = windows;
- _services = services;
+ 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;
}
- // ─── Form-Events ──────────────────────────────────────────────────────────
+ private void BuildUi()
+ {
+ var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new Size(24, 24) };
+ WindowMenu.Wire(menu, _uiHost, null);
+
+ foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
+ {
+ var id = view.Id;
+ var btn = new Button
+ {
+ Text = view.Title, Width = 210, Height = 48, Margin = new Padding(8),
+ TextAlign = ContentAlignment.MiddleLeft, Image = view.Icon,
+ ImageAlign = ContentAlignment.MiddleLeft, TextImageRelation = TextImageRelation.ImageBeforeText
+ };
+ btn.Click += (_, _) => _uiHost.OpenView(id);
+ _viewButtons[id] = btn;
+ _buttonPanel.Controls.Add(btn);
+ }
+
+ var statusStrip = new StatusStrip();
+ statusStrip.Items.Add(_status);
+
+ // Reihenfolge: Fill-Panel zuerst, dann Bottom, dann Top (WinForms Dock-Stacking).
+ Controls.Add(_buttonPanel);
+ Controls.Add(statusStrip);
+ Controls.Add(menu);
+ MainMenuStrip = menu;
+ }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
- InitializeLogPanel();
- InitializeWorkerList();
- InitializeSettingsGrid();
- InitializeModulePanel();
_ = StartupAsync();
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
- base.OnFormClosing(e);
- // Offene Modul-Fenster schließen, dann Worker sauber beenden.
- _windows.CloseAll();
- _workerEngine.StopAllAsync().GetAwaiter().GetResult();
- }
-
- // ─── Initialisierung ──────────────────────────────────────────────────────
-
- private void InitializeLogPanel()
- {
- // RichTextBox auf Dock.Fill setzen
- rtb_logs.Dock = DockStyle.Fill;
- _logPanel = new LogPanelController(rtb_logs, _logger);
-
- // LogLevel aus Settings setzen
- var levelStr = _settings.Settings.Logging.Level;
- if (Enum.TryParse(levelStr, true, out var level))
- _logger.SetMinLevel(level);
- }
-
- private void InitializeWorkerList()
- {
- WorkerListBindingSource.Setup(dgv_workerlist, _workerEngine.WorkerInfos);
- }
-
- private void InitializeSettingsGrid()
- {
- pg_settings.SelectedObject = _settings.Settings;
- }
-
- /// Baut je Modul eine Karte mit „Fenster öffnen"-Button.
- private void InitializeModulePanel()
- {
- flp_modules.FlowDirection = FlowDirection.LeftToRight;
- flp_modules.WrapContents = true;
- flp_modules.Controls.Clear();
-
- foreach (var module in _modules.Modules)
- flp_modules.Controls.Add(BuildModuleCard(module));
- }
-
- private Control BuildModuleCard(IModule module)
- {
- var card = new Panel
+ // Auch das Schließen-X läuft über die Sicherheitsabfrage.
+ if (!_uiHost.ShutdownConfirmed)
{
- Width = 320,
- Height = 150,
- Margin = new Padding(8),
- BorderStyle = BorderStyle.FixedSingle
- };
+ e.Cancel = true;
+ BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
+ return;
+ }
- var title = new Label
- {
- Text = $"{module.DisplayName} [{module.Key}]",
- Font = new Font(Font.FontFamily, 11f, FontStyle.Bold),
- Location = new Point(10, 10),
- AutoSize = true
- };
-
- var desc = new Label
- {
- Text = module.Description,
- Location = new Point(10, 42),
- Size = new Size(300, 60),
- AutoEllipsis = true
- };
-
- var open = new Button
- {
- Text = "Fenster öffnen",
- Location = new Point(10, 108),
- Width = 140,
- Tag = module
- };
- open.Click += (_, _) => OpenModuleWindow(module);
-
- var version = new Label
- {
- Text = $"v{module.Version}",
- Location = new Point(240, 113),
- AutoSize = true,
- ForeColor = SystemColors.GrayText
- };
-
- card.Controls.Add(title);
- card.Controls.Add(desc);
- card.Controls.Add(open);
- card.Controls.Add(version);
- return card;
- }
-
- /// Öffnet (oder fokussiert) das eigenständige Fenster eines Moduls.
- private void OpenModuleWindow(IModule module)
- {
+ _uiHost.CloseAllViews();
try
{
- _windows.OpenOrFocus(module.Key, () => module.CreateWindow(_services));
+ foreach (var module in _modules)
+ module.StopAsync(default).GetAwaiter().GetResult();
}
- catch (Exception ex)
- {
- _logger.Error(module.Key, $"{module.DisplayName}: Fenster konnte nicht geöffnet werden.", ex);
- MessageBox.Show(this,
- $"Fenster für '{module.DisplayName}' konnte nicht geöffnet werden:\n{ex.Message}",
- "Modul-Fenster", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- }
- }
+ catch { /* Shutdown darf nicht am Modul scheitern */ }
+ _workerEngine.StopAllAsync().GetAwaiter().GetResult();
- // ─── Async Startup ────────────────────────────────────────────────────────
+ base.OnFormClosing(e);
+ }
private async Task StartupAsync()
{
_logger.Info("Core", "=== IBKRTrader startet ===");
_logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}");
- // DB-Verbindung testen und Core-Migrationen laufen lassen
+ // Log-Level aus Settings.
+ var levelStr = _services.GetRequiredService().Settings.Logging.Level;
+ if (Enum.TryParse(levelStr, true, out var level))
+ _logger.SetMinLevel(level);
+
+ SetStatus("Migrationen...");
try
{
- _logger.Info("Core", "Verbinde mit Datenbank...");
- await _migrations.RunAsync();
- await _ibkrMigrations.RunAsync();
+ await _services.GetRequiredService().RunAsync();
+ await _services.GetRequiredService().RunAsync();
}
catch (Exception ex)
{
_logger.Error("Core", "Core-Datenbankfehler beim Start.", ex);
}
- // Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul).
- foreach (var module in _modules.Modules)
+ foreach (var module in _modules)
{
- try
- {
- await module.InitializeAsync(_services);
- }
- catch (Exception ex)
- {
- _logger.Error(module.Key, $"{module.DisplayName}: Initialisierung fehlgeschlagen.", ex);
- }
+ try { await module.StartAsync(default); }
+ catch (Exception ex) { _logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
}
- // Worker starten
await _workerEngine.StartAllAsync();
_logger.Info("Core", "IBKRTrader bereit.");
- UpdateStatusBar("Bereit");
+ SetStatus("Bereit");
+ UpdateButtonStates();
}
- // ─── StatusBar ────────────────────────────────────────────────────────────
-
- private void UpdateStatusBar(string text)
+ private void UpdateButtonStates()
{
- if (statusStrip1.InvokeRequired)
- statusStrip1.BeginInvoke(() => UpdateStatusBar(text));
- else
- {
- // Vorhandenes Label nutzen oder neues anlegen
- if (statusStrip1.Items.Count == 0)
- statusStrip1.Items.Add(new ToolStripStatusLabel());
- statusStrip1.Items[0].Text = $"Status: {text} | {DateTime.Now:HH:mm:ss}";
- }
+ if (IsDisposed) return;
+ if (InvokeRequired) { BeginInvoke((Action)UpdateButtonStates); return; }
+ foreach (var (id, btn) in _viewButtons)
+ btn.Font = new Font(btn.Font, _uiHost.IsOpen(id) ? FontStyle.Bold : FontStyle.Regular);
+ }
+
+ private void SetStatus(string text)
+ {
+ if (IsDisposed) return;
+ if (InvokeRequired) { BeginInvoke((Action)(() => SetStatus(text))); return; }
+ _status.Text = $"Status: {text} | {DateTime.Now:HH:mm:ss}";
}
}
diff --git a/LauncherForm.resx b/LauncherForm.resx
deleted file mode 100644
index d136883..0000000
--- a/LauncherForm.resx
+++ /dev/null
@@ -1,150 +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
-
-
- 17, 17
-
-
- 175, 17
-
-
- 320, 17
-
-
- 625, 17
-
-
- 480, 17
-
-
-
-
- iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
- YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAEBSURBVEhL3ZIvC8JAGIf3OTTZ/A62+RXs2g2rFsGi2P0C
- WlwT02wGMQjLhgVRnAMR/5bVV37CHeduf9TdFQfPDt7wPOy9GWEYkk4MvGzbJsuylAInD2BgmqZS4JQC
- 99tDCcoCl+OBPKdH3qxHe3eiPuAOTFq0CxwWURYQ5QBfojfgKA5sl0Mux7pwJx8H/NOdmvPj64yKs8gM
- QFqdBlQc7V/nt5HUwDq4cTnj20hiYLW7Unnsv8l/icQGKrVGojwrgpk4lwL1VpdKw40kjCMaYfclzqVA
- 3z1LojSYTPwZ2Bxrzh1gsujPALBmrDt3IA2sG2vXFgBw/lFAJwh0dGLofp4xWrfOyeSKuAAAAABJRU5E
- rkJggg==
-
-
-
- 625, 17
-
-
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index b0047dd..f66da72 100644
--- a/Program.cs
+++ b/Program.cs
@@ -4,107 +4,183 @@ using IBKRTrader.Core.Database;
using IBKRTrader.Core.Database.Migrations;
using IBKRTrader.Core.IBKR;
using IBKRTrader.Core.Logging;
-using IBKRTrader.Core.Modules;
+using IBKRTrader.Core.Modularity;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Trading;
using IBKRTrader.Core.Workers;
using IBKRTrader.Core.Workers.BuiltIn;
using IBKRTrader.Modules.CongressTrading;
using IBKRTrader.UI;
+using IBKRTrader.UI.Views;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
namespace IBKRTrader;
internal static class Program
{
+ public static IHost? AppHost { get; private set; }
+
[STAThread]
- static void Main()
+ 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();
- // ── DI-Container aufbauen ────────────────────────────────────────────
- var services = new ServiceCollection();
+ var modules = new List { new CongressTradingModule() };
- // Core: Settings (zuerst laden)
+ AppHost = Host.CreateDefaultBuilder()
+ .UseContentRoot(AppContext.BaseDirectory)
+ .ConfigureAppConfiguration((_, config) =>
+ config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
+ .ConfigureServices((context, services) =>
+ {
+ RegisterCoreServices(services);
+ foreach (var module in modules)
+ {
+ services.AddSingleton(module);
+ module.RegisterServices(services, context.Configuration);
+ }
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton();
+ })
+ .Build();
+
+ // Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz.
+ AppHost.Services.GetRequiredService()
+ .SetEngine(AppHost.Services.GetRequiredService());
+
+ // 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);
+
+ Application.Run(AppHost.Services.GetRequiredService());
+ AppHost.StopAsync().GetAwaiter().GetResult();
+ }
+
+ /// Registriert alle Core-Services im DI-Container.
+ private static void RegisterCoreServices(IServiceCollection services)
+ {
+ // Settings zuerst laden (eine Quelle, als Singleton weitergereicht).
var settingsService = new SettingsService();
settingsService.Load();
services.AddSingleton(settingsService);
- // Core: Logging
services.AddSingleton();
- // Core: Database
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- // Core: IBKR Services
services.AddSingleton();
services.AddSingleton();
- // Core: Other Services
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- // Core: Trading-Kern
+ // Trading-Kern
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- // Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter
- // angebunden und gegen den Paper-Gateway verifiziert ist.
+ // Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter verifiziert ist.
services.AddSingleton();
- // Core: Built-In Worker
+ // Core-Worker/Services
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- // ── Module registrieren ──────────────────────────────────────────────
- // Der Core kennt konkrete Module nicht: Module werden hier instanziiert,
- // hängen ihre Services über IModule.RegisterServices ein und werden über
- // die ModuleRegistry zugänglich gemacht.
- IModule[] modules =
- [
- new CongressTradingModule(),
- ];
-
- foreach (var module in modules)
- module.RegisterServices(services);
-
- var moduleRegistry = new ModuleRegistry(modules);
- services.AddSingleton(moduleRegistry);
-
- // ── IWorker-Sammlung: Core + Module ──────────────────────────────────
- services.AddSingleton>(sp => new IWorker[]
- {
- // Core-Worker
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- // IBKR Market Data Worker
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- }
- // Modul-Worker dynamisch anhängen
- .Concat(moduleRegistry.Modules.SelectMany(m => m.GetWorkers(sp)))
- .ToArray());
+ // Als IWorker registrieren → die WorkerEngine erhält alle über IEnumerable.
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp => sp.GetRequiredService());
services.AddSingleton();
-
- // UI: Launcher + Fenster-Verwaltung
- services.AddSingleton();
- services.AddSingleton();
-
- var provider = services.BuildServiceProvider();
-
- // WebApiService bekommt Engine-Referenz (zirkuläre Abhängigkeit auflösen)
- var webApi = provider.GetRequiredService();
- var engine = provider.GetRequiredService();
- webApi.SetEngine(engine);
-
- Application.Run(provider.GetRequiredService());
}
-}
\ No newline at end of file
+
+ /// Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.
+ private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp)
+ {
+ uiHost.RegisterView(new ModuleView
+ {
+ Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
+ CreateForm = () => new WorkersView(sp.GetRequiredService())
+ });
+ uiHost.RegisterView(new ModuleView
+ {
+ Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
+ CreateForm = () => new LogsView(sp.GetRequiredService())
+ });
+ uiHost.RegisterView(new ModuleView
+ {
+ Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
+ CreateForm = () => 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.
+ ///
+ private static int RunSmokeUi()
+ {
+ ApplicationConfiguration.Initialize();
+
+ var modules = new List { new CongressTradingModule() };
+
+ using var host = Host.CreateDefaultBuilder()
+ .UseContentRoot(AppContext.BaseDirectory)
+ .ConfigureAppConfiguration((_, config) =>
+ config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
+ .ConfigureServices((context, services) =>
+ {
+ RegisterCoreServices(services);
+ foreach (var module in modules)
+ {
+ services.AddSingleton(module);
+ module.RegisterServices(services, context.Configuration);
+ }
+ services.AddSingleton();
+ services.AddSingleton(s => s.GetRequiredService());
+ services.AddSingleton();
+ })
+ .Build();
+
+ host.Services.GetRequiredService()
+ .SetEngine(host.Services.GetRequiredService());
+
+ var uiHost = host.Services.GetRequiredService();
+ RegisterCoreViews(uiHost, host.Services);
+ foreach (var module in modules)
+ module.RegisterUi(uiHost, host.Services);
+
+ var failures = 0;
+ Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
+ foreach (var view in uiHost.Views)
+ {
+ try { using var form = view.CreateForm(); 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/UI/ShellUiHost.cs b/UI/ShellUiHost.cs
new file mode 100644
index 0000000..d436f31
--- /dev/null
+++ b/UI/ShellUiHost.cs
@@ -0,0 +1,107 @@
+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;
+ }
+
+ var form = view.CreateForm();
+ 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/Views/LogsView.cs b/UI/Views/LogsView.cs
new file mode 100644
index 0000000..adb7f14
--- /dev/null
+++ b/UI/Views/LogsView.cs
@@ -0,0 +1,21 @@
+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/SettingsView.cs b/UI/Views/SettingsView.cs
new file mode 100644
index 0000000..427dc7e
--- /dev/null
+++ b/UI/Views/SettingsView.cs
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 0000000..1113c58
--- /dev/null
+++ b/UI/Views/WorkersView.cs
@@ -0,0 +1,20 @@
+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/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..d09a23c
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Database": {
+ "MySqlConnectionString": ""
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Warning"
+ }
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 5c272e7..f7773dc 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -79,12 +79,13 @@ IBKRTrader.App (WinExe, Root) – Generic Host + Shell (Launcher) + C
- [x] `tests/IBKRTrader.Tests` – Referenzen auf Core + Modul; Fixture-Pfad angepasst
- [x] Neue `.slnx`; ungenutztes HtmlAgilityPack entfernt; Build + **38/38 Tests grün**; App startet
-### R2 – Core-Contracts + Generic Host + Shell-UI
-- [ ] `IModule` (PolytraderSharp-Stil) + `ModuleView` + `IModuleUiHost` + `WindowMenu` in Core
-- [ ] `Program.cs` → `Host.CreateDefaultBuilder`; `IConfiguration` (appsettings.json + appsettings.Local.json)
-- [ ] `ShellUiHost` + `LauncherForm` (View-Buttons + Fenster-Menü); Core-Views registrieren
-- [ ] CongressTrading auf neuen `IModule`-Vertrag; Module über `IHostedService`
-- [ ] `--smoke-ui` Headless-Test
+### R2 – Core-Contracts + Generic Host + Shell-UI ✅
+- [x] `IModule` (PolytraderSharp-Stil) + `ModuleView` + `IModuleUiHost` + `WindowMenu` in Core
+- [x] `Program.cs` → `Host.CreateDefaultBuilder`; `IConfiguration` (appsettings.json + appsettings.Local.json, gitignored)
+- [x] `ShellUiHost` + `LauncherForm` (View-Buttons + gemeinsames Fenster-Menü); Core-Views (Workers/Logs/Settings) als eigene Fenster
+- [x] CongressTrading auf neuen `IModule`-Vertrag; Modul-Worker als `IWorker` registriert
+- [x] `--smoke-ui` Headless-Test (alle Views + Launcher konstruieren) → grün
+- [ ] **Offen (R4/R5):** Worker von `WorkerEngine`/`IWorker` auf `IHostedService` umstellen (aktuell noch WorkerEngine)
### R3 – Persistenz auf EF Core
- [ ] `AddCorePersistence` + `CoreDbContext` + Entities + EF-Repos (core_)
diff --git a/src/IBKRTrader.Core/IBKRTrader.Core.csproj b/src/IBKRTrader.Core/IBKRTrader.Core.csproj
index 24b7fb4..80374ad 100644
--- a/src/IBKRTrader.Core/IBKRTrader.Core.csproj
+++ b/src/IBKRTrader.Core/IBKRTrader.Core.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/IBKRTrader.Core/Modularity/IModule.cs b/src/IBKRTrader.Core/Modularity/IModule.cs
new file mode 100644
index 0000000..19b7d23
--- /dev/null
+++ b/src/IBKRTrader.Core/Modularity/IModule.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.Core.Modularity;
+
+///
+/// Vertrag für ein IBKRTrader-Modul. Module referenzieren nur den Core, niemals einander.
+/// Ein Modul kapselt einen eigenständigen Funktionsbereich (z. B. eine Trading-Strategie)
+/// mit eigenen Services, Jobs, DB-Objekten sowie eigenen UI-Ansichten.
+///
+/// Vorbild: PolytraderSharp IPolyTraderModule.
+///
+public interface IModule
+{
+ /// Eindeutiger Anzeigename des Moduls, z. B. "CongressTrading".
+ string Name { get; }
+
+ /// DB-Präfix für die Objekte des Moduls (z. B. "ct_"), damit sich Module nicht überschneiden.
+ string DbPrefix { get; }
+
+ ///
+ /// Registriert Services/Jobs des Moduls im DI-Container. Wird beim App-Start aufgerufen,
+ /// bevor der Host gebaut wird.
+ ///
+ void RegisterServices(IServiceCollection services, IConfiguration configuration);
+
+ ///
+ /// Registriert die UI-Ansichten des Moduls bei der Shell (Launcher). Die Shell öffnet jede Ansicht
+ /// auf Wunsch in einem eigenen Fenster; Module liefern eigene Forms, die Shell bleibt modul-agnostisch.
+ ///
+ void RegisterUi(IModuleUiHost host, IServiceProvider services);
+
+ ///
+ /// Optionale Vorab-Prüfung, ob das Modul aktiviert werden kann. Gibt null zurück, wenn
+ /// aktivierbar, sonst einen kurzen Grund (z. B. „IBKR-Gateway nicht konfiguriert").
+ ///
+ string? GetActivationBlocker(IConfiguration configuration) => null;
+
+ /// Wird nach der Core-Initialisierung gestartet (kein Anlaufen gegen leeren State).
+ Task StartAsync(CancellationToken cancellationToken);
+
+ Task StopAsync(CancellationToken cancellationToken);
+}
diff --git a/src/IBKRTrader.Core/Modularity/ModuleView.cs b/src/IBKRTrader.Core/Modularity/ModuleView.cs
new file mode 100644
index 0000000..5340ab8
--- /dev/null
+++ b/src/IBKRTrader.Core/Modularity/ModuleView.cs
@@ -0,0 +1,57 @@
+using System.Windows.Forms;
+
+namespace IBKRTrader.Core.Modularity;
+
+///
+/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist ein
+/// , das über erzeugt wird (mit DI-Abhängigkeiten).
+/// Die Shell zeigt je View höchstens eine Instanz und holt ein offenes Fenster wieder nach vorne.
+///
+public sealed class ModuleView
+{
+ /// Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).
+ public string Id { get; init; } = Guid.NewGuid().ToString();
+
+ /// Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt).
+ public string Title { get; init; } = "Fenster";
+
+ /// Optionale Gruppierung (z. B. "Core", "CongressTrading").
+ public string? Group { get; init; }
+
+ /// Optionale Sortierreihenfolge in Menü/Buttons.
+ public int Order { get; init; } = 0;
+
+ /// Optionales Icon für Menü/Buttons.
+ public System.Drawing.Image? Icon { get; set; }
+
+ /// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).
+ public Func