From 261032f9f9c1cf635668e4b9f9660d56abdc1ea6 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 28 Jul 2026 10:40:09 +0200 Subject: [PATCH] @ 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 @ --- NuGet.config | 6 + Program.cs | 14 +- docs/ARCHITECTURE.md | 11 +- .../Configuration/DatabaseOptions.cs | 25 +++ .../ServiceCollectionExtensions.cs | 21 ++ src/IBKRTrader.Core/IBKRTrader.Core.csproj | 6 + .../Persistence/Ef/CoreDbContext.cs | 75 +++++++ .../Persistence/Ef/CoreDbContextFactory.cs | 25 +++ .../20260728083916_InitialCore.Designer.cs | 198 ++++++++++++++++++ .../Migrations/20260728083916_InitialCore.cs | 157 ++++++++++++++ .../Migrations/CoreDbContextModelSnapshot.cs | 195 +++++++++++++++++ .../Persistence/Entities/CoreEntities.cs | 58 +++++ 12 files changed, 782 insertions(+), 9 deletions(-) create mode 100644 src/IBKRTrader.Core/Configuration/DatabaseOptions.cs create mode 100644 src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/IBKRTrader.Core/Persistence/Ef/CoreDbContext.cs create mode 100644 src/IBKRTrader.Core/Persistence/Ef/CoreDbContextFactory.cs create mode 100644 src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.Designer.cs create mode 100644 src/IBKRTrader.Core/Persistence/Ef/Migrations/20260728083916_InitialCore.cs create mode 100644 src/IBKRTrader.Core/Persistence/Ef/Migrations/CoreDbContextModelSnapshot.cs create mode 100644 src/IBKRTrader.Core/Persistence/Entities/CoreEntities.cs 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; } +}