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.Time; 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 { 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(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); }) .Build(); // Betriebszeitzone VOR dem ersten Logeintrag setzen – sie bestimmt die Tagesgrenzen der // Logdateien. Wird sie später gesetzt, landen die ersten Zeilen in der falschen Datei. var startupLog = AppHost.Services.GetRequiredService(); ConfigureAppTimeZone(AppHost.Services, startupLog); // Sicherheit: Master-Key laden (VOR jeder Entschlüsselung) und DB-TLS prüfen. ConfigureSecretProtection(startupLog); WarnIfDbTlsNotEnforced(AppHost.Services, startupLog); // Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz (vor dem Start). AppHost.Services.GetRequiredService() .SetEngine(AppHost.Services.GetRequiredService()); // Host starten → alle Worker/Services (IHostedService) laufen an. AppHost.Start(); // Views registrieren (Core + Module), dann Launcher starten. var uiHost = AppHost.Services.GetRequiredService(); RegisterCoreViews(uiHost, AppHost.Services); foreach (var module in modules) module.RegisterUi(uiHost, AppHost.Services); ModuleViews.Register(uiHost, AppHost.Services); ViewIcons.AssignDefaults(uiHost); Application.Run(AppHost.Services.GetRequiredService()); AppHost.StopAsync().GetAwaiter().GetResult(); } /// Registriert alle Core-Services im DI-Container. 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(); services.AddSingleton(); // core_settings via EF services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Datenfundament für Analyse/Forensik (Supervisor): Entscheidungsjournal + Order-Events. services.AddSingleton(); services.AddSingleton(); // Trading-Kern services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // 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(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); } else { services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); } // Core-Worker/Services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Als IWorker registrieren → die WorkerEngine erhält alle über IEnumerable (nur UI/Registry). services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); // Lebenszyklus über den Generic Host (jeder Worker ist ein IHostedService). services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddSingleton(); } /// Registriert die Core-Views (Logs, Settings, Workers) bei der Shell. 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(), sp.GetRequiredService(), sp.GetServices(), sp.GetRequiredService(), sp.GetServices()) }); uiHost.RegisterView(new ModuleView { Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10, CreateView = () => new WorkersView(sp.GetRequiredService()) }); uiHost.RegisterView(new ModuleView { Id = "core.logs", Title = "Logs", Group = "Core", Order = 20, CreateView = () => new LogsView(sp.GetRequiredService()) }); uiHost.RegisterView(new ModuleView { Id = "core.settings", Title = "Settings", Group = "Core", Order = 30, CreateView = () => new SettingsView(sp.GetRequiredService()) }); } /// /// Setzt die Betriebszeitzone dieser Instanz aus den Einstellungen. Sie gilt für Anzeige, /// Logdatei-Tagesgrenzen, Berichtszeiten und Buchungsperioden; gespeichert wird weiterhin UTC. /// /// Wir betreiben Instanzen in EU und US. Ohne diese Festlegung hinge die Ortszeit an der /// Zeitzone des Rechners – auf einem UTC-Container also woanders als auf dem Windows-Desktop, /// lautlos und ohne Fehlermeldung. Die Zeitzone wird vor den ersten Trades gesetzt und danach /// nicht mehr gewechselt. /// private static void ConfigureAppTimeZone(IServiceProvider services, LoggingService logger) { var configured = services.GetRequiredService().Settings.Trading.ApplicationTimeZoneId; AppTimeZone.Configure(configured, msg => logger.Warn("Core", msg)); logger.Info("Core", $"Betriebszeitzone: {AppTimeZone.CurrentId} (Persistenz bleibt UTC)."); } /// /// 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. /// 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)."); } /// Warnt, wenn der DB-Connection-String keine TLS-Option (SslMode) enthält. Der String wird NICHT geloggt. private static void WarnIfDbTlsNotEnforced(IServiceProvider services, LoggingService logger) { try { var conn = services.GetService()?["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 */ } } /// Diagnose: öffnet die DB (aus settings.json) und gibt die Serverversion aus. Kein UI. 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}"); } } /// /// Headless-Smoke-Test: baut den Host, registriert Views + Modul-UI und konstruiert jede /// registrierte View sowie den Launcher – ohne Message-Loop. Gibt die Fehleranzahl zurück. /// private static int RunSmokeUi() { ApplicationConfiguration.Initialize(); var modules = new List { new CongressTradingModule(), 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(); services.AddSingleton(s => s.GetRequiredService()); services.AddSingleton(); }) .Build(); ConfigureAppTimeZone(host.Services, host.Services.GetRequiredService()); host.Services.GetRequiredService() .SetEngine(host.Services.GetRequiredService()); var uiHost = host.Services.GetRequiredService(); RegisterCoreViews(uiHost, host.Services); foreach (var module in modules) module.RegisterUi(uiHost, host.Services); 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(); 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; } }