- Core TradingState (in Core): globale Schalter, Accounts, MarketCache, GlobalPnl. - Neuer CopyTradingState (im Modul): Traders, MasterTraderPositions, TraderAnalyticsCache, TotalCopyTrades/GetNextTradeId, PendingOrderTimestamps, SixSharesMinimum. - 10 Konsumenten umgestellt (Program, frm_main, CopyTradingEngine, TraderMonitor, Alchemy, WSS, Snapshot, StartupHydration, beide Analytics-Jobs): Modul-Felder von _state.* auf _copyState.* umgeleitet, CopyTradingState via DI. - Rein mechanische Feld-Umleitung, keine Logikänderung. Build 0 Fehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1859 lines
84 KiB
C#
1859 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 CopyTradingState _copyState;
|
|
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, CopyTradingState copyState, TerminalLogger logger, ThreemaService threemaService, MullvadVpnService vpnService, IMongoDatabase db, PolymarketApiService api, PolymarketClobClient clob, JobManager jobManager)
|
|
{
|
|
InitializeComponent();
|
|
_tradingState = tradingState;
|
|
_copyState = copyState;
|
|
_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 _copyState.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 = _copyState.SixSharesMinimum;
|
|
btn_sixshares.BackColor = _copyState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
|
btn_sixshares.Click += (s, ev) =>
|
|
{
|
|
_copyState.SixSharesMinimum = !_copyState.SixSharesMinimum;
|
|
btn_sixshares.Checked = _copyState.SixSharesMinimum;
|
|
btn_sixshares.BackColor = _copyState.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 (_copyState.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 (_copyState.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 = _copyState.Traders.Count > 0 ? _copyState.Traders.Keys.Max() + 1 : 1;
|
|
var newTrd = new TrackedTrader { Id = newId, DisplayName = "Neuer Master", WalletAddress = "0x..." };
|
|
_copyState.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)
|
|
{
|
|
_copyState.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 = _copyState.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 = _copyState.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 = _copyState.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;
|
|
}
|
|
}
|