diff --git a/.gitignore b/.gitignore index f9a4c33..a6704b8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ server_settings.xml appsettings.*.json !appsettings.json +# Mongo-Exporte (enthalten Secrets: PrivateKey, ApiSecret) – niemals committen +MongoDB/ + # ── Logs & temporäre Dateien ───────────────────── *.log *.tmp diff --git a/Program.cs b/Program.cs index 9f4958f..e26fda6 100644 --- a/Program.cs +++ b/Program.cs @@ -33,6 +33,14 @@ internal static class Program return; } + // Config-Migration aus mongoexport-JSON (Ordner, Default "MongoDB") -> MySQL. + if (args.Length > 0 && string.Equals(args[0], "--migrate-json", StringComparison.OrdinalIgnoreCase)) + { + var folder = args.Length > 1 ? args[1] : "MongoDB"; + RunConfigMigrationFromJson(folder); + return; + } + ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List @@ -238,4 +246,26 @@ internal static class Program Services.ConfigMigrator.Run(mongoConn, mongoDbName, mySql); } + + private static void RunConfigMigrationFromJson(string folder) + { + 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 mySql = config["Database:MySqlConnectionString"] ?? string.Empty; + if (string.IsNullOrWhiteSpace(mySql)) + { + Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json)."); + return; + } + + if (!System.IO.Path.IsPathRooted(folder)) + folder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), folder); + + Services.ConfigMigrator.RunFromJson(folder, mySql); + } } diff --git a/services/ConfigMigrator.cs b/services/ConfigMigrator.cs index 1a0e0df..e2e0816 100644 --- a/services/ConfigMigrator.cs +++ b/services/ConfigMigrator.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; @@ -97,6 +100,181 @@ namespace PolyTraderSharp.Services Console.WriteLine("=== Migration abgeschlossen ==="); } + /// + /// Config-Migration aus mongoexport-JSON (Ordner mit PolyTraderDB.accounts.json / + /// .trackers.json) → MySQL. Markets bleiben aussen vor (bereits migriert und werden + /// vom MarketSync ohnehin laufend aktualisiert). Idempotent via Upserts. + /// + public static void RunFromJson(string folder, string mySqlConnection) + { + Console.WriteLine("=== Config-Migration JSON-Export -> MySQL ==="); + Console.WriteLine($"Quelle: {folder}"); + Console.WriteLine($"Ziel (MySQL): {Redact(mySqlConnection)}"); + + 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); + ITrackedTraderRepository dstTraders = new EfTrackedTraderRepository(ctFactory); + ICopyTradingAccountSettingsRepository dstSettings = new EfCopyTradingAccountSettingsRepository(ctFactory); + + // 1) Accounts + Copytrading-Settings (Limit-Felder haengen im Alt-Dokument am Account) + var accPath = Path.Combine(folder, "PolyTraderDB.accounts.json"); + int accCount = 0, setCount = 0; + if (File.Exists(accPath)) + { + using var doc = JsonDocument.Parse(File.ReadAllText(accPath)); + foreach (var el in doc.RootElement.EnumerateArray()) + { + var acc = new AccountState + { + AccountId = el.GetProperty("_id").GetInt32(), + Name = Str(el, "Name"), + WalletAddress = Str(el, "WalletAddress"), + ApiKey = Str(el, "ApiKey"), + ApiSecret = Str(el, "ApiSecret"), + ApiPassphrase = Str(el, "ApiPassphrase"), + PrivateKey = Str(el, "PrivateKey"), + IsDemo = Bool(el, "IsDemo"), + IsActive = Bool(el, "IsActive", true), + CloseOnlyMode = Bool(el, "CloseOnlyMode"), + PayoutAddress = Str(el, "PayoutAddress"), + PayoutLimitUsd = Dec(el, "PayoutLimitUsd", 0m), + TotalBalance = Dec(el, "TotalBalance", 0m), + AvailableBalance = Dec(el, "AvailableBalance", 0m), + HasOpenLimitOrders = Bool(el, "HasOpenLimitOrders") + }; + dstAccounts.Upsert(acc); + accCount++; + + var s = new CopyTradingAccountSettings + { + AccountId = acc.AccountId, + PerMarketLimit = Dec(el, "PerMarketLimit", 5.0m), + MaxPriceDifference = Dec(el, "MaxPriceDifference", 2.0m), + MaxBuyPrice = Dec(el, "MaxBuyPrice", 0.98m), + ProfitTarget = Dec(el, "ProfitTarget", 50.0m), + PreRedeemLimit = Dec(el, "PreRedeemLimit", 0.0m), + PerMasterLimit = Dec(el, "PerMasterLimit", 10.0m), + perMaxTime6h = Dec(el, "perMaxTime6h", 20.0m), + perMaxTime24h = Dec(el, "perMaxTime24h", 20.0m), + perMaxTime72h = Dec(el, "perMaxTime72h", 20.0m), + perMaxTimeNone = Dec(el, "perMaxTimeNone", 40.0m) + }; + dstSettings.Upsert(s); + setCount++; + } + } + else + { + Console.WriteLine($"[WARN] Nicht gefunden: {accPath}"); + } + Console.WriteLine($"[OK] Accounts: {accCount} | Copytrading-Settings: {setCount}"); + + // 2) Master-Trader + var trPath = Path.Combine(folder, "PolyTraderDB.trackers.json"); + int trCount = 0; + if (File.Exists(trPath)) + { + using var doc = JsonDocument.Parse(File.ReadAllText(trPath)); + foreach (var el in doc.RootElement.EnumerateArray()) + { + var t = new TrackedTrader + { + Id = el.GetProperty("_id").GetInt32(), + WalletAddress = Str(el, "WalletAddress"), + DisplayName = Str(el, "DisplayName"), + Category = Str(el, "Category", "NEW_BIG_BET"), + Description = Str(el, "Description"), + Reasoning = Str(el, "Reasoning"), + IsActive = Bool(el, "IsActive", true), + IsHidden = Bool(el, "IsHidden"), + TotalTrades = Int(el, "TotalTrades"), + WinningTrades = Int(el, "WinningTrades"), + Winrate30t = Dbl(el, "Winrate30t"), + TotalPnl = Dbl(el, "TotalPnl"), + AssignedAccountIds = IntSet(el, "AssignedAccountIds") + }; + dstTraders.Upsert(t); + trCount++; + } + } + else + { + Console.WriteLine($"[WARN] Nicht gefunden: {trPath}"); + } + Console.WriteLine($"[OK] Master-Trader: {trCount}"); + Console.WriteLine("[INFO] Markets uebersprungen (bereits migriert / MarketSync aktualisiert laufend)."); + Console.WriteLine("=== Migration abgeschlossen ==="); + } + + // --- JSON-Helfer (mongoexport: Dezimalwerte als Strings, null moeglich) --- + private static string Str(JsonElement el, string name, string fallback = "") + { + if (el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String) + return v.GetString() ?? fallback; + return fallback; + } + + private static bool Bool(JsonElement el, string name, bool fallback = false) + { + if (el.TryGetProperty(name, out var v)) + { + if (v.ValueKind == JsonValueKind.True) return true; + if (v.ValueKind == JsonValueKind.False) return false; + } + return fallback; + } + + private static int Int(JsonElement el, string name, int fallback = 0) + { + if (el.TryGetProperty(name, out var v)) + { + if (v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n)) return n; + if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var s)) return s; + } + return fallback; + } + + private static double Dbl(JsonElement el, string name, double fallback = 0) + { + if (el.TryGetProperty(name, out var v)) + { + if (v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out var n)) return n; + if (v.ValueKind == JsonValueKind.String && double.TryParse(v.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var s)) return s; + } + return fallback; + } + + private static decimal Dec(JsonElement el, string name, decimal fallback) + { + if (el.TryGetProperty(name, out var v)) + { + if (v.ValueKind == JsonValueKind.Number && v.TryGetDecimal(out var n)) return n; + if (v.ValueKind == JsonValueKind.String && decimal.TryParse(v.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var s)) return s; + } + return fallback; + } + + private static HashSet IntSet(JsonElement el, string name) + { + var set = new HashSet(); + if (el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Array) + { + foreach (var item in v.EnumerateArray()) + { + if (item.ValueKind == JsonValueKind.Number && item.TryGetInt32(out var n)) set.Add(n); + else if (item.ValueKind == JsonValueKind.String && int.TryParse(item.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var s)) set.Add(s); + } + } + return set; + } + private static int MigrateSettings( List accounts, ICopyTradingAccountSettingsRepository src,