using System; using MongoDB.Driver; using PolyTraderSharp.Extensions; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows.Forms; using System.ComponentModel; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTraderSharp { public partial class frm_main : Form { private readonly TradingState _tradingState; private readonly TerminalLogger _logger; private readonly ThreemaService _threemaService; private readonly MullvadVpnService _vpnService; private ServerSettings _serverSettings = null!; private readonly string _settingsFilePath = "server_settings.xml"; // UI Bindings private BindingSource _bsSlaves = new BindingSource(); private BindingSource _bsMasters = new BindingSource(); private BindingList _dashboardList = new BindingList(); private readonly System.Collections.Concurrent.ConcurrentQueue _logQueue = new System.Collections.Concurrent.ConcurrentQueue(); private System.Windows.Forms.Timer _uiLogTimer = null!; private List GetActivePositions(AccountState acc) { var activeList = new List(); foreach (var p in acc.OpenPositions.Values) { if (_tradingState.MarketCache.TryGetValue(p.TokenId, out var md)) { if (!md.Closed) activeList.Add(p); } else { // Fallback pass-through if completely unknown activeList.Add(p); } } return activeList; } private System.Windows.Forms.Timer _dashboardTimer = null!; private System.Windows.Forms.Timer _metricsTimer = null!; private System.Windows.Forms.Timer _backupTimer = null!; private System.Windows.Forms.Timer _threemaReportTimer = null!; private readonly IMongoDatabase _db; private readonly PolymarketApiService _api; private readonly PolymarketClobClient _clob; private readonly JobManager _jobManager; private DateTimePicker _dtpClosedTrades; private bool _terminalAutoScroll = true; public frm_main(TradingState tradingState, TerminalLogger logger, ThreemaService threemaService, MullvadVpnService vpnService, IMongoDatabase db, PolymarketApiService api, PolymarketClobClient clob, JobManager jobManager) { InitializeComponent(); _tradingState = tradingState; _logger = logger; _threemaService = threemaService; _vpnService = vpnService; _db = db; _api = api; _clob = clob; _jobManager = jobManager; _logger.OnLogMessage += Logger_OnLogMessage; _threemaService.OnCommandReceived += ThreemaService_OnCommandReceived; dgv_openTrades.CellContentClick += DgvOpenTrades_CellContentClick; dgv_closedTrades.CellContentClick += DgvTrades_CellContentClick; dgv_jobs.CellContentClick += DgvJobs_CellContentClick; if (btn_debugLiteDB != null) btn_debugLiteDB.Click += Btn_debugLiteDB_Click; btn_autoscroll.Click += BtnAutoscroll_Click; btn_autoscroll.BackColor = System.Drawing.Color.LightGreen; rtb_Terminal.BackColor = System.Drawing.Color.Black; rtb_Terminal.ForeColor = System.Drawing.Color.White; btn_cleanMasterTraders.Click += BtnCleanMasterTraders_Click; // Setup DatePicker for Closed Trades ToolStrip _dtpClosedTrades = new DateTimePicker(); _dtpClosedTrades.Format = DateTimePickerFormat.Short; _dtpClosedTrades.Value = DateTime.Today; _dtpClosedTrades.Width = 120; _dtpClosedTrades.ValueChanged += (s, ev) => RefreshClosedTrades(); var host = new ToolStripControlHost(_dtpClosedTrades); toolStrip_closedtrades.Items.Add(new ToolStripSeparator()); toolStrip_closedtrades.Items.Add(new ToolStripLabel("Datum:")); toolStrip_closedtrades.Items.Add(host); _uiLogTimer = new System.Windows.Forms.Timer { Interval = 250 }; _uiLogTimer.Tick += ProcessLogQueue; _uiLogTimer.Start(); } private void Logger_OnLogMessage(object? sender, LogMessageEventArgs e) { _logQueue.Enqueue(e); } private void ProcessLogQueue(object? sender, EventArgs e) { if (_logQueue.IsEmpty || !IsHandleCreated) return; string filter = "Alle"; if (cb_terminalLogLevel.ComboBox != null && cb_terminalLogLevel.ComboBox.InvokeRequired) { filter = (string)cb_terminalLogLevel.ComboBox.Invoke(new Func(() => cb_terminalLogLevel.SelectedItem?.ToString() ?? "Alle")); } else if (cb_terminalLogLevel.SelectedItem != null) { filter = cb_terminalLogLevel.SelectedItem.ToString() ?? "Alle"; } TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); bool appended = false; int maxProcess = 500; int count = 0; rtb_Terminal.SuspendLayout(); while (count < maxProcess && _logQueue.TryDequeue(out var logEvent)) { count++; if (filter != "Alle" && logEvent.Level.ToString() != filter) continue; DateTime logTime = logEvent.Timestamp.Kind == DateTimeKind.Utc ? TimeZoneInfo.ConvertTimeFromUtc(logEvent.Timestamp, berlinTz) : TimeZoneInfo.ConvertTime(logEvent.Timestamp, berlinTz); string timeStr = $"[{logTime:HH:mm:ss}]"; System.Drawing.Color c = System.Drawing.Color.White; if (logEvent.Level == LogLevel.Error) c = System.Drawing.Color.Red; else if (logEvent.Level == LogLevel.Warning) c = System.Drawing.Color.Yellow; else if (logEvent.Level == LogLevel.Trade) c = System.Drawing.Color.LightGreen; else if (logEvent.Level == LogLevel.TradeReasoning) c = System.Drawing.Color.Orange; rtb_Terminal.SelectionStart = rtb_Terminal.TextLength; rtb_Terminal.SelectionLength = 0; rtb_Terminal.SelectionColor = c; rtb_Terminal.AppendText($"{timeStr} [{logEvent.Level}] {logEvent.Message}\n"); appended = true; } if (appended) { if (rtb_Terminal.TextLength > 80000) { rtb_Terminal.Clear(); rtb_Terminal.SelectionColor = System.Drawing.Color.LightPink; rtb_Terminal.AppendText($"[{DateTime.Now:HH:mm:ss}] [System] Terminal Auto-Clear (RAM Limit erreicht). Vollständige Logs befinden sich im Order /Logs.\n"); } if (_terminalAutoScroll) { rtb_Terminal.ScrollToCaret(); } } rtb_Terminal.ResumeLayout(); } private void AppendToTerminal(LogMessageEventArgs e) { _logQueue.Enqueue(e); } private void BtnAutoscroll_Click(object? sender, EventArgs e) { _terminalAutoScroll = !_terminalAutoScroll; btn_autoscroll.BackColor = _terminalAutoScroll ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; btn_autoscroll.Text = _terminalAutoScroll ? "Stop Autoscroll" : "Start Autoscroll"; } private void BtnCleanMasterTraders_Click(object? sender, EventArgs e) { int clearedCount = 0; var validAccountIds = _tradingState.Accounts.Keys.ToList(); foreach (var trader in _tradingState.Traders.Values) { var orphanedIds = trader.AssignedAccountIds.Where(id => !validAccountIds.Contains(id)).ToList(); if (orphanedIds.Count > 0) { foreach (var badId in orphanedIds) { trader.AssignedAccountIds.Remove(badId); } _db.GetCollection("traders").Update(trader); clearedCount += orphanedIds.Count; } } MessageBox.Show($"Zuweisung entfernt: {clearedCount} verwaiste Master-Trader Verknüpfungen wurden erfolgreich bereinigt.", "Bereinigung abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information); // Reload grid if master traders UI logic requires it if (dgv_masterTraders.DataSource is BindingSource bs) bs.ResetBindings(false); } private void ThreemaService_OnCommandReceived(string command) { if (!IsHandleCreated) return; Invoke(new Action(() => { string cmd = command.Trim().ToLower(); if (cmd == "/stop") { _tradingState.LiveTradingMode = TradingMode.Inactive; _tradingState.DemoTradingMode = TradingMode.Inactive; UpdateToggleButtons(); _logger.Warning("🛑 THREEMA COMMAND: Trading wurde komplett GESTOPPT!"); _ = _threemaService.SendMessageAsync("🛑 Trading Gestoppt\nAlle Live und Demo Aktivitäten wurden deaktiviert."); } else if (cmd == "/abverkauf") { _tradingState.LiveTradingMode = TradingMode.SellOnly; _tradingState.DemoTradingMode = TradingMode.SellOnly; UpdateToggleButtons(); _logger.Warning("📉 THREEMA COMMAND: Trading auf ABVERKAUF gesetzt!"); _ = _threemaService.SendMessageAsync("📉 Abverkauf Modus\nAlle Accounts dürfen nur noch offene Positionen verkaufen. Neukäufe sind blockiert."); } else if (cmd == "/bericht") { _logger.Info("📊 THREEMA COMMAND: Manueller Bericht angefordert."); _ = SendThreemaReport(); } })); } private async Task SendThreemaReport() { var timeAgo = DateTime.UtcNow.AddHours(-24); decimal totalPnl = 0; var sb = new System.Text.StringBuilder(); sb.AppendLine($"📊 PolyTrader — 24h Slave Bericht"); sb.AppendLine($"{DateTime.UtcNow:dd.MM.yyyy HH:mm} UTC"); sb.AppendLine(""); try { var allClosed = await Task.Run(() => _db.GetCollection("closed_trades").LiteFind(t => t.ClosedAt >= timeAgo).ToList()); foreach (var acc in _tradingState.Accounts.Values.OrderBy(a => a.IsDemo ? 0 : 1)) { if (!acc.IsActive) continue; var accClosed = allClosed.Where(t => t.AccountId == acc.AccountId).ToList(); decimal periodPnl = accClosed.Sum(t => t.RealizedPnl); totalPnl += periodPnl; int winning = accClosed.Count(t => t.RealizedPnl > 0); double winRate = accClosed.Count > 0 ? (winning * 100.0 / accClosed.Count) : 0; int openCount = acc.OpenPositions.Count; // Total Portfolio Balance = Liquid + Invested decimal balance = acc.AvailableBalance + acc.OpenPositions.Values.Sum(p => p.AmountUsd); string modeIcon = acc.IsDemo ? "🧪" : "🔴"; sb.AppendLine($"{modeIcon} {acc.Name}"); sb.AppendLine($"Balance: ${balance:N2} | P&L: ${(periodPnl > 0 ? "+" : "")}{periodPnl:N2}"); sb.AppendLine($"Winrate: {winRate:F0}% | Trades (24h): {accClosed.Count}"); sb.AppendLine($"Offen: {openCount}"); sb.AppendLine(""); } sb.AppendLine($"📈 Gesamtes P&L (24h): ${(totalPnl > 0 ? "+" : "")}{totalPnl:N2}"); } catch (Exception ex) { _logger.Error($"Fehler beim Erstellen des Threema Berichts: {ex.Message}"); sb.AppendLine("Fehler beim Abrufen der Statistiken."); } await _threemaService.SendMessageAsync(sb.ToString()); } private void RenderTerminalHistory() { rtb_Terminal.Clear(); var history = _logger.GetHistory(TimeSpan.FromHours(6)); foreach (var log in history) { AppendToTerminal(log); } } private void frm_main_Load(object sender, EventArgs e) { dgv_dashboard.AutoGenerateColumns = false; dgv_openTrades.AutoGenerateColumns = false; dgv_closedTrades.AutoGenerateColumns = false; dgv_masterTraders.AutoGenerateColumns = false; dgv_SlaveTraders.AutoGenerateColumns = false; // Setup Dashboard Analytics Grids dgv_dash_toptraders.AutoGenerateColumns = false; col_name.DataPropertyName = "SourceTraderName"; col_Winrate.DataPropertyName = "Winrate30T"; col_Winrate.DefaultCellStyle.Format = "N1"; col_pl.DataPropertyName = "Pnl30T"; col_pl.DefaultCellStyle.Format = "C2"; col_trades.DataPropertyName = "Trades7D"; dgv_dash_floptraders.AutoGenerateColumns = false; dataGridViewLinkColumn1.DataPropertyName = "SourceTraderName"; dataGridViewTextBoxColumn1.DataPropertyName = "Winrate30T"; dataGridViewTextBoxColumn1.DefaultCellStyle.Format = "N1"; dataGridViewTextBoxColumn2.DataPropertyName = "Pnl30T"; dataGridViewTextBoxColumn2.DefaultCellStyle.Format = "C2"; dataGridViewTextBoxColumn3.DataPropertyName = "Trades7D"; dgv_dash_toptraders.CellContentClick += DgvTrades_CellContentClick; dgv_dash_floptraders.CellContentClick += DgvTrades_CellContentClick; // Setup Jobs DataGrid dgv_jobs.AutoGenerateColumns = false; dgv_jobs.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "JobName", HeaderText = "Job Name", Width = 150, ReadOnly = true }); dgv_jobs.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "StatusText", HeaderText = "Status", Width = 150, ReadOnly = true }); dgv_jobs.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "LastRun", HeaderText = "Zuletzt ausgeführt", Width = 130, ReadOnly = true, DefaultCellStyle = new DataGridViewCellStyle { Format = "HH:mm:ss" } }); dgv_jobs.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "NextRun", HeaderText = "Nächster Lauf", Width = 130, ReadOnly = true, DefaultCellStyle = new DataGridViewCellStyle { Format = "HH:mm:ss" } }); var enableCol = new DataGridViewCheckBoxColumn { DataPropertyName = "IsEnabled", HeaderText = "Aktiv", Name = "col_dgv_jobs_Enabled", Width = 60 }; dgv_jobs.Columns.Add(enableCol); var runCol = new DataGridViewButtonColumn { HeaderText = "Aktion", Name = "col_dgv_jobs_Run", Text = "Run Now", UseColumnTextForButtonValue = true, Width = 80 }; dgv_jobs.Columns.Add(runCol); dgv_jobs.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = "Description", HeaderText = "Beschreibung", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, ReadOnly = true }); dgv_jobs.DataSource = _jobManager.Jobs; this.FormClosing += Frm_main_FormClosing; // Setup Terminal Filter cb_terminalLogLevel.Items.Clear(); cb_terminalLogLevel.Items.Add("Alle"); foreach (var lvl in Enum.GetNames(typeof(LogLevel))) { cb_terminalLogLevel.Items.Add(lvl); } cb_terminalLogLevel.SelectedIndex = 0; cb_terminalLogLevel.SelectedIndexChanged += (s, ev) => RenderTerminalHistory(); _serverSettings = ServerSettings.Load(_settingsFilePath); propertyGrid_serversettings.SelectedObject = _serverSettings; // Threema Report Job Setup var reportJob = new JobStatusRow { JobName = "Threema Auto-Report", Description = "Sends the Threema trading summary.", StatusText = "Idle", IsEnabled = _serverSettings.ThreemaEnabled }; reportJob.ManualTriggerAction = async () => { if (!reportJob.IsEnabled) return; reportJob.StatusText = "Sending Report..."; await SendThreemaReport(); reportJob.StatusText = "Idle"; reportJob.LastRun = DateTime.Now; int intervalHours = _serverSettings.ThreemaReportIntervalHours > 0 ? _serverSettings.ThreemaReportIntervalHours : 6; reportJob.NextRun = DateTime.Now.AddHours(intervalHours); }; _jobManager.RegisterJob(reportJob); int initialHours = _serverSettings.ThreemaReportIntervalHours > 0 ? _serverSettings.ThreemaReportIntervalHours : 6; reportJob.NextRun = DateTime.Now.AddHours(initialHours); _threemaReportTimer = new System.Windows.Forms.Timer(); _threemaReportTimer.Interval = 60000; // Check every 1 minute _threemaReportTimer.Tick += (s, ev) => { reportJob.IsEnabled = _serverSettings.ThreemaEnabled; // Keep updated if (DateTime.Now >= reportJob.NextRun) { if (reportJob.IsEnabled) { reportJob.ManualTriggerAction.Invoke(); } else { int currentHours = _serverSettings.ThreemaReportIntervalHours > 0 ? _serverSettings.ThreemaReportIntervalHours : 6; reportJob.NextRun = DateTime.Now.AddHours(currentHours); reportJob.LastRun = DateTime.Now; reportJob.StatusText = "Paused / Disabled"; } } }; _threemaReportTimer.Start(); btn_savesettings.Click -= btn_savesettings_Click; btn_savesettings.Click += btn_savesettings_Click; propertyGrid_slaves.PropertyValueChanged += (s, ev) => { if (propertyGrid_slaves.SelectedObject is AccountState acc) SaveAccount(acc); }; propertyGrid_masters.PropertyValueChanged += (s, ev) => { if (propertyGrid_masters.SelectedObject is TrackedTrader trd) SaveTrader(trd); }; SetupTraderManagementUI(); UpdateToggleButtons(); SetupDashboard(); SetupMetrics(); // Setup new ToolStrip Bindings btn_dashboardRefresh.Click += (s, ev) => RefreshDashboardData(); btn_opentrades_refresh.Click += (s, ev) => RefreshOpenTrades(); btn_closedtrades_refresh.Click += (s, ev) => RefreshClosedTrades(); btn_debug_pollinglog.Checked = _tradingState.DebugPollingLog; btn_debug_pollinglog.Click += (s, ev) => { _tradingState.DebugPollingLog = !_tradingState.DebugPollingLog; btn_debug_pollinglog.Checked = _tradingState.DebugPollingLog; }; btn_debugorderpayload.Checked = _tradingState.DebugOrderPayloadLog; btn_debugorderpayload.Click += (s, ev) => { _tradingState.DebugOrderPayloadLog = !_tradingState.DebugOrderPayloadLog; btn_debugorderpayload.Checked = _tradingState.DebugOrderPayloadLog; }; btn_sixshares.Checked = _tradingState.SixSharesMinimum; btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; btn_sixshares.Click += (s, ev) => { _tradingState.SixSharesMinimum = !_tradingState.SixSharesMinimum; btn_sixshares.Checked = _tradingState.SixSharesMinimum; btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; }; btn_debugMTHistory.Click += async (s, ev) => { var mtJob = _jobManager.Jobs.FirstOrDefault(j => j.JobName == "MasterTrader History"); if (mtJob != null && mtJob.ManualTriggerAction != null) { _logger.Info("📡 Manueller MasterTrader Historien-Download gestartet..."); await mtJob.ManualTriggerAction(); } else { _logger.Warning("MasterTrader History Job nicht gefunden."); } }; UpdateAccountDropdowns(); cb_opentrades_account.SelectedIndexChanged += (s, ev) => RefreshOpenTrades(); cb_closedTradesAccounts.SelectedIndexChanged += (s, ev) => RefreshClosedTrades(); cb_opentrades_laufzeit.SelectedIndexChanged += (s, ev) => RefreshOpenTrades(); // Setup new ExpiryColumn dynamically var expCol = new DataGridViewTextBoxColumn { Name = "col_dgv_openTrades_ExpiryDate", HeaderText = "Ablaufdatum", DataPropertyName = "ExpiryDate" }; dgv_openTrades.Columns.Add(expCol); if (dgv_openTrades.Columns.Contains("col_dgv_openTrades_AmountUsd")) dgv_openTrades.Columns["col_dgv_openTrades_AmountUsd"].HeaderText = "Buy In"; if (dgv_openTrades.Columns.Contains("col_dgv_openTrades_Side")) dgv_openTrades.Columns["col_dgv_openTrades_Side"].Visible = false; try { var execAssembly = System.Reflection.Assembly.GetExecutingAssembly(); var fileInfo = new System.IO.FileInfo(execAssembly.Location); DateTime buildTime = fileInfo.LastWriteTimeUtc; TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); DateTime meszTime = TimeZoneInfo.ConvertTimeFromUtc(buildTime, berlinTz); toolStripStatusLabel_build.Text = $"Build: {meszTime:dd.MM.yyyy HH:mm} MESZ"; } catch { } // Timezone Formatters dgv_closedTrades.CellFormatting += Dgv_CellFormatting_TimeZone; dgv_openTrades.CellFormatting += Dgv_CellFormatting_TimeZone; // Closed Trades PnL row coloring dgv_closedTrades.CellFormatting += Dgv_ClosedTrades_CellFormatting; // VPN Buttons btn_vpnConnect.Click += async (s, ev) => await _vpnService.ConnectAsync(); btn_vpndisconnect.Click += async (s, ev) => await _vpnService.DisconnectAsync(); // Default Inactive overriding snapshot (State-Hydration erfolgt im StartupHydrationService) _tradingState.LiveTradingMode = TradingMode.Inactive; _tradingState.DemoTradingMode = TradingMode.Inactive; UpdateToggleButtons(); // Open Trades Snapshot system LoadOpenTradesSnapshot(); System.Windows.Forms.Timer snapshotTimer = new System.Windows.Forms.Timer { Interval = 30000 }; snapshotTimer.Tick += (s, ev) => SaveOpenTradesSnapshot(); snapshotTimer.Start(); // Auto-Connect VPN if (_serverSettings.VpnEnabled) { _ = Task.Run(() => _vpnService.ConnectAsync()); } // Expiry Dropdown Colors cb_opentrades_laufzeit.ComboBox.DrawMode = DrawMode.OwnerDrawFixed; cb_opentrades_laufzeit.ComboBox.DrawItem += Cb_opentrades_laufzeit_DrawItem; } private void Frm_main_FormClosing(object? sender, FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { var result = MessageBox.Show("Möchten Sie den PolyTrader wirklich beenden?\n\nAlle laufenden Automatisierungen werden gestoppt!", "Sicherheitsabfrage", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result == DialogResult.No) { e.Cancel = true; } } } private void SetupMetrics() { _metricsTimer = new System.Windows.Forms.Timer(); _metricsTimer.Interval = 2000; _metricsTimer.Tick += (s, e) => UpdateStatusStrip(); _metricsTimer.Start(); // LiteDB Backup every 30 minutes _backupTimer = new System.Windows.Forms.Timer(); _backupTimer.Interval = 30 * 60 * 1000; // 30 min _backupTimer.Tick += (s, e) => BackupDatabase(); _backupTimer.Start(); BackupDatabase(); // Initial backup on startup } private async void UpdateStatusStrip() { try { using var process = System.Diagnostics.Process.GetCurrentProcess(); long memoryMb = process.PrivateMemorySize64 / (1024 * 1024); toolStripStatusLabel_cpuram.Text = $"RAM: {memoryMb} MB"; if (_vpnService.IsConnected) { toolStripStatusLabel_vpn.Text = "VPN: Verbunden"; try { toolStripStatusLabel_vpn.Image = Properties.Resources.traffic_lights_green; } catch { } try { btn_vpnConnect.Image = Properties.Resources.traffic_lights_green; } catch { } toolStripStatusLabel_vpn.ForeColor = System.Drawing.Color.Green; } else { toolStripStatusLabel_vpn.Text = "VPN: Getrennt"; try { toolStripStatusLabel_vpn.Image = Properties.Resources.traffic_lights_red; } catch { } try { btn_vpnConnect.Image = Properties.Resources.traffic_lights_red; } catch { } toolStripStatusLabel_vpn.ForeColor = System.Drawing.Color.Red; } int ping = await _api.MeasurePingAsync(); toolStripStatusLabel_ping.Text = ping >= 0 ? $"Ping: {ping} ms" : "Ping: Error"; var rates = _api.GetRateLimitsPerTenSeconds(); var limits = PolymarketApiService.RateLimits; toolStripStatusLabel_ratelimit.Text = $"Activity: {rates["Activity"]}/{limits["Activity"]} · Pos: {rates["Positions"]}/{limits["Positions"]} · Gamma: {rates["Gamma"]}/{limits["Gamma"]} · CLOB: {rates["CLOB"]}/{limits["CLOB"]} (/10s)"; } catch { // Ignore metrics errors } } private void SetupDashboard() { dgv_dashboard.DataSource = _dashboardList; FormatDataGrid(dgv_dashboard); dgv_dashboard.CellFormatting += Dgv_dashboard_CellFormatting; dgv_dashboard.SelectionChanged += Dgv_dashboard_SelectionChanged; _dashboardTimer = new System.Windows.Forms.Timer(); _dashboardTimer.Interval = 300000; // 5 mins _dashboardTimer.Tick += (s, e) => RefreshDashboardData(); _dashboardTimer.Start(); RefreshDashboardData(); } private async void RefreshDashboardData() { if (InvokeRequired) { Invoke(new Action(RefreshDashboardData)); return; } var cutoff24h = DateTime.UtcNow.AddHours(-24); var cutoff7d = DateTime.UtcNow.AddDays(-7); var sortedAccounts = _tradingState.Accounts.Values .OrderBy(a => a.IsDemo) .ThenBy(a => a.AccountId).ToList(); var allRecentTrades = new List(); try { allRecentTrades = await Task.Run(() => { return _db.GetCollection("closed_trades") .LiteFind(t => t.ClosedAt >= cutoff7d).ToList(); }); } catch { } _dashboardList.Clear(); foreach (var acc in sortedAccounts) { if (!acc.IsDemo && !string.IsNullOrEmpty(acc.WalletAddress)) { decimal liveBal = await _clob.GetUsdcBalanceAsync(acc); acc.UpdateBalance(liveBal); } var trades24h = allRecentTrades.Where(t => t.AccountId == acc.AccountId && t.ClosedAt >= cutoff24h).ToList(); var trades7d = allRecentTrades.Where(t => t.AccountId == acc.AccountId).ToList(); var pnl24 = trades24h.Sum(t => t.RealizedPnl); var pnl7 = trades7d.Sum(t => t.RealizedPnl); var wins24 = trades24h.Count(t => t.RealizedPnl > 0); var wins7 = trades7d.Count(t => t.RealizedPnl > 0); var activePositions = GetActivePositions(acc); var row = new DashboardRow { AccountId = acc.AccountId, IsDemo = acc.IsDemo, IsActive = acc.IsActive, AccountName = acc.Name, TotalBalance = Math.Round(acc.TotalBalance, 2), AvailableBalance = Math.Round(acc.AvailableBalance, 2), PositionBalance = Math.Round(activePositions.Sum(p => p.AmountUsd), 2), OpenTradesCount = activePositions.Count, ClosedTrades24h = trades24h.Count, Pnl24h = Math.Round(pnl24, 2), Winrate24h = trades24h.Count > 0 ? $"{(wins24 * 100.0m / trades24h.Count):F1}%" : "0%", ClosedTrades7d = trades7d.Count, Pnl7d = Math.Round(pnl7, 2), Winrate7d = trades7d.Count > 0 ? $"{(wins7 * 100.0m / trades7d.Count):F1}%" : "0%" }; _dashboardList.Add(row); } Dgv_dashboard_SelectionChanged(this, EventArgs.Empty); } private void Dgv_dashboard_CellFormatting(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex >= 0 && e.RowIndex < _dashboardList.Count) { var row = _dashboardList[e.RowIndex]; e.CellStyle!.BackColor = row.IsDemo ? System.Drawing.Color.Aquamarine : System.Drawing.Color.Wheat; var colName = dgv_dashboard.Columns[e.ColumnIndex].Name; if (colName == nameof(DashboardRow.Pnl24h) || colName == nameof(DashboardRow.Pnl7d)) { if (e.Value is decimal val) { e.CellStyle!.ForeColor = val < 0 ? System.Drawing.Color.Red : (val > 0 ? System.Drawing.Color.Green : System.Drawing.Color.Black); } } } } private void Dgv_ClosedTrades_CellFormatting(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0) return; var dgv = sender as DataGridView; if (dgv != null && dgv.Rows[e.RowIndex].DataBoundItem is ClosedTrade ct) { decimal netPnl = ct.RealizedPnl - ct.TotalFees; if (netPnl > 1m) e.CellStyle!.BackColor = System.Drawing.Color.LightGreen; else if (netPnl < -1m) e.CellStyle!.BackColor = System.Drawing.Color.LightCoral; // else stays default (white) } } private void Dgv_dashboard_SelectionChanged(object? sender, EventArgs e) { if (dgv_dashboard_detaillaufzeit != null) dgv_dashboard_detaillaufzeit.Rows.Clear(); if (dgv_permaster != null) dgv_permaster.Rows.Clear(); if (dgv_dashboard.CurrentRow?.DataBoundItem is DashboardRow row) { if (_tradingState.Accounts.TryGetValue(row.AccountId, out var acc)) { var now = DateTime.UtcNow; decimal under6 = 0, under24 = 0, under72 = 0, over72 = 0; var activePositions = GetActivePositions(acc); foreach (var pos in activePositions) { if (pos.ExpiryDate == null) { over72 += pos.AmountUsd; continue; } var diff = pos.ExpiryDate.Value - now; if (diff.TotalHours < 6) under6 += pos.AmountUsd; else if (diff.TotalHours < 24) under24 += pos.AmountUsd; else if (diff.TotalHours < 72) under72 += pos.AmountUsd; else over72 += pos.AmountUsd; } if (dgv_dashboard_detaillaufzeit != null) { decimal SafeMaxUsd(decimal pct, decimal tot) => tot * (pct / 100m); decimal SafeUtil(decimal curr, decimal maxUsd) => maxUsd > 0 ? (curr / maxUsd) * 100m : 0m; decimal max6 = SafeMaxUsd(acc.perMaxTime6h, acc.TotalBalance); dgv_dashboard_detaillaufzeit.Rows.Add( "< 6h", $"{acc.perMaxTime6h:F2}%", $"${max6:F2}", $"{SafeUtil(under6, max6):F2}%", $"${under6:F2}"); decimal max24 = SafeMaxUsd(acc.perMaxTime24h, acc.TotalBalance); dgv_dashboard_detaillaufzeit.Rows.Add( "< 24h", $"{acc.perMaxTime24h:F2}%", $"${max24:F2}", $"{SafeUtil(under24, max24):F2}%", $"${under24:F2}"); decimal max72 = SafeMaxUsd(acc.perMaxTime72h, acc.TotalBalance); dgv_dashboard_detaillaufzeit.Rows.Add( "< 72h", $"{acc.perMaxTime72h:F2}%", $"${max72:F2}", $"{SafeUtil(under72, max72):F2}%", $"${under72:F2}"); decimal maxOver72 = SafeMaxUsd(acc.perMaxTimeNone, acc.TotalBalance); dgv_dashboard_detaillaufzeit.Rows.Add( "> 72h", $"{acc.perMaxTimeNone:F2}%", $"${maxOver72:F2}", $"{SafeUtil(over72, maxOver72):F2}%", $"${over72:F2}"); } if (dgv_permaster != null) { var masterPositions = activePositions .GroupBy(p => p.SourceTraderName) .Select(g => new { Name = string.IsNullOrEmpty(g.Key) ? "Unbekannt" : g.Key, TotalUsd = g.Sum(p => p.AmountUsd) }) .Where(x => x.TotalUsd > 0) .OrderByDescending(x => x.TotalUsd); foreach (var mp in masterPositions) { decimal pct = acc.TotalBalance > 0 ? (mp.TotalUsd / acc.TotalBalance) * 100m : 0m; dgv_permaster.Rows.Add( mp.Name, $"{Math.Round(pct, 2):F2}%", $"${Math.Round(mp.TotalUsd, 2):F2}" ); } } UpdateTraderAnalyticsUI(acc.AccountId); } } } private void UpdateTraderAnalyticsUI(int accountId) { if (_tradingState.TraderAnalyticsCache.TryGetValue(accountId, out var results) && results != null) { // Top Traders (Positive or 0 PnL, descending) var top = results.Where(x => x.Pnl30T >= 0).OrderByDescending(x => x.Pnl30T).ToList(); dgv_dash_toptraders.DataSource = top; // Flop Traders (Negative PnL, ascending) var flop = results.Where(x => x.Pnl30T < 0).OrderBy(x => x.Pnl30T).ToList(); dgv_dash_floptraders.DataSource = flop; } else { dgv_dash_toptraders.DataSource = null; dgv_dash_floptraders.DataSource = null; } } private void UpdateAccountDropdowns() { var accounts = _tradingState.Accounts.Values.OrderBy(a => a.IsDemo).ThenBy(a => a.Name).ToList(); cb_opentrades_account.Items.Clear(); cb_opentrades_account.Items.Add("Alle"); foreach (var acc in accounts) cb_opentrades_account.Items.Add(acc.Name); if (cb_opentrades_account.Items.Count > 0) cb_opentrades_account.SelectedIndex = 0; if (cb_opentrades_laufzeit.Items.Count > 0 && cb_opentrades_laufzeit.SelectedIndex < 0) cb_opentrades_laufzeit.SelectedIndex = 0; cb_closedTradesAccounts.Items.Clear(); cb_closedTradesAccounts.Items.Add("Alle"); foreach (var acc in accounts) cb_closedTradesAccounts.Items.Add(acc.Name); if (cb_closedTradesAccounts.Items.Count > 0) cb_closedTradesAccounts.SelectedIndex = 0; } private void RefreshOpenTrades() { var list = new BindingList(); string filter = cb_opentrades_account.SelectedItem?.ToString() ?? "Alle"; string runtimeFilter = cb_opentrades_laufzeit.SelectedItem?.ToString() ?? "Alle"; foreach (var acc in _tradingState.Accounts.Values) { if (filter != "Alle" && acc.Name != filter) continue; foreach (var pos in acc.OpenPositions.Values) { if (runtimeFilter != "Alle") { if (!pos.ExpiryDate.HasValue) { if (runtimeFilter != "Über 72h" && runtimeFilter != "Über 72h") continue; } else { var rest = pos.ExpiryDate.Value - DateTime.UtcNow; if (runtimeFilter == "Unter 6h" && rest.TotalHours >= 6) continue; if (runtimeFilter == "Unter 24h" && rest.TotalHours >= 24) continue; if (runtimeFilter == "Unter 72h" && rest.TotalHours >= 72) continue; if ((runtimeFilter == "Über 72h" || runtimeFilter == "Über 72h") && rest.TotalHours < 72) continue; } } list.Add(new OpenTradeRow { AccountName = acc.Name, TokenId = pos.TokenId, SourceTraderName = pos.SourceTraderName, SourceTraderAddress = pos.SourceTraderAddress, MarketQuestion = string.IsNullOrEmpty(pos.MarketQuestion) ? pos.ConditionId : pos.MarketQuestion, MarketSlug = pos.MarketSlug, Outcome = pos.Outcome, Side = pos.Side, EntryPrice = pos.EntryPrice, CurrentPrice = pos.CurrentPrice, Size = pos.Size, AmountUsd = pos.AmountUsd, CurrentValueUsd = pos.CurrentValueUsd, ExpiryDate = pos.ExpiryDate }); } } var bs = new BindingSource(); bs.DataSource = list; dgv_openTrades.DataSource = bs; FormatTradeGrid(dgv_openTrades); } private async void RefreshClosedTrades() { string filter = cb_closedTradesAccounts.SelectedItem?.ToString() ?? "Alle"; DateTime selectedDate = _dtpClosedTrades?.Value.Date ?? DateTime.Today; selectedDate = DateTime.SpecifyKind(selectedDate, DateTimeKind.Unspecified); var berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); var utcStart = TimeZoneInfo.ConvertTimeToUtc(selectedDate, berlinTz); var utcEnd = TimeZoneInfo.ConvertTimeToUtc(selectedDate.AddDays(1), berlinTz); var targetAcc = _tradingState.Accounts.Values.FirstOrDefault(a => a.Name == filter); var trades = await Task.Run(() => { var col = _db.GetCollection("closed_trades"); if (filter == "Alle") { return col.LiteFind(x => x.ClosedAt >= utcStart && x.ClosedAt < utcEnd) .OrderByDescending(x => x.ClosedAt) .Take(100).ToList(); } else { if (targetAcc != null) { return col.LiteFind(x => x.AccountId == targetAcc.AccountId && x.ClosedAt >= utcStart && x.ClosedAt < utcEnd) .OrderByDescending(x => x.ClosedAt).Take(100).ToList(); } return new List(); } }); var rowTrades = trades.Select(t => { var r = new ClosedTradeRow { TradeId = t.TradeId, AccountId = t.AccountId, SourceTraderId = t.SourceTraderId, IsDemo = t.IsDemo, TokenId = t.TokenId, MarketSlug = t.MarketSlug, MarketQuestion = t.MarketQuestion, Outcome = t.Outcome, Side = t.Side, EntryPrice = t.EntryPrice, ExitPrice = t.ExitPrice, Size = t.Size, RealizedPnl = t.RealizedPnl, PnlPercent = t.PnlPercent, TotalFees = t.TotalFees, OpenedAt = t.OpenedAt, ClosedAt = t.ClosedAt, ExitReason = t.ExitReason }; r.AccountName = _tradingState.Accounts.TryGetValue(t.AccountId, out var a) ? a.Name : "Unknown"; if (_tradingState.Traders.TryGetValue(t.SourceTraderId, out var mt)) { r.SourceTraderName = mt.DisplayName; r.SourceTraderAddress = mt.WalletAddress; } else { r.SourceTraderName = t.SourceTraderId > 0 ? $"Trader #{t.SourceTraderId}" : "Unbekannt"; } return r; }).ToList(); var bs = new BindingSource(); bs.DataSource = new BindingList(rowTrades); dgv_closedTrades.DataSource = bs; FormatTradeGrid(dgv_closedTrades); } // Hydration des TradingState erfolgt jetzt zentral im StartupHydrationService // (läuft vor den Trading-Services). Die frühere LoadDatabaseAndState()-Methode // wurde entfernt, um doppelte/überschreibende Hydration zu vermeiden. private void SetupTraderManagementUI() { dgv_SlaveTraders.DataSource = _bsSlaves; dgv_SlaveTraders.SelectionChanged += (s, ev) => { propertyGrid_slaves.SelectedObject = _bsSlaves.Current; }; dgv_masterTraders.DataSource = _bsMasters; dgv_masterTraders.SelectionChanged += (s, ev) => { propertyGrid_masters.SelectedObject = _bsMasters.Current; UpdateAssignedAccountsList(); }; // Load initial data RefreshSlaveGrid(); RefreshMasterGrid(); // Format DGV Columns (Hide complex/unwanted stuff) FormatDataGrid(dgv_SlaveTraders); FormatDataGrid(dgv_masterTraders); } private void SaveAccount(AccountState acc) { _db.GetCollection("accounts").Upsert(acc); } private void SaveTrader(TrackedTrader trd) { _db.GetCollection("trackers").Upsert(trd); } private void btn_Slavetraders_add_Click(object sender, EventArgs e) { int newId = _tradingState.Accounts.Count > 0 ? _tradingState.Accounts.Keys.Max() + 1 : 1; var newAcc = new AccountState { AccountId = newId, Name = "Neuer Slave Account" }; _tradingState.Accounts[newAcc.AccountId] = newAcc; SaveAccount(newAcc); RefreshSlaveGrid(); _bsSlaves.Position = _bsSlaves.Count - 1; } private void btn_Slavetraders_del_Click(object? sender, EventArgs e) { if (_bsSlaves.Current is AccountState acc) { var confirm = MessageBox.Show($"Möchten Sie den Slave-Account '{acc.Name}' wirklich löschen?", "Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirm == DialogResult.Yes) { _tradingState.Accounts.TryRemove(acc.AccountId, out _); _db.GetCollection("accounts").DeleteMany(x => x.AccountId == acc.AccountId); RefreshSlaveGrid(); UpdateAccountDropdowns(); } } } private void btn_Mastertraders_add_Click(object? sender, EventArgs e) { int newId = _tradingState.Traders.Count > 0 ? _tradingState.Traders.Keys.Max() + 1 : 1; var newTrd = new TrackedTrader { Id = newId, DisplayName = "Neuer Master", WalletAddress = "0x..." }; _tradingState.Traders[newTrd.Id] = newTrd; SaveTrader(newTrd); RefreshMasterGrid(); _bsMasters.Position = _bsMasters.Count - 1; } private void btn_Mastertraders_del_Click(object? sender, EventArgs e) { if (_bsMasters.Current is TrackedTrader trd) { var confirm = MessageBox.Show($"Möchten Sie den Master-Trader '{trd.DisplayName}' wirklich löschen?", "Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirm == DialogResult.Yes) { _tradingState.Traders.TryRemove(trd.Id, out _); _db.GetCollection("trackers").DeleteMany(x => x.Id == trd.Id); RefreshMasterGrid(); } } } private void UpdateAssignedAccountsList() { clb_assignedAccounts.ItemCheck -= clb_assignedAccounts_ItemCheck; clb_assignedAccounts.Items.Clear(); if (_bsMasters.Current is TrackedTrader trd) { foreach (var acc in _tradingState.Accounts.Values) { bool isChecked = trd.AssignedAccountIds.Contains(acc.AccountId); clb_assignedAccounts.Items.Add(new AccountWrapper(acc), isChecked); } } clb_assignedAccounts.ItemCheck += clb_assignedAccounts_ItemCheck; } private void clb_assignedAccounts_ItemCheck(object? sender, ItemCheckEventArgs e) { if (_bsMasters.Current is TrackedTrader trd) { if (clb_assignedAccounts.Items[e.Index] is AccountWrapper wrapper) { if (e.NewValue == CheckState.Checked) trd.AssignedAccountIds.Add(wrapper.Account.AccountId); else trd.AssignedAccountIds.Remove(wrapper.Account.AccountId); SaveTrader(trd); } } } private void btn_deposit_Click(object? sender, EventArgs e) { if (_bsSlaves.Current is AccountState acc && acc.IsDemo) { if (decimal.TryParse(toolStripTextBox_amount.Text, out decimal amount)) { acc.UpdateBalance(acc.AvailableBalance + amount); SaveAccount(acc); _logger.Info($"[{acc.Name}] {amount} USD eingezahlt. Neuer verfügbarer Saldo: {acc.AvailableBalance}"); RefreshSlaveGrid(); toolStripTextBox_amount.Text = ""; } else { MessageBox.Show("Bitte gib eine gültige Zahl im Buchführungs-Feld ein.", "Warnung", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } else { MessageBox.Show("Ein- / Auszahlungen sind nur für Demo-Konten erlaubt!"); } } private void btn_withdraw_Click(object? sender, EventArgs e) { if (_bsSlaves.Current is AccountState acc && acc.IsDemo) { if (decimal.TryParse(toolStripTextBox_amount.Text, out decimal amount)) { acc.UpdateBalance(acc.AvailableBalance - amount); SaveAccount(acc); _logger.Info($"[{acc.Name}] {amount} USD ausgezahlt. Neuer verfügbarer Saldo: {acc.AvailableBalance}"); RefreshSlaveGrid(); toolStripTextBox_amount.Text = ""; } else { MessageBox.Show("Bitte gib eine gültige Zahl im Buchführungs-Feld ein.", "Warnung", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } else { MessageBox.Show("Ein- / Auszahlungen sind nur für Demo-Konten erlaubt!"); } } private void btn_demoreset_Click(object? sender, EventArgs e) { if (_bsSlaves.Current is AccountState acc) { if (!acc.IsDemo) { MessageBox.Show("Diese Funktion ist nur für Demo-Accounts verfügbar.", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var confirmResult = MessageBox.Show($"Möchten Sie den Demo-Account '{acc.Name}' wirklich komplett zurücksetzen?\nAlle Positionen und generierten Trades dieses Accounts werden unwiderruflich gelöscht. Das Guthaben wird auf $1000 zurückgesetzt.", "Demo Account Reset", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirmResult == DialogResult.Yes) { var previousMode = _tradingState.DemoTradingMode; _tradingState.DemoTradingMode = TradingMode.Inactive; UpdateToggleButtons(); try { acc.OpenPositions.Clear(); acc.TotalBalance = 1000m; acc.AvailableBalance = 1000m; _db.DropCollection($"demo_positions_{acc.AccountId}"); _db.GetCollection("closed_trades").DeleteMany(t => t.AccountId == acc.AccountId); SaveAccount(acc); _logger.Warning($"🔄 Demo-Account '{acc.Name}' wurde komplett zurückgesetzt!"); RefreshSlaveGrid(); RefreshDashboardData(); RefreshOpenTrades(); RefreshClosedTrades(); } catch (Exception ex) { _logger.Error($"Fehler beim Zurücksetzen des Kontos: {ex.Message}"); MessageBox.Show($"Fehler: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { MessageBox.Show($"Rücksetzung von {acc.Name} erfolgreich.", "Erfolgreich", MessageBoxButtons.OK, MessageBoxIcon.Information); _tradingState.DemoTradingMode = previousMode; UpdateToggleButtons(); } } } } private class AccountWrapper { public AccountState Account { get; } public AccountWrapper(AccountState acc) { Account = acc; } public override string ToString() => $"[{Account.AccountId}] {Account.Name} ({(Account.IsDemo ? "DEMO" : "LIVE")})"; } private void FormatDataGrid(DataGridView dgv) { dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; dgv.RowHeadersVisible = false; dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dgv.MultiSelect = false; } private void RefreshSlaveGrid() { var currentPos = _bsSlaves.Position; _bsSlaves.DataSource = _tradingState.Accounts.Values.ToList(); _bsSlaves.ResetBindings(false); if (currentPos >= 0 && currentPos < _bsSlaves.Count) _bsSlaves.Position = currentPos; } private void RefreshMasterGrid() { var currentPos = _bsMasters.Position; _bsMasters.DataSource = _tradingState.Traders.Values.ToList(); _bsMasters.ResetBindings(false); if (currentPos >= 0 && currentPos < _bsMasters.Count) _bsMasters.Position = currentPos; } private void Dgv_CellFormatting_TimeZone(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.Value is DateTime dt) { try { TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); if (dt.Kind == DateTimeKind.Utc) { e.Value = TimeZoneInfo.ConvertTimeFromUtc(dt, berlinTz).ToString("dd.MM.yyyy HH:mm:ss"); e.FormattingApplied = true; } else if (dt.Kind == DateTimeKind.Unspecified) { e.Value = TimeZoneInfo.ConvertTimeFromUtc(DateTime.SpecifyKind(dt, DateTimeKind.Utc), berlinTz).ToString("dd.MM.yyyy HH:mm:ss"); e.FormattingApplied = true; } } catch { } } else if (e.Value is decimal decVal) { e.Value = decVal.ToString("N2"); e.FormattingApplied = true; } if (sender is DataGridView dgv && dgv == dgv_openTrades && e.RowIndex >= 0) { if (dgv.Rows[e.RowIndex].DataBoundItem is OpenTradeRow rowData) { if (rowData.ExpiryDate.HasValue) { var rest = rowData.ExpiryDate.Value - DateTime.UtcNow; if (rest.TotalHours < 6) e.CellStyle!.BackColor = System.Drawing.Color.LightCoral; else if (rest.TotalHours < 24) e.CellStyle!.BackColor = System.Drawing.Color.LightSalmon; else if (rest.TotalHours < 72) e.CellStyle!.BackColor = System.Drawing.Color.LightYellow; else e.CellStyle!.BackColor = System.Drawing.Color.LightGreen; } else { e.CellStyle!.BackColor = System.Drawing.Color.LightGreen; } } } } private void btn_savesettings_Click(object? sender, EventArgs e) { try { _serverSettings.Save(_settingsFilePath); _threemaService.ReloadSettings(); _vpnService.ReloadSettings(); _logger.Info("Server-Einstellungen erfolgreich gespeichert und Services neu geladen."); // Refresh Grids in case property edits need persistence (Mock save here) RefreshSlaveGrid(); RefreshMasterGrid(); MessageBox.Show("Server-Einstellungen & Trader gespeichert.", "Erfolg", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Fehler beim Speichern: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void btn_livetradingactive_Click(object sender, EventArgs e) { // Cycle LiveTradingMode: Inactive -> SellOnly -> Active if (_tradingState.LiveTradingMode == TradingMode.Inactive) _tradingState.LiveTradingMode = TradingMode.SellOnly; else if (_tradingState.LiveTradingMode == TradingMode.SellOnly) _tradingState.LiveTradingMode = TradingMode.Active; else _tradingState.LiveTradingMode = TradingMode.Inactive; UpdateToggleButtons(); } private void btn_demotradingactive_Click(object sender, EventArgs e) { // Cycle DemoTradingMode: Inactive -> SellOnly -> Active if (_tradingState.DemoTradingMode == TradingMode.Inactive) _tradingState.DemoTradingMode = TradingMode.SellOnly; else if (_tradingState.DemoTradingMode == TradingMode.SellOnly) _tradingState.DemoTradingMode = TradingMode.Active; else _tradingState.DemoTradingMode = TradingMode.Inactive; UpdateToggleButtons(); } private void UpdateToggleButtons() { // Update Demo Toggle if (_tradingState.DemoTradingMode == TradingMode.Active) { btn_demotradingactive.Text = "Demotrading (AKTIV)"; btn_demotradingactive.BackColor = System.Drawing.Color.LightGreen; } else if (_tradingState.DemoTradingMode == TradingMode.SellOnly) { btn_demotradingactive.Text = "Demotrading (SELL-ONLY)"; btn_demotradingactive.BackColor = System.Drawing.Color.Orange; } else { btn_demotradingactive.Text = "Demotrading (DEAKTIVIERT)"; btn_demotradingactive.BackColor = System.Drawing.Color.IndianRed; } // Update Live Toggle if (_tradingState.LiveTradingMode == TradingMode.Active) { btn_livetradingactive.Text = "LiveTrading (AKTIV)"; btn_livetradingactive.BackColor = System.Drawing.Color.LightGreen; } else if (_tradingState.LiveTradingMode == TradingMode.SellOnly) { btn_livetradingactive.Text = "LiveTrading (SELL-ONLY)"; btn_livetradingactive.BackColor = System.Drawing.Color.Orange; } else { btn_livetradingactive.Text = "LiveTrading (DEAKTIVIERT)"; btn_livetradingactive.BackColor = System.Drawing.Color.IndianRed; } } private void groupBox1_Enter(object sender, EventArgs e) { } private void DgvOpenTrades_CellContentClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) return; var dgv = sender as DataGridView; if (dgv == null) return; var colName = dgv.Columns[e.ColumnIndex].Name; var row = dgv.Rows[e.RowIndex]; if (colName == "col_dgv_openTrades_CloseBtn") { if (row.DataBoundItem is OpenTradeRow otr) { _ = CloseTradeManuallyAsync(otr); } return; } // Delegate to common link handler DgvTrades_CellContentClick(sender, e); } private async Task CloseTradeManuallyAsync(OpenTradeRow tradeRow) { // Find the account var account = _tradingState.Accounts.Values.FirstOrDefault(a => a.Name == tradeRow.AccountName); if (account == null) { MessageBox.Show($"Account '{tradeRow.AccountName}' nicht gefunden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Confirmation dialog var confirm = MessageBox.Show( $"Möchten Sie diesen Trade wirklich manuell schließen?\n\n" + $"Markt: {tradeRow.MarketQuestion}\n" + $"Konto: {tradeRow.AccountName}\n" + $"Outcome: {tradeRow.Outcome}\n" + $"Einsatz: ${tradeRow.AmountUsd:F2}\n" + $"Shares: {tradeRow.Size:F2} @ ${tradeRow.EntryPrice:F3}", "Trade schließen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirm != DialogResult.Yes) return; if (account.IsDemo) { // Fetch current orderbook price _logger.Info($"📊 Hole aktuellen Orderbuch-Preis für Token {tradeRow.TokenId}..."); var exitPrice = await _api.GetOrderBookPriceAsync(tradeRow.TokenId, "SELL"); if (exitPrice == null || exitPrice <= 0) { var fallbackConfirm = MessageBox.Show( "Kein Orderbuch-Preis verfügbar. Soll der Trade mit Exit-Preis $0.00 (Totalverlust) geschlossen werden?", "Kein Preis", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (fallbackConfirm != DialogResult.Yes) return; exitPrice = 0m; } // Remove from open positions if (account.OpenPositions.TryRemove(tradeRow.TokenId, out var openPos)) { // Delete from LiteDB demo collection _db.GetCollection($"demo_positions_{account.AccountId}").Delete(tradeRow.TokenId); decimal exitUsd = openPos.Size * exitPrice.Value; decimal realizedPnl = exitUsd - openPos.AmountUsd; _tradingState.GlobalPnl += realizedPnl; account.UpdateBalance(account.AvailableBalance + exitUsd); SaveAccount(account); var ct = new ClosedTrade { TradeId = _tradingState.GetNextTradeId(), AccountId = account.AccountId, IsDemo = true, MarketSlug = openPos.MarketSlug, MarketQuestion = openPos.MarketQuestion, TokenId = openPos.TokenId, Outcome = openPos.Outcome, Side = "SELL", EntryPrice = openPos.EntryPrice, ExitPrice = exitPrice.Value, Size = openPos.Size, RealizedPnl = realizedPnl, PnlPercent = openPos.AmountUsd > 0 ? (realizedPnl / openPos.AmountUsd * 100m) : 0m, OpenedAt = openPos.OpenedAt, ClosedAt = DateTime.UtcNow, ExitReason = "Manuell geschlossen" }; _db.GetCollection("closed_trades").Insert(ct); _logger.Trade($"✅ [MANUELL GESCHLOSSEN]\n" + $" Konto: {account.Name}\n" + $" Markt: {openPos.MarketQuestion}\n" + $" Exit: {openPos.Size:F2} Shares @ ${exitPrice.Value:F3}\n" + $" PnL: ${realizedPnl:F2} ({(realizedPnl >= 0 ? "+" : "")}{(openPos.AmountUsd > 0 ? (realizedPnl / openPos.AmountUsd * 100m) : 0m):F1}%)"); MessageBox.Show( $"Trade erfolgreich geschlossen!\n\n" + $"Exit-Preis: ${exitPrice.Value:F3}\n" + $"Realisierter PnL: ${realizedPnl:F2}", "Erfolg", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show("Position konnte nicht gefunden werden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { // Live mode: place FAK SELL via CLOB _logger.Info($"📊 Hole aktuellen Orderbuch-Preis für Token {tradeRow.TokenId}..."); var exitPrice = await _api.GetOrderBookPriceAsync(tradeRow.TokenId, "SELL"); if (exitPrice == null || exitPrice <= 0) { MessageBox.Show("Kein Orderbuch-Preis verfügbar. Live-Verkauf abgebrochen, um Totalverlust zu vermeiden.", "Schutz", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // Polymarket Rejects Orders with Amount = 0. // We use an aggressive limit (e.g. 10% slippage) clamped at 1 cent. decimal sellLimit = Math.Max(0.01m, exitPrice.Value * 0.90m); var liveConfirm = MessageBox.Show( $"Live-Trade manuell schließen?\n\n" + $"Orderbuch-Bid: ${(exitPrice.Value > 0 ? exitPrice.Value.ToString("F3") : "N/A")}\n\n" + $"Achtung: Dies sendet eine MARKET-Order (Mit 10% Slippage Toleranz) um sofortige Ausführung zu erzwingen. " + $"Bei geringer Liquidität kann es zu starker Slippage kommen.", "Live-MARKET-Verkauf bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (liveConfirm != DialogResult.Yes) return; if (account.OpenPositions.TryRemove(tradeRow.TokenId, out var liveOpenPos)) { decimal takerUsdMinimum = liveOpenPos.Size * sellLimit; // Absolute hard floor for Polymarket USDC amounts is 0.01 if (takerUsdMinimum < 0.01m) takerUsdMinimum = 0.01m; bool isNegRisk = false; try { var mkts = _db.GetCollection("markets"); var md = mkts.LiteFind(x => x.ClobTokenIds != null && x.ClobTokenIds.Contains(tradeRow.TokenId)).FirstOrDefault(); if (md != null) isNegRisk = md.NegRisk; } catch { } _logger.Info($"🌐 [MANUELLER VERKAUF] Sende MARKET SELL an Polymarket CTF-Router...\n" + $" Account: {account.Name}\n" + $" Typ: MARKET (Ohne Limit)"); var result = await _clob.PlaceOrderAsync(account, tradeRow.TokenId, "SELL", takerUsdMinimum, sellLimit, "MARKET", false, isNegRisk); if (result == "OK") { decimal exitUsd = liveOpenPos.Size * exitPrice.Value; decimal realizedPnl = exitUsd - liveOpenPos.AmountUsd; _tradingState.GlobalPnl += realizedPnl; account.UpdateBalance(account.AvailableBalance + exitUsd); var ct = new ClosedTrade { TradeId = _tradingState.GetNextTradeId(), AccountId = account.AccountId, IsDemo = false, MarketSlug = liveOpenPos.MarketSlug, MarketQuestion = liveOpenPos.MarketQuestion, TokenId = liveOpenPos.TokenId, Outcome = liveOpenPos.Outcome, Side = "SELL", EntryPrice = liveOpenPos.EntryPrice, ExitPrice = exitPrice.Value, Size = liveOpenPos.Size, RealizedPnl = realizedPnl, PnlPercent = liveOpenPos.AmountUsd > 0 ? (realizedPnl / liveOpenPos.AmountUsd * 100m) : 0m, OpenedAt = liveOpenPos.OpenedAt, ClosedAt = DateTime.UtcNow, ExitReason = "Manuell geschlossen (Live)" }; _db.GetCollection("closed_trades").Insert(ct); _logger.Trade($"✅ [LIVE MANUELL GESCHLOSSEN] - {account.Name} - Exakte Beträge im nächsten API Sync."); MessageBox.Show("Live SELL Order wurde gesendet.\nExakte Beträge werden beim nächsten API Sync aktualisiert.", "Erfolg", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { // Re-add position if sell failed account.OpenPositions.TryAdd(tradeRow.TokenId, liveOpenPos); MessageBox.Show($"Fehler beim Senden der SELL Order:\n{result}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { MessageBox.Show("Position konnte nicht gefunden werden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // Closes Live Mode else block RefreshOpenTrades(); } private void DgvJobs_CellContentClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) return; var dgv = (DataGridView)sender!; var colName = dgv.Columns[e.ColumnIndex].Name; if (dgv.Rows[e.RowIndex].DataBoundItem is JobStatusRow job) { if (colName == "col_dgv_jobs_Run") { if (job.ManualTriggerAction != null) { _ = Task.Run(() => job.ManualTriggerAction.Invoke()); } } else if (colName == "col_dgv_jobs_Enabled") { // Toggle because clicking checkbox doesn't instantly commit to binding source dgv.EndEdit(); } } } private async void DgvTrades_CellContentClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) return; var dgv = sender as DataGridView; if (dgv == null) return; var colName = dgv.Columns[e.ColumnIndex].Name; var row = dgv.Rows[e.RowIndex]; if (colName.Contains("MarketQuestion") || colName.Contains("MarketSlug")) { string slug = ""; string tokenId = ""; if (dgv == dgv_openTrades && row.DataBoundItem is OpenTradeRow otr) { slug = otr.MarketSlug; tokenId = otr.TokenId; } else if (dgv == dgv_closedTrades && row.DataBoundItem is ClosedTrade ct) { slug = ct.MarketSlug; tokenId = ct.TokenId; } // If slug is missing or looks like a ConditionId (0x...), resolve via our markets dictionary if (string.IsNullOrEmpty(slug) || slug.StartsWith("0x") || slug.Length > 200) { try { var mkts = _db.GetCollection("markets"); var md = mkts.LiteFind(x => x.ClobTokenIds != null && x.ClobTokenIds.Contains(tokenId)).FirstOrDefault(); if (md != null && !string.IsNullOrEmpty(md.Slug)) { slug = md.Slug; } } catch { } // Ignore DB lookup errors for UI stability } string eventSlug = await _api.ResolveEventSlugAsync(slug, tokenId); if (!string.IsNullOrEmpty(eventSlug) && !eventSlug.StartsWith("0x")) { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo($"https://polymarket.com/event/{eventSlug}") { UseShellExecute = true }); } } else if (colName.Contains("SourceTraderName") || colName == "col_name" || colName == "dataGridViewLinkColumn1") { string address = ""; if (dgv == dgv_openTrades && row.DataBoundItem is OpenTradeRow otr) address = otr.SourceTraderAddress; else if (dgv == dgv_closedTrades && row.DataBoundItem is ClosedTradeRow ctr) address = ctr.SourceTraderAddress; else if ((dgv == dgv_dash_toptraders || dgv == dgv_dash_floptraders) && row.DataBoundItem is TraderAnalyticsResult tar) address = tar.SourceTraderAddress; if (!string.IsNullOrEmpty(address)) { // If it's a raw wallet address (0x...) route properly if (address.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo($"https://polymarket.com/profile/{address}") { UseShellExecute = true }); else System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo($"https://polymarket.com/profile/{address}") { UseShellExecute = true }); } } } private void Cb_opentrades_laufzeit_DrawItem(object? sender, DrawItemEventArgs e) { if (e.Index < 0) return; e.DrawBackground(); string text = cb_opentrades_laufzeit.Items[e.Index]?.ToString() ?? ""; System.Drawing.Color bgColor = e.BackColor; if (text.Contains("6h")) bgColor = System.Drawing.Color.LightCoral; else if (text.Contains("24h")) bgColor = System.Drawing.Color.LightSalmon; else if (text.Contains("72h") && text.Contains("Unter")) bgColor = System.Drawing.Color.LightYellow; else if (text.Contains("72h") && (text.Contains("Über") || text.Contains("Über"))) bgColor = System.Drawing.Color.LightGreen; using (var brush = new System.Drawing.SolidBrush(bgColor)) { e.Graphics?.FillRectangle(brush, e.Bounds); } using (var textBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black)) { e.Graphics?.DrawString(text, cb_opentrades_laufzeit.Font, textBrush, e.Bounds); } e.DrawFocusRectangle(); } private async void BackupDatabase() { } private void FormatTradeGrid(DataGridView dgv) { FormatDataGrid(dgv); foreach (DataGridViewColumn col in dgv.Columns) { if (col.Name == "MarketQuestion" || col.Name == "SourceTraderName") { if (!(col is DataGridViewLinkColumn)) { var linkCol = new DataGridViewLinkColumn { DataPropertyName = col.DataPropertyName, HeaderText = col.HeaderText, Name = col.Name, ActiveLinkColor = System.Drawing.Color.White, LinkBehavior = LinkBehavior.SystemDefault, LinkColor = System.Drawing.Color.Blue, TrackVisitedState = true, VisitedLinkColor = System.Drawing.Color.Purple }; int idx = col.Index; dgv.Columns.RemoveAt(idx); dgv.Columns.Insert(idx, linkCol); } } } } private void SaveOpenTradesSnapshot() { var backupFile = "open_trades.json"; var data = new Dictionary>(); foreach (var acc in _tradingState.Accounts) data[acc.Key] = acc.Value.OpenPositions; System.IO.File.WriteAllText(backupFile, Newtonsoft.Json.JsonConvert.SerializeObject(data)); } private void LoadOpenTradesSnapshot() { var backupFile = "open_trades.json"; if (System.IO.File.Exists(backupFile)) { try { var json = System.IO.File.ReadAllText(backupFile); var data = Newtonsoft.Json.JsonConvert.DeserializeObject>>(json); if (data != null) { foreach (var kvp in data) { if (_tradingState.Accounts.TryGetValue(kvp.Key, out var acc)) { foreach (var pos in kvp.Value) acc.OpenPositions[pos.Key] = pos.Value; acc.UpdateBalance(acc.AvailableBalance); } } } } catch (Exception ex) { _logger.Error("Error loading open_trades.json: $($ex.Message)"); } } } private void btn_showProgrammFolder_Click(object sender, EventArgs e) { System.Diagnostics.Process.Start("explorer.exe", Environment.CurrentDirectory); } private async void Btn_debugLiteDB_Click(object? sender, EventArgs e) { if (MessageBox.Show("Moechtest du wirklich einen EINMALIGEN Import der alten 'data.db' in die NEUE MongoDB starten? Dies sollte nur genau einmal passieren!", "LiteDB Migration", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; string dbPath = "data.db"; if (!System.IO.File.Exists(dbPath)) { MessageBox.Show("Die Datei data.db wurde im Ordner nicht gefunden!", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } try { _logger.Info("Starte LiteDB -> MongoDB Datenmigration..."); using var litedb = new LiteDB.LiteDatabase($"Filename={dbPath};ReadOnly=true"); var mapper = litedb.Mapper; var accColRaw = litedb.GetCollection("accounts").FindAll().ToList(); var trdColRaw = litedb.GetCollection("trackers").FindAll().ToList(); var mdColRaw = litedb.GetCollection("markets").FindAll().ToList(); var ctColRaw = litedb.GetCollection("closed_trades").FindAll().ToList(); var mtColRaw = litedb.GetCollection("mt_history").FindAll().ToList(); var accCol = accColRaw.Select(d => mapper.ToObject(d)).ToList(); var trdCol = trdColRaw.Select(d => mapper.ToObject(d)).ToList(); var mdCol = mdColRaw.Select(d => mapper.ToObject(d)).ToList(); var ctCol = new System.Collections.Generic.List(); foreach (var doc in ctColRaw) { if (doc.TryGetValue("_id", out var id) && id.IsInt32) doc["_id"] = id.AsInt32.ToString(); try { ctCol.Add(mapper.ToObject(doc)); } catch { } } var mtCol = new System.Collections.Generic.List(); foreach (var doc in mtColRaw) { if (doc.TryGetValue("_id", out var id) && id.IsInt32) doc["_id"] = id.AsInt32.ToString(); try { mtCol.Add(mapper.ToObject(doc)); } catch { } } _logger.Info($"LiteDB Export: {accCol.Count} Accounts, {trdCol.Count} MasterTrader, {mdCol.Count} Markets, {ctCol.Count} Trades, {mtCol.Count} mt_history."); var mAccounts = _db.GetCollection("accounts"); var mTrackers = _db.GetCollection("trackers"); var mMarkets = _db.GetCollection("markets"); var mClosed = _db.GetCollection("closed_trades"); var mMtHist = _db.GetCollection("mt_history"); // Clear Collections completely before migration to ensure no ghost-duplicates _db.DropCollection("accounts"); _db.DropCollection("trackers"); _db.DropCollection("markets"); _db.DropCollection("closed_trades"); _db.DropCollection("mt_history"); // Re-fetch after drop mAccounts = _db.GetCollection("accounts"); mTrackers = _db.GetCollection("trackers"); mMarkets = _db.GetCollection("markets"); mClosed = _db.GetCollection("closed_trades"); mMtHist = _db.GetCollection("mt_history"); foreach (var a in accCol) mAccounts.Upsert(a); foreach (var a in trdCol) mTrackers.Upsert(a); foreach (var a in mdCol) mMarkets.Upsert(a); foreach (var a in ctCol) { try { mClosed.InsertOne(a); } catch { } } foreach (var a in mtCol) { try { if (string.IsNullOrEmpty(a.Id) || a.Id == "0") a.Id = MongoDB.Bson.ObjectId.GenerateNewId().ToString(); mMtHist.InsertOne(a); } catch { } // Ignore duplicates! } _logger.Info("✅ Migration ERFOLGREICH abgeschlossen!"); MessageBox.Show("Migration in MongoDB abgeschlossen! Bitte PolyTrader neustarten.", "Erfolg", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { _logger.Warning($"Migrationsfehler: {ex.Message}"); MessageBox.Show($"Fehler bei der Migration:\n{ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void maintab_jobs_Click(object sender, EventArgs e) { } } public class OpenTradeRow { public string AccountName { get; set; } = string.Empty; public string SourceTraderName { get; set; } = string.Empty; [System.ComponentModel.Browsable(false)] public string SourceTraderAddress { get; set; } = string.Empty; public string MarketQuestion { get; set; } = string.Empty; [System.ComponentModel.Browsable(false)] public string MarketSlug { get; set; } = string.Empty; public string Outcome { get; set; } = string.Empty; public string Side { get; set; } = string.Empty; public decimal EntryPrice { get; set; } public decimal CurrentPrice { get; set; } public decimal Size { get; set; } // Shares public decimal AmountUsd { get; set; } // Renamed to 'Buy In' in UI public decimal CurrentValueUsd { get; set; } public decimal PnL => CurrentValueUsd > 0 ? (CurrentValueUsd - AmountUsd) : 0m; public DateTime? ExpiryDate { get; set; } [System.ComponentModel.Browsable(false)] public string TokenId { get; set; } = string.Empty; } }