using IBKRTrader.Core.Configuration; using IBKRTrader.Core.Logging; using IBKRTrader.Modules.Supervisor.Counterfactual; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace IBKRTrader.Modules.Supervisor.Persistence; /// EF-Kontext des Supervisor-Moduls (gleiche MariaDB, Tabellen mit Präfix sup_). public class SupervisorDbContext : DbContext { public SupervisorDbContext(DbContextOptions options) : base(options) { } public DbSet Reports => Set(); public DbSet Counterfactuals => Set(); protected override void OnModelCreating(ModelBuilder b) { b.Entity(e => { e.ToTable("sup_reports"); e.HasKey(x => x.Id); e.Property(x => x.Id).ValueGeneratedOnAdd(); e.Property(x => x.Profile).HasMaxLength(50); e.Property(x => x.Model).HasMaxLength(120); e.Property(x => x.Question).HasMaxLength(4000); e.Property(x => x.Answer).HasColumnType("text"); e.Property(x => x.ToolCallsJson).HasColumnType("text"); e.HasIndex(x => x.CreatedAt); }); b.Entity(e => { e.ToTable("sup_counterfactuals"); e.HasKey(x => x.Id); e.Property(x => x.Id).ValueGeneratedOnAdd(); e.Property(x => x.SignalId).HasMaxLength(64); e.Property(x => x.Module).HasMaxLength(50); e.Property(x => x.Symbol).HasMaxLength(20); e.Property(x => x.Reason).HasMaxLength(40); e.Property(x => x.Side).HasMaxLength(10); e.Property(x => x.SignalPrice).HasPrecision(18, 4); e.Property(x => x.LaterPrice).HasPrecision(18, 4); e.Property(x => x.HypotheticalPnlPerShare).HasPrecision(18, 4); e.HasIndex(x => x.DecisionId).IsUnique(); // ein Ergebnis je Entscheidung e.HasIndex(x => x.CheckedAt); e.HasIndex(x => x.Reason); }); } } /// Design-Time-Factory für EF-Tooling (dotnet ef). Connection aus env IBKRTRADER_MYSQL. public class SupervisorDbContextFactory : IDesignTimeDbContextFactory { public SupervisorDbContext 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 SupervisorDbContext(options); } } /// Bericht-Ablage. Write fehlertolerant (Analyse darf nie an der Persistenz scheitern). public interface ISupervisorReportRepository { void Insert(SupervisorReport report); List GetRecent(int limit); } /// Counterfactual-Ablage. Write fehlertolerant. public interface ISupervisorCounterfactualRepository { HashSet ExistingDecisionIds(IEnumerable decisionIds); void Insert(CounterfactualRecord record); List GetRecent(int limit); } public sealed class EfSupervisorReportRepository : ISupervisorReportRepository { private readonly IDbContextFactory _dbf; private readonly LoggingService _logger; public EfSupervisorReportRepository(IDbContextFactory dbf, LoggingService logger) { _dbf = dbf; _logger = logger; } public void Insert(SupervisorReport report) { try { using var db = _dbf.CreateDbContext(); db.Reports.Add(report); db.SaveChanges(); } catch (Exception ex) { _logger.Warn("Supervisor", $"Report-Write fehlgeschlagen (ignoriert): {ex.Message}"); } } public List GetRecent(int limit) { using var db = _dbf.CreateDbContext(); return db.Reports.AsNoTracking().OrderByDescending(r => r.CreatedAt).Take(limit).ToList(); } } public sealed class EfSupervisorCounterfactualRepository : ISupervisorCounterfactualRepository { private readonly IDbContextFactory _dbf; private readonly LoggingService _logger; public EfSupervisorCounterfactualRepository(IDbContextFactory dbf, LoggingService logger) { _dbf = dbf; _logger = logger; } public HashSet ExistingDecisionIds(IEnumerable decisionIds) { var ids = decisionIds.ToList(); using var db = _dbf.CreateDbContext(); return db.Counterfactuals.AsNoTracking() .Where(c => ids.Contains(c.DecisionId)) .Select(c => c.DecisionId) .ToHashSet(); } public void Insert(CounterfactualRecord record) { try { using var db = _dbf.CreateDbContext(); db.Counterfactuals.Add(record); db.SaveChanges(); } catch (Exception ex) { _logger.Warn("Supervisor", $"Counterfactual-Write fehlgeschlagen (ignoriert): {ex.Message}"); } } public List GetRecent(int limit) { using var db = _dbf.CreateDbContext(); return db.Counterfactuals.AsNoTracking().OrderByDescending(c => c.CheckedAt).Take(limit).ToList(); } }