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:
co-authored by
Claude Opus 4.8
parent
ad6c676e60
commit
80e6ad9b2d
@@ -47,7 +47,20 @@ namespace PolyTrader.Modules.ResolutionFarming
|
|||||||
|
|
||||||
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services)
|
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;
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user