Files
IBKRTrader/Program.cs
T
RichardandClaude Opus 5 b96a20788c R10: Lesender Bestandsabgleich (IBrokerPortfolioReader) + Datenlage-Konzepte
Eigener Seam neben IBrokerClient: Wer handelt, braucht ihn nicht; wer die
eigene Buchfuehrung gegen den Broker abstimmt, braucht nur ihn. Zuteilung
und Verfall aendern Positionen ohne Order von uns - ohne Abgleich laeuft
das Managementbuch zwangslaeufig auseinander.

- IBrokerPortfolioReader mit GetPositionsAsync/GetExecutionsAsync;
  implementiert von IbkrBrokerClient und NullBrokerClient (DI registriert
  beide Rollen auf derselben Instanz).
- IbkrConnection: reqAccountUpdates statt reqPositions (nur dieser Weg
  liefert Marktwert und unrealisierten G/V), reqExecutions inkl. Zuordnung
  der verspaetet eintreffenden commissionReport-Callbacks ueber die ExecId.
- BrokerPosition/BrokerExecution als Broker-Wahrheit neben Position;
  IbkrMapping: ParseSide, ParseExecutionTime, FormatExecutionFilterTime
  (UTC wegen TWS-Warnung 2174) - mit Unit-Tests.
- Verifiziert gegen Paper-Konto DUR371528: 2 Positionen, 2 Ausfuehrungen
  inkl. Kommissionen.

Doku: Kapital- und Buchmodell (drei Wahrheiten, Kapitalzuteilung),
KONZEPT-Datenlage-und-Strategien (gemessen, was die API auf diesem Konto
liefert). Options-Wheel: Greeks bei verzoegerten Daten funktionieren
(Feld 83); Earnings-Termine sind ueber die TWS API nicht erreichbar
(Fehler 10358) - Behelf ueber IV-Filter statt Fremddatenquelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:51:51 +02:00

322 lines
15 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
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>();
// 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>
/// 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;
}
}