Portierung der beiden fehlenden Grundbausteine aus PolytraderSharp (voller Ausbau). Core S-0 (Datenfundament fuer Analyse/Forensik): - core_decision_journal + core_order_events (+ ReasonCode/Decision/OrderEvent-Enums), IDecisionJournal/IOrderEventLog mit fehlertoleranten EF-Impls (Handel bricht nie). - SignalId-Durchreichung TradeSignal -> ExecutionService -> core_trade_history; ExecutionService schreibt an jeder Verzweigung Journal/Order-Events. - JSONL-Log-Sink (LogJson + Dual-Sink), pure Analytik: RealizedPnlEngine (FIFO), TradeAnalytics, DossierBuilder. Migration AddAnalysisFoundation. Accounting-Modul (acc_): unabhaengiger IBKR-Kontoauszug (Activity Flex Query) hinter Interfaces mit Offline-Null-Stubs -> append-only Ledger + Periodenabrechnung/BWA + FX (USD/EUR) + CSV/PDF (PDFsharp/MigraDoc). Steuerschicht bewusst offen (Platzhalter-Tab). Kein Handel. Migration InitialAccounting. Supervisor-Modul (sup_): read-only OpenRouter-Agent (Function-Calling-Loop) + read-only Tool-Registry (8 Tools) + Profile + Dossier-Browser + Counterfactual-Job (Stub) + Tagesbericht/MCP-Light (opt-in). Migration InitialSupervisor. Verdrahtung: Program.cs (beide Module + Icons), slnx/App/Tests-Referenzen, provision-db.ps1, AppSettings-Sektionen, docs/konzepte, README. Tests: 79 -> 117 gruen (FIFO/KPIs/Dossier/JSONL, Classifier/Engine/FX/Idempotenz, OpenRouter/Registry/Agent/MCP, STA-Konstruktion beider neuen Fenster). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
309 lines
14 KiB
C#
309 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.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);
|
||
AssignViewIcons(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>();
|
||
// Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter verifiziert ist.
|
||
services.AddSingleton<IBrokerClient, 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>
|
||
/// Weist den registrierten Views ihr Button-/Menü-Icon aus den App-Ressourcen zu (über die stabile
|
||
/// View-ID). Icons stammen aus PolytraderSharp; nicht passende können später ausgetauscht werden.
|
||
/// Bereits gesetzte Icons bleiben erhalten.
|
||
/// </summary>
|
||
private static void AssignViewIcons(IModuleUiHost uiHost)
|
||
{
|
||
var map = new Dictionary<string, Image>
|
||
{
|
||
["core.dashboard"] = Properties.Resources.dashboard,
|
||
["core.workers"] = Properties.Resources.system_time,
|
||
["core.logs"] = Properties.Resources.error_log,
|
||
["core.settings"] = Properties.Resources.setting_tools,
|
||
["congresstrading.main"] = Properties.Resources.cross_reference,
|
||
["accounting.main"] = Properties.Resources.coins_in_hand,
|
||
["supervisor.main"] = Properties.Resources.token_quantifier,
|
||
};
|
||
foreach (var view in uiHost.Views)
|
||
if (view.Icon is null && map.TryGetValue(view.Id, out var img))
|
||
view.Icon = img;
|
||
}
|
||
|
||
/// <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,
|
||
CreateForm = () => 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,
|
||
CreateForm = () => new WorkersView(sp.GetRequiredService<WorkerEngine>())
|
||
});
|
||
uiHost.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
|
||
CreateForm = () => new LogsView(sp.GetRequiredService<LoggingService>())
|
||
});
|
||
uiHost.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
|
||
CreateForm = () => new SettingsView(sp.GetRequiredService<SettingsService>())
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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);
|
||
|
||
var failures = 0;
|
||
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
|
||
foreach (var view in uiHost.Views)
|
||
{
|
||
try { using var form = view.CreateForm(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); }
|
||
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); }
|
||
}
|
||
|
||
try { using var launcher = host.Services.GetRequiredService<LauncherForm>(); Console.WriteLine("[OK] LauncherForm konstruiert"); }
|
||
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}"); }
|
||
|
||
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
|
||
return failures;
|
||
}
|
||
}
|