L5: WinForms vollstaendig entfernt - das Projekt ist plattformneutral

Die Avalonia-Shell hatte in L4 Funktionsgleichheit erreicht, damit ist die
WinForms-Fassung ueberfluessig. Entfernt: IBKRTrader.App.csproj, Program.cs,
LauncherForm.cs, UI/ (14 Dateien) und Properties/Resources.* - zusammen rund
1.900 Zeilen. Rueckfallpunkt bleibt der Tag winforms-final.

Resources/ mit den 24 PNG-Symbolen bleibt: die Avalonia-Shell bindet sie
ueber AvaloniaResource ein und nutzt dieselben Symbolschluessel.

Kein Projekt traegt mehr eine Plattformbindung. Verifiziert: alle acht
Projekte bauen einzeln fuer linux-x64, und die gesamte Projektmappe baut
ohne Fehler und ohne Warnung.

Die CI kann damit vereinfacht werden: der Behelf aus L4 - unter Linux die
Projekte einzeln statt der Mappe zu bauen, weil IBKRTrader.App dort nicht
baubar war - entfaellt. Beide Plattformen bauen und testen jetzt dieselbe
Mappe.

Doku nachgezogen: README (Architektur-Uebersicht, Start- und Pruefbefehle),
ARCHITECTURE (Ziel-Architektur auf den Ist-Stand, neuer Abschnitt L0-L5 in
der Phasen-Checkliste, AppTimeZone ergaenzt). Der Hinweis "UI-Contract liegt
im Core" war nach L1a falsch - WindowMenu ist dort raus, der Contract ist
jetzt toolkit-neutral.

Verifiziert: Build 0 Fehler/0 Warnungen, 193 Tests gruen, Smoke-UI
konstruiert alle 7 Ansichten + Launcher + Dialog, Daemon-Prueflauf OK.

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