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);
+ }
+}