diff --git a/NuGet.config b/NuGet.config
index adbd11f..edd55f6 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -18,6 +18,12 @@
+
+
+
+
+
+
diff --git a/Program.cs b/Program.cs
index d292cf9..6c1442a 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,7 +1,9 @@
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;
@@ -49,7 +51,7 @@ internal static class Program
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
- RegisterCoreServices(services);
+ RegisterCoreServices(services, context.Configuration);
foreach (var module in modules)
{
services.AddSingleton(module);
@@ -77,8 +79,14 @@ internal static class Program
}
/// Registriert alle Core-Services im DI-Container.
- private static void RegisterCoreServices(IServiceCollection services)
+ 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();
@@ -194,7 +202,7 @@ internal static class Program
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
- RegisterCoreServices(services);
+ RegisterCoreServices(services, context.Configuration);
foreach (var module in modules)
{
services.AddSingleton(module);
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index bf18cbb..8f75804 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -92,12 +92,11 @@ IBKRTrader.App (WinExe, Root) – Generic Host + Shell (Launcher) + C
App legt keine Tabellen zur Laufzeit an. Server: **MariaDB 11.8.6** (via `--db-version` bestätigt) →
Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettings.Local.json`.
- [x] `--db-version`-Diagnose (Serverversion für den EF-Pin)
-- [ ] `Configuration/DatabaseOptions` (aus appsettings.Local.json) + `DatabaseServerVersion`-Pin
-- [ ] `AddCorePersistence` + `CoreDbContext` + Entities + EF-Repos (core_position, core_trade_history, core_budget, core_worker_log, core_settings)
-- [ ] Consumer umstellen: `PortfolioService`, `BudgetService`, `TradeHistoryService`, `WorkerBase`-Log, IBKR
-- [ ] Modul-DbContext (ct_) im CongressTrading-Projekt + EF-Repo
-- [ ] EF-Migrationen erzeugen (`dotnet ef migrations add`); Dapper + manuelle Migrationen entfernen
-- Hinweis: nur build-verifizierbar (Unit-Tests laufen ohne DB); Schema-Anwendung erfolgt extern
+- [x] **Slice 1:** `Configuration/DatabaseOptions` + `DatabaseServerVersion`-Pin (MariaDB 11.8.6); `AddCorePersistence` (`AddDbContextFactory`) + `CoreDbContext` + Entities (core_position, core_trade_history, core_budget, core_worker_log, core_settings) + Design-Time-Factory; **EF-Migration `InitialCore` erzeugt**
+- [ ] **Slice 2:** Consumer umstellen: `PortfolioService`, `BudgetService`, `TradeHistoryService`, `WorkerBase`-Log
+- [ ] **Slice 3:** Modul-DbContext (ct_) im CongressTrading-Projekt + Umstellung
+- [ ] **Slice 4:** Dapper + `DatabaseService` + manuelle Migrationen (CoreMigrations/IBKRMigrations/CongressMigrations) entfernen; IBKR-Marktdaten auf EF
+- Hinweis: nur build-verifizierbar (Unit-Tests ohne DB); Schema-Anwendung extern via `dotnet ef database update` (env `IBKRTRADER_MYSQL`)
### R4 – Trading-Kern einфügen
- [ ] Risk/Execution/Portfolio/Broker-Seam nach Core/Trading (aus Phase 3 portiert)
diff --git a/src/IBKRTrader.Core/Configuration/DatabaseOptions.cs b/src/IBKRTrader.Core/Configuration/DatabaseOptions.cs
new file mode 100644
index 0000000..5bbbb96
--- /dev/null
+++ b/src/IBKRTrader.Core/Configuration/DatabaseOptions.cs
@@ -0,0 +1,25 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace IBKRTrader.Core.Configuration;
+
+///
+/// Datenbank-Konfiguration, gebunden an die "Database"-Sektion in appsettings(.Local).json.
+///
+public class DatabaseOptions
+{
+ public const string SectionName = "Database";
+
+ /// MySQL/MariaDB-Connection-String (aus gitignorierter appsettings.Local.json).
+ public string MySqlConnectionString { get; set; } = string.Empty;
+}
+
+///
+/// Fest gepinnte Ziel-Server-Version (MariaDB 11.8.6, via --db-version bestätigt). Bewusst
+/// gepinnt statt ServerVersion.AutoDetect: AutoDetect öffnet beim Bau der DbContext-Optionen
+/// eine blockierende DB-Verbindung – ist die DB langsam/nicht erreichbar, hängt der Start. Mit fester
+/// Version startet die App unabhängig von der DB, und Migrations-Scaffolding läuft ohne DB-Verbindung.
+///
+public static class DatabaseServerVersion
+{
+ public static ServerVersion Value => new MariaDbServerVersion(new Version(11, 8, 6));
+}
diff --git a/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..caaad6a
--- /dev/null
+++ b/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using IBKRTrader.Core.Configuration;
+using IBKRTrader.Core.Persistence.Ef;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.Core.DependencyInjection;
+
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registriert die Core-Persistenzschicht (EF Core / Pomelo / MariaDB) über einen
+ /// DbContextFactory (thread-safe, kurzlebiger Context je Operation).
+ ///
+ public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
+ {
+ services.AddDbContextFactory(o =>
+ o.UseMySql(options.MySqlConnectionString, DatabaseServerVersion.Value));
+
+ return services;
+ }
+}
diff --git a/src/IBKRTrader.Core/IBKRTrader.Core.csproj b/src/IBKRTrader.Core/IBKRTrader.Core.csproj
index 80374ad..70152d8 100644
--- a/src/IBKRTrader.Core/IBKRTrader.Core.csproj
+++ b/src/IBKRTrader.Core/IBKRTrader.Core.csproj
@@ -15,6 +15,12 @@
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs
new file mode 100644
index 0000000..2d9ff82
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs
@@ -0,0 +1,75 @@
+using IBKRTrader.Core.Persistence.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace IBKRTrader.Core.Persistence.Ef;
+
+///
+/// EF-Core-Kontext für die Core-Entitäten (core_-Tabellen). Modul-Entitäten liegen in eigenen
+/// Modul-DbContexts (gleiche MariaDB, andere Tabellen mit Modul-Präfix).
+///
+public class CoreDbContext : DbContext
+{
+ public CoreDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet Positions => Set();
+ public DbSet TradeHistory => Set();
+ public DbSet Budgets => Set();
+ public DbSet WorkerLog => Set();
+ public DbSet Settings => Set();
+
+ protected override void OnModelCreating(ModelBuilder b)
+ {
+ b.Entity(e =>
+ {
+ e.ToTable("core_position");
+ e.HasKey(x => new { x.Module, x.Symbol });
+ e.Property(x => x.Module).HasMaxLength(50);
+ e.Property(x => x.Symbol).HasMaxLength(20);
+ e.Property(x => x.AvgPrice).HasPrecision(18, 4);
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("core_trade_history");
+ e.HasKey(x => x.Id);
+ e.Property(x => x.Module).HasMaxLength(50);
+ e.Property(x => x.Symbol).HasMaxLength(20);
+ e.Property(x => x.Action).HasMaxLength(10);
+ e.Property(x => x.IbkrOrderId).HasMaxLength(100);
+ e.Property(x => x.Status).HasMaxLength(50);
+ e.Property(x => x.Quantity).HasPrecision(18, 4);
+ e.Property(x => x.Price).HasPrecision(18, 4);
+ e.Property(x => x.TotalValue).HasPrecision(18, 4);
+ e.HasIndex(x => x.Symbol);
+ e.HasIndex(x => x.Module);
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("core_budget");
+ e.HasKey(x => x.Module);
+ e.Property(x => x.Module).HasMaxLength(50);
+ e.Property(x => x.TotalBudget).HasPrecision(18, 2);
+ e.Property(x => x.UsedBudget).HasPrecision(18, 2);
+ e.Property(x => x.MaxPerTrade).HasPrecision(18, 2);
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("core_worker_log");
+ e.HasKey(x => x.Id);
+ e.Property(x => x.WorkerName).HasMaxLength(100);
+ e.Property(x => x.Module).HasMaxLength(50);
+ e.Property(x => x.Status).HasMaxLength(20);
+ e.HasIndex(x => new { x.WorkerName, x.StartedAt });
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("core_settings");
+ e.HasKey(x => x.Key);
+ e.Property(x => x.Key).HasMaxLength(100);
+ e.Property(x => x.Value).HasColumnType("text");
+ });
+ }
+}
diff --git a/src/IBKRTrader.Core/Persistence/Ef/CoreDbContextFactory.cs b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContextFactory.cs
new file mode 100644
index 0000000..38761f4
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Ef/CoreDbContextFactory.cs
@@ -0,0 +1,25 @@
+using IBKRTrader.Core.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace IBKRTrader.Core.Persistence.Ef;
+
+///
+/// Design-Time-Factory für EF-Tooling (dotnet ef migrations/database). Liest den Connection-String
+/// aus der Umgebungsvariable IBKRTRADER_MYSQL, damit keine Zugangsdaten im Repo landen. Nutzt die
+/// fest gepinnte Server-Version, sodass Migrations-Scaffolding OHNE DB-Verbindung funktioniert.
+///
+public class CoreDbContextFactory : IDesignTimeDbContextFactory
+{
+ public CoreDbContext CreateDbContext(string[] args)
+ {
+ var conn = Environment.GetEnvironmentVariable("IBKRTRADER_MYSQL")
+ ?? "Server=localhost;Port=3306;Database=ibkrtrader;User ID=root;Password=;";
+
+ var options = new DbContextOptionsBuilder()
+ .UseMySql(conn, DatabaseServerVersion.Value)
+ .Options;
+
+ return new CoreDbContext(options);
+ }
+}
diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.Designer.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.Designer.cs
new file mode 100644
index 0000000..e5ad538
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.Designer.cs
@@ -0,0 +1,198 @@
+//
+using System;
+using IBKRTrader.Core.Persistence.Ef;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace IBKRTrader.Core.Persistence.Ef.Migrations
+{
+ [DbContext(typeof(CoreDbContext))]
+ [Migration("20260728083916_InitialCore")]
+ partial class InitialCore
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b =>
+ {
+ b.Property("Module")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("MaxPerTrade")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("TotalBudget")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UsedBudget")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Module");
+
+ b.ToTable("core_budget", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CorePosition", b =>
+ {
+ b.Property("Module")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Symbol")
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("AvgPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Quantity")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Module", "Symbol");
+
+ b.ToTable("core_position", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreSetting", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Key");
+
+ b.ToTable("core_settings", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("varchar(10)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IbkrOrderId")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("Module")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Notes")
+ .HasColumnType("longtext");
+
+ b.Property("Price")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Quantity")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Status")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Symbol")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("TotalValue")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TradedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Module");
+
+ b.HasIndex("Symbol");
+
+ b.ToTable("core_trade_history", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreWorkerLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("FinishedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Message")
+ .HasColumnType("longtext");
+
+ b.Property("Module")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("StartedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("WorkerName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkerName", "StartedAt");
+
+ b.ToTable("core_worker_log", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.cs
new file mode 100644
index 0000000..5c31f56
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.cs
@@ -0,0 +1,157 @@
+using System;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IBKRTrader.Core.Persistence.Ef.Migrations
+{
+ ///
+ public partial class InitialCore : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "core_budget",
+ columns: table => new
+ {
+ Module = table.Column(type: "varchar(50)", maxLength: 50, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ TotalBudget = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
+ UsedBudget = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
+ MaxPerTrade = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
+ UpdatedAt = table.Column(type: "datetime(6)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_core_budget", x => x.Module);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "core_position",
+ columns: table => new
+ {
+ Module = table.Column(type: "varchar(50)", maxLength: 50, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Symbol = table.Column(type: "varchar(20)", maxLength: 20, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Quantity = table.Column(type: "int", nullable: false),
+ AvgPrice = table.Column(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
+ UpdatedAt = table.Column(type: "datetime(6)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_core_position", x => new { x.Module, x.Symbol });
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "core_settings",
+ columns: table => new
+ {
+ Key = table.Column(type: "varchar(100)", maxLength: 100, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Value = table.Column(type: "text", nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ UpdatedAt = table.Column(type: "datetime(6)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_core_settings", x => x.Key);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "core_trade_history",
+ columns: table => new
+ {
+ Id = table.Column(type: "bigint", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Module = table.Column(type: "varchar(50)", maxLength: 50, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Symbol = table.Column(type: "varchar(20)", maxLength: 20, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Action = table.Column(type: "varchar(10)", maxLength: 10, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Quantity = table.Column(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
+ Price = table.Column(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
+ TotalValue = table.Column(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
+ TradedAt = table.Column(type: "datetime(6)", nullable: false),
+ IbkrOrderId = table.Column(type: "varchar(100)", maxLength: 100, nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Status = table.Column(type: "varchar(50)", maxLength: 50, nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Notes = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ CreatedAt = table.Column(type: "datetime(6)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_core_trade_history", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "core_worker_log",
+ columns: table => new
+ {
+ Id = table.Column(type: "bigint", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ WorkerName = table.Column(type: "varchar(100)", maxLength: 100, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Module = table.Column(type: "varchar(50)", maxLength: 50, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ StartedAt = table.Column(type: "datetime(6)", nullable: true),
+ FinishedAt = table.Column(type: "datetime(6)", nullable: true),
+ Status = table.Column(type: "varchar(20)", maxLength: 20, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Message = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_core_worker_log", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_core_trade_history_Module",
+ table: "core_trade_history",
+ column: "Module");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_core_trade_history_Symbol",
+ table: "core_trade_history",
+ column: "Symbol");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_core_worker_log_WorkerName_StartedAt",
+ table: "core_worker_log",
+ columns: new[] { "WorkerName", "StartedAt" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "core_budget");
+
+ migrationBuilder.DropTable(
+ name: "core_position");
+
+ migrationBuilder.DropTable(
+ name: "core_settings");
+
+ migrationBuilder.DropTable(
+ name: "core_trade_history");
+
+ migrationBuilder.DropTable(
+ name: "core_worker_log");
+ }
+ }
+}
diff --git a/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs b/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs
new file mode 100644
index 0000000..28746b1
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs
@@ -0,0 +1,195 @@
+//
+using System;
+using IBKRTrader.Core.Persistence.Ef;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace IBKRTrader.Core.Persistence.Ef.Migrations
+{
+ [DbContext(typeof(CoreDbContext))]
+ partial class CoreDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreBudget", b =>
+ {
+ b.Property("Module")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("MaxPerTrade")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("TotalBudget")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UsedBudget")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Module");
+
+ b.ToTable("core_budget", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CorePosition", b =>
+ {
+ b.Property("Module")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Symbol")
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("AvgPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Quantity")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Module", "Symbol");
+
+ b.ToTable("core_position", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreSetting", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Key");
+
+ b.ToTable("core_settings", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreTrade", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("varchar(10)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IbkrOrderId")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("Module")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Notes")
+ .HasColumnType("longtext");
+
+ b.Property("Price")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Quantity")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("Status")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Symbol")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("TotalValue")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("TradedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Module");
+
+ b.HasIndex("Symbol");
+
+ b.ToTable("core_trade_history", (string)null);
+ });
+
+ modelBuilder.Entity("IBKRTrader.Core.Persistence.Entities.CoreWorkerLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("FinishedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Message")
+ .HasColumnType("longtext");
+
+ b.Property("Module")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("StartedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("WorkerName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkerName", "StartedAt");
+
+ b.ToTable("core_worker_log", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/IBKRTrader.Core/Persistence/Entities/CoreEntities.cs b/src/IBKRTrader.Core/Persistence/Entities/CoreEntities.cs
new file mode 100644
index 0000000..de7d8c7
--- /dev/null
+++ b/src/IBKRTrader.Core/Persistence/Entities/CoreEntities.cs
@@ -0,0 +1,58 @@
+namespace IBKRTrader.Core.Persistence.Entities;
+
+/// Offene Position eines Moduls (Tabelle core_position, PK Module+Symbol).
+public class CorePosition
+{
+ public string Module { get; set; } = "";
+ public string Symbol { get; set; } = "";
+ public int Quantity { get; set; }
+ public decimal AvgPrice { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}
+
+/// Trade-Historie (Tabelle core_trade_history).
+public class CoreTrade
+{
+ public long Id { get; set; }
+ public string Module { get; set; } = "";
+ public string Symbol { get; set; } = "";
+ public string Action { get; set; } = ""; // BUY / SELL
+ public decimal Quantity { get; set; }
+ public decimal Price { get; set; }
+ public decimal TotalValue { get; set; }
+ public DateTime TradedAt { get; set; }
+ public string? IbkrOrderId { get; set; }
+ public string? Status { get; set; }
+ public string? Notes { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
+
+/// Budget je Modul (Tabelle core_budget, PK Module).
+public class CoreBudget
+{
+ public string Module { get; set; } = "";
+ public decimal TotalBudget { get; set; }
+ public decimal UsedBudget { get; set; }
+ public decimal MaxPerTrade { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}
+
+/// Worker-Lauf-Protokoll (Tabelle core_worker_log).
+public class CoreWorkerLog
+{
+ public long Id { get; set; }
+ public string WorkerName { get; set; } = "";
+ public string Module { get; set; } = "Core";
+ public DateTime? StartedAt { get; set; }
+ public DateTime? FinishedAt { get; set; }
+ public string Status { get; set; } = "Running";
+ public string? Message { get; set; }
+}
+
+/// Key-Value-Einstellungen (Tabelle core_settings, PK Key).
+public class CoreSetting
+{
+ public string Key { get; set; } = "";
+ public string? Value { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}