RF-Slice 2: ResolutionFarming-Persistenz (EF/MySQL) + Repos + Migration
- Entities RfCandidate/RfPosition/RfClosedTrade (+ RfSettings aus Slice 1). - ResolutionFarmingDbContext: Tabellen rf_settings/rf_candidates/rf_positions/ rf_closed_trades. Autoincrement-PKs (Identity) fuer Candidate/ClosedTrade von Anfang an (Lehre aus dem CopyTrading-TradeId-Problem), zusammengesetzter PK (AccountId,TokenId) fuer Positions, Indizes + Decimal-Precision. - 4 Repos (Settings/Candidate/Position/ClosedTrade) mit serverseitigen Aggregaten (RealizedPnlSince fuer Kill-Switch, CountOpenedSince fuer Tages-Drossel). - Modul registriert DbContextFactory + Repos. - Design-Time-Factory nutzt die fest gepinnte Server-Version -> Migration wurde OHNE DB-Verbindung generiert (kein Zugriff auf die produktive DB). Anwenden per 'dotnet ef database update' bewusst im Zielland/lokal durch den Nutzer. 6 neue EF-InMemory-Repo-Tests. Build 0 Fehler, 289 Tests gruen, --smoke-ui ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c9eb11afe4
commit
1b6e194b50
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||
|
||||
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
|
||||
{
|
||||
public class EfRfSettingsRepository : IRfSettingsRepository
|
||||
{
|
||||
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
|
||||
public EfRfSettingsRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
|
||||
|
||||
public List<RfSettings> GetAll()
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Settings.AsNoTracking().ToList();
|
||||
}
|
||||
|
||||
public RfSettings? Get(int accountId)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Settings.AsNoTracking().FirstOrDefault(s => s.AccountId == accountId);
|
||||
}
|
||||
|
||||
public void Upsert(RfSettings settings)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
bool exists = ctx.Settings.Any(s => s.AccountId == settings.AccountId);
|
||||
if (exists) ctx.Settings.Update(settings);
|
||||
else ctx.Settings.Add(settings);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public class EfRfCandidateRepository : IRfCandidateRepository
|
||||
{
|
||||
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
|
||||
public EfRfCandidateRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
|
||||
|
||||
public void Insert(RfCandidate candidate)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
ctx.Candidates.Add(candidate);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public List<RfCandidate> GetRecent(int accountId, int limit)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Candidates.AsNoTracking()
|
||||
.Where(c => c.AccountId == accountId)
|
||||
.OrderByDescending(c => c.ScannedAt)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class EfRfPositionRepository : IRfPositionRepository
|
||||
{
|
||||
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
|
||||
public EfRfPositionRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
|
||||
|
||||
public List<RfPosition> GetOpen(int accountId)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Positions.AsNoTracking().Where(p => p.AccountId == accountId).ToList();
|
||||
}
|
||||
|
||||
public List<RfPosition> GetAllOpen()
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Positions.AsNoTracking().ToList();
|
||||
}
|
||||
|
||||
public RfPosition? Find(int accountId, string tokenId)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Positions.AsNoTracking().FirstOrDefault(p => p.AccountId == accountId && p.TokenId == tokenId);
|
||||
}
|
||||
|
||||
public void Upsert(RfPosition position)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
bool exists = ctx.Positions.Any(p => p.AccountId == position.AccountId && p.TokenId == position.TokenId);
|
||||
if (exists) ctx.Positions.Update(position);
|
||||
else ctx.Positions.Add(position);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public void Delete(int accountId, string tokenId)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
var row = ctx.Positions.FirstOrDefault(p => p.AccountId == accountId && p.TokenId == tokenId);
|
||||
if (row != null) { ctx.Positions.Remove(row); ctx.SaveChanges(); }
|
||||
}
|
||||
|
||||
public int CountOpenedSince(int accountId, DateTime since)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.Positions.AsNoTracking().Count(p => p.AccountId == accountId && p.OpenedAt >= since);
|
||||
}
|
||||
}
|
||||
|
||||
public class EfRfClosedTradeRepository : IRfClosedTradeRepository
|
||||
{
|
||||
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
|
||||
public EfRfClosedTradeRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
|
||||
|
||||
public void Insert(RfClosedTrade trade)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
ctx.ClosedTrades.Add(trade);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public List<RfClosedTrade> Find(Expression<Func<RfClosedTrade, bool>> predicate)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.ClosedTrades.AsNoTracking().Where(predicate).ToList();
|
||||
}
|
||||
|
||||
public decimal RealizedPnlSince(int accountId, DateTime since)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.ClosedTrades.AsNoTracking()
|
||||
.Where(t => t.AccountId == accountId && t.ClosedAt >= since)
|
||||
.Select(t => (decimal?)t.RealizedPnl)
|
||||
.Sum() ?? 0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||
|
||||
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
|
||||
{
|
||||
/// <summary>
|
||||
/// EF-Core-Kontext des ResolutionFarming-Moduls (gleiche MySQL-DB wie der Core, eigene Tabellen
|
||||
/// mit Präfix rf_). Autoincrement-PKs von Anfang an (keine code-vergebenen Schlüssel – vermeidet
|
||||
/// die im Copytrading nachträglich aufgefallene TradeId-Kollisionsklasse).
|
||||
/// </summary>
|
||||
public class ResolutionFarmingDbContext : DbContext
|
||||
{
|
||||
public ResolutionFarmingDbContext(DbContextOptions<ResolutionFarmingDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<RfSettings> Settings => Set<RfSettings>();
|
||||
public DbSet<RfCandidate> Candidates => Set<RfCandidate>();
|
||||
public DbSet<RfPosition> Positions => Set<RfPosition>();
|
||||
public DbSet<RfClosedTrade> ClosedTrades => Set<RfClosedTrade>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
b.Entity<RfSettings>(e =>
|
||||
{
|
||||
e.ToTable("rf_settings");
|
||||
e.HasKey(x => x.AccountId);
|
||||
e.Property(x => x.AccountId).ValueGeneratedNever();
|
||||
e.Property(x => x.MinPrice).HasPrecision(18, 6);
|
||||
e.Property(x => x.MaxPrice).HasPrecision(18, 6);
|
||||
e.Property(x => x.MinEdgePct).HasPrecision(18, 6);
|
||||
e.Property(x => x.MaxPerMarketUsd).HasPrecision(18, 6);
|
||||
e.Property(x => x.MaxPerClusterPct).HasPrecision(18, 6);
|
||||
e.Property(x => x.MaxTotalExposurePct).HasPrecision(18, 6);
|
||||
e.Property(x => x.DailyLossKillSwitchUsd).HasPrecision(18, 6);
|
||||
e.Property(x => x.CategoryWhitelistCsv).HasMaxLength(500);
|
||||
e.Property(x => x.BlacklistCsv).HasMaxLength(1000);
|
||||
});
|
||||
|
||||
b.Entity<RfCandidate>(e =>
|
||||
{
|
||||
e.ToTable("rf_candidates");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Id).ValueGeneratedOnAdd();
|
||||
e.Property(x => x.TokenId).HasMaxLength(120);
|
||||
e.Property(x => x.MarketSlug).HasMaxLength(300);
|
||||
e.Property(x => x.EventSlug).HasMaxLength(300);
|
||||
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
|
||||
e.Property(x => x.Outcome).HasMaxLength(200);
|
||||
e.Property(x => x.Category).HasMaxLength(100);
|
||||
e.Property(x => x.ClusterKey).HasMaxLength(300);
|
||||
e.Property(x => x.RejectReason).HasMaxLength(300);
|
||||
e.Property(x => x.Ask).HasPrecision(18, 6);
|
||||
e.Property(x => x.NetEdgePct).HasPrecision(18, 6);
|
||||
e.Property(x => x.Score).HasPrecision(18, 6);
|
||||
e.HasIndex(x => x.ScannedAt);
|
||||
e.HasIndex(x => new { x.AccountId, x.Accepted });
|
||||
});
|
||||
|
||||
b.Entity<RfPosition>(e =>
|
||||
{
|
||||
e.ToTable("rf_positions");
|
||||
e.HasKey(x => new { x.AccountId, x.TokenId });
|
||||
e.Property(x => x.TokenId).HasMaxLength(120);
|
||||
e.Property(x => x.MarketSlug).HasMaxLength(300);
|
||||
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
|
||||
e.Property(x => x.Outcome).HasMaxLength(200);
|
||||
e.Property(x => x.Category).HasMaxLength(100);
|
||||
e.Property(x => x.ClusterKey).HasMaxLength(300);
|
||||
e.Property(x => x.Status).HasMaxLength(20);
|
||||
e.Property(x => x.EntryPrice).HasPrecision(18, 6);
|
||||
e.Property(x => x.Size).HasPrecision(18, 6);
|
||||
e.Property(x => x.AmountUsd).HasPrecision(18, 6);
|
||||
});
|
||||
|
||||
b.Entity<RfClosedTrade>(e =>
|
||||
{
|
||||
e.ToTable("rf_closed_trades");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Id).ValueGeneratedOnAdd();
|
||||
e.Property(x => x.TokenId).HasMaxLength(120);
|
||||
e.Property(x => x.MarketSlug).HasMaxLength(300);
|
||||
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
|
||||
e.Property(x => x.Outcome).HasMaxLength(200);
|
||||
e.Property(x => x.Category).HasMaxLength(100);
|
||||
e.Property(x => x.ClusterKey).HasMaxLength(300);
|
||||
e.Property(x => x.ExitReason).HasMaxLength(200);
|
||||
e.Property(x => x.RedeemStatus).HasMaxLength(20);
|
||||
e.Property(x => x.EntryPrice).HasPrecision(18, 6);
|
||||
e.Property(x => x.ExitPrice).HasPrecision(18, 6);
|
||||
e.Property(x => x.Size).HasPrecision(18, 6);
|
||||
e.Property(x => x.RealizedPnl).HasPrecision(18, 6);
|
||||
e.Property(x => x.PnlPercent).HasPrecision(18, 6);
|
||||
e.Property(x => x.TotalFees).HasPrecision(18, 6);
|
||||
e.HasIndex(x => new { x.AccountId, x.TokenId });
|
||||
e.HasIndex(x => x.ClosedAt);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using PolyTrader.Core.Configuration;
|
||||
|
||||
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
|
||||
{
|
||||
/// <summary>
|
||||
/// Design-Time-Factory für EF-Tooling. Nutzt die fest gepinnte Server-Version
|
||||
/// (<see cref="DatabaseServerVersion"/>) statt <c>ServerVersion.AutoDetect</c>, damit
|
||||
/// Migrations-Scaffolding OHNE DB-Verbindung funktioniert (kein Zugriff auf die produktive DB
|
||||
/// beim Generieren). Connection-String rein nominell über POLYTRADER_MYSQL; es wird beim
|
||||
/// bloßen Scaffolding keine Verbindung geöffnet.
|
||||
/// </summary>
|
||||
public class ResolutionFarmingDbContextFactory : IDesignTimeDbContextFactory<ResolutionFarmingDbContext>
|
||||
{
|
||||
public ResolutionFarmingDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var conn = Environment.GetEnvironmentVariable("POLYTRADER_MYSQL")
|
||||
?? "Server=localhost;Port=3306;Database=polytrader;User ID=root;Password=;";
|
||||
|
||||
var options = new DbContextOptionsBuilder<ResolutionFarmingDbContext>()
|
||||
.UseMySql(conn, DatabaseServerVersion.Value)
|
||||
.Options;
|
||||
|
||||
return new ResolutionFarmingDbContext(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||
|
||||
namespace PolyTrader.Modules.ResolutionFarming.Persistence
|
||||
{
|
||||
public interface IRfSettingsRepository
|
||||
{
|
||||
List<RfSettings> GetAll();
|
||||
RfSettings? Get(int accountId);
|
||||
void Upsert(RfSettings settings);
|
||||
}
|
||||
|
||||
public interface IRfCandidateRepository
|
||||
{
|
||||
void Insert(RfCandidate candidate);
|
||||
List<RfCandidate> GetRecent(int accountId, int limit);
|
||||
}
|
||||
|
||||
public interface IRfPositionRepository
|
||||
{
|
||||
List<RfPosition> GetOpen(int accountId);
|
||||
List<RfPosition> GetAllOpen();
|
||||
RfPosition? Find(int accountId, string tokenId);
|
||||
void Upsert(RfPosition position);
|
||||
void Delete(int accountId, string tokenId);
|
||||
/// <summary>Anzahl heute (seit <paramref name="since"/>) neu eröffneter Positionen (Tages-Drossel).</summary>
|
||||
int CountOpenedSince(int accountId, DateTime since);
|
||||
}
|
||||
|
||||
public interface IRfClosedTradeRepository
|
||||
{
|
||||
void Insert(RfClosedTrade trade);
|
||||
List<RfClosedTrade> Find(Expression<Func<RfClosedTrade, bool>> predicate);
|
||||
/// <summary>Summe des realisierten PnL seit <paramref name="since"/> (Kill-Switch).</summary>
|
||||
decimal RealizedPnlSince(int accountId, DateTime since);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user