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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b951b912f4
commit
05e0576965
@@ -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
|
||||
|
||||
+1
-1
@@ -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<PolyTraderSharp.Ui.LauncherForm>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="services"/> lösen Module ihre
|
||||
/// Abhängigkeiten (Repos, State) für die View-Erzeugung auf.
|
||||
/// </summary>
|
||||
void RegisterUi(IModuleUiHost host);
|
||||
void RegisterUi(IModuleUiHost host, IServiceProvider services);
|
||||
|
||||
/// <summary>Wird nach der Core-Hydration gestartet (kein Anlaufen gegen leeren State).</summary>
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
@@ -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<TraderAnalyticsJob>();
|
||||
}
|
||||
|
||||
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<ITrackedTraderRepository>(),
|
||||
services.GetRequiredService<TradingState>(),
|
||||
services.GetRequiredService<CopyTradingState>())
|
||||
});
|
||||
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "copytrading.closedtrades",
|
||||
Title = "Geschlossene Copytrades",
|
||||
Group = "CopyTrading",
|
||||
Order = 110,
|
||||
CreateForm = () => new ClosedTradesView(
|
||||
services.GetRequiredService<ICopyTradeLogRepository>(),
|
||||
services.GetRequiredService<TradingState>(),
|
||||
services.GetRequiredService<CopyTradingState>())
|
||||
});
|
||||
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "copytrading.accountsettings",
|
||||
Title = "Copytrading-Account-Einstellungen",
|
||||
Group = "CopyTrading",
|
||||
Order = 120,
|
||||
CreateForm = () => new AccountSettingsView(
|
||||
services.GetRequiredService<ICopyTradingAccountSettingsRepository>(),
|
||||
services.GetRequiredService<TradingState>(),
|
||||
services.GetRequiredService<CopyTradingState>())
|
||||
});
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Bearbeitet die copytrading-spezifischen Detail-Einstellungen je Account
|
||||
/// (Investment-/Zeit-Limits, <see cref="CopyTradingAccountSettings"/>). Persistiert über
|
||||
/// das Repo und aktualisiert den Hot-Path-State (<see cref="CopyTradingState.AccountSettings"/>).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Zeigt die geschlossenen Copytrades des Moduls (mod_copytrading_closed_trades) mit
|
||||
/// aufgelösten Account-/Master-Trader-Namen und einer Kurzauswertung.
|
||||
/// </summary>
|
||||
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<ClosedTradeRow>(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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (<see cref="CopyTradingState.Traders"/>) synchron.
|
||||
/// </summary>
|
||||
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<TrackedTrader> _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<TrackedTrader>(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<AccountItem>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user