diff --git a/services/MasterTraderAnalyticsJob.cs b/services/MasterTraderAnalyticsJob.cs index dbaa4ca..220867a 100644 --- a/services/MasterTraderAnalyticsJob.cs +++ b/services/MasterTraderAnalyticsJob.cs @@ -1,6 +1,5 @@ using System; -using MongoDB.Driver; -using PolyTraderSharp.Extensions; +using PolyTrader.Modules.CopyTrading.Persistence; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -16,16 +15,18 @@ namespace PolyTraderSharp.Services private readonly TradingState _state; private readonly CopyTradingState _copyState; private readonly TerminalLogger _logger; - private readonly IMongoDatabase _db; + private readonly IMasterTraderHistoryRepository _historyRepo; + private readonly ITrackedTraderRepository _traderRepo; private readonly JobStatusRow _jobStatus; private readonly PolymarketApiService _api; - public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMongoDatabase db, JobManager jobManager, PolymarketApiService api) + public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMasterTraderHistoryRepository historyRepo, ITrackedTraderRepository traderRepo, JobManager jobManager, PolymarketApiService api) { _state = state; _copyState = copyState; _logger = logger; - _db = db; + _historyRepo = historyRepo; + _traderRepo = traderRepo; _api = api; _jobStatus = new JobStatusRow @@ -87,9 +88,7 @@ namespace PolyTraderSharp.Services { _logger.Info("🔄 Starte Master-Trader Historien-Download und Performance-Analyse..."); - var historyColl = _db.GetCollection("mt_history"); - historyColl.EnsureIndex(x => x.TraderId); - historyColl.EnsureIndex(x => x.ClosedAt); + _historyRepo.EnsureIndexes(); DateTime cutoff7Days = DateTime.UtcNow.AddDays(-7); var tradersToAnalyze = _copyState.Traders.Values.Where(t => t.IsActive && !string.IsNullOrEmpty(t.WalletAddress)).ToList(); @@ -145,7 +144,7 @@ namespace PolyTraderSharp.Services DateTime windowStart = closedTs.AddSeconds(-2); DateTime windowEnd = closedTs.AddSeconds(2); - bool exists = historyColl.LiteFindOne(x => x.TraderId == trader.Id && x.TokenId == tokenId && x.ClosedAt >= windowStart && x.ClosedAt <= windowEnd) != null; + bool exists = _historyRepo.Exists(trader.Id, tokenId, windowStart, windowEnd); if (!exists) { var record = new MasterTraderHistoryRecord @@ -155,7 +154,7 @@ namespace PolyTraderSharp.Services ClosedAt = closedTs, RealizedPnl = pnl }; - historyColl.Insert(record); + _historyRepo.Insert(record); inserted++; } } @@ -164,7 +163,7 @@ namespace PolyTraderSharp.Services await Task.Delay(200); // 2. Calculate Stats from DB - var last7DaysTrades = historyColl.LiteFind(x => x.TraderId == trader.Id && x.ClosedAt >= cutoff7Days).ToList(); + var last7DaysTrades = _historyRepo.GetByTraderSince(trader.Id, cutoff7Days); trader.TotalTrades = last7DaysTrades.Count; trader.TotalPnl = (double)last7DaysTrades.Sum(x => x.RealizedPnl); @@ -174,8 +173,7 @@ namespace PolyTraderSharp.Services trader.Winrate30t = trader.TotalTrades > 0 ? Math.Round(((double)trader.WinningTrades / trader.TotalTrades) * 100, 2) : 0; // Save updated trader to DB so UI updates - var tColl = _db.GetCollection("tracked_traders"); - tColl.Update(trader); + _traderRepo.Update(trader); if (inserted > 0 && trader.TotalTrades > 0) { diff --git a/services/StartupHydrationService.cs b/services/StartupHydrationService.cs index f5b2106..fc36b42 100644 --- a/services/StartupHydrationService.cs +++ b/services/StartupHydrationService.cs @@ -25,6 +25,7 @@ namespace PolyTraderSharp.Services private readonly IAccountRepository _accountRepo; private readonly IPositionRepository _positionRepo; private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo; + private readonly ITrackedTraderRepository _traderRepo; private readonly IMongoDatabase _db; private readonly TerminalLogger _logger; @@ -34,6 +35,7 @@ namespace PolyTraderSharp.Services IAccountRepository accountRepo, IPositionRepository positionRepo, ICopyTradingAccountSettingsRepository accountSettingsRepo, + ITrackedTraderRepository traderRepo, IMongoDatabase db, TerminalLogger logger) { @@ -42,6 +44,7 @@ namespace PolyTraderSharp.Services _accountRepo = accountRepo; _positionRepo = positionRepo; _accountSettingsRepo = accountSettingsRepo; + _traderRepo = traderRepo; _db = db; _logger = logger; } @@ -65,10 +68,7 @@ namespace PolyTraderSharp.Services HydrateAccountSettings(accounts); - // Trader (TrackedTrader) sind aktuell noch modulnah; bis zum Trader-Repo in - // Phase 5 direkt aus der "trackers"-Collection geladen. - var tradersCol = _db.GetCollection("trackers"); - foreach (var trd in tradersCol.Find(_ => true).ToList()) + foreach (var trd in _traderRepo.GetAll()) { _copyState.Traders[trd.Id] = trd; } diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs index 359dac8..ccd57ed 100644 --- a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs @@ -1,10 +1,14 @@ +using System; using System.Threading; using System.Threading.Tasks; using System.Threading.Channels; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Modularity; using PolyTrader.Modules.CopyTrading.Persistence; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; +using PolyTrader.Modules.CopyTrading.Persistence.Mongo; using PolyTraderSharp; using PolyTraderSharp.Models; using PolyTraderSharp.Services; @@ -33,11 +37,25 @@ namespace PolyTrader.Modules.CopyTrading services.AddSingleton(closedTradeChannel.Writer); services.AddSingleton(closedTradeChannel.Reader); - // Modul-eigener Trade-Log - services.AddSingleton(); + // Modul-Persistenz: Provider-Toggle (MySql via EF / Mongo im Übergang). + var provider = configuration["Database:Provider"] ?? "Mongo"; + if (string.Equals(provider, "MySql", StringComparison.OrdinalIgnoreCase)) + { + var conn = configuration["Database:MySqlConnectionString"] ?? string.Empty; + services.AddDbContextFactory(o => o.UseMySql(conn, ServerVersion.AutoDetect(conn))); - // Copytrading-Account-Detail-Einstellungen - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } // Modul-Services (Signalquelle, Ausführung, Analytics) services.AddSingleton(); diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs new file mode 100644 index 0000000..01d3134 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + /// + /// EF-Core-Kontext für die Entitäten des Copytrading-Moduls (gleiche MySQL-DB wie der + /// Core, eigene Tabellen mit Präfix mod_copytrading_). + /// + public class CopyTradingDbContext : DbContext + { + public CopyTradingDbContext(DbContextOptions options) : base(options) { } + + public DbSet ClosedTrades => Set(); + public DbSet Traders => Set(); + public DbSet AccountSettings => Set(); + public DbSet History => Set(); + + protected override void OnModelCreating(ModelBuilder b) + { + b.Ignore(); // reine UI-Anzeige-Klasse + + b.Entity(e => + { + e.ToTable("mod_copytrading_closed_trades"); + e.HasKey(x => x.TradeId); + e.Property(x => x.TradeId).ValueGeneratedNever(); + 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.Side).HasMaxLength(10); + e.Property(x => x.ExitReason).HasMaxLength(200); + 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 => x.AccountId); + e.HasIndex(x => x.TokenId); + e.HasIndex(x => x.SourceTraderId); + }); + + b.Entity(e => + { + e.ToTable("mod_copytrading_traders"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).ValueGeneratedNever(); + e.Property(x => x.WalletAddress).HasMaxLength(128); + e.Property(x => x.DisplayName).HasMaxLength(200); + e.Property(x => x.Category).HasMaxLength(64); + e.Property(x => x.Description).HasMaxLength(1000); + e.Property(x => x.Reasoning).HasMaxLength(1000); + + var comparer = new ValueComparer>( + (a, c) => (a == null && c == null) || (a != null && c != null && a.SetEquals(c)), + v => v.Aggregate(0, (h, i) => HashCode.Combine(h, i)), + v => new HashSet(v)); + + e.Property(x => x.AssignedAccountIds) + .HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => string.IsNullOrEmpty(v) ? new HashSet() : (JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null) ?? new HashSet())) + .HasColumnType("text"); + e.Property(x => x.AssignedAccountIds).Metadata.SetValueComparer(comparer); + }); + + b.Entity(e => + { + e.ToTable("mod_copytrading_account_settings"); + e.HasKey(x => x.AccountId); + e.Property(x => x.AccountId).ValueGeneratedNever(); + e.Property(x => x.PerMarketLimit).HasPrecision(18, 6); + e.Property(x => x.MaxPriceDifference).HasPrecision(18, 6); + e.Property(x => x.MaxBuyPrice).HasPrecision(18, 6); + e.Property(x => x.ProfitTarget).HasPrecision(18, 6); + e.Property(x => x.PreRedeemLimit).HasPrecision(18, 6); + e.Property(x => x.PerMasterLimit).HasPrecision(18, 6); + e.Property(x => x.perMaxTime6h).HasPrecision(18, 6); + e.Property(x => x.perMaxTime24h).HasPrecision(18, 6); + e.Property(x => x.perMaxTime72h).HasPrecision(18, 6); + e.Property(x => x.perMaxTimeNone).HasPrecision(18, 6); + }); + + b.Entity(e => + { + e.ToTable("mod_copytrading_mt_history"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasMaxLength(64); + e.Property(x => x.TokenId).HasMaxLength(120); + e.Property(x => x.RealizedPnl).HasPrecision(18, 6); + e.HasIndex(x => x.TraderId); + e.HasIndex(x => x.ClosedAt); + }); + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContextFactory.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContextFactory.cs new file mode 100644 index 0000000..69302f0 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContextFactory.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + /// + /// Design-Time-Factory für EF-Tooling. Connection über Umgebungsvariable POLYTRADER_MYSQL + /// (keine Zugangsdaten im Code/Repo). + /// + public class CopyTradingDbContextFactory : IDesignTimeDbContextFactory + { + public CopyTradingDbContext CreateDbContext(string[] args) + { + var conn = Environment.GetEnvironmentVariable("POLYTRADER_MYSQL") + ?? "Server=localhost;Port=3306;Database=polytrader;User ID=root;Password=;"; + + var options = new DbContextOptionsBuilder() + .UseMySql(conn, ServerVersion.AutoDetect(conn)) + .Options; + + return new CopyTradingDbContext(options); + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradeLogRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradeLogRepository.cs new file mode 100644 index 0000000..cc12c88 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradeLogRepository.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + public class EfCopyTradeLogRepository : ICopyTradeLogRepository + { + private readonly IDbContextFactory _factory; + + public EfCopyTradeLogRepository(IDbContextFactory factory) => _factory = factory; + + public void EnsureIndexes() { } + + public bool Exists(int accountId, string tokenId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.ClosedTrades.AsNoTracking().Any(x => x.AccountId == accountId && x.TokenId == tokenId); + } + + public void Insert(ClosedTrade trade) + { + using var ctx = _factory.CreateDbContext(); + ctx.ClosedTrades.Add(trade); + ctx.SaveChanges(); + } + + public List Find(Expression> predicate) + { + using var ctx = _factory.CreateDbContext(); + return ctx.ClosedTrades.AsNoTracking().Where(predicate).ToList(); + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradingAccountSettingsRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradingAccountSettingsRepository.cs new file mode 100644 index 0000000..3a94a05 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfCopyTradingAccountSettingsRepository.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + public class EfCopyTradingAccountSettingsRepository : ICopyTradingAccountSettingsRepository + { + private readonly IDbContextFactory _factory; + + public EfCopyTradingAccountSettingsRepository(IDbContextFactory factory) => _factory = factory; + + public List GetAll() + { + using var ctx = _factory.CreateDbContext(); + return ctx.AccountSettings.AsNoTracking().ToList(); + } + + public CopyTradingAccountSettings? Get(int accountId) + { + using var ctx = _factory.CreateDbContext(); + return ctx.AccountSettings.AsNoTracking().FirstOrDefault(x => x.AccountId == accountId); + } + + public void Upsert(CopyTradingAccountSettings settings) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.AccountSettings.Find(settings.AccountId); + if (existing == null) + ctx.AccountSettings.Add(settings); + else + ctx.Entry(existing).CurrentValues.SetValues(settings); + ctx.SaveChanges(); + } + + public void Delete(int accountId) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.AccountSettings.Find(accountId); + if (existing != null) + { + ctx.AccountSettings.Remove(existing); + ctx.SaveChanges(); + } + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfMasterTraderHistoryRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfMasterTraderHistoryRepository.cs new file mode 100644 index 0000000..82df2ef --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfMasterTraderHistoryRepository.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + public class EfMasterTraderHistoryRepository : IMasterTraderHistoryRepository + { + private readonly IDbContextFactory _factory; + + public EfMasterTraderHistoryRepository(IDbContextFactory factory) => _factory = factory; + + public void EnsureIndexes() { } + + public bool Exists(int traderId, string tokenId, DateTime windowStart, DateTime windowEnd) + { + using var ctx = _factory.CreateDbContext(); + return ctx.History.AsNoTracking().Any(x => x.TraderId == traderId && x.TokenId == tokenId + && x.ClosedAt >= windowStart && x.ClosedAt <= windowEnd); + } + + public void Insert(MasterTraderHistoryRecord record) + { + using var ctx = _factory.CreateDbContext(); + ctx.History.Add(record); + ctx.SaveChanges(); + } + + public List GetByTraderSince(int traderId, DateTime since) + { + using var ctx = _factory.CreateDbContext(); + return ctx.History.AsNoTracking().Where(x => x.TraderId == traderId && x.ClosedAt >= since).ToList(); + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfTrackedTraderRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfTrackedTraderRepository.cs new file mode 100644 index 0000000..aa071c9 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/EfTrackedTraderRepository.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef +{ + public class EfTrackedTraderRepository : ITrackedTraderRepository + { + private readonly IDbContextFactory _factory; + + public EfTrackedTraderRepository(IDbContextFactory factory) => _factory = factory; + + public List GetAll() + { + using var ctx = _factory.CreateDbContext(); + return ctx.Traders.AsNoTracking().ToList(); + } + + public void Upsert(TrackedTrader trader) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Traders.Find(trader.Id); + if (existing == null) + ctx.Traders.Add(trader); + else + ctx.Entry(existing).CurrentValues.SetValues(trader); + ctx.SaveChanges(); + } + + public void Update(TrackedTrader trader) => Upsert(trader); + + public void Delete(int id) + { + using var ctx = _factory.CreateDbContext(); + var existing = ctx.Traders.Find(id); + if (existing != null) + { + ctx.Traders.Remove(existing); + ctx.SaveChanges(); + } + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.Designer.cs new file mode 100644 index 0000000..805daf0 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.Designer.cs @@ -0,0 +1,252 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; + +#nullable disable + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations +{ + [DbContext(typeof(CopyTradingDbContext))] + [Migration("20260705120059_InitialCopyTrading")] + partial class InitialCopyTrading + { + /// + 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("PolyTraderSharp.Models.ClosedTrade", b => + { + b.Property("TradeId") + .HasColumnType("int"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntryPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ExitPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ExitReason") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsDemo") + .HasColumnType("tinyint(1)"); + + b.Property("MarketQuestion") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("MarketSlug") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OpenedAt") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PnlPercent") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("RealizedPnl") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Side") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Size") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SourceTraderId") + .HasColumnType("int"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("TotalFees") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("TradeId"); + + b.HasIndex("AccountId"); + + b.HasIndex("SourceTraderId"); + + b.HasIndex("TokenId"); + + b.ToTable("mod_copytrading_closed_trades", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.CopyTradingAccountSettings", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("MaxBuyPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("MaxPriceDifference") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PerMarketLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PerMasterLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PreRedeemLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ProfitTarget") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime24h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime6h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime72h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTimeNone") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("AccountId"); + + b.ToTable("mod_copytrading_account_settings", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.MasterTraderHistoryRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("RealizedPnl") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClosedAt"); + + b.HasIndex("TraderId"); + + b.ToTable("mod_copytrading_mt_history", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.TrackedTrader", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("AssignedAccountIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsHidden") + .HasColumnType("tinyint(1)"); + + b.Property("Reasoning") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("TotalPnl") + .HasColumnType("double"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("WalletAddress") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("WinningTrades") + .HasColumnType("int"); + + b.Property("Winrate30t") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("mod_copytrading_traders", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.cs new file mode 100644 index 0000000..3ca6ab0 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260705120059_InitialCopyTrading.cs @@ -0,0 +1,164 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations +{ + /// + public partial class InitialCopyTrading : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "mod_copytrading_account_settings", + columns: table => new + { + AccountId = table.Column(type: "int", nullable: false), + PerMarketLimit = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + MaxPriceDifference = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + MaxBuyPrice = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + ProfitTarget = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + PreRedeemLimit = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + PerMasterLimit = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + perMaxTime6h = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + perMaxTime24h = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + perMaxTime72h = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + perMaxTimeNone = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_mod_copytrading_account_settings", x => x.AccountId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "mod_copytrading_closed_trades", + columns: table => new + { + TradeId = table.Column(type: "int", nullable: false), + AccountId = table.Column(type: "int", nullable: false), + SourceTraderId = table.Column(type: "int", nullable: false), + IsDemo = table.Column(type: "tinyint(1)", nullable: false), + TokenId = table.Column(type: "varchar(120)", maxLength: 120, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MarketSlug = table.Column(type: "varchar(300)", maxLength: 300, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MarketQuestion = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Outcome = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Side = table.Column(type: "varchar(10)", maxLength: 10, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EntryPrice = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + ExitPrice = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + Size = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + RealizedPnl = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + PnlPercent = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + TotalFees = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + OpenedAt = table.Column(type: "datetime(6)", nullable: false), + ClosedAt = table.Column(type: "datetime(6)", nullable: false), + ExitReason = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_mod_copytrading_closed_trades", x => x.TradeId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "mod_copytrading_mt_history", + columns: table => new + { + Id = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TraderId = table.Column(type: "int", nullable: false), + TokenId = table.Column(type: "varchar(120)", maxLength: 120, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + RealizedPnl = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + ClosedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_mod_copytrading_mt_history", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "mod_copytrading_traders", + columns: table => new + { + Id = table.Column(type: "int", nullable: false), + WalletAddress = table.Column(type: "varchar(128)", maxLength: 128, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + DisplayName = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Reasoning = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IsActive = table.Column(type: "tinyint(1)", nullable: false), + IsHidden = table.Column(type: "tinyint(1)", nullable: false), + TotalTrades = table.Column(type: "int", nullable: false), + WinningTrades = table.Column(type: "int", nullable: false), + Winrate30t = table.Column(type: "double", nullable: false), + TotalPnl = table.Column(type: "double", nullable: false), + AssignedAccountIds = table.Column(type: "text", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_mod_copytrading_traders", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_mod_copytrading_closed_trades_AccountId", + table: "mod_copytrading_closed_trades", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_mod_copytrading_closed_trades_SourceTraderId", + table: "mod_copytrading_closed_trades", + column: "SourceTraderId"); + + migrationBuilder.CreateIndex( + name: "IX_mod_copytrading_closed_trades_TokenId", + table: "mod_copytrading_closed_trades", + column: "TokenId"); + + migrationBuilder.CreateIndex( + name: "IX_mod_copytrading_mt_history_ClosedAt", + table: "mod_copytrading_mt_history", + column: "ClosedAt"); + + migrationBuilder.CreateIndex( + name: "IX_mod_copytrading_mt_history_TraderId", + table: "mod_copytrading_mt_history", + column: "TraderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "mod_copytrading_account_settings"); + + migrationBuilder.DropTable( + name: "mod_copytrading_closed_trades"); + + migrationBuilder.DropTable( + name: "mod_copytrading_mt_history"); + + migrationBuilder.DropTable( + name: "mod_copytrading_traders"); + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs new file mode 100644 index 0000000..baf2604 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs @@ -0,0 +1,249 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; + +#nullable disable + +namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations +{ + [DbContext(typeof(CopyTradingDbContext))] + partial class CopyTradingDbContextModelSnapshot : 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("PolyTraderSharp.Models.ClosedTrade", b => + { + b.Property("TradeId") + .HasColumnType("int"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntryPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ExitPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ExitReason") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsDemo") + .HasColumnType("tinyint(1)"); + + b.Property("MarketQuestion") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("MarketSlug") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OpenedAt") + .HasColumnType("datetime(6)"); + + b.Property("Outcome") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PnlPercent") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("RealizedPnl") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Side") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Size") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SourceTraderId") + .HasColumnType("int"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("TotalFees") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("TradeId"); + + b.HasIndex("AccountId"); + + b.HasIndex("SourceTraderId"); + + b.HasIndex("TokenId"); + + b.ToTable("mod_copytrading_closed_trades", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.CopyTradingAccountSettings", b => + { + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("MaxBuyPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("MaxPriceDifference") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PerMarketLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PerMasterLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PreRedeemLimit") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ProfitTarget") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime24h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime6h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTime72h") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("perMaxTimeNone") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("AccountId"); + + b.ToTable("mod_copytrading_account_settings", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.MasterTraderHistoryRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("RealizedPnl") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("TokenId") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("varchar(120)"); + + b.Property("TraderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClosedAt"); + + b.HasIndex("TraderId"); + + b.ToTable("mod_copytrading_mt_history", (string)null); + }); + + modelBuilder.Entity("PolyTraderSharp.Models.TrackedTrader", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("AssignedAccountIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsHidden") + .HasColumnType("tinyint(1)"); + + b.Property("Reasoning") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("TotalPnl") + .HasColumnType("double"); + + b.Property("TotalTrades") + .HasColumnType("int"); + + b.Property("WalletAddress") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("WinningTrades") + .HasColumnType("int"); + + b.Property("Winrate30t") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("mod_copytrading_traders", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/IMasterTraderHistoryRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/IMasterTraderHistoryRepository.cs new file mode 100644 index 0000000..b1a2e0b --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/IMasterTraderHistoryRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence +{ + /// + /// Persistenz der Master-Trader-Historie (geschlossene Trades der Master, für Analytics). + /// + public interface IMasterTraderHistoryRepository + { + void EnsureIndexes(); + + /// Prüft, ob ein Eintrag für Trader + Token im Zeitfenster existiert (Dedup). + bool Exists(int traderId, string tokenId, DateTime windowStart, DateTime windowEnd); + + void Insert(MasterTraderHistoryRecord record); + + List GetByTraderSince(int traderId, DateTime since); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/ITrackedTraderRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/ITrackedTraderRepository.cs new file mode 100644 index 0000000..ee12590 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/ITrackedTraderRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence +{ + /// + /// Persistenz der kopierten Master-Trader (Collection/Tabelle der Tracker). + /// Konsolidiert die frühere Inkonsistenz (trackers vs. tracked_traders) auf EINE Quelle. + /// + public interface ITrackedTraderRepository + { + List GetAll(); + void Upsert(TrackedTrader trader); + void Update(TrackedTrader trader); + void Delete(int id); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoMasterTraderHistoryRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoMasterTraderHistoryRepository.cs new file mode 100644 index 0000000..f39a3d5 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoMasterTraderHistoryRepository.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Mongo +{ + public class MongoMasterTraderHistoryRepository : IMasterTraderHistoryRepository + { + private readonly IMongoCollection _col; + + public MongoMasterTraderHistoryRepository(IMongoDatabase db) + { + _col = db.GetCollection("mt_history"); + } + + public void EnsureIndexes() + { + try + { + _col.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.TraderId))); + _col.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.ClosedAt))); + } + catch { } + } + + public bool Exists(int traderId, string tokenId, DateTime windowStart, DateTime windowEnd) => + _col.Find(x => x.TraderId == traderId && x.TokenId == tokenId + && x.ClosedAt >= windowStart && x.ClosedAt <= windowEnd).Any(); + + public void Insert(MasterTraderHistoryRecord record) => _col.InsertOne(record); + + public List GetByTraderSince(int traderId, DateTime since) => + _col.Find(x => x.TraderId == traderId && x.ClosedAt >= since).ToList(); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoTrackedTraderRepository.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoTrackedTraderRepository.cs new file mode 100644 index 0000000..f408956 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Mongo/MongoTrackedTraderRepository.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Persistence.Mongo +{ + public class MongoTrackedTraderRepository : ITrackedTraderRepository + { + private readonly IMongoCollection _col; + + public MongoTrackedTraderRepository(IMongoDatabase db) + { + _col = db.GetCollection("trackers"); + } + + public List GetAll() => _col.Find(_ => true).ToList(); + + public void Upsert(TrackedTrader trader) => + _col.ReplaceOne(x => x.Id == trader.Id, trader, new ReplaceOptions { IsUpsert = true }); + + public void Update(TrackedTrader trader) => _col.ReplaceOne(x => x.Id == trader.Id, trader); + + public void Delete(int id) => _col.DeleteOne(x => x.Id == id); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj b/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj index fa9046a..b691065 100644 --- a/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj +++ b/src/PolyTrader.Modules.CopyTrading/PolyTrader.Modules.CopyTrading.csproj @@ -4,6 +4,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + net8.0-windows enable