Files
PolyTraderSharp/frm_main.cs
T
bergmandClaude Opus 4.8 c88eac5f8a Phase 4.5: Startup-Reihenfolge-Fix (State-Hydration vor Trading-Services)
- Neuer StartupHydrationService (IHostedService), als ERSTER HostedService
  registriert: hydriert Accounts (+ Demo-Positionen via Repos) und Trader
  (trackers-Collection) in den TradingState, bevor die Trading-Services
  ihre ExecuteAsync starten.
- frm_main.LoadDatabaseAndState() + beide Aufrufe entfernt (verhinderte
  doppelte, überschreibende Hydration nach Service-Start).
- Behebt den Race, bei dem Services gegen leeren State anliefen.
- Build 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:38:47 +02:00

1857 lines
84 KiB
C#

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<DashboardRow> _dashboardList = new BindingList<DashboardRow>();
private readonly System.Collections.Concurrent.ConcurrentQueue<LogMessageEventArgs> _logQueue = new System.Collections.Concurrent.ConcurrentQueue<LogMessageEventArgs>();
private System.Windows.Forms.Timer _uiLogTimer = null!;
private List<Position> GetActivePositions(AccountState acc)
{
var activeList = new List<Position>();
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<string>(() => 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<TrackedTrader>("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("🛑 <b>Trading Gestoppt</b>\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("📉 <b>Abverkauf Modus</b>\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($"<b>📊 PolyTrader — 24h Slave Bericht</b>");
sb.AppendLine($"<i>{DateTime.UtcNow:dd.MM.yyyy HH:mm} UTC</i>");
sb.AppendLine("");
try
{
var allClosed = await Task.Run(() => _db.GetCollection<ClosedTrade>("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($"<b>{modeIcon} {acc.Name}</b>");
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($"<b>📈 Gesamtes P&L (24h): ${(totalPnl > 0 ? "+" : "")}{totalPnl:N2}</b>");
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Erstellen des Threema Berichts: {ex.Message}");
sb.AppendLine("<i>Fehler beim Abrufen der Statistiken.</i>");
}
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<ClosedTrade>();
try
{
allRecentTrades = await Task.Run(() =>
{
return _db.GetCollection<ClosedTrade>("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<OpenTradeRow>();
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<ClosedTrade>("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<ClosedTrade>();
}
});
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<ClosedTradeRow>(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<AccountState>("accounts").Upsert(acc);
}
private void SaveTrader(TrackedTrader trd)
{
_db.GetCollection<TrackedTrader>("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<AccountState>("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<TrackedTrader>("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<ClosedTrade>("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<Position>($"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<ClosedTrade>("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<MarketData>("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<ClosedTrade>("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<MarketData>("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<int, ConcurrentDictionary<string, Position>>();
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<Dictionary<int, ConcurrentDictionary<string, Position>>>(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<AccountState>(d)).ToList();
var trdCol = trdColRaw.Select(d => mapper.ToObject<TrackedTrader>(d)).ToList();
var mdCol = mdColRaw.Select(d => mapper.ToObject<MarketData>(d)).ToList();
var ctCol = new System.Collections.Generic.List<ClosedTrade>();
foreach (var doc in ctColRaw)
{
if (doc.TryGetValue("_id", out var id) && id.IsInt32) doc["_id"] = id.AsInt32.ToString();
try { ctCol.Add(mapper.ToObject<ClosedTrade>(doc)); } catch { }
}
var mtCol = new System.Collections.Generic.List<MasterTraderHistoryRecord>();
foreach (var doc in mtColRaw)
{
if (doc.TryGetValue("_id", out var id) && id.IsInt32) doc["_id"] = id.AsInt32.ToString();
try { mtCol.Add(mapper.ToObject<MasterTraderHistoryRecord>(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<AccountState>("accounts");
var mTrackers = _db.GetCollection<TrackedTrader>("trackers");
var mMarkets = _db.GetCollection<MarketData>("markets");
var mClosed = _db.GetCollection<ClosedTrade>("closed_trades");
var mMtHist = _db.GetCollection<MasterTraderHistoryRecord>("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<AccountState>("accounts");
mTrackers = _db.GetCollection<TrackedTrader>("trackers");
mMarkets = _db.GetCollection<MarketData>("markets");
mClosed = _db.GetCollection<ClosedTrade>("closed_trades");
mMtHist = _db.GetCollection<MasterTraderHistoryRecord>("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;
}
}