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();
}
}