Files
IBKRTrader/Program.cs
T
Richard 261032f9f9 @
R3 Slice 1: EF-Core-Infrastruktur im Core + InitialCore-Migration

- Pomelo.EntityFrameworkCore.MySql 8.0.3 + EF Core Design 8.0.11 (Core), NuGet-Allowlist erweitert
- Configuration/DatabaseOptions + DatabaseServerVersion (MariaDB 11.8.6 gepinnt)
- Persistence/Entities: CorePosition, CoreTrade, CoreBudget, CoreWorkerLog, CoreSetting
- Persistence/Ef/CoreDbContext (core_-Tabellen) + Design-Time-Factory (env IBKRTRADER_MYSQL)
- DependencyInjection/AddCorePersistence (AddDbContextFactory), in Program verdrahtet
  (Connection aus appsettings.Local.json)
- EF-Migration InitialCore erzeugt (5 core_-Tabellen)
- Consumer noch auf Dapper (folgt in Slice 2); Build + 30/30 Tests + smoke-ui gruen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-28 10:40:09 +02:00

240 lines
10 KiB
C#
Raw 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.Database;
using IBKRTrader.Core.Database.Migrations;
using IBKRTrader.Core.DependencyInjection;
using IBKRTrader.Core.IBKR;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Trading;
using IBKRTrader.Core.Workers;
using IBKRTrader.Core.Workers.BuiltIn;
using IBKRTrader.Modules.CongressTrading;
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() };
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();
// Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz.
AppHost.Services.GetRequiredService<WebApiService>()
.SetEngine(AppHost.Services.GetRequiredService<WorkerEngine>());
// 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<DatabaseService>();
services.AddSingleton<CoreMigrations>();
services.AddSingleton<IBKRMigrations>();
services.AddSingleton<IBKRGatewayService>();
services.AddSingleton<IBKRMarketDataRepository>();
services.AddSingleton<BudgetService>();
services.AddSingleton<TradeHistoryService>();
services.AddSingleton<AIModelService>();
// Trading-Kern
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>.
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>());
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.workers"] = Properties.Resources.system_time,
["core.logs"] = Properties.Resources.error_log,
["core.settings"] = Properties.Resources.setting_tools,
["congresstrading.main"] = Properties.Resources.cross_reference,
};
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.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>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() };
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;
}
}