From 05e0576965c0b1188c3bcc55b43d41e3626fff7f Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 5 Jul 2026 20:09:56 +0200 Subject: [PATCH] Phase 5-UI: Copytrading-Modul-Views (Master-Trader, Closed Trades, Account-Settings) - MasterTradersView: Liste + PropertyGrid-Editor + Account-Zuweisung (CheckedListBox -> AssignedAccountIds), Neu/Speichern/Loeschen; persistiert via ITrackedTraderRepository + synct CopyTradingState.Traders. - ClosedTradesView: read-only Grid der geschlossenen Copytrades mit aufgeloesten Account-/Trader-Namen + Kurzauswertung (ICopyTradeLogRepository). - AccountSettingsView: Account-Auswahl + PropertyGrid fuer CopyTradingAccountSettings, Speichern via Repo + State-Sync. - IPolyTraderModule.RegisterUi erhaelt jetzt IServiceProvider (Module loesen ihre View-Abhaengigkeiten auf); Program uebergibt viewServices. - CopyTradingModule.RegisterUi registriert die 3 Views (Group "CopyTrading"). - LauncherForm haengt Modul-Views dynamisch als Buttons an toolstrip_windows (Core kennt keine Modulnamen; Bindung nur ueber View-IDs). Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 + Program.cs | 2 +- Ui/LauncherForm.cs | 23 ++ .../Modularity/IPolyTraderModule.cs | 8 +- .../CopyTradingModule.cs | 39 +++- .../Ui/AccountSettingsView.cs | 100 +++++++++ .../Ui/ClosedTradesView.cs | 144 ++++++++++++ .../Ui/MasterTradersView.cs | 206 ++++++++++++++++++ 8 files changed, 519 insertions(+), 6 deletions(-) create mode 100644 src/PolyTrader.Modules.CopyTrading/Ui/AccountSettingsView.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs diff --git a/.gitignore b/.gitignore index a6704b8..c6e435c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ appsettings.*.json # Mongo-Exporte (enthalten Secrets: PrivateKey, ApiSecret) – niemals committen MongoDB/ +# Gitea Personal Access Token für Pushes – niemals committen +.gitea-token + # ── Logs & temporäre Dateien ───────────────────── *.log *.tmp diff --git a/Program.cs b/Program.cs index dd3dcae..ea433fa 100644 --- a/Program.cs +++ b/Program.cs @@ -189,7 +189,7 @@ internal static class Program // Modul-UI registrieren (Module steuern ihre Views selbst bei). foreach (var module in modules) { - module.RegisterUi(uiHost); + module.RegisterUi(uiHost, viewServices); } var launcher = AppHost.Services.GetRequiredService(); diff --git a/Ui/LauncherForm.cs b/Ui/LauncherForm.cs index bd35214..043f647 100644 --- a/Ui/LauncherForm.cs +++ b/Ui/LauncherForm.cs @@ -44,6 +44,29 @@ namespace PolyTraderSharp.Ui btn.Click += (_, _) => _uiHost.OpenView(viewId); } + // Modul-Views dynamisch anhängen (der Launcher kennt keine Modulnamen; er bindet + // nur an registrierte View-IDs). Core-Views sind bereits fest verdrahtet. + var moduleViews = _uiHost.Views + .Where(v => !_viewButtons.ContainsKey(v.Id)) + .OrderBy(v => v.Order) + .ToList(); + if (moduleViews.Count > 0) + toolstrip_windows.Items.Add(new ToolStripSeparator()); + foreach (var view in moduleViews) + { + var btn = new ToolStripButton(view.Title) + { + Name = "btn_" + view.Id, + Overflow = ToolStripItemOverflow.AsNeeded, + TextImageRelation = TextImageRelation.ImageAboveText, + AutoSize = true + }; + var viewId = view.Id; + btn.Click += (_, _) => _uiHost.OpenView(viewId); + toolstrip_windows.Items.Add(btn); + _viewButtons[view.Id] = btn; + } + btn_liveTrading.Click += (_, _) => CycleLiveTrading(); btn_demoTrading.Click += (_, _) => CycleDemoTrading(); miBeenden.Click += (_, _) => Close(); diff --git a/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs b/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs index 56c4399..7d07f79 100644 --- a/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs +++ b/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -29,10 +30,11 @@ namespace PolyTrader.Core.Modularity /// /// Registriert die UI-Ansichten des Moduls bei der Shell (Launcher). Die Shell öffnet - /// jede Ansicht auf Wunsch in einem eigenen Host-Fenster. Module liefern designbare - /// UserControls; die Shell bleibt modul-agnostisch. + /// jede Ansicht auf Wunsch in einem eigenen Host-Fenster. Module liefern eigene Forms; + /// die Shell bleibt modul-agnostisch. Über lösen Module ihre + /// Abhängigkeiten (Repos, State) für die View-Erzeugung auf. /// - void RegisterUi(IModuleUiHost host); + void RegisterUi(IModuleUiHost host, IServiceProvider services); /// Wird nach der Core-Hydration gestartet (kein Anlaufen gegen leeren State). Task StartAsync(CancellationToken cancellationToken); diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs index fcd9915..9699fb7 100644 --- a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Modularity; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.CopyTrading.Persistence.Ef; +using PolyTrader.Modules.CopyTrading.Ui; using PolyTraderSharp; using PolyTraderSharp.Models; using PolyTraderSharp.Services; @@ -52,9 +53,43 @@ namespace PolyTrader.Modules.CopyTrading services.AddHostedService(); } - public void RegisterUi(IModuleUiHost host) + public void RegisterUi(IModuleUiHost host, IServiceProvider services) { - // TODO: Master-Traders- und Geschlossene-Trades-Views als Modul-UI beisteuern. + host.RegisterView(new ModuleView + { + Id = "copytrading.masters", + Title = "Master-Trader", + Group = "CopyTrading", + Order = 100, + CreateForm = () => new MasterTradersView( + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + }); + + host.RegisterView(new ModuleView + { + Id = "copytrading.closedtrades", + Title = "Geschlossene Copytrades", + Group = "CopyTrading", + Order = 110, + CreateForm = () => new ClosedTradesView( + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + }); + + host.RegisterView(new ModuleView + { + Id = "copytrading.accountsettings", + Title = "Copytrading-Account-Einstellungen", + Group = "CopyTrading", + Order = 120, + CreateForm = () => new AccountSettingsView( + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + }); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/AccountSettingsView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/AccountSettingsView.cs new file mode 100644 index 0000000..61b0885 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Ui/AccountSettingsView.cs @@ -0,0 +1,100 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using PolyTrader.Modules.CopyTrading.Persistence; +using PolyTraderSharp; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Ui +{ + /// + /// Bearbeitet die copytrading-spezifischen Detail-Einstellungen je Account + /// (Investment-/Zeit-Limits, ). Persistiert über + /// das Repo und aktualisiert den Hot-Path-State (). + /// + public class AccountSettingsView : Form + { + private readonly ICopyTradingAccountSettingsRepository _repo; + private readonly TradingState _state; + private readonly CopyTradingState _copyState; + + private readonly ComboBox _accounts = new(); + private readonly PropertyGrid _grid = new(); + private readonly Label _hint = new(); + + private CopyTradingAccountSettings? _current; + + public AccountSettingsView(ICopyTradingAccountSettingsRepository repo, TradingState state, CopyTradingState copyState) + { + _repo = repo; + _state = state; + _copyState = copyState; + + Text = "Copytrading-Account-Einstellungen"; + StartPosition = FormStartPosition.CenterScreen; + Size = new Size(560, 620); + MinimumSize = new Size(420, 400); + + var top = new Panel { Dock = DockStyle.Top, Height = 44, Padding = new Padding(8, 8, 8, 6) }; + var lbl = new Label { Text = "Account:", Dock = DockStyle.Left, Width = 60, TextAlign = ContentAlignment.MiddleLeft }; + _accounts.Dock = DockStyle.Fill; + _accounts.DropDownStyle = ComboBoxStyle.DropDownList; + _accounts.SelectedIndexChanged += (_, _) => LoadSelected(); + top.Controls.Add(_accounts); + top.Controls.Add(lbl); + + _grid.Dock = DockStyle.Fill; + _grid.ToolbarVisible = false; + _grid.PropertySort = PropertySort.Categorized; + + var bottom = new Panel { Dock = DockStyle.Bottom, Height = 48, Padding = new Padding(8) }; + var btnSave = new Button { Text = "Speichern", Dock = DockStyle.Right, Width = 120 }; + btnSave.Click += (_, _) => Save(); + _hint.Dock = DockStyle.Fill; + _hint.TextAlign = ContentAlignment.MiddleLeft; + _hint.ForeColor = Color.DimGray; + bottom.Controls.Add(_hint); + bottom.Controls.Add(btnSave); + + Controls.Add(_grid); + Controls.Add(bottom); + Controls.Add(top); + + PopulateAccounts(); + } + + private void PopulateAccounts() + { + var items = _state.Accounts.Values + .OrderBy(a => a.AccountId) + .Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{a.AccountId}){(a.IsDemo ? " · Demo" : "")}")) + .ToList(); + + _accounts.DisplayMember = nameof(AccountItem.Label); + _accounts.ValueMember = nameof(AccountItem.Id); + _accounts.DataSource = items; + + if (items.Count > 0) LoadSelected(); + else _hint.Text = "Keine Accounts vorhanden."; + } + + private void LoadSelected() + { + if (_accounts.SelectedItem is not AccountItem item) return; + _current = _repo.Get(item.Id) ?? new CopyTradingAccountSettings { AccountId = item.Id }; + _grid.SelectedObject = _current; + _hint.Text = $"Einstellungen für Account #{item.Id}."; + } + + private void Save() + { + if (_current == null) return; + _repo.Upsert(_current); + _copyState.AccountSettings[_current.AccountId] = _current; + _hint.Text = $"Gespeichert für Account #{_current.AccountId} um {DateTime.Now:HH:mm:ss}."; + } + + private sealed record AccountItem(int Id, string Label); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs new file mode 100644 index 0000000..7a9d413 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs @@ -0,0 +1,144 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using PolyTrader.Modules.CopyTrading.Persistence; +using PolyTraderSharp; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Ui +{ + /// + /// Zeigt die geschlossenen Copytrades des Moduls (mod_copytrading_closed_trades) mit + /// aufgelösten Account-/Master-Trader-Namen und einer Kurzauswertung. + /// + public class ClosedTradesView : Form + { + private readonly ICopyTradeLogRepository _tradeLog; + private readonly TradingState _state; + private readonly CopyTradingState _copyState; + + private readonly DataGridView _grid = new(); + private readonly Label _summary = new(); + + public ClosedTradesView(ICopyTradeLogRepository tradeLog, TradingState state, CopyTradingState copyState) + { + _tradeLog = tradeLog; + _state = state; + _copyState = copyState; + + Text = "Geschlossene Copytrades"; + StartPosition = FormStartPosition.CenterScreen; + Size = new Size(1100, 620); + MinimumSize = new Size(700, 400); + + var top = new Panel { Dock = DockStyle.Top, Height = 40, Padding = new Padding(8, 6, 8, 6) }; + var btnRefresh = new Button { Text = "Aktualisieren", Dock = DockStyle.Left, Width = 120 }; + btnRefresh.Click += (_, _) => LoadData(); + _summary.Dock = DockStyle.Fill; + _summary.TextAlign = ContentAlignment.MiddleLeft; + _summary.Padding = new Padding(12, 0, 0, 0); + top.Controls.Add(_summary); + top.Controls.Add(btnRefresh); + + _grid.Dock = DockStyle.Fill; + _grid.ReadOnly = true; + _grid.AllowUserToAddRows = false; + _grid.AllowUserToDeleteRows = false; + _grid.RowHeadersVisible = false; + _grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + _grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + _grid.AutoGenerateColumns = true; + + Controls.Add(_grid); + Controls.Add(top); + + LoadData(); + } + + private void LoadData() + { + var rows = _tradeLog.Find(_ => true) + .OrderByDescending(t => t.ClosedAt) + .Select(t => 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, + AccountName = ResolveAccount(t.AccountId, t.IsDemo), + SourceTraderName = ResolveTrader(t.SourceTraderId) + }) + .ToList(); + + _grid.DataSource = new BindingList(rows); + FormatColumns(); + + decimal totalPnl = rows.Sum(x => x.RealizedPnl); + int wins = rows.Count(x => x.RealizedPnl > 0); + double winrate = rows.Count > 0 ? (double)wins / rows.Count * 100 : 0; + _summary.Text = $"{rows.Count} Trades | PnL gesamt: {totalPnl:F2} USDC | Winrate: {winrate:F1}%"; + } + + private void FormatColumns() + { + void Hide(string name) { if (_grid.Columns[name] is { } c) c.Visible = false; } + void Fmt(string name, string f) { if (_grid.Columns[name] is { } c) c.DefaultCellStyle.Format = f; } + void Head(string name, string h) { if (_grid.Columns[name] is { } c) c.HeaderText = h; } + + Hide(nameof(ClosedTradeRow.TokenId)); + Hide(nameof(ClosedTradeRow.MarketSlug)); + Hide(nameof(ClosedTradeRow.AccountId)); + Hide(nameof(ClosedTradeRow.SourceTraderId)); + Hide(nameof(ClosedTradeRow.IsDemo)); + + Fmt(nameof(ClosedTradeRow.EntryPrice), "F3"); + Fmt(nameof(ClosedTradeRow.ExitPrice), "F3"); + Fmt(nameof(ClosedTradeRow.Size), "F2"); + Fmt(nameof(ClosedTradeRow.RealizedPnl), "F2"); + Fmt(nameof(ClosedTradeRow.PnlPercent), "F1"); + Fmt(nameof(ClosedTradeRow.TotalFees), "F2"); + Fmt(nameof(ClosedTradeRow.OpenedAt), "dd.MM.yyyy HH:mm"); + Fmt(nameof(ClosedTradeRow.ClosedAt), "dd.MM.yyyy HH:mm"); + + Head(nameof(ClosedTradeRow.TradeId), "#"); + Head(nameof(ClosedTradeRow.AccountName), "Account"); + Head(nameof(ClosedTradeRow.SourceTraderName), "Master-Trader"); + Head(nameof(ClosedTradeRow.MarketQuestion), "Markt"); + Head(nameof(ClosedTradeRow.RealizedPnl), "PnL"); + Head(nameof(ClosedTradeRow.PnlPercent), "PnL %"); + Head(nameof(ClosedTradeRow.ClosedAt), "Geschlossen"); + Head(nameof(ClosedTradeRow.OpenedAt), "Eröffnet"); + } + + private string ResolveAccount(int accountId, bool isDemo) + { + string suffix = isDemo ? " (Demo)" : ""; + if (_state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name)) + return acc.Name + suffix; + return $"#{accountId}{suffix}"; + } + + private string ResolveTrader(int traderId) + { + if (_copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName)) + return t.DisplayName; + return traderId > 0 ? $"#{traderId}" : "Unbekannt"; + } + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs new file mode 100644 index 0000000..8cd7877 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs @@ -0,0 +1,206 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using PolyTrader.Modules.CopyTrading.Persistence; +using PolyTraderSharp; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Ui +{ + /// + /// Verwaltung der Master-Trader (mod_copytrading_traders): Liste, Detail-Editor + /// (PropertyGrid) und Account-Zuweisung (welche Accounts diesen Trader kopieren). + /// Persistiert über das Repo und hält den Hot-Path-State + /// () synchron. + /// + public class MasterTradersView : Form + { + private readonly ITrackedTraderRepository _repo; + private readonly TradingState _state; + private readonly CopyTradingState _copyState; + + private readonly DataGridView _grid = new(); + private readonly PropertyGrid _detail = new(); + private readonly CheckedListBox _accounts = new(); + private readonly Label _hint = new(); + + private BindingList _binding = new(); + private TrackedTrader? _current; + + public MasterTradersView(ITrackedTraderRepository repo, TradingState state, CopyTradingState copyState) + { + _repo = repo; + _state = state; + _copyState = copyState; + + Text = "Master-Trader"; + StartPosition = FormStartPosition.CenterScreen; + Size = new Size(1180, 640); + MinimumSize = new Size(820, 460); + + // Toolbar + var toolbar = new ToolStrip { GripStyle = ToolStripGripStyle.Hidden }; + var tsNew = new ToolStripButton("Neu"); + var tsSave = new ToolStripButton("Speichern"); + var tsDelete = new ToolStripButton("Löschen"); + var tsRefresh = new ToolStripButton("Aktualisieren"); + tsNew.Click += (_, _) => AddNew(); + tsSave.Click += (_, _) => SaveCurrent(); + tsDelete.Click += (_, _) => DeleteCurrent(); + tsRefresh.Click += (_, _) => LoadData(); + toolbar.Items.AddRange(new ToolStripItem[] + { + tsNew, tsSave, tsDelete, new ToolStripSeparator(), tsRefresh + }); + + // Liste links + _grid.Dock = DockStyle.Fill; + _grid.ReadOnly = true; + _grid.AllowUserToAddRows = false; + _grid.AllowUserToDeleteRows = false; + _grid.RowHeadersVisible = false; + _grid.MultiSelect = false; + _grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + _grid.AutoGenerateColumns = true; + _grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + _grid.SelectionChanged += (_, _) => OnSelectionChanged(); + + // Detail rechts: PropertyGrid + Account-Zuweisung + _detail.Dock = DockStyle.Fill; + _detail.ToolbarVisible = false; + _detail.PropertySort = PropertySort.Categorized; + + var accBox = new GroupBox { Text = "Zugewiesene Accounts (kopieren diesen Trader)", Dock = DockStyle.Bottom, Height = 190, Padding = new Padding(8) }; + _accounts.Dock = DockStyle.Fill; + _accounts.CheckOnClick = true; + accBox.Controls.Add(_accounts); + + var right = new Panel { Dock = DockStyle.Right, Width = 460, Padding = new Padding(6, 0, 0, 0) }; + right.Controls.Add(_detail); + right.Controls.Add(accBox); + + var split = new Splitter { Dock = DockStyle.Right, Width = 5 }; + + var status = new Panel { Dock = DockStyle.Bottom, Height = 26 }; + _hint.Dock = DockStyle.Fill; + _hint.TextAlign = ContentAlignment.MiddleLeft; + _hint.ForeColor = Color.DimGray; + _hint.Padding = new Padding(8, 0, 0, 0); + status.Controls.Add(_hint); + + Controls.Add(_grid); + Controls.Add(split); + Controls.Add(right); + Controls.Add(status); + Controls.Add(toolbar); + + LoadData(); + } + + private void LoadData() + { + var traders = _repo.GetAll().OrderBy(t => t.Id).ToList(); + _binding = new BindingList(traders); + _grid.DataSource = _binding; + FormatColumns(); + if (traders.Count > 0) + _grid.CurrentCell = _grid.Rows[0].Cells[0]; + else + ClearDetail(); + _hint.Text = $"{traders.Count} Master-Trader geladen."; + } + + private void FormatColumns() + { + void Hide(string n) { if (_grid.Columns[n] is { } c) c.Visible = false; } + void Head(string n, string h) { if (_grid.Columns[n] is { } c) c.HeaderText = h; } + void Fmt(string n, string f) { if (_grid.Columns[n] is { } c) c.DefaultCellStyle.Format = f; } + + Hide(nameof(TrackedTrader.Description)); + Hide(nameof(TrackedTrader.Reasoning)); + Hide(nameof(TrackedTrader.IsHidden)); + Hide(nameof(TrackedTrader.WinningTrades)); + + Head(nameof(TrackedTrader.Id), "#"); + Head(nameof(TrackedTrader.WalletAddress), "Wallet"); + Head(nameof(TrackedTrader.DisplayName), "Name"); + Head(nameof(TrackedTrader.Category), "Kategorie"); + Head(nameof(TrackedTrader.IsActive), "Aktiv"); + Head(nameof(TrackedTrader.TotalTrades), "Trades (7T)"); + Head(nameof(TrackedTrader.Winrate30t), "Winrate %"); + Head(nameof(TrackedTrader.TotalPnl), "PnL (7T)"); + Fmt(nameof(TrackedTrader.Winrate30t), "F1"); + Fmt(nameof(TrackedTrader.TotalPnl), "F2"); + } + + private void OnSelectionChanged() + { + if (_grid.CurrentRow?.DataBoundItem is TrackedTrader t) + BindDetail(t); + } + + private void BindDetail(TrackedTrader trader) + { + _current = trader; + _detail.SelectedObject = trader; + + _accounts.Items.Clear(); + foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId)) + { + string label = string.IsNullOrEmpty(acc.Name) ? $"#{acc.AccountId}" : $"{acc.Name} (#{acc.AccountId}){(acc.IsDemo ? " · Demo" : "")}"; + int idx = _accounts.Items.Add(new AccountItem(acc.AccountId, label)); + _accounts.SetItemChecked(idx, trader.AssignedAccountIds.Contains(acc.AccountId)); + } + } + + private void ClearDetail() + { + _current = null; + _detail.SelectedObject = null; + _accounts.Items.Clear(); + } + + private void AddNew() + { + int nextId = _binding.Count > 0 ? _binding.Max(t => t.Id) + 1 : 1; + var trader = new TrackedTrader { Id = nextId, DisplayName = $"Neuer Trader {nextId}" }; + _binding.Add(trader); + _grid.CurrentCell = _grid.Rows[_binding.Count - 1].Cells[0]; + _hint.Text = $"Neuer Master-Trader #{nextId} – Felder ausfüllen und Speichern."; + } + + private void SaveCurrent() + { + if (_current == null) { _hint.Text = "Kein Trader ausgewählt."; return; } + + _current.AssignedAccountIds = _accounts.CheckedItems.Cast().Select(a => a.Id).ToHashSet(); + + _repo.Upsert(_current); + _copyState.Traders[_current.Id] = _current; + _grid.Refresh(); + _hint.Text = $"Gespeichert: #{_current.Id} {_current.DisplayName} ({_current.AssignedAccountIds.Count} Account(s)) um {DateTime.Now:HH:mm:ss}."; + } + + private void DeleteCurrent() + { + if (_current == null) { _hint.Text = "Kein Trader ausgewählt."; return; } + var id = _current.Id; + if (MessageBox.Show($"Master-Trader #{id} ({_current.DisplayName}) wirklich löschen?", + "Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) + return; + + _repo.Delete(id); + _copyState.Traders.TryRemove(id, out _); + _binding.Remove(_current); + ClearDetail(); + _hint.Text = $"Master-Trader #{id} gelöscht."; + } + + private sealed record AccountItem(int Id, string Label) + { + public override string ToString() => Label; + } + } +}