RF-Slice 4: ResolutionFarming-UI (Tabs Kandidaten/Positionen/Historie/Settings)

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 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-09 22:59:41 +02:00
co-authored by Claude Opus 4.8
parent ad6c676e60
commit 80e6ad9b2d
2 changed files with 233 additions and 1 deletions
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>Injiziert Repos/State (nach DI-Auflösung) und lädt die Ansichten.</summary>
public void Initialize(IServiceProvider services)
{
_candidates = services.GetRequiredService<IRfCandidateRepository>();
_positions = services.GetRequiredService<IRfPositionRepository>();
_closed = services.GetRequiredService<IRfClosedTradeRepository>();
_settingsRepo = services.GetRequiredService<IRfSettingsRepository>();
_state = services.GetRequiredService<TradingState>();
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<RfClosedTrade> 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);
}
}