From 80e6ad9b2dc5a312b1c3bede43a2b2680071e0c3 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 9 Jul 2026 22:59:41 +0200 Subject: [PATCH] RF-Slice 4: ResolutionFarming-UI (Tabs Kandidaten/Positionen/Historie/Settings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Modul-Fenster mit TabControl, code-only konstruiert (kein Designer/.resx): - Kandidaten: DataGridView der letzten Scans je Konto (akzeptiert+abgelehnt inkl. Grund). - Positionen: offene rf_positions. - Historie/Statistik: abgeschlossene Trades + Summary (Winrate gesamt/je 5-¢-Preisband, Netto-PnL, Fees) fuer die Kalibrierung. - Settings: PropertyGrid auf RfSettings je Konto + Speichern (Muster AccountSettingsView). RegisterUi verdrahtet die View (Launcher-Button 'ResolutionFarming'). DB-Zugriffe defensiv (Guarded try/catch) -> UI bleibt bedienbar auch vor Anwenden der rf_-Migration (zeigt dann nur Statushinweis statt zu crashen). Build 0 Fehler, 296 Tests gruen, --smoke-ui konstruiert die RF-View ([OK] resolutionfarming.main). Co-Authored-By: Claude Opus 4.8 --- .../ResolutionFarmingModule.cs | 15 +- .../Ui/ResolutionFarmingMainForm.cs | 219 ++++++++++++++++++ 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/PolyTrader.Modules.ResolutionFarming/Ui/ResolutionFarmingMainForm.cs diff --git a/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs b/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs index 172eafc..d88c4d9 100644 --- a/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs +++ b/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs @@ -47,7 +47,20 @@ namespace PolyTrader.Modules.ResolutionFarming public void RegisterUi(IModuleUiHost host, System.IServiceProvider services) { - // Slice 1: noch keine UI-Tabs. Folgen mit Persistenz/Scanner. + // EIN Fenster fürs ganze Modul (Tabs: Kandidaten/Positionen/Historie/Settings). + host.RegisterView(new ModuleView + { + Id = "resolutionfarming.main", + Title = "ResolutionFarming", + Group = "ResolutionFarming", + Order = 200, + CreateForm = () => + { + var form = new Ui.ResolutionFarmingMainForm(); + form.Initialize(services); + return form; + } + }); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/PolyTrader.Modules.ResolutionFarming/Ui/ResolutionFarmingMainForm.cs b/src/PolyTrader.Modules.ResolutionFarming/Ui/ResolutionFarmingMainForm.cs new file mode 100644 index 0000000..819dc5e --- /dev/null +++ b/src/PolyTrader.Modules.ResolutionFarming/Ui/ResolutionFarmingMainForm.cs @@ -0,0 +1,219 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +using PolyTrader.Modules.ResolutionFarming.Models; +using PolyTrader.Modules.ResolutionFarming.Persistence; +using PolyTraderSharp; + +namespace PolyTrader.Modules.ResolutionFarming.Ui +{ + /// + /// Hauptfenster des ResolutionFarming-Moduls: ein TabControl mit Kandidaten, Positionen, + /// Historie/Statistik und Settings. Bewusst code-only konstruiert (kein Designer/.resx). DB-Zugriffe + /// sind defensiv (try/catch) – die UI bleibt bedienbar, auch bevor die rf_-Migration angewendet ist. + /// + public sealed class ResolutionFarmingMainForm : Form + { + private IRfCandidateRepository? _candidates; + private IRfPositionRepository? _positions; + private IRfClosedTradeRepository? _closed; + private IRfSettingsRepository? _settingsRepo; + private TradingState? _state; + + private readonly ComboBox _candAccount = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 260 }; + private readonly ComboBox _settingsAccount = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 260 }; + private readonly DataGridView _candGrid = NewGrid(); + private readonly DataGridView _posGrid = NewGrid(); + private readonly DataGridView _histGrid = NewGrid(); + private readonly PropertyGrid _settingsGrid = new() { Dock = DockStyle.Fill }; + private readonly Label _histSummary = new() { Dock = DockStyle.Top, Height = 48, Padding = new Padding(6), Text = "" }; + private readonly Label _status = new() { Dock = DockStyle.Bottom, Height = 22, Padding = new Padding(6, 2, 6, 2), Text = "" }; + + private RfSettings? _currentSettings; + + public ResolutionFarmingMainForm() + { + Text = "ResolutionFarming"; + Width = 1000; + Height = 640; + StartPosition = FormStartPosition.CenterScreen; + BuildUi(); + } + + /// Injiziert Repos/State (nach DI-Auflösung) und lädt die Ansichten. + public void Initialize(IServiceProvider services) + { + _candidates = services.GetRequiredService(); + _positions = services.GetRequiredService(); + _closed = services.GetRequiredService(); + _settingsRepo = services.GetRequiredService(); + _state = services.GetRequiredService(); + + PopulateAccounts(); + RefreshCandidates(); + RefreshPositions(); + RefreshHistory(); + } + + // ---------------- UI-Aufbau ---------------- + + private static DataGridView NewGrid() => new() + { + Dock = DockStyle.Fill, + ReadOnly = true, + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + AutoGenerateColumns = true, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells, + SelectionMode = DataGridViewSelectionMode.FullRowSelect, + RowHeadersVisible = false + }; + + private void BuildUi() + { + var tabs = new TabControl { Dock = DockStyle.Fill }; + + // --- Kandidaten --- + var candTab = new TabPage("Kandidaten"); + var candTop = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(4) }; + candTop.Controls.Add(new Label { Text = "Konto:", AutoSize = true, Padding = new Padding(0, 6, 4, 0) }); + candTop.Controls.Add(_candAccount); + var candRefresh = new Button { Text = "Aktualisieren" }; + candRefresh.Click += (_, _) => RefreshCandidates(); + candTop.Controls.Add(candRefresh); + candTab.Controls.Add(_candGrid); + candTab.Controls.Add(candTop); + _candAccount.SelectedIndexChanged += (_, _) => RefreshCandidates(); + + // --- Positionen --- + var posTab = new TabPage("Positionen"); + var posTop = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(4) }; + var posRefresh = new Button { Text = "Aktualisieren" }; + posRefresh.Click += (_, _) => RefreshPositions(); + posTop.Controls.Add(posRefresh); + posTab.Controls.Add(_posGrid); + posTab.Controls.Add(posTop); + + // --- Historie / Statistik --- + var histTab = new TabPage("Historie / Statistik"); + var histTop = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(4) }; + var histRefresh = new Button { Text = "Aktualisieren" }; + histRefresh.Click += (_, _) => RefreshHistory(); + histTop.Controls.Add(histRefresh); + histTab.Controls.Add(_histGrid); + histTab.Controls.Add(_histSummary); + histTab.Controls.Add(histTop); + + // --- Settings --- + var setTab = new TabPage("Settings"); + var setTop = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(4) }; + setTop.Controls.Add(new Label { Text = "Konto:", AutoSize = true, Padding = new Padding(0, 6, 4, 0) }); + setTop.Controls.Add(_settingsAccount); + var setSave = new Button { Text = "Speichern" }; + setSave.Click += (_, _) => SaveSettings(); + setTop.Controls.Add(setSave); + setTab.Controls.Add(_settingsGrid); + setTab.Controls.Add(setTop); + _settingsAccount.SelectedIndexChanged += (_, _) => LoadSettings(); + + tabs.TabPages.AddRange(new[] { candTab, posTab, histTab, setTab }); + Controls.Add(tabs); + Controls.Add(_status); + } + + // ---------------- Daten ---------------- + + private void PopulateAccounts() + { + if (_state == null) return; + 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(); + + foreach (var combo in new[] { _candAccount, _settingsAccount }) + { + combo.DisplayMember = nameof(AccountItem.Label); + combo.ValueMember = nameof(AccountItem.Id); + combo.DataSource = items.ToList(); + } + + if (items.Count > 0) LoadSettings(); + else SetStatus("Keine Accounts vorhanden."); + } + + private int? SelectedAccountId(ComboBox combo) => + combo.SelectedItem is AccountItem it ? it.Id : (int?)null; + + private void RefreshCandidates() + { + if (_candidates == null || SelectedAccountId(_candAccount) is not int accId) return; + Guarded("Kandidaten", () => _candGrid.DataSource = _candidates.GetRecent(accId, 200)); + } + + private void RefreshPositions() + { + if (_positions == null) return; + Guarded("Positionen", () => _posGrid.DataSource = _positions.GetAllOpen()); + } + + private void RefreshHistory() + { + if (_closed == null) return; + Guarded("Historie", () => + { + var trades = _closed.Find(_ => true); + _histGrid.DataSource = trades; + _histSummary.Text = BuildSummary(trades); + }); + } + + private static string BuildSummary(System.Collections.Generic.List trades) + { + if (trades.Count == 0) return "Noch keine abgeschlossenen Trades."; + int wins = trades.Count(t => t.RealizedPnl > 0m); + decimal pnl = trades.Sum(t => t.RealizedPnl); + decimal fees = trades.Sum(t => t.TotalFees); + double winrate = 100.0 * wins / trades.Count; + // Winrate je Preisband (Kalibrierung: realisierte Winrate sollte > Band-Mitte liegen). + var bands = trades + .GroupBy(t => $"{Math.Floor(t.EntryPrice * 20m) / 20m:F2}") // 5-¢-Bänder + .OrderBy(g => g.Key) + .Select(g => $"{g.Key}: {100.0 * g.Count(x => x.RealizedPnl > 0m) / g.Count():F0}% ({g.Count()})"); + return $"Trades: {trades.Count} | Winrate: {winrate:F1}% | Netto-PnL: {pnl:F2} USDC | Fees: {fees:F2}\n" + + $"Winrate je Preisband: {string.Join(" | ", bands)}"; + } + + private void LoadSettings() + { + if (_settingsRepo == null || SelectedAccountId(_settingsAccount) is not int accId) return; + Guarded("Settings", () => + { + _currentSettings = _settingsRepo.Get(accId) ?? new RfSettings { AccountId = accId }; + _settingsGrid.SelectedObject = _currentSettings; + SetStatus($"Einstellungen für Konto #{accId}."); + }); + } + + private void SaveSettings() + { + if (_settingsRepo == null || _currentSettings == null) return; + Guarded("Speichern", () => + { + _settingsRepo.Upsert(_currentSettings); + SetStatus($"Gespeichert für Konto #{_currentSettings.AccountId} um {DateTime.Now:HH:mm:ss}."); + }); + } + + private void Guarded(string what, Action action) + { + try { action(); } + catch (Exception ex) { SetStatus($"{what}: DB nicht bereit ({ex.Message}). Migration angewendet?"); } + } + + private void SetStatus(string text) => _status.Text = text; + + private sealed record AccountItem(int Id, string Label); + } +}