From e21a3515d1dba650f7aec0f77f86c3a0d7dcd64a Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 5 Jul 2026 14:36:36 +0200 Subject: [PATCH] Phase 6 (Stufe 4): einmalige Config-Migration Mongo -> MySQL - ConfigMigrator: kopiert Accounts, aktive Markets, Master-Trader, Copytrading-Settings (mit Fallback auf Alt-Felder im accounts-Dokument) und Demo-Positionen; Historie startet leer. Idempotent via Upserts. - Getrennte Mini-Container fuer Quelle (Mongo) und Ziel (EF/MySQL), unabhaengig vom Provider-Toggle. - Aufruf: PolyTrader.App --migrate-config (kein UI-Start). - Erstlauf: 2344 Markets migriert; Accounts/Trader/Settings in lokaler Mongo leer (nichts zu migrieren). Co-Authored-By: Claude Opus 4.8 --- Program.cs | 32 +++++++- services/ConfigMigrator.cs | 157 +++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 services/ConfigMigrator.cs diff --git a/Program.cs b/Program.cs index dd92633..9f4958f 100644 --- a/Program.cs +++ b/Program.cs @@ -4,6 +4,7 @@ using PolyTraderSharp.Extensions; using System.Net.Http; using System.Threading.Channels; using System.Windows.Forms; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -23,8 +24,15 @@ internal static class Program public static IHost? AppHost { get; private set; } [STAThread] - private static void Main() + private static void Main(string[] args) { + // Einmalige Config-Migration Mongo -> MySQL (Phase 6, Stufe 4). Kein UI-Start. + if (args.Length > 0 && string.Equals(args[0], "--migrate-config", StringComparison.OrdinalIgnoreCase)) + { + RunConfigMigration(); + return; + } + ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List @@ -208,4 +216,26 @@ internal static class Program Application.Run(launcher); AppHost.StopAsync().GetAwaiter().GetResult(); } + + private static void RunConfigMigration() + { + var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("appsettings.Local.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + var mongoConn = config["Database:ConnectionString"] ?? "mongodb://localhost:27017"; + var mongoDbName = config["Database:DatabaseName"] ?? "PolyTraderDB"; + var mySql = config["Database:MySqlConnectionString"] ?? string.Empty; + + if (string.IsNullOrWhiteSpace(mySql)) + { + Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json)."); + return; + } + + Services.ConfigMigrator.Run(mongoConn, mongoDbName, mySql); + } } diff --git a/services/ConfigMigrator.cs b/services/ConfigMigrator.cs new file mode 100644 index 0000000..1a0e0df --- /dev/null +++ b/services/ConfigMigrator.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Driver; +using PolyTrader.Core.Persistence; +using PolyTrader.Core.Persistence.Ef; +using PolyTrader.Core.Persistence.Mongo; +using PolyTrader.Modules.CopyTrading.Persistence; +using PolyTrader.Modules.CopyTrading.Persistence.Ef; +using PolyTrader.Modules.CopyTrading.Persistence.Mongo; +using PolyTraderSharp.Models; + +namespace PolyTraderSharp.Services +{ + /// + /// Einmalige Config-Migration Mongo → MySQL (Phase 6, Stufe 4). + /// Kopiert NUR Konfiguration: Accounts, aktive Markets, Master-Trader, + /// Copytrading-Account-Settings (inkl. Fallback auf Alt-Felder im accounts-Dokument) + /// und Demo-Positionen (reines App-State). Handels-/Master-Trader-Historie startet leer. + /// + /// Aufruf: PolyTraderSharp.exe --migrate-config (kein UI-Start). + /// Liest die Mongo-Verbindung aus Database:ConnectionString/DatabaseName, die + /// MySQL-Verbindung aus Database:MySqlConnectionString (appsettings[.Local].json). + /// Idempotent über Upserts (mehrfacher Aufruf überschreibt, dupliziert nicht). + /// + public static class ConfigMigrator + { + public static void Run(string mongoConnection, string mongoDbName, string mySqlConnection) + { + Console.WriteLine("=== Config-Migration Mongo -> MySQL ==="); + Console.WriteLine($"Quelle (Mongo): {mongoDbName} @ {mongoConnection}"); + Console.WriteLine($"Ziel (MySQL): {Redact(mySqlConnection)}"); + + // --- Quelle: Mongo --- + var mongoDb = new MongoClient(mongoConnection).GetDatabase(mongoDbName); + var srcAccounts = new MongoAccountRepository(mongoDb); + var srcMarkets = new MongoMarketRepository(mongoDb); + var srcPositions = new MongoPositionRepository(mongoDb); + var srcTraders = new MongoTrackedTraderRepository(mongoDb); + var srcSettings = new MongoCopyTradingAccountSettingsRepository(mongoDb); + var rawAccounts = mongoDb.GetCollection("accounts"); + + // --- Ziel: MySQL/EF (eigene Mini-Container für die DbContext-Factories) --- + var target = new ServiceCollection() + .AddDbContextFactory(o => o.UseMySql(mySqlConnection, ServerVersion.AutoDetect(mySqlConnection))) + .AddDbContextFactory(o => o.UseMySql(mySqlConnection, ServerVersion.AutoDetect(mySqlConnection))) + .BuildServiceProvider(); + + var coreFactory = target.GetRequiredService>(); + var ctFactory = target.GetRequiredService>(); + + IAccountRepository dstAccounts = new EfAccountRepository(coreFactory); + IMarketRepository dstMarkets = new EfMarketRepository(coreFactory); + IPositionRepository dstPositions = new EfPositionRepository(coreFactory); + ITrackedTraderRepository dstTraders = new EfTrackedTraderRepository(ctFactory); + ICopyTradingAccountSettingsRepository dstSettings = new EfCopyTradingAccountSettingsRepository(ctFactory); + + dstMarkets.EnsureIndexes(); + + // 1) Accounts + var accounts = srcAccounts.GetAll(); + foreach (var acc in accounts) + dstAccounts.Upsert(acc); + Console.WriteLine($"[OK] Accounts: {accounts.Count}"); + + // 2) Aktive Markets + var markets = srcMarkets.GetActive(); + foreach (var m in markets) + dstMarkets.Upsert(m); + Console.WriteLine($"[OK] Markets (aktiv): {markets.Count}"); + + // 3) Master-Trader + var traders = srcTraders.GetAll(); + foreach (var t in traders) + dstTraders.Upsert(t); + Console.WriteLine($"[OK] Master-Trader: {traders.Count}"); + + // 4) Copytrading-Account-Settings (mit Fallback auf Alt-Felder) + int settingsCount = MigrateSettings(accounts, srcSettings, dstSettings, rawAccounts); + Console.WriteLine($"[OK] Copytrading-Settings: {settingsCount}"); + + // 5) Demo-Positionen (reines App-State; Live-Positionen werden aus der Chain rekonstruiert) + int demoPos = 0; + foreach (var acc in accounts.Where(a => a.IsDemo)) + { + foreach (var pos in srcPositions.GetDemo(acc.AccountId)) + { + dstPositions.UpsertDemo(acc.AccountId, pos); + demoPos++; + } + } + Console.WriteLine($"[OK] Demo-Positionen: {demoPos}"); + + Console.WriteLine("=== Migration abgeschlossen ==="); + } + + private static int MigrateSettings( + List accounts, + ICopyTradingAccountSettingsRepository src, + ICopyTradingAccountSettingsRepository dst, + IMongoCollection rawAccounts) + { + var existing = src.GetAll().ToDictionary(s => s.AccountId); + int count = 0; + + foreach (var acc in accounts) + { + if (existing.TryGetValue(acc.AccountId, out var s)) + { + dst.Upsert(s); + count++; + continue; + } + + var settings = new CopyTradingAccountSettings { AccountId = acc.AccountId }; + try + { + var raw = rawAccounts.Find(Builders.Filter.Eq("_id", acc.AccountId)).FirstOrDefault(); + if (raw != null) + { + settings.PerMarketLimit = Dec(raw, "PerMarketLimit", settings.PerMarketLimit); + settings.MaxPriceDifference = Dec(raw, "MaxPriceDifference", settings.MaxPriceDifference); + settings.MaxBuyPrice = Dec(raw, "MaxBuyPrice", settings.MaxBuyPrice); + settings.ProfitTarget = Dec(raw, "ProfitTarget", settings.ProfitTarget); + settings.PreRedeemLimit = Dec(raw, "PreRedeemLimit", settings.PreRedeemLimit); + settings.PerMasterLimit = Dec(raw, "PerMasterLimit", settings.PerMasterLimit); + settings.perMaxTime6h = Dec(raw, "perMaxTime6h", settings.perMaxTime6h); + settings.perMaxTime24h = Dec(raw, "perMaxTime24h", settings.perMaxTime24h); + settings.perMaxTime72h = Dec(raw, "perMaxTime72h", settings.perMaxTime72h); + settings.perMaxTimeNone = Dec(raw, "perMaxTimeNone", settings.perMaxTimeNone); + } + } + catch { } + + dst.Upsert(settings); + count++; + } + + return count; + } + + private static decimal Dec(BsonDocument doc, string field, decimal fallback) + { + if (doc.TryGetValue(field, out var v)) + { + try { return v.ToDecimal(); } catch { } + } + return fallback; + } + + private static string Redact(string conn) => + System.Text.RegularExpressions.Regex.Replace(conn, "(?i)(password=)[^;]*", "$1***"); + } +}