Files
PolyTraderSharp/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs
T
RichardandClaude Opus 4.8 0725a81dbd UI Slice 3: CopyTrading - Zeilenfaerbung, Filter, Offene-Trades-Tab, Master-Grid-Fix
- Geschlossene Trades: Zeilenfaerbung nach PnL-% (TradeRowColoring, pur+getestet: <0 rot,
  0-10% hellgruen, >10% gruen) via DataBindingComplete. Filter-Panel (Designer): Markt (Text),
  Master-Trader (Combo, dynamisch), Ergebnis (Alle/Gewinner/Verlierer), Von/Bis (optionale
  DateTimePicker mit Checkbox), Zuruecksetzen. Summary zeigt gefiltert/gesamt.
- Neuer Tab 'Offene Trades' (OpenTradesView, designerfaehig): alle offenen Positionen aus dem
  Laufzeit-State mit Entry/Aktuell/Wert/Buchgewinn/-% und Status (offen/Exit laeuft), Zeilenfaerbung
  nach Buchgewinn-%, Summenzeile. In CopyTradingMainForm zwischen Master-Trader und Geschlossene eingehaengt.
- Master-Trader-Grid: AutoSizeColumnsMode=Fill + FillWeights -> Spaltenbreiten teilen sich immer die
  Breite (Fix der 'verbuggten' Breiten); RowHeader war bereits aus.

Tests: +8 (TradeRowColoring-Schwellen). Build 0 Fehler, 396 Tests gruen, --smoke-ui gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:49:41 +02:00

178 lines
7.1 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Zeigt die geschlossenen Copytrades des Moduls (mod_copytrading_closed_trades) mit
/// aufgelösten Account-/Master-Trader-Namen und einer Kurzauswertung.
/// Layout im Designer (ClosedTradesView.Designer.cs), Daten/Logik hier.
/// </summary>
public partial class ClosedTradesView : UserControl
{
private ICopyTradeLogRepository? _tradeLog;
private TradingState? _state;
private CopyTradingState? _copyState;
private List<ClosedTradeRow> _allRows = new();
private bool _loading;
// Parameterloser Konstruktor für den WinForms-Designer.
public ClosedTradesView()
{
InitializeComponent();
colEntry.DefaultCellStyle.Format = "F3";
colExit.DefaultCellStyle.Format = "F3";
colSize.DefaultCellStyle.Format = "F2";
colPnl.DefaultCellStyle.Format = "F2";
colPnlPct.DefaultCellStyle.Format = "F1";
colOpenedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
colClosedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
tsRefresh.Click += (_, _) => LoadData();
// Zeilenfärbung nach PnL-% (grün/hellgrün/rot) nach jedem (Neu-)Binden.
dgvTrades.DataBindingComplete += (_, _) => ColorRows();
// Filter: Markt (Text), Master (Combo), Ergebnis (Win/Loss), Datum (Von/Bis, optional).
tbMarket.TextChanged += (_, _) => ApplyFilter();
cbMaster.SelectedIndexChanged += (_, _) => ApplyFilter();
cbResult.SelectedIndexChanged += (_, _) => ApplyFilter();
dtFrom.ValueChanged += (_, _) => ApplyFilter();
dtTo.ValueChanged += (_, _) => ApplyFilter();
btnReset.Click += (_, _) => ResetFilters();
if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0;
}
/// <summary>Injiziert die Abhängigkeiten (nach der DI-Auflösung) und lädt die Daten.</summary>
public void Initialize(ICopyTradeLogRepository tradeLog, TradingState state, CopyTradingState copyState)
{
_tradeLog = tradeLog;
_state = state;
_copyState = copyState;
LoadData();
}
private void LoadData()
{
if (_tradeLog == null) return;
_allRows = _tradeLog.Find(_ => true)
.OrderByDescending(t => t.ClosedAt)
.Select(t => new ClosedTradeRow
{
TradeId = t.TradeId,
AccountId = t.AccountId,
SourceTraderId = t.SourceTraderId,
IsDemo = t.IsDemo,
TokenId = t.TokenId,
MarketSlug = t.MarketSlug,
MarketQuestion = t.MarketQuestion,
Outcome = t.Outcome,
Side = t.Side,
EntryPrice = t.EntryPrice,
ExitPrice = t.ExitPrice,
Size = t.Size,
RealizedPnl = t.RealizedPnl,
PnlPercent = t.PnlPercent,
TotalFees = t.TotalFees,
OpenedAt = t.OpenedAt,
ClosedAt = t.ClosedAt,
ExitReason = t.ExitReason,
AccountName = ResolveAccount(t.AccountId, t.IsDemo),
SourceTraderName = ResolveTrader(t.SourceTraderId)
})
.ToList();
PopulateMasters();
ApplyFilter();
}
private void PopulateMasters()
{
_loading = true;
string prev = cbMaster.SelectedItem as string ?? "Alle";
cbMaster.Items.Clear();
cbMaster.Items.Add("Alle");
foreach (var m in _allRows.Select(r => r.SourceTraderName)
.Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s))
cbMaster.Items.Add(m);
int idx = cbMaster.Items.IndexOf(prev);
cbMaster.SelectedIndex = idx >= 0 ? idx : 0;
_loading = false;
}
private void ApplyFilter()
{
if (_loading) return;
IEnumerable<ClosedTradeRow> q = _allRows;
string market = tbMarket.Text.Trim();
if (market.Length > 0)
q = q.Where(r => (r.MarketQuestion?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false)
|| (r.MarketSlug?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false));
if (cbMaster.SelectedItem is string master && master != "Alle")
q = q.Where(r => r.SourceTraderName == master);
string result = cbResult.SelectedItem as string ?? "Alle";
if (result == "Gewinner") q = q.Where(r => r.RealizedPnl > 0m);
else if (result == "Verlierer") q = q.Where(r => r.RealizedPnl < 0m);
if (dtFrom.Checked) { DateTime f = dtFrom.Value.Date; q = q.Where(r => r.ClosedAt >= f); }
if (dtTo.Checked) { DateTime t = dtTo.Value.Date.AddDays(1).AddTicks(-1); q = q.Where(r => r.ClosedAt <= t); }
var rows = q.ToList();
dgvTrades.DataSource = new BindingList<ClosedTradeRow>(rows);
decimal totalPnl = rows.Sum(x => x.RealizedPnl);
int wins = rows.Count(x => x.RealizedPnl > 0);
double winrate = rows.Count > 0 ? (double)wins / rows.Count * 100 : 0;
lblSummary.Text = $"{rows.Count} / {_allRows.Count} Trades | PnL: {totalPnl:F2} USDC | Winrate: {winrate:F1}%";
}
private void ResetFilters()
{
_loading = true;
tbMarket.Text = string.Empty;
if (cbMaster.Items.Count > 0) cbMaster.SelectedIndex = 0;
if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0;
dtFrom.Checked = false;
dtTo.Checked = false;
_loading = false;
ApplyFilter();
}
private void ColorRows()
{
foreach (DataGridViewRow row in dgvTrades.Rows)
if (row.DataBoundItem is ClosedTradeRow r)
row.DefaultCellStyle.BackColor = TradeRowColoring.ForPnlPercent(r.PnlPercent);
}
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
private string ResolveTrader(int traderId)
{
if (_copyState != null && _copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName))
return t.DisplayName;
return traderId > 0 ? $"#{traderId}" : "Unbekannt";
}
}
}