@
R2: Generic Host + Modul-Vertrag + Shell-UI (PolytraderSharp-Konzept) - Core/Modularity: neuer IModule (Name, DbPrefix, RegisterServices(services,config), RegisterUi(host,sp), StartAsync/StopAsync, GetActivationBlocker) + ModuleView + IModuleUiHost + WindowMenu. Alte IModule/ModuleRegistry/WindowManager/ModuleFormBase entfernt. - Program.cs: Host.CreateDefaultBuilder + IConfiguration (appsettings.json/.Local.json); Core-Services registriert, Module via RegisterServices, Views via RegisterUi. - UI: ShellUiHost (Einzelinstanz-Fenster + Fenster-Menue), LauncherForm als Shell (Buttons je View), Core-Views Logs/Settings/Workers als eigene Fenster. - CongressTrading auf neuen Vertrag; Worker als IWorker registriert. - --smoke-ui Headless-Test (konstruiert jede View + Launcher). - appsettings.Local.json gitignored. - Tests angepasst -> 30/30 gruen; Build + Smoke-UI + App-Start verifiziert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -39,6 +39,7 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
## Application data (do NOT commit secrets or runtime data)
|
## Application data (do NOT commit secrets or runtime data)
|
||||||
settings.json
|
settings.json
|
||||||
|
appsettings.Local.json
|
||||||
Backups/
|
Backups/
|
||||||
Logs/
|
Logs/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
<None Update="settings.json">
|
<None Update="settings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="appsettings.Local.json" Condition="Exists('appsettings.Local.json')">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Generated
-274
@@ -1,274 +0,0 @@
|
|||||||
namespace IBKRTrader
|
|
||||||
{
|
|
||||||
partial class LauncherForm
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Required designer variable.
|
|
||||||
/// </summary>
|
|
||||||
private System.ComponentModel.IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clean up any resources being used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && (components != null))
|
|
||||||
{
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows Form Designer generated code
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Required method for Designer support - do not modify
|
|
||||||
/// the contents of this method with the code editor.
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+95
-164
@@ -1,224 +1,155 @@
|
|||||||
using IBKRTrader.Core.Database.Migrations;
|
using IBKRTrader.Core.Database.Migrations;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Modules;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.UI;
|
using IBKRTrader.UI;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace IBKRTrader;
|
namespace IBKRTrader;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Launcher – das Basis-Fenster der Anwendung.
|
/// Launcher – das Basis-Fenster (Shell). Zeigt je registrierter View einen Button, führt beim Start
|
||||||
/// Enthält die Core-Panels (Workers, Logs, Settings) und eine Modul-Liste,
|
/// Migrationen aus, startet Module und die WorkerEngine, und trägt das gemeinsame Fenster-Menü.
|
||||||
/// aus der jedes Modul als eigenständiges Fenster geöffnet wird.
|
/// Die inhaltlichen Ansichten (Logs, Settings, Workers, Module) sind eigenständige Fenster.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LauncherForm : Form
|
public sealed class LauncherForm : Form
|
||||||
{
|
{
|
||||||
|
private readonly ShellUiHost _uiHost;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
private readonly WorkerEngine _workerEngine;
|
private readonly WorkerEngine _workerEngine;
|
||||||
private readonly SettingsService _settings;
|
private readonly IReadOnlyList<IModule> _modules;
|
||||||
private readonly CoreMigrations _migrations;
|
|
||||||
private readonly IBKRMigrations _ibkrMigrations;
|
|
||||||
private readonly ModuleRegistry _modules;
|
|
||||||
private readonly WindowManager _windows;
|
|
||||||
private readonly IServiceProvider _services;
|
|
||||||
|
|
||||||
private LogPanelController? _logPanel;
|
private readonly Dictionary<string, Button> _viewButtons = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly FlowLayoutPanel _buttonPanel = new()
|
||||||
public LauncherForm(
|
|
||||||
LoggingService logger,
|
|
||||||
WorkerEngine workerEngine,
|
|
||||||
SettingsService settings,
|
|
||||||
CoreMigrations migrations,
|
|
||||||
IBKRMigrations ibkrMigrations,
|
|
||||||
ModuleRegistry modules,
|
|
||||||
WindowManager windows,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
Dock = DockStyle.Fill, Padding = new Padding(16), AutoScroll = true
|
||||||
|
};
|
||||||
|
private readonly ToolStripStatusLabel _status = new("Start...");
|
||||||
|
|
||||||
_logger = logger;
|
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
|
||||||
_workerEngine = workerEngine;
|
{
|
||||||
_settings = settings;
|
_uiHost = uiHost;
|
||||||
_migrations = migrations;
|
|
||||||
_ibkrMigrations = ibkrMigrations;
|
|
||||||
_modules = modules;
|
|
||||||
_windows = windows;
|
|
||||||
_services = services;
|
_services = services;
|
||||||
|
_logger = services.GetRequiredService<LoggingService>();
|
||||||
|
_workerEngine = services.GetRequiredService<WorkerEngine>();
|
||||||
|
_modules = services.GetServices<IModule>().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)
|
protected override void OnLoad(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnLoad(e);
|
base.OnLoad(e);
|
||||||
InitializeLogPanel();
|
|
||||||
InitializeWorkerList();
|
|
||||||
InitializeSettingsGrid();
|
|
||||||
InitializeModulePanel();
|
|
||||||
_ = StartupAsync();
|
_ = StartupAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnFormClosing(e);
|
// Auch das Schließen-X läuft über die Sicherheitsabfrage.
|
||||||
// Offene Modul-Fenster schließen, dann Worker sauber beenden.
|
if (!_uiHost.ShutdownConfirmed)
|
||||||
_windows.CloseAll();
|
{
|
||||||
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
e.Cancel = true;
|
||||||
|
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Initialisierung ──────────────────────────────────────────────────────
|
_uiHost.CloseAllViews();
|
||||||
|
|
||||||
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<AppLogLevel>(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Baut je Modul eine Karte mit „Fenster öffnen"-Button.</summary>
|
|
||||||
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
|
|
||||||
{
|
|
||||||
Width = 320,
|
|
||||||
Height = 150,
|
|
||||||
Margin = new Padding(8),
|
|
||||||
BorderStyle = BorderStyle.FixedSingle
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Öffnet (oder fokussiert) das eigenständige Fenster eines Moduls.</summary>
|
|
||||||
private void OpenModuleWindow(IModule module)
|
|
||||||
{
|
|
||||||
try
|
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()
|
private async Task StartupAsync()
|
||||||
{
|
{
|
||||||
_logger.Info("Core", "=== IBKRTrader startet ===");
|
_logger.Info("Core", "=== IBKRTrader startet ===");
|
||||||
_logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}");
|
_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<SettingsService>().Settings.Logging.Level;
|
||||||
|
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
||||||
|
_logger.SetMinLevel(level);
|
||||||
|
|
||||||
|
SetStatus("Migrationen...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Info("Core", "Verbinde mit Datenbank...");
|
await _services.GetRequiredService<CoreMigrations>().RunAsync();
|
||||||
await _migrations.RunAsync();
|
await _services.GetRequiredService<IBKRMigrations>().RunAsync();
|
||||||
await _ibkrMigrations.RunAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error("Core", "Core-Datenbankfehler beim Start.", ex);
|
_logger.Error("Core", "Core-Datenbankfehler beim Start.", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul).
|
foreach (var module in _modules)
|
||||||
foreach (var module in _modules.Modules)
|
|
||||||
{
|
{
|
||||||
try
|
try { await module.StartAsync(default); }
|
||||||
{
|
catch (Exception ex) { _logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
|
||||||
await module.InitializeAsync(_services);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error(module.Key, $"{module.DisplayName}: Initialisierung fehlgeschlagen.", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Worker starten
|
|
||||||
await _workerEngine.StartAllAsync();
|
await _workerEngine.StartAllAsync();
|
||||||
|
|
||||||
_logger.Info("Core", "IBKRTrader bereit.");
|
_logger.Info("Core", "IBKRTrader bereit.");
|
||||||
UpdateStatusBar("Bereit");
|
SetStatus("Bereit");
|
||||||
|
UpdateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── StatusBar ────────────────────────────────────────────────────────────
|
private void UpdateButtonStates()
|
||||||
|
{
|
||||||
|
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 UpdateStatusBar(string text)
|
private void SetStatus(string text)
|
||||||
{
|
{
|
||||||
if (statusStrip1.InvokeRequired)
|
if (IsDisposed) return;
|
||||||
statusStrip1.BeginInvoke(() => UpdateStatusBar(text));
|
if (InvokeRequired) { BeginInvoke((Action)(() => SetStatus(text))); return; }
|
||||||
else
|
_status.Text = $"Status: {text} | {DateTime.Now:HH:mm:ss}";
|
||||||
{
|
|
||||||
// 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}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>17, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>175, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>320, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="toolStrip3.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>625, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="toolStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>480, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
|
||||||
<data name="btn_activateTrading.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>
|
|
||||||
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
|
||||||
YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAEBSURBVEhL3ZIvC8JAGIf3OTTZ/A62+RXs2g2rFsGi2P0C
|
|
||||||
WlwT02wGMQjLhgVRnAMR/5bVV37CHeduf9TdFQfPDt7wPOy9GWEYkk4MvGzbJsuylAInD2BgmqZS4JQC
|
|
||||||
99tDCcoCl+OBPKdH3qxHe3eiPuAOTFq0CxwWURYQ5QBfojfgKA5sl0Mux7pwJx8H/NOdmvPj64yKs8gM
|
|
||||||
QFqdBlQc7V/nt5HUwDq4cTnj20hiYLW7Unnsv8l/icQGKrVGojwrgpk4lwL1VpdKw40kjCMaYfclzqVA
|
|
||||||
3z1LojSYTPwZ2Bxrzh1gsujPALBmrDt3IA2sG2vXFgBw/lFAJwh0dGLofp4xWrfOyeSKuAAAAABJRU5E
|
|
||||||
rkJggg==
|
|
||||||
</value>
|
|
||||||
</data>
|
|
||||||
<metadata name="toolStrip3.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>625, 17</value>
|
|
||||||
</metadata>
|
|
||||||
</root>
|
|
||||||
+126
-50
@@ -4,107 +4,183 @@ using IBKRTrader.Core.Database;
|
|||||||
using IBKRTrader.Core.Database.Migrations;
|
using IBKRTrader.Core.Database.Migrations;
|
||||||
using IBKRTrader.Core.IBKR;
|
using IBKRTrader.Core.IBKR;
|
||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Modules;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
using IBKRTrader.Core.Trading;
|
using IBKRTrader.Core.Trading;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Core.Workers.BuiltIn;
|
using IBKRTrader.Core.Workers.BuiltIn;
|
||||||
using IBKRTrader.Modules.CongressTrading;
|
using IBKRTrader.Modules.CongressTrading;
|
||||||
using IBKRTrader.UI;
|
using IBKRTrader.UI;
|
||||||
|
using IBKRTrader.UI.Views;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace IBKRTrader;
|
namespace IBKRTrader;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
|
public static IHost? AppHost { get; private set; }
|
||||||
|
|
||||||
[STAThread]
|
[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();
|
ApplicationConfiguration.Initialize();
|
||||||
|
|
||||||
// ── DI-Container aufbauen ────────────────────────────────────────────
|
var modules = new List<IModule> { new CongressTradingModule() };
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// 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<ShellUiHost>();
|
||||||
|
services.AddSingleton<IModuleUiHost>(sp => sp.GetRequiredService<ShellUiHost>());
|
||||||
|
services.AddSingleton<LauncherForm>();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz.
|
||||||
|
AppHost.Services.GetRequiredService<WebApiService>()
|
||||||
|
.SetEngine(AppHost.Services.GetRequiredService<WorkerEngine>());
|
||||||
|
|
||||||
|
// Views registrieren (Core + Module), dann Launcher starten.
|
||||||
|
var uiHost = AppHost.Services.GetRequiredService<ShellUiHost>();
|
||||||
|
RegisterCoreViews(uiHost, AppHost.Services);
|
||||||
|
foreach (var module in modules)
|
||||||
|
module.RegisterUi(uiHost, AppHost.Services);
|
||||||
|
|
||||||
|
Application.Run(AppHost.Services.GetRequiredService<LauncherForm>());
|
||||||
|
AppHost.StopAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Registriert alle Core-Services im DI-Container.</summary>
|
||||||
|
private static void RegisterCoreServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Settings zuerst laden (eine Quelle, als Singleton weitergereicht).
|
||||||
var settingsService = new SettingsService();
|
var settingsService = new SettingsService();
|
||||||
settingsService.Load();
|
settingsService.Load();
|
||||||
services.AddSingleton(settingsService);
|
services.AddSingleton(settingsService);
|
||||||
|
|
||||||
// Core: Logging
|
|
||||||
services.AddSingleton<LoggingService>();
|
services.AddSingleton<LoggingService>();
|
||||||
|
|
||||||
// Core: Database
|
|
||||||
services.AddSingleton<DatabaseService>();
|
services.AddSingleton<DatabaseService>();
|
||||||
services.AddSingleton<CoreMigrations>();
|
services.AddSingleton<CoreMigrations>();
|
||||||
services.AddSingleton<IBKRMigrations>();
|
services.AddSingleton<IBKRMigrations>();
|
||||||
|
|
||||||
// Core: IBKR Services
|
|
||||||
services.AddSingleton<IBKRGatewayService>();
|
services.AddSingleton<IBKRGatewayService>();
|
||||||
services.AddSingleton<IBKRMarketDataRepository>();
|
services.AddSingleton<IBKRMarketDataRepository>();
|
||||||
|
|
||||||
// Core: Other Services
|
|
||||||
services.AddSingleton<BudgetService>();
|
services.AddSingleton<BudgetService>();
|
||||||
services.AddSingleton<TradeHistoryService>();
|
services.AddSingleton<TradeHistoryService>();
|
||||||
services.AddSingleton<AIModelService>();
|
services.AddSingleton<AIModelService>();
|
||||||
|
|
||||||
// Core: Trading-Kern
|
// Trading-Kern
|
||||||
services.AddSingleton<IRiskService, RiskService>();
|
services.AddSingleton<IRiskService, RiskService>();
|
||||||
services.AddSingleton<IPortfolioService, PortfolioService>();
|
services.AddSingleton<IPortfolioService, PortfolioService>();
|
||||||
services.AddSingleton<IExecutionService, ExecutionService>();
|
services.AddSingleton<IExecutionService, ExecutionService>();
|
||||||
// Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter
|
// Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter verifiziert ist.
|
||||||
// angebunden und gegen den Paper-Gateway verifiziert ist.
|
|
||||||
services.AddSingleton<IBrokerClient, NullBrokerClient>();
|
services.AddSingleton<IBrokerClient, NullBrokerClient>();
|
||||||
|
|
||||||
// Core: Built-In Worker
|
// Core-Worker/Services
|
||||||
services.AddSingleton<BackupWorker>();
|
services.AddSingleton<BackupWorker>();
|
||||||
services.AddSingleton<WebserverService>();
|
services.AddSingleton<WebserverService>();
|
||||||
services.AddSingleton<WebApiService>();
|
services.AddSingleton<WebApiService>();
|
||||||
services.AddSingleton<IBKRInstrumentSyncWorker>();
|
services.AddSingleton<IBKRInstrumentSyncWorker>();
|
||||||
services.AddSingleton<IBKRPriceHistoryWorker>();
|
services.AddSingleton<IBKRPriceHistoryWorker>();
|
||||||
|
|
||||||
// ── Module registrieren ──────────────────────────────────────────────
|
// Als IWorker registrieren → die WorkerEngine erhält alle über IEnumerable<IWorker>.
|
||||||
// Der Core kennt konkrete Module nicht: Module werden hier instanziiert,
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<BackupWorker>());
|
||||||
// hängen ihre Services über IModule.RegisterServices ein und werden über
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<WebserverService>());
|
||||||
// die ModuleRegistry zugänglich gemacht.
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<WebApiService>());
|
||||||
IModule[] modules =
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<IBKRInstrumentSyncWorker>());
|
||||||
[
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<IBKRPriceHistoryWorker>());
|
||||||
new CongressTradingModule(),
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach (var module in modules)
|
|
||||||
module.RegisterServices(services);
|
|
||||||
|
|
||||||
var moduleRegistry = new ModuleRegistry(modules);
|
|
||||||
services.AddSingleton(moduleRegistry);
|
|
||||||
|
|
||||||
// ── IWorker-Sammlung: Core + Module ──────────────────────────────────
|
|
||||||
services.AddSingleton<IEnumerable<IWorker>>(sp => new IWorker[]
|
|
||||||
{
|
|
||||||
// Core-Worker
|
|
||||||
sp.GetRequiredService<BackupWorker>(),
|
|
||||||
sp.GetRequiredService<WebserverService>(),
|
|
||||||
sp.GetRequiredService<WebApiService>(),
|
|
||||||
// IBKR Market Data Worker
|
|
||||||
sp.GetRequiredService<IBKRInstrumentSyncWorker>(),
|
|
||||||
sp.GetRequiredService<IBKRPriceHistoryWorker>(),
|
|
||||||
}
|
|
||||||
// Modul-Worker dynamisch anhängen
|
|
||||||
.Concat(moduleRegistry.Modules.SelectMany(m => m.GetWorkers(sp)))
|
|
||||||
.ToArray());
|
|
||||||
|
|
||||||
services.AddSingleton<WorkerEngine>();
|
services.AddSingleton<WorkerEngine>();
|
||||||
|
}
|
||||||
|
|
||||||
// UI: Launcher + Fenster-Verwaltung
|
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary>
|
||||||
services.AddSingleton<WindowManager>();
|
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<WorkerEngine>())
|
||||||
|
});
|
||||||
|
uiHost.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
|
||||||
|
CreateForm = () => new LogsView(sp.GetRequiredService<LoggingService>())
|
||||||
|
});
|
||||||
|
uiHost.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
|
||||||
|
CreateForm = () => new SettingsView(sp.GetRequiredService<SettingsService>())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private static int RunSmokeUi()
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
|
||||||
|
var modules = new List<IModule> { 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<ShellUiHost>();
|
||||||
|
services.AddSingleton<IModuleUiHost>(s => s.GetRequiredService<ShellUiHost>());
|
||||||
services.AddSingleton<LauncherForm>();
|
services.AddSingleton<LauncherForm>();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
host.Services.GetRequiredService<WebApiService>()
|
||||||
|
.SetEngine(host.Services.GetRequiredService<WorkerEngine>());
|
||||||
|
|
||||||
// WebApiService bekommt Engine-Referenz (zirkuläre Abhängigkeit auflösen)
|
var uiHost = host.Services.GetRequiredService<ShellUiHost>();
|
||||||
var webApi = provider.GetRequiredService<WebApiService>();
|
RegisterCoreViews(uiHost, host.Services);
|
||||||
var engine = provider.GetRequiredService<WorkerEngine>();
|
foreach (var module in modules)
|
||||||
webApi.SetEngine(engine);
|
module.RegisterUi(uiHost, host.Services);
|
||||||
|
|
||||||
Application.Run(provider.GetRequiredService<LauncherForm>());
|
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<LauncherForm>(); 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>ShellUiHost</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShellUiHost : IModuleUiHost
|
||||||
|
{
|
||||||
|
private readonly List<ModuleView> _views = new();
|
||||||
|
private readonly Dictionary<string, Form> _open = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private Form? _mainWindow;
|
||||||
|
|
||||||
|
/// <summary>True, sobald das Beenden bestätigt wurde (der Launcher wertet das in FormClosing aus).</summary>
|
||||||
|
public bool ShutdownConfirmed { get; private set; }
|
||||||
|
|
||||||
|
public event Action? OpenStateChanged;
|
||||||
|
|
||||||
|
public IReadOnlyList<ModuleView> Views => _views;
|
||||||
|
|
||||||
|
public void RegisterView(ModuleView view) => _views.Add(view);
|
||||||
|
|
||||||
|
/// <summary>Setzt das Hauptfenster (Launcher) – Ziel für <see cref="ActivateMain"/>.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sicherheitsabfrage; bei Bestätigung wird die Message-Loop beendet.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Schließt alle offenen View-Fenster (beim Herunterfahren).</summary>
|
||||||
|
public void CloseAllViews()
|
||||||
|
{
|
||||||
|
foreach (var form in _open.Values.ToList())
|
||||||
|
if (!form.IsDisposed)
|
||||||
|
form.Close();
|
||||||
|
_open.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI.Views;
|
||||||
|
|
||||||
|
/// <summary>Core-View: Live-Log (RichTextBox), an den LoggingService gebunden.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using IBKRTrader.Core.Settings;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI.Views;
|
||||||
|
|
||||||
|
/// <summary>Core-View: Einstellungen (PropertyGrid auf AppSettings) mit Speichern-Button.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using IBKRTrader.Core.Workers;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI.Views;
|
||||||
|
|
||||||
|
/// <summary>Core-View: Worker/Services-Übersicht (DataGridView, live an die WorkerEngine gebunden).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"Database": {
|
||||||
|
"MySqlConnectionString": ""
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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] `tests/IBKRTrader.Tests` – Referenzen auf Core + Modul; Fixture-Pfad angepasst
|
||||||
- [x] Neue `.slnx`; ungenutztes HtmlAgilityPack entfernt; Build + **38/38 Tests grün**; App startet
|
- [x] Neue `.slnx`; ungenutztes HtmlAgilityPack entfernt; Build + **38/38 Tests grün**; App startet
|
||||||
|
|
||||||
### R2 – Core-Contracts + Generic Host + Shell-UI
|
### R2 – Core-Contracts + Generic Host + Shell-UI ✅
|
||||||
- [ ] `IModule` (PolytraderSharp-Stil) + `ModuleView` + `IModuleUiHost` + `WindowMenu` in Core
|
- [x] `IModule` (PolytraderSharp-Stil) + `ModuleView` + `IModuleUiHost` + `WindowMenu` in Core
|
||||||
- [ ] `Program.cs` → `Host.CreateDefaultBuilder`; `IConfiguration` (appsettings.json + appsettings.Local.json)
|
- [x] `Program.cs` → `Host.CreateDefaultBuilder`; `IConfiguration` (appsettings.json + appsettings.Local.json, gitignored)
|
||||||
- [ ] `ShellUiHost` + `LauncherForm` (View-Buttons + Fenster-Menü); Core-Views registrieren
|
- [x] `ShellUiHost` + `LauncherForm` (View-Buttons + gemeinsames Fenster-Menü); Core-Views (Workers/Logs/Settings) als eigene Fenster
|
||||||
- [ ] CongressTrading auf neuen `IModule`-Vertrag; Module über `IHostedService`
|
- [x] CongressTrading auf neuen `IModule`-Vertrag; Modul-Worker als `IWorker` registriert
|
||||||
- [ ] `--smoke-ui` Headless-Test
|
- [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
|
### R3 – Persistenz auf EF Core
|
||||||
- [ ] `AddCorePersistence` + `CoreDbContext` + Entities + EF-Repos (core_)
|
- [ ] `AddCorePersistence` + `CoreDbContext` + Entities + EF-Repos (core_)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Erlaubt dem Testprojekt, interne Helfer zu testen. -->
|
<!-- Erlaubt dem Testprojekt, interne Helfer zu testen. -->
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>IPolyTraderModule</c>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModule
|
||||||
|
{
|
||||||
|
/// <summary>Eindeutiger Anzeigename des Moduls, z. B. "CongressTrading".</summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>DB-Präfix für die Objekte des Moduls (z. B. "ct_"), damit sich Module nicht überschneiden.</summary>
|
||||||
|
string DbPrefix { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registriert Services/Jobs des Moduls im DI-Container. Wird beim App-Start aufgerufen,
|
||||||
|
/// bevor der Host gebaut wird.
|
||||||
|
/// </summary>
|
||||||
|
void RegisterServices(IServiceCollection services, IConfiguration configuration);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
void RegisterUi(IModuleUiHost host, IServiceProvider services);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionale Vorab-Prüfung, ob das Modul aktiviert werden kann. Gibt <c>null</c> zurück, wenn
|
||||||
|
/// aktivierbar, sonst einen kurzen Grund (z. B. „IBKR-Gateway nicht konfiguriert").
|
||||||
|
/// </summary>
|
||||||
|
string? GetActivationBlocker(IConfiguration configuration) => null;
|
||||||
|
|
||||||
|
/// <summary>Wird nach der Core-Initialisierung gestartet (kein Anlaufen gegen leeren State).</summary>
|
||||||
|
Task StartAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task StopAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist ein
|
||||||
|
/// <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird (mit DI-Abhängigkeiten).
|
||||||
|
/// Die Shell zeigt je View höchstens eine Instanz und holt ein offenes Fenster wieder nach vorne.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ModuleView
|
||||||
|
{
|
||||||
|
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
||||||
|
public string Id { get; init; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt).</summary>
|
||||||
|
public string Title { get; init; } = "Fenster";
|
||||||
|
|
||||||
|
/// <summary>Optionale Gruppierung (z. B. "Core", "CongressTrading").</summary>
|
||||||
|
public string? Group { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Optionale Sortierreihenfolge in Menü/Buttons.</summary>
|
||||||
|
public int Order { get; init; } = 0;
|
||||||
|
|
||||||
|
/// <summary>Optionales Icon für Menü/Buttons.</summary>
|
||||||
|
public System.Drawing.Image? Icon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
|
||||||
|
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten.
|
||||||
|
/// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core kennen)
|
||||||
|
/// das gemeinsame „Fenster"-Menü bauen (siehe <see cref="WindowMenu"/>).
|
||||||
|
/// </summary>
|
||||||
|
public interface IModuleUiHost
|
||||||
|
{
|
||||||
|
void RegisterView(ModuleView view);
|
||||||
|
|
||||||
|
/// <summary>Alle registrierten Ansichten (Core + Module).</summary>
|
||||||
|
IReadOnlyList<ModuleView> Views { get; }
|
||||||
|
|
||||||
|
/// <summary>Ist das Fenster dieser View gerade offen?</summary>
|
||||||
|
bool IsOpen(string viewId);
|
||||||
|
|
||||||
|
/// <summary>Öffnet die View bzw. holt ein bereits offenes Fenster in den Vordergrund.</summary>
|
||||||
|
void OpenView(string viewId);
|
||||||
|
|
||||||
|
/// <summary>Holt das Hauptfenster (Launcher) in den Vordergrund.</summary>
|
||||||
|
void ActivateMain();
|
||||||
|
|
||||||
|
/// <summary>Leitet das (bestätigte) Herunterfahren der App ein. Aus jedem Fenster aufrufbar.</summary>
|
||||||
|
void RequestShutdown();
|
||||||
|
|
||||||
|
/// <summary>Feuert, wenn sich der Offen-Status irgendeiner View ändert.</summary>
|
||||||
|
event Action? OpenStateChanged;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut das gemeinsame Fenster-Menü, das auf JEDEM Fenster erscheint und das Wechseln zwischen allen
|
||||||
|
/// Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract <see cref="IModuleUiHost"/>
|
||||||
|
/// nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
|
||||||
|
/// </summary>
|
||||||
|
public static class WindowMenu
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verdrahtet einen <see cref="MenuStrip"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Baut die Menüleiste komplett neu auf (Launcher, alle Views nebeneinander, Aktion rechts).</summary>
|
||||||
|
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 item = new ToolStripMenuItem(view.Title)
|
||||||
|
{
|
||||||
|
Image = view.Icon,
|
||||||
|
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||||
|
DisplayStyle = view.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using IBKRTrader.Core.Workers;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Core.Modules;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vertrag für ein eigenständiges Trading-Modul.
|
|
||||||
///
|
|
||||||
/// Architektur-Regel: Der Core kennt konkrete Module NICHT. Module hängen sich
|
|
||||||
/// ausschließlich über diesen Vertrag und den DI-Container ein
|
|
||||||
/// (Abhängigkeit nur Modul → Core, niemals umgekehrt).
|
|
||||||
///
|
|
||||||
/// Lebenszyklus:
|
|
||||||
/// 1. <see cref="RegisterServices"/> – vor dem Bauen des DI-Containers
|
|
||||||
/// 2. <see cref="InitializeAsync"/> – beim App-Start (Migrationen etc.)
|
|
||||||
/// 3. <see cref="GetWorkers"/> – Worker werden der WorkerEngine übergeben
|
|
||||||
/// 4. <see cref="CreateWindow"/> – Launcher öffnet das Modul-Fenster
|
|
||||||
/// </summary>
|
|
||||||
public interface IModule
|
|
||||||
{
|
|
||||||
/// <summary>Eindeutiges Modul-Kürzel, zugleich DB-Tabellen-Präfix (z. B. "CT").</summary>
|
|
||||||
string Key { get; }
|
|
||||||
|
|
||||||
/// <summary>Anzeigename im Launcher (z. B. "Congress Trading").</summary>
|
|
||||||
string DisplayName { get; }
|
|
||||||
|
|
||||||
/// <summary>Kurzbeschreibung der Strategie für den Launcher.</summary>
|
|
||||||
string Description { get; }
|
|
||||||
|
|
||||||
/// <summary>Modul-Version.</summary>
|
|
||||||
string Version { get; }
|
|
||||||
|
|
||||||
/// <summary>Registriert alle Modul-Services im DI-Container (vor dem Build).</summary>
|
|
||||||
void RegisterServices(IServiceCollection services);
|
|
||||||
|
|
||||||
/// <summary>Führt Modul-Migrationen und einmalige Initialisierung aus (beim Start).</summary>
|
|
||||||
Task InitializeAsync(IServiceProvider provider);
|
|
||||||
|
|
||||||
/// <summary>Liefert die Worker/Services des Moduls für die WorkerEngine.</summary>
|
|
||||||
IEnumerable<IWorker> GetWorkers(IServiceProvider provider);
|
|
||||||
|
|
||||||
/// <summary>Erzeugt das eigenständige Fenster des Moduls (vom Launcher geöffnet).</summary>
|
|
||||||
Form CreateWindow(IServiceProvider provider);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
namespace IBKRTrader.Core.Modules;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Zentrale Registry aller geladenen Module.
|
|
||||||
/// Wird in Program.cs mit den Modul-Instanzen befüllt und als Singleton
|
|
||||||
/// bereitgestellt, damit Launcher und Startup-Code über die Module iterieren
|
|
||||||
/// können, ohne konkrete Modultypen zu kennen.
|
|
||||||
/// </summary>
|
|
||||||
public class ModuleRegistry
|
|
||||||
{
|
|
||||||
private readonly List<IModule> _modules;
|
|
||||||
|
|
||||||
public ModuleRegistry(IEnumerable<IModule> modules)
|
|
||||||
=> _modules = modules?.ToList() ?? throw new ArgumentNullException(nameof(modules));
|
|
||||||
|
|
||||||
/// <summary>Alle registrierten Module in Registrierungsreihenfolge.</summary>
|
|
||||||
public IReadOnlyList<IModule> Modules => _modules;
|
|
||||||
|
|
||||||
/// <summary>Findet ein Modul anhand seines Kürzels (case-insensitive) oder null.</summary>
|
|
||||||
public IModule? Find(string key)
|
|
||||||
=> _modules.FirstOrDefault(m => string.Equals(m.Key, key, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace IBKRTrader.UI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Basisklasse für eigenständige Modul-Fenster.
|
|
||||||
/// Stellt gemeinsame Grundeinstellungen (Größe, Startposition) bereit und
|
|
||||||
/// hält den Modul-Key, damit der Launcher/WindowManager das Fenster zuordnen kann.
|
|
||||||
/// </summary>
|
|
||||||
public class ModuleFormBase : Form
|
|
||||||
{
|
|
||||||
/// <summary>Kürzel des zugehörigen Moduls (z. B. "CT").</summary>
|
|
||||||
public string ModuleKey { get; }
|
|
||||||
|
|
||||||
// Parameterloser Ctor nur für den WinForms-Designer.
|
|
||||||
protected ModuleFormBase() : this("") { }
|
|
||||||
|
|
||||||
protected ModuleFormBase(string moduleKey)
|
|
||||||
{
|
|
||||||
ModuleKey = moduleKey;
|
|
||||||
Width = 900;
|
|
||||||
Height = 600;
|
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
|
||||||
ShowInTaskbar = true;
|
|
||||||
MinimumSize = new Size(600, 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
namespace IBKRTrader.UI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verwaltet die eigenständigen Modul-Fenster des Launchers.
|
|
||||||
/// Pro Modul-Key wird höchstens ein Fenster gehalten: erneutes Öffnen
|
|
||||||
/// fokussiert das bestehende Fenster statt ein zweites zu erzeugen.
|
|
||||||
///
|
|
||||||
/// Die tatsächlichen UI-Aktionen (Show/Focus) sind in überschreibbare
|
|
||||||
/// Methoden gekapselt, damit die Tracking-Logik ohne Message-Loop testbar ist.
|
|
||||||
/// </summary>
|
|
||||||
public class WindowManager
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, Form> _open = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
/// <summary>Öffnet das Fenster zum Key oder fokussiert ein bereits offenes.</summary>
|
|
||||||
public Form OpenOrFocus(string key, Func<Form> factory)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
|
||||||
ArgumentNullException.ThrowIfNull(factory);
|
|
||||||
|
|
||||||
if (_open.TryGetValue(key, out var existing))
|
|
||||||
{
|
|
||||||
if (!existing.IsDisposed)
|
|
||||||
{
|
|
||||||
Focus(existing);
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
_open.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
var form = factory();
|
|
||||||
_open[key] = form;
|
|
||||||
form.FormClosed += OnFormClosed;
|
|
||||||
Present(form);
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>True, wenn zum Key aktuell ein nicht-disposetes Fenster offen ist.</summary>
|
|
||||||
public bool IsOpen(string key)
|
|
||||||
=> _open.TryGetValue(key, out var f) && !f.IsDisposed;
|
|
||||||
|
|
||||||
/// <summary>Anzahl aktuell offener Fenster.</summary>
|
|
||||||
public int OpenCount => _open.Values.Count(f => !f.IsDisposed);
|
|
||||||
|
|
||||||
/// <summary>Schließt alle offenen Fenster (z. B. beim Beenden des Launchers).</summary>
|
|
||||||
public void CloseAll()
|
|
||||||
{
|
|
||||||
foreach (var f in _open.Values.ToList())
|
|
||||||
{
|
|
||||||
if (!f.IsDisposed)
|
|
||||||
{
|
|
||||||
f.FormClosed -= OnFormClosed;
|
|
||||||
f.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_open.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Überschreibbare UI-Aktionen (für Tests) ──────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>Zeigt ein neu erzeugtes Fenster an.</summary>
|
|
||||||
protected virtual void Present(Form form) => form.Show();
|
|
||||||
|
|
||||||
/// <summary>Bringt ein bestehendes Fenster in den Vordergrund.</summary>
|
|
||||||
protected virtual void Focus(Form form)
|
|
||||||
{
|
|
||||||
if (form.WindowState == FormWindowState.Minimized)
|
|
||||||
form.WindowState = FormWindowState.Normal;
|
|
||||||
form.Activate();
|
|
||||||
form.BringToFront();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFormClosed(object? sender, FormClosedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Form f) return;
|
|
||||||
f.FormClosed -= OnFormClosed;
|
|
||||||
|
|
||||||
var entry = _open.FirstOrDefault(p => ReferenceEquals(p.Value, f));
|
|
||||||
if (entry.Key is not null)
|
|
||||||
_open.Remove(entry.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +1,68 @@
|
|||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Modules;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
using IBKRTrader.Modules.CongressTrading.UI;
|
using IBKRTrader.Modules.CongressTrading.UI;
|
||||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.CongressTrading;
|
namespace IBKRTrader.Modules.CongressTrading;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CongressTrading-Modul: kopiert Aktien-Trades von US-Kongressmitgliedern
|
/// CongressTrading-Modul: kopiert Aktien-Trades von US-Kongressmitgliedern (capitoltrades.com).
|
||||||
/// (Quelle: capitoltrades.com).
|
/// Hängt sich ausschließlich über den <see cref="IModule"/>-Vertrag ein (Abhängigkeit nur Modul → Core).
|
||||||
///
|
|
||||||
/// Architektur-Regel: Dieses Modul hängt sich ausschließlich über den
|
|
||||||
/// <see cref="IModule"/>-Vertrag ein. Der Core kennt dieses Modul NICHT –
|
|
||||||
/// die Abhängigkeit geht nur von Modul → Core.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CongressTradingModule : IModule
|
public sealed class CongressTradingModule : IModule
|
||||||
{
|
{
|
||||||
public const string ModuleKey = "CT";
|
/// <summary>Kürzel für Logging/DB-Präfix.</summary>
|
||||||
|
public const string LogTag = "CT";
|
||||||
|
|
||||||
public string Key => ModuleKey;
|
public string Name => "CongressTrading";
|
||||||
public string DisplayName => "Congress Trading";
|
public string DbPrefix => "ct_";
|
||||||
public string Description => "Kopiert Aktien-Trades von US-Kongressmitgliedern (capitoltrades.com).";
|
|
||||||
public string Version => "1.0.0";
|
|
||||||
|
|
||||||
/// <summary>Registriert alle Modul-Services im DI-Container.</summary>
|
// In RegisterUi gesetzt (hat den Provider); für Modul-Migrationen in StartAsync genutzt.
|
||||||
public void RegisterServices(IServiceCollection services)
|
private IServiceProvider? _services;
|
||||||
|
|
||||||
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// Datenbank
|
|
||||||
services.AddSingleton<CongressMigrations>();
|
services.AddSingleton<CongressMigrations>();
|
||||||
services.AddSingleton<CongressRepository>();
|
services.AddSingleton<CongressRepository>();
|
||||||
|
|
||||||
// Scraper
|
|
||||||
services.AddSingleton<CapitolTradesScraper>();
|
services.AddSingleton<CapitolTradesScraper>();
|
||||||
|
|
||||||
// Worker
|
|
||||||
services.AddSingleton<CongressHistoryImportWorker>();
|
services.AddSingleton<CongressHistoryImportWorker>();
|
||||||
services.AddSingleton<CongressScrapeWorker>();
|
services.AddSingleton<CongressScrapeWorker>();
|
||||||
|
|
||||||
|
// Worker als IWorker registrieren → die WorkerEngine erhält sie über IEnumerable<IWorker>.
|
||||||
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<CongressHistoryImportWorker>());
|
||||||
|
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<CongressScrapeWorker>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Führt die Modul-Migrationen aus.</summary>
|
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
||||||
public async Task InitializeAsync(IServiceProvider provider)
|
|
||||||
{
|
{
|
||||||
var migrations = provider.GetRequiredService<CongressMigrations>();
|
_services = services;
|
||||||
|
|
||||||
|
host.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "congresstrading.main",
|
||||||
|
Title = "Congress Trading",
|
||||||
|
Group = Name,
|
||||||
|
Order = 100,
|
||||||
|
CreateForm = () => new CongressTradingForm(
|
||||||
|
services.GetRequiredService<CongressRepository>(),
|
||||||
|
services.GetRequiredService<WorkerEngine>(),
|
||||||
|
services.GetRequiredService<LoggingService>())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Modul-Migrationen laufen nach der Core-Initialisierung.
|
||||||
|
if (_services is null) return;
|
||||||
|
var migrations = _services.GetRequiredService<CongressMigrations>();
|
||||||
await migrations.RunAsync();
|
await migrations.RunAsync();
|
||||||
|
|
||||||
// Einmaliger Reset (deaktiviert): löscht alten Fortschritt für sauberen Neustart.
|
|
||||||
// var repo = provider.GetRequiredService<CongressRepository>();
|
|
||||||
// await repo.ResetHistoryImportAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Gibt die Modul-Worker für die WorkerEngine zurück.</summary>
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
public IEnumerable<IWorker> GetWorkers(IServiceProvider provider) =>
|
|
||||||
[
|
|
||||||
provider.GetRequiredService<CongressHistoryImportWorker>(),
|
|
||||||
provider.GetRequiredService<CongressScrapeWorker>()
|
|
||||||
];
|
|
||||||
|
|
||||||
/// <summary>Erzeugt das eigenständige Modul-Fenster.</summary>
|
|
||||||
public Form CreateWindow(IServiceProvider provider)
|
|
||||||
=> new CongressTradingForm(
|
|
||||||
provider.GetRequiredService<CongressRepository>(),
|
|
||||||
provider.GetRequiredService<WorkerEngine>(),
|
|
||||||
provider.GetRequiredService<LoggingService>());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.UI;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.CongressTrading.UI;
|
namespace IBKRTrader.Modules.CongressTrading.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Eigenständiges Fenster des CongressTrading-Moduls.
|
/// Eigenständiges Fenster des CongressTrading-Moduls.
|
||||||
/// Phase 2: Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger.
|
/// Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger.
|
||||||
/// Die vollständige Trade-/Positions-Ansicht folgt in Phase 4.
|
/// Die vollständige Trade-/Positions-Ansicht folgt in einer späteren Phase.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CongressTradingForm : ModuleFormBase
|
public sealed class CongressTradingForm : Form
|
||||||
{
|
{
|
||||||
private const string ScrapeWorkerName = "CT-ScrapeWorker";
|
private const string ScrapeWorkerName = "CT-ScrapeWorker";
|
||||||
|
|
||||||
@@ -25,13 +24,16 @@ public sealed class CongressTradingForm : ModuleFormBase
|
|||||||
private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText };
|
private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText };
|
||||||
|
|
||||||
public CongressTradingForm(CongressRepository repo, WorkerEngine engine, LoggingService logger)
|
public CongressTradingForm(CongressRepository repo, WorkerEngine engine, LoggingService logger)
|
||||||
: base(CongressTradingModule.ModuleKey)
|
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_engine = engine;
|
_engine = engine;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
Text = "Congress Trading [CT]";
|
Text = "Congress Trading [CT]";
|
||||||
|
Width = 900;
|
||||||
|
Height = 600;
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
MinimumSize = new Size(600, 400);
|
||||||
BuildLayout();
|
BuildLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ public sealed class CongressTradingForm : ModuleFormBase
|
|||||||
_lblTrades.Text = "Trades in DB: n/v";
|
_lblTrades.Text = "Trades in DB: n/v";
|
||||||
_lblMembers.Text = "Mitglieder in DB: n/v";
|
_lblMembers.Text = "Mitglieder in DB: n/v";
|
||||||
_lblStatus.Text = $"DB nicht erreichbar: {ex.Message}";
|
_lblStatus.Text = $"DB nicht erreichbar: {ex.Message}";
|
||||||
_logger.Warn(CongressTradingModule.ModuleKey, $"Kennzahlen konnten nicht geladen werden: {ex.Message}");
|
_logger.Warn(CongressTradingModule.LogTag, $"Kennzahlen konnten nicht geladen werden: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +98,12 @@ public sealed class CongressTradingForm : ModuleFormBase
|
|||||||
{
|
{
|
||||||
_lblStatus.Text = "Scrape angestoßen...";
|
_lblStatus.Text = "Scrape angestoßen...";
|
||||||
await _engine.TriggerWorkerAsync(ScrapeWorkerName);
|
await _engine.TriggerWorkerAsync(ScrapeWorkerName);
|
||||||
_logger.Info(CongressTradingModule.ModuleKey, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster).");
|
_logger.Info(CongressTradingModule.LogTag, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster).");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}";
|
_lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}";
|
||||||
_logger.Error(CongressTradingModule.ModuleKey, "Manueller Scrape-Trigger fehlgeschlagen.", ex);
|
_logger.Error(CongressTradingModule.LogTag, "Manueller Scrape-Trigger fehlgeschlagen.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,49 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using IBKRTrader.Core.Database;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Core.Logging;
|
|
||||||
using IBKRTrader.Core.Settings;
|
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading;
|
using IBKRTrader.Modules.CongressTrading;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
using IBKRTrader.UI;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
namespace IBKRTrader.Tests.Modules;
|
namespace IBKRTrader.Tests.Modules;
|
||||||
|
|
||||||
[Trait("cat", "unit")]
|
[Trait("cat", "unit")]
|
||||||
public class CongressTradingModuleTests
|
public class CongressTradingModuleTests
|
||||||
{
|
{
|
||||||
|
private sealed class CapturingUiHost : IModuleUiHost
|
||||||
|
{
|
||||||
|
private readonly List<ModuleView> _views = new();
|
||||||
|
public IReadOnlyList<ModuleView> Views => _views;
|
||||||
|
public void RegisterView(ModuleView view) => _views.Add(view);
|
||||||
|
public bool IsOpen(string viewId) => false;
|
||||||
|
public void OpenView(string viewId) { }
|
||||||
|
public void ActivateMain() { }
|
||||||
|
public void RequestShutdown() { }
|
||||||
|
public event Action? OpenStateChanged { add { } remove { } }
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Metadata_IsAsExpected()
|
public void Metadata_IsAsExpected()
|
||||||
{
|
{
|
||||||
var module = new CongressTradingModule();
|
var module = new CongressTradingModule();
|
||||||
|
|
||||||
module.Key.Should().Be("CT");
|
module.Name.Should().Be("CongressTrading");
|
||||||
module.DisplayName.Should().NotBeNullOrWhiteSpace();
|
module.DbPrefix.Should().Be("ct_");
|
||||||
module.Version.Should().Be("1.0.0");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RegisterServices_RegistersAllModuleServices()
|
public void RegisterServices_RegistersServicesAndWorkers()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
new CongressTradingModule().RegisterServices(services);
|
new CongressTradingModule().RegisterServices(services, Substitute.For<IConfiguration>());
|
||||||
|
|
||||||
var registered = services.Select(d => d.ServiceType).ToList();
|
var types = services.Select(d => d.ServiceType).ToList();
|
||||||
registered.Should().Contain(new[]
|
types.Should().Contain(new[]
|
||||||
{
|
{
|
||||||
typeof(CongressMigrations),
|
typeof(CongressMigrations),
|
||||||
typeof(CongressRepository),
|
typeof(CongressRepository),
|
||||||
@@ -41,25 +51,29 @@ public class CongressTradingModuleTests
|
|||||||
typeof(CongressHistoryImportWorker),
|
typeof(CongressHistoryImportWorker),
|
||||||
typeof(CongressScrapeWorker),
|
typeof(CongressScrapeWorker),
|
||||||
});
|
});
|
||||||
|
// Worker werden zusätzlich als IWorker registriert (für die WorkerEngine).
|
||||||
|
services.Count(d => d.ServiceType == typeof(IWorker)).Should().Be(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateWindow_ReturnsModuleForm()
|
public void RegisterUi_RegistersMainView()
|
||||||
{
|
{
|
||||||
// Minimaler DI-Container mit allen von CreateWindow benötigten Abhängigkeiten.
|
var host = new CapturingUiHost();
|
||||||
var services = new ServiceCollection();
|
// CreateForm wird hier NICHT aufgerufen – daher genügt ein leerer Provider.
|
||||||
services.AddSingleton<SettingsService>();
|
var provider = new ServiceCollection().BuildServiceProvider();
|
||||||
services.AddSingleton<LoggingService>();
|
|
||||||
services.AddSingleton<DatabaseService>();
|
|
||||||
services.AddSingleton<IEnumerable<IWorker>>(_ => Array.Empty<IWorker>());
|
|
||||||
services.AddSingleton<WorkerEngine>();
|
|
||||||
var module = new CongressTradingModule();
|
|
||||||
module.RegisterServices(services);
|
|
||||||
|
|
||||||
using var provider = services.BuildServiceProvider();
|
new CongressTradingModule().RegisterUi(host, provider);
|
||||||
using var form = module.CreateWindow(provider);
|
|
||||||
|
|
||||||
form.Should().BeAssignableTo<ModuleFormBase>();
|
host.Views.Should().ContainSingle();
|
||||||
((ModuleFormBase)form).ModuleKey.Should().Be("CT");
|
host.Views[0].Id.Should().Be("congresstrading.main");
|
||||||
|
host.Views[0].Title.Should().Be("Congress Trading");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetActivationBlocker_DefaultsToNull()
|
||||||
|
{
|
||||||
|
// Default-Interface-Methode → über die Interface-Referenz aufrufen.
|
||||||
|
IModule module = new CongressTradingModule();
|
||||||
|
module.GetActivationBlocker(Substitute.For<IConfiguration>()).Should().BeNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using IBKRTrader.Core.Modules;
|
|
||||||
using IBKRTrader.Core.Workers;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Tests.Modules;
|
|
||||||
|
|
||||||
[Trait("cat", "unit")]
|
|
||||||
public class ModuleRegistryTests
|
|
||||||
{
|
|
||||||
private sealed class FakeModule(string key) : IModule
|
|
||||||
{
|
|
||||||
public string Key => key;
|
|
||||||
public string DisplayName => $"Fake {key}";
|
|
||||||
public string Description => "test";
|
|
||||||
public string Version => "0.0.1";
|
|
||||||
|
|
||||||
public void RegisterServices(IServiceCollection services) { }
|
|
||||||
public Task InitializeAsync(IServiceProvider provider) => Task.CompletedTask;
|
|
||||||
public IEnumerable<IWorker> GetWorkers(IServiceProvider provider) => [];
|
|
||||||
public Form CreateWindow(IServiceProvider provider) => new();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Modules_PreservesRegistrationOrder()
|
|
||||||
{
|
|
||||||
var registry = new ModuleRegistry([new FakeModule("A"), new FakeModule("B")]);
|
|
||||||
|
|
||||||
registry.Modules.Select(m => m.Key).Should().ContainInOrder("A", "B");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Find_IsCaseInsensitive()
|
|
||||||
{
|
|
||||||
var registry = new ModuleRegistry([new FakeModule("CT")]);
|
|
||||||
|
|
||||||
registry.Find("ct").Should().NotBeNull();
|
|
||||||
registry.Find("CT").Should().NotBeNull();
|
|
||||||
registry.Find("XX").Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Ctor_NullModules_Throws()
|
|
||||||
{
|
|
||||||
var act = () => new ModuleRegistry(null!);
|
|
||||||
|
|
||||||
act.Should().Throw<ArgumentNullException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using IBKRTrader.UI;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Tests.UI;
|
|
||||||
|
|
||||||
[Trait("cat", "unit")]
|
|
||||||
public class WindowManagerTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Testbare Variante: unterdrückt echte Show-/Focus-Aufrufe (kein Message-Loop),
|
|
||||||
/// zählt aber die Focus-Aufrufe.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class TestWindowManager : WindowManager
|
|
||||||
{
|
|
||||||
public int FocusCount;
|
|
||||||
protected override void Present(Form form) { /* kein Show im Test */ }
|
|
||||||
protected override void Focus(Form form) => FocusCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenOrFocus_NewKey_InvokesFactory_AndTracks()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
var created = 0;
|
|
||||||
|
|
||||||
using var form = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
|
||||||
|
|
||||||
created.Should().Be(1);
|
|
||||||
wm.IsOpen("CT").Should().BeTrue();
|
|
||||||
wm.OpenCount.Should().Be(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenOrFocus_SameKey_FocusesExisting_DoesNotRecreate()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
var created = 0;
|
|
||||||
|
|
||||||
using var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
|
||||||
var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
|
||||||
|
|
||||||
created.Should().Be(1);
|
|
||||||
wm.FocusCount.Should().Be(1);
|
|
||||||
second.Should().BeSameAs(first);
|
|
||||||
wm.OpenCount.Should().Be(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenOrFocus_DifferentKeys_TracksSeparately()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
|
|
||||||
using var a = wm.OpenOrFocus("A", () => new Form());
|
|
||||||
using var b = wm.OpenOrFocus("B", () => new Form());
|
|
||||||
|
|
||||||
wm.OpenCount.Should().Be(2);
|
|
||||||
wm.IsOpen("A").Should().BeTrue();
|
|
||||||
wm.IsOpen("B").Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenOrFocus_AfterDisposed_RecreatesWindow()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
var created = 0;
|
|
||||||
|
|
||||||
var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
|
||||||
first.Dispose();
|
|
||||||
|
|
||||||
wm.IsOpen("CT").Should().BeFalse();
|
|
||||||
|
|
||||||
using var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
|
||||||
|
|
||||||
created.Should().Be(2);
|
|
||||||
second.Should().NotBeSameAs(first);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CloseAll_RemovesAllWindows()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
wm.OpenOrFocus("A", () => new Form());
|
|
||||||
wm.OpenOrFocus("B", () => new Form());
|
|
||||||
|
|
||||||
wm.CloseAll();
|
|
||||||
|
|
||||||
wm.OpenCount.Should().Be(0);
|
|
||||||
wm.IsOpen("A").Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void OpenOrFocus_BlankKey_Throws()
|
|
||||||
{
|
|
||||||
var wm = new TestWindowManager();
|
|
||||||
|
|
||||||
var act = () => wm.OpenOrFocus(" ", () => new Form());
|
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user