Core, alle drei Module und das Testprojekt tragen keinen UI-Code mehr und bauen fuer linux-x64. Nur noch IBKRTrader.App ist Windows-gebunden. UI-Contract toolkit-neutral (Vorbild: PolytraderSharp): - ModuleView.CreateForm (Func<Form>) -> CreateView (Func<object>) - ModuleView.Icon (System.Drawing.Image) -> IconKey (string). System.Drawing.Common ist seit .NET 7 Windows-only und wirft auf Linux. - WindowMenu.cs war reine WinForms-Umsetzung -> in die Shell verschoben. LoggingService haelt keine RichTextBox mehr, sondern meldet Eintraege ueber event EntryWritten. Einfaerbung und UI-Thread-Wechsel liegen jetzt im LogPanelController der Shell. Nebenbei: ToUpper() -> ToUpperInvariant() (tr-TR haette aus "info" ein "İNFO" gemacht) und \r\n -> Environment.NewLine. Die drei Modul-Fenster liegen jetzt unter UI/Views/Modules/; RegisterUi der Module ist bewusst leer, die Shell registriert sie zentral ueber UI/ModuleViews.cs (nur fuer tatsaechlich geladene Module). ViewIcons loest IconKey gegen die PNG-Ressourcen auf - dieselben Schluessel bekommt spaeter die Avalonia-Shell. UiConstructionTests entfernt: die Konstruktionspruefung deckt --smoke-ui ab, das Testprojekt braucht dafuer keine UI-Referenz mehr. Der Test RegisterUi_RegistersMainView haelt jetzt das Gegenteil fest - das Modul darf nichts registrieren, sonst waere es wieder toolkit-gebunden. Verifiziert: Build 0 Fehler/0 Warnungen, 163 Tests gruen, --smoke-ui konstruiert alle 7 Fenster, und Core + 3 Module + Tests bauen fuer linux-x64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
303 lines
14 KiB
C#
303 lines
14 KiB
C#
using IBKRTrader.Core.AI;
|
||
using IBKRTrader.Core.Budget;
|
||
using IBKRTrader.Core.Configuration;
|
||
using IBKRTrader.Core.DependencyInjection;
|
||
using IBKRTrader.Core.IBKR;
|
||
using IBKRTrader.Core.Logging;
|
||
using IBKRTrader.Core.Modularity;
|
||
using IBKRTrader.Core.Persistence;
|
||
using IBKRTrader.Core.Persistence.Ef;
|
||
using IBKRTrader.Core.Security;
|
||
using IBKRTrader.Core.Settings;
|
||
using IBKRTrader.Core.Trading;
|
||
using IBKRTrader.Core.Trading.Ibkr;
|
||
using IBKRTrader.Core.Workers;
|
||
using IBKRTrader.Core.Workers.BuiltIn;
|
||
using IBKRTrader.Modules.Accounting;
|
||
using IBKRTrader.Modules.CongressTrading;
|
||
using IBKRTrader.Modules.Supervisor;
|
||
using IBKRTrader.UI;
|
||
using IBKRTrader.UI.Views;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Hosting;
|
||
|
||
namespace IBKRTrader;
|
||
|
||
internal static class Program
|
||
{
|
||
public static IHost? AppHost { get; private set; }
|
||
|
||
[STAThread]
|
||
static void Main(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;
|
||
}
|
||
|
||
// Diagnose: gibt die MySQL/MariaDB-Serverversion aus (für das EF-ServerVersion-Pinning).
|
||
if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
RunDbVersion();
|
||
return;
|
||
}
|
||
|
||
ApplicationConfiguration.Initialize();
|
||
|
||
var modules = new List<IModule> { new CongressTradingModule(), new AccountingModule(), new SupervisorModule() };
|
||
|
||
AppHost = Host.CreateDefaultBuilder()
|
||
.UseContentRoot(AppContext.BaseDirectory)
|
||
.ConfigureAppConfiguration((_, config) =>
|
||
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
|
||
.ConfigureServices((context, services) =>
|
||
{
|
||
RegisterCoreServices(services, context.Configuration);
|
||
foreach (var module in modules)
|
||
{
|
||
services.AddSingleton(module);
|
||
module.RegisterServices(services, context.Configuration);
|
||
}
|
||
services.AddSingleton<ShellUiHost>();
|
||
services.AddSingleton<IModuleUiHost>(sp => sp.GetRequiredService<ShellUiHost>());
|
||
services.AddSingleton<LauncherForm>();
|
||
})
|
||
.Build();
|
||
|
||
// Sicherheit: Master-Key laden (VOR jeder Entschlüsselung) und DB-TLS prüfen.
|
||
var startupLog = AppHost.Services.GetRequiredService<LoggingService>();
|
||
ConfigureSecretProtection(startupLog);
|
||
WarnIfDbTlsNotEnforced(AppHost.Services, startupLog);
|
||
|
||
// Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz (vor dem Start).
|
||
AppHost.Services.GetRequiredService<WebApiService>()
|
||
.SetEngine(AppHost.Services.GetRequiredService<WorkerEngine>());
|
||
|
||
// 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>Registriert alle Core-Services im DI-Container.</summary>
|
||
private static void RegisterCoreServices(IServiceCollection services, IConfiguration configuration)
|
||
{
|
||
// EF-Core-Persistenz (Connection aus appsettings.Local.json).
|
||
services.AddCorePersistence(new DatabaseOptions
|
||
{
|
||
MySqlConnectionString = configuration["Database:MySqlConnectionString"] ?? string.Empty
|
||
});
|
||
|
||
// Settings zuerst laden (eine Quelle, als Singleton weitergereicht).
|
||
var settingsService = new SettingsService();
|
||
settingsService.Load();
|
||
services.AddSingleton(settingsService);
|
||
|
||
services.AddSingleton<LoggingService>();
|
||
|
||
services.AddSingleton<CoreSettingsService>(); // core_settings via EF
|
||
|
||
services.AddSingleton<IBKRGatewayService>();
|
||
services.AddSingleton<IBKRMarketDataRepository>();
|
||
|
||
services.AddSingleton<BudgetService>();
|
||
services.AddSingleton<TradeHistoryService>();
|
||
services.AddSingleton<AIModelService>();
|
||
|
||
// Datenfundament für Analyse/Forensik (Supervisor): Entscheidungsjournal + Order-Events.
|
||
services.AddSingleton<IDecisionJournal, EfDecisionJournal>();
|
||
services.AddSingleton<IOrderEventLog, EfOrderEventLog>();
|
||
|
||
// Trading-Kern
|
||
services.AddSingleton<DashboardService>();
|
||
services.AddSingleton<IRiskService, RiskService>();
|
||
services.AddSingleton<IPortfolioService, PortfolioService>();
|
||
services.AddSingleton<IExecutionService, ExecutionService>();
|
||
// Echter TWS-Broker nur, wenn ausdrücklich aktiviert – sonst der NullBroker, der nie handelt.
|
||
// Beide Rollen (Handel + lesender Bestandsabgleich) bedient dieselbe Instanz.
|
||
if (settingsService.Settings.IBKR.UseTwsApi)
|
||
{
|
||
services.AddSingleton<IbkrBrokerClient>();
|
||
services.AddSingleton<IBrokerClient>(sp => sp.GetRequiredService<IbkrBrokerClient>());
|
||
services.AddSingleton<IBrokerPortfolioReader>(sp => sp.GetRequiredService<IbkrBrokerClient>());
|
||
}
|
||
else
|
||
{
|
||
services.AddSingleton<NullBrokerClient>();
|
||
services.AddSingleton<IBrokerClient>(sp => sp.GetRequiredService<NullBrokerClient>());
|
||
services.AddSingleton<IBrokerPortfolioReader>(sp => sp.GetRequiredService<NullBrokerClient>());
|
||
}
|
||
|
||
// Core-Worker/Services
|
||
services.AddSingleton<BackupWorker>();
|
||
services.AddSingleton<WebserverService>();
|
||
services.AddSingleton<WebApiService>();
|
||
services.AddSingleton<IBKRInstrumentSyncWorker>();
|
||
services.AddSingleton<IBKRPriceHistoryWorker>();
|
||
|
||
// Als IWorker registrieren → die WorkerEngine erhält alle über IEnumerable<IWorker> (nur UI/Registry).
|
||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<BackupWorker>());
|
||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<WebserverService>());
|
||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<WebApiService>());
|
||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<IBKRInstrumentSyncWorker>());
|
||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<IBKRPriceHistoryWorker>());
|
||
|
||
// Lebenszyklus über den Generic Host (jeder Worker ist ein IHostedService).
|
||
services.AddHostedService(sp => sp.GetRequiredService<BackupWorker>());
|
||
services.AddHostedService(sp => sp.GetRequiredService<WebserverService>());
|
||
services.AddHostedService(sp => sp.GetRequiredService<WebApiService>());
|
||
services.AddHostedService(sp => sp.GetRequiredService<IBKRInstrumentSyncWorker>());
|
||
services.AddHostedService(sp => sp.GetRequiredService<IBKRPriceHistoryWorker>());
|
||
|
||
services.AddSingleton<WorkerEngine>();
|
||
}
|
||
|
||
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) 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>
|
||
/// Lädt den Master-Key (env IBKRTRADER_MASTER_KEY, sonst gitignorierte master.key) und aktiviert die
|
||
/// at-rest-Verschlüsselung. Ohne Key läuft die App mit Klartext – mit deutlicher Warnung.
|
||
/// </summary>
|
||
private static void ConfigureSecretProtection(LoggingService logger)
|
||
{
|
||
var masterKey = Environment.GetEnvironmentVariable("IBKRTRADER_MASTER_KEY");
|
||
if (string.IsNullOrWhiteSpace(masterKey))
|
||
{
|
||
var keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
|
||
if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim();
|
||
}
|
||
SecretProtection.Configure(masterKey);
|
||
|
||
if (SecretProtection.IsConfigured)
|
||
logger.Info("Core", "🔐 Secret-Verschlüsselung aktiv – sensible Daten werden at-rest verschlüsselt (AES-256-GCM).");
|
||
else
|
||
logger.Warn("Core", "⚠️ SICHERHEIT: Kein IBKRTRADER_MASTER_KEY gesetzt – sensible Daten würden UNVERSCHLÜSSELT gespeichert. " +
|
||
"Master-Key setzen (env IBKRTRADER_MASTER_KEY oder Datei master.key).");
|
||
}
|
||
|
||
/// <summary>Warnt, wenn der DB-Connection-String keine TLS-Option (SslMode) enthält. Der String wird NICHT geloggt.</summary>
|
||
private static void WarnIfDbTlsNotEnforced(IServiceProvider services, LoggingService logger)
|
||
{
|
||
try
|
||
{
|
||
var conn = services.GetService<IConfiguration>()?["Database:MySqlConnectionString"] ?? string.Empty;
|
||
if (string.IsNullOrEmpty(conn)) return;
|
||
if (conn.IndexOf("sslmode", StringComparison.OrdinalIgnoreCase) < 0)
|
||
logger.Warn("Core", "⚠️ SICHERHEIT: DB-Verbindung ohne SslMode – Transportverschlüsselung nicht erzwungen. " +
|
||
"Im Connection-String 'SslMode=Required' setzen.");
|
||
}
|
||
catch { /* best-effort, darf den Start nie stören */ }
|
||
}
|
||
|
||
/// <summary>Diagnose: öffnet die DB (aus settings.json) und gibt die Serverversion aus. Kein UI.</summary>
|
||
private static void RunDbVersion()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Load();
|
||
var conn = settings.Settings.Database.BuildConnectionString();
|
||
try
|
||
{
|
||
using var c = new MySqlConnector.MySqlConnection(conn);
|
||
c.Open();
|
||
Console.WriteLine($"ServerVersion: {c.ServerVersion}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Headless-Smoke-Test: baut den Host, registriert Views + Modul-UI und konstruiert jede
|
||
/// registrierte View sowie den Launcher – ohne Message-Loop. Gibt die Fehleranzahl zurück.
|
||
/// </summary>
|
||
private static int RunSmokeUi()
|
||
{
|
||
ApplicationConfiguration.Initialize();
|
||
|
||
var modules = new List<IModule> { new CongressTradingModule(), new AccountingModule(), new SupervisorModule() };
|
||
|
||
using var host = Host.CreateDefaultBuilder()
|
||
.UseContentRoot(AppContext.BaseDirectory)
|
||
.ConfigureAppConfiguration((_, config) =>
|
||
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
|
||
.ConfigureServices((context, services) =>
|
||
{
|
||
RegisterCoreServices(services, context.Configuration);
|
||
foreach (var module in modules)
|
||
{
|
||
services.AddSingleton(module);
|
||
module.RegisterServices(services, context.Configuration);
|
||
}
|
||
services.AddSingleton<ShellUiHost>();
|
||
services.AddSingleton<IModuleUiHost>(s => s.GetRequiredService<ShellUiHost>());
|
||
services.AddSingleton<LauncherForm>();
|
||
})
|
||
.Build();
|
||
|
||
host.Services.GetRequiredService<WebApiService>()
|
||
.SetEngine(host.Services.GetRequiredService<WorkerEngine>());
|
||
|
||
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;
|
||
}
|
||
}
|