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:
@@ -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.
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Generated
-303
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 0–23.\n" +
|
||||
"MCP-Light (opt-in): env IBKRTRADER_MCP_PORT = Port (bindet nur 127.0.0.1).\n\n" +
|
||||
"Sicherheit: OpenRouter ist ein bewusst freigegebener externer Datenempfänger. Es werden nur\n" +
|
||||
"Analyse-Daten der Tools gesendet, niemals Secrets. Kein Tool kann handeln oder schreiben."
|
||||
});
|
||||
|
||||
tabs.TabPages.AddRange(new[] { tabChat, tabDossier, tabReports, tabSettings });
|
||||
Controls.Add(tabs);
|
||||
}
|
||||
|
||||
// ── Analyse ──
|
||||
|
||||
private async Task AskAsync()
|
||||
{
|
||||
var question = _question.Text.Trim();
|
||||
if (string.IsNullOrEmpty(question)) return;
|
||||
|
||||
_ask.Enabled = false;
|
||||
_answer.Clear();
|
||||
var profile = SupervisorProfiles.ByName(_profile.Text);
|
||||
var progress = new Progress<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}"); }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
### L0–L5 – Linux-Portierung: Avalonia statt WinForms ✅ (2026-08-07)
|
||||
Analyse und Begründung: [konzepte/KONZEPT-Linux-Portierung.md](konzepte/KONZEPT-Linux-Portierung.md).
|
||||
Rückfallpunkt für den letzten WinForms-Stand: Tag `winforms-final`.
|
||||
|
||||
- [x] **L0** `NuGet.config` repariert – drei Pakete hatten kein `packageSourceMapping`-Muster; ein frischer Klon konnte nicht wiederherstellen (auf dem Entwicklungsrechner unsichtbar, weil gecacht)
|
||||
- [x] **L1a** Core und Module von WinForms entkoppelt: `net10.0` statt `net10.0-windows`. UI-Contract toolkit-neutral (`Func<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 (R1–R7)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user