RF-UI designerfaehig (Richards Vorgabe, letzter code-only-Altbestand):
- ResolutionFarmingMainForm auf partial + .Designer.cs umgestellt (4 Tabs Kandidaten/
Positionen/Historie/Settings, alle Controls im Designer; Verhalten unveraendert).
S-3 Counterfactual ('Was waere aus abgelehnten BUYs geworden?'):
- CounterfactualJob (alle 6h, API-gedrosselt): nimmt Rejected-BUY-Entscheidungen mit
abgelaufenem MarketEndDate aus dem Journal, prueft die Marktaufloesung
(ICounterfactualResolutionSource; live = Adapter um CheckMarketResolutionAsync) und
speichert IsWinner + hypothetischen PnL/Share (CounterfactualMath, pur) nach
sup_counterfactuals (unique je DecisionId -> idempotent). Migration generiert+angewendet.
- Agent-Tool query_counterfactuals + UI-Tab 'Counterfactual' (via Designer).
S-3 Threema-Tagesbericht:
- DailyReportService: taeglich zur konfigurierten Stunde (OPT-IN via
POLYTRADER_SUPERVISOR_DAILY=0-23) laesst der Agent einen 24h-Kurzbericht erstellen
(KPIs je Modul, Fehler, Reject-Haeufungen), sendet via Threema und legt ihn als
sup_report ab. Ohne OpenRouter-Key: stiller Skip mit Log.
Tests: +7 (CounterfactualMath, Job aufgeloest/unaufgeloest/idempotent, Tagesbericht
mit/ohne Key). Build 0 Fehler, 367 Tests gruen, --smoke-ui alle 5 Views gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
6.0 KiB
C#
148 lines
6.0 KiB
C#
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: Tabs Kandidaten, Positionen, Historie/Statistik,
|
|
/// Settings. Layout im Designer (ResolutionFarmingMainForm.Designer.cs), Verhalten/Daten hier.
|
|
/// DB-Zugriffe defensiv (Guarded), damit die UI auch ohne angewendete rf_-Migration bedienbar bleibt.
|
|
/// </summary>
|
|
public partial class ResolutionFarmingMainForm : Form
|
|
{
|
|
private IRfCandidateRepository? _candidates;
|
|
private IRfPositionRepository? _positions;
|
|
private IRfClosedTradeRepository? _closed;
|
|
private IRfSettingsRepository? _settingsRepo;
|
|
private TradingState? _state;
|
|
|
|
private RfSettings? _currentSettings;
|
|
|
|
public ResolutionFarmingMainForm()
|
|
{
|
|
InitializeComponent();
|
|
|
|
btnCandRefresh.Click += (_, _) => RefreshCandidates();
|
|
cbCandAccount.SelectedIndexChanged += (_, _) => RefreshCandidates();
|
|
btnPosRefresh.Click += (_, _) => RefreshPositions();
|
|
btnHistRefresh.Click += (_, _) => RefreshHistory();
|
|
btnSettingsSave.Click += (_, _) => SaveSettings();
|
|
cbSettingsAccount.SelectedIndexChanged += (_, _) => LoadSettings();
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
|
|
// ---------------- 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[] { cbCandAccount, cbSettingsAccount })
|
|
{
|
|
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(cbCandAccount) is not int accId) return;
|
|
Guarded("Kandidaten", () => dgvCandidates.DataSource = _candidates.GetRecent(accId, 200));
|
|
}
|
|
|
|
private void RefreshPositions()
|
|
{
|
|
if (_positions == null) return;
|
|
Guarded("Positionen", () => dgvPositions.DataSource = _positions.GetAllOpen());
|
|
}
|
|
|
|
private void RefreshHistory()
|
|
{
|
|
if (_closed == null) return;
|
|
Guarded("Historie", () =>
|
|
{
|
|
var trades = _closed.Find(_ => true);
|
|
dgvHistory.DataSource = trades;
|
|
lblHistSummary.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(cbSettingsAccount) is not int accId) return;
|
|
Guarded("Settings", () =>
|
|
{
|
|
_currentSettings = _settingsRepo.Get(accId) ?? new RfSettings { AccountId = accId };
|
|
pgSettings.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) => lblRfStatus.Text = text;
|
|
|
|
private sealed record AccountItem(int Id, string Label);
|
|
}
|
|
}
|