UI: Dashboard-Ueberarbeitung (Scope-Filter, KPIs, ScottPlot-Charts, Tradehistorie)

Das Dashboard ist keine stumpfe Trade-Liste mehr, sondern ein Auswertungs-Fenster:
- ToolStrip (via Designer): Scope-Filter Konto/Modul/Live-Demo/Zeitraum + Aktualisieren.
- TabControl mit 2 Tabs:
  - Dashboard: KPI-Kacheln (Netto-PnL/Winrate/Trades/O-PnL/Profit-Faktor) + 3 Charts
    (Equity-Kurve, PnL je Modul, PnL je Tag) fuer den gewaehlten Scope.
  - Tradehistorie: gefilterte Trade-Liste (Spalten via Designer) + Suche + Gewinner/Verlierer.
- Charts via ScottPlot CORE-Paket (nur SkiaSharp, .NET-nativ) -> als Bitmap in PictureBoxen
  gerendert; KEINE OpenTK/.NET-Framework-Transitiven (bewusst nicht ScottPlot.WinForms).
- Auswertungslogik pur in TradeAnalytics (getestet). In-Memory-Filter auf gecachtem Recent-Set.
- Smoke-UI konstruiert die DashboardView jetzt direkt -> verifiziert das Chart-Rendering headless.

Build 0 Fehler, 331 Tests gruen, --smoke-ui: [OK] core.dashboard konstruiert (inkl. Charts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-15 19:54:49 +02:00
co-authored by Claude Opus 4.8
parent 7d63791e38
commit b6dff7e210
4 changed files with 568 additions and 72 deletions
+2
View File
@@ -70,10 +70,12 @@
<ItemGroup>
<PackageReference Include="Nethereum.Web3" Version="6.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<!-- Dashboard-Charts (MIT, keine Umsatzschwelle, kein Phone-Home siehe docs/sicherheit). -->
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<!-- Angehoben, um Downgrade-Konflikt (EF Core zieht 8.0.2) aufzulösen. -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="ScottPlot" Version="5.1.59" />
</ItemGroup>
<ItemGroup>
+13
View File
@@ -454,6 +454,19 @@ internal static class Program
Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}");
}
// Core-Dashboard direkt konstruieren (im Smoke nicht via uiHost registriert) - prueft das ScottPlot-Rendering headless.
try
{
using var dash = new PolyTraderSharp.Ui.Views.DashboardView();
dash.Initialize(host.Services.GetRequiredService<ITradeLogRepository>(), host.Services.GetRequiredService<TradingState>());
Console.WriteLine("[OK] core.dashboard konstruiert (inkl. Charts)");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] core.dashboard: {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures;
}
+344 -46
View File
@@ -17,9 +17,29 @@ namespace PolyTraderSharp.Ui.Views
private void InitializeComponent()
{
this.pnlTop = new System.Windows.Forms.Panel();
this.lblSummary = new System.Windows.Forms.Label();
this.btnRefresh = new System.Windows.Forms.Button();
this.toolStripDash = new System.Windows.Forms.ToolStrip();
this.tslKonto = new System.Windows.Forms.ToolStripLabel();
this.cbAccount = new System.Windows.Forms.ToolStripComboBox();
this.tslModul = new System.Windows.Forms.ToolStripLabel();
this.cbModule = new System.Windows.Forms.ToolStripComboBox();
this.tslArt = new System.Windows.Forms.ToolStripLabel();
this.cbMode = new System.Windows.Forms.ToolStripComboBox();
this.tslZeit = new System.Windows.Forms.ToolStripLabel();
this.cbRange = new System.Windows.Forms.ToolStripComboBox();
this.tsRefresh = new System.Windows.Forms.ToolStripButton();
this.tabControlDash = new System.Windows.Forms.TabControl();
this.tabDashboard = new System.Windows.Forms.TabPage();
this.picEquity = new System.Windows.Forms.PictureBox();
this.pnlChartsBottom = new System.Windows.Forms.Panel();
this.picDay = new System.Windows.Forms.PictureBox();
this.picModule = new System.Windows.Forms.PictureBox();
this.flpKpis = new System.Windows.Forms.FlowLayoutPanel();
this.lblKpiPnl = new System.Windows.Forms.Label();
this.lblKpiWin = new System.Windows.Forms.Label();
this.lblKpiTrades = new System.Windows.Forms.Label();
this.lblKpiAvg = new System.Windows.Forms.Label();
this.lblKpiPf = new System.Windows.Forms.Label();
this.tabHistory = new System.Windows.Forms.TabPage();
this.dgvTrades = new System.Windows.Forms.DataGridView();
this.colModule = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAccount = new System.Windows.Forms.DataGridViewTextBoxColumn();
@@ -32,37 +52,242 @@ namespace PolyTraderSharp.Ui.Views
this.colPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnlPct = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colClosedAt = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.pnlTop.SuspendLayout();
this.pnlHistFilters = new System.Windows.Forms.Panel();
this.lblSuche = new System.Windows.Forms.Label();
this.tbSearch = new System.Windows.Forms.TextBox();
this.lblErgebnis = new System.Windows.Forms.Label();
this.cbWinLoss = new System.Windows.Forms.ComboBox();
this.toolStripDash.SuspendLayout();
this.tabControlDash.SuspendLayout();
this.tabDashboard.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.picEquity)).BeginInit();
this.pnlChartsBottom.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.picDay)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.picModule)).BeginInit();
this.flpKpis.SuspendLayout();
this.tabHistory.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).BeginInit();
this.pnlHistFilters.SuspendLayout();
this.SuspendLayout();
//
// pnlTop
// toolStripDash
//
this.pnlTop.Controls.Add(this.lblSummary);
this.pnlTop.Controls.Add(this.btnRefresh);
this.pnlTop.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlTop.Location = new System.Drawing.Point(0, 0);
this.pnlTop.Name = "pnlTop";
this.pnlTop.Size = new System.Drawing.Size(1100, 40);
this.pnlTop.TabIndex = 0;
this.toolStripDash.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.tslKonto, this.cbAccount, this.tslModul, this.cbModule, this.tslArt, this.cbMode,
this.tslZeit, this.cbRange, this.tsRefresh});
this.toolStripDash.Location = new System.Drawing.Point(0, 0);
this.toolStripDash.Name = "toolStripDash";
this.toolStripDash.Size = new System.Drawing.Size(1200, 27);
this.toolStripDash.TabIndex = 0;
//
// lblSummary
// tslKonto
//
this.lblSummary.AutoSize = true;
this.lblSummary.Location = new System.Drawing.Point(148, 12);
this.lblSummary.Name = "lblSummary";
this.lblSummary.Size = new System.Drawing.Size(63, 15);
this.lblSummary.TabIndex = 1;
this.lblSummary.Text = "—";
this.tslKonto.Name = "tslKonto";
this.tslKonto.Text = "Konto:";
//
// btnRefresh
// cbAccount
//
this.btnRefresh.Location = new System.Drawing.Point(8, 6);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(130, 28);
this.btnRefresh.TabIndex = 0;
this.btnRefresh.Text = "Aktualisieren";
this.btnRefresh.UseVisualStyleBackColor = true;
this.cbAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbAccount.Name = "cbAccount";
this.cbAccount.Size = new System.Drawing.Size(170, 27);
//
// tslModul
//
this.tslModul.Name = "tslModul";
this.tslModul.Text = "Modul:";
//
// cbModule
//
this.cbModule.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbModule.Name = "cbModule";
this.cbModule.Size = new System.Drawing.Size(150, 27);
//
// tslArt
//
this.tslArt.Name = "tslArt";
this.tslArt.Text = "Art:";
//
// cbMode
//
this.cbMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbMode.Name = "cbMode";
this.cbMode.Size = new System.Drawing.Size(100, 27);
//
// tslZeit
//
this.tslZeit.Name = "tslZeit";
this.tslZeit.Text = "Zeitraum:";
//
// cbRange
//
this.cbRange.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbRange.Name = "cbRange";
this.cbRange.Size = new System.Drawing.Size(110, 27);
//
// tsRefresh
//
this.tsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsRefresh.Name = "tsRefresh";
this.tsRefresh.Text = "Aktualisieren";
//
// tabControlDash
//
this.tabControlDash.Controls.Add(this.tabDashboard);
this.tabControlDash.Controls.Add(this.tabHistory);
this.tabControlDash.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControlDash.Location = new System.Drawing.Point(0, 27);
this.tabControlDash.Name = "tabControlDash";
this.tabControlDash.SelectedIndex = 0;
this.tabControlDash.Size = new System.Drawing.Size(1200, 623);
this.tabControlDash.TabIndex = 1;
//
// tabDashboard
//
this.tabDashboard.Controls.Add(this.picEquity);
this.tabDashboard.Controls.Add(this.pnlChartsBottom);
this.tabDashboard.Controls.Add(this.flpKpis);
this.tabDashboard.Location = new System.Drawing.Point(4, 24);
this.tabDashboard.Name = "tabDashboard";
this.tabDashboard.Padding = new System.Windows.Forms.Padding(3);
this.tabDashboard.Size = new System.Drawing.Size(1192, 595);
this.tabDashboard.TabIndex = 0;
this.tabDashboard.Text = "Dashboard";
this.tabDashboard.UseVisualStyleBackColor = true;
//
// picEquity
//
this.picEquity.BackColor = System.Drawing.Color.White;
this.picEquity.Dock = System.Windows.Forms.DockStyle.Fill;
this.picEquity.Location = new System.Drawing.Point(3, 79);
this.picEquity.Name = "picEquity";
this.picEquity.Size = new System.Drawing.Size(1186, 273);
this.picEquity.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.picEquity.TabIndex = 2;
this.picEquity.TabStop = false;
//
// pnlChartsBottom
//
this.pnlChartsBottom.Controls.Add(this.picDay);
this.pnlChartsBottom.Controls.Add(this.picModule);
this.pnlChartsBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
this.pnlChartsBottom.Location = new System.Drawing.Point(3, 352);
this.pnlChartsBottom.Name = "pnlChartsBottom";
this.pnlChartsBottom.Size = new System.Drawing.Size(1186, 240);
this.pnlChartsBottom.TabIndex = 1;
//
// picDay
//
this.picDay.BackColor = System.Drawing.Color.White;
this.picDay.Dock = System.Windows.Forms.DockStyle.Fill;
this.picDay.Location = new System.Drawing.Point(593, 0);
this.picDay.Name = "picDay";
this.picDay.Size = new System.Drawing.Size(593, 240);
this.picDay.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.picDay.TabIndex = 1;
this.picDay.TabStop = false;
//
// picModule
//
this.picModule.BackColor = System.Drawing.Color.White;
this.picModule.Dock = System.Windows.Forms.DockStyle.Left;
this.picModule.Location = new System.Drawing.Point(0, 0);
this.picModule.Name = "picModule";
this.picModule.Size = new System.Drawing.Size(593, 240);
this.picModule.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.picModule.TabIndex = 0;
this.picModule.TabStop = false;
//
// flpKpis
//
this.flpKpis.Controls.Add(this.lblKpiPnl);
this.flpKpis.Controls.Add(this.lblKpiWin);
this.flpKpis.Controls.Add(this.lblKpiTrades);
this.flpKpis.Controls.Add(this.lblKpiAvg);
this.flpKpis.Controls.Add(this.lblKpiPf);
this.flpKpis.Dock = System.Windows.Forms.DockStyle.Top;
this.flpKpis.Location = new System.Drawing.Point(3, 3);
this.flpKpis.Name = "flpKpis";
this.flpKpis.Padding = new System.Windows.Forms.Padding(4);
this.flpKpis.Size = new System.Drawing.Size(1186, 76);
this.flpKpis.TabIndex = 0;
this.flpKpis.WrapContents = false;
//
// lblKpiPnl
//
this.lblKpiPnl.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblKpiPnl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
this.lblKpiPnl.Margin = new System.Windows.Forms.Padding(4);
this.lblKpiPnl.MinimumSize = new System.Drawing.Size(190, 56);
this.lblKpiPnl.Name = "lblKpiPnl";
this.lblKpiPnl.Padding = new System.Windows.Forms.Padding(8);
this.lblKpiPnl.Size = new System.Drawing.Size(190, 56);
this.lblKpiPnl.TabIndex = 0;
this.lblKpiPnl.Text = "Netto-PnL\n—";
this.lblKpiPnl.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblKpiWin
//
this.lblKpiWin.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblKpiWin.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
this.lblKpiWin.Margin = new System.Windows.Forms.Padding(4);
this.lblKpiWin.MinimumSize = new System.Drawing.Size(150, 56);
this.lblKpiWin.Name = "lblKpiWin";
this.lblKpiWin.Padding = new System.Windows.Forms.Padding(8);
this.lblKpiWin.Size = new System.Drawing.Size(150, 56);
this.lblKpiWin.TabIndex = 1;
this.lblKpiWin.Text = "Winrate\n—";
this.lblKpiWin.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblKpiTrades
//
this.lblKpiTrades.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblKpiTrades.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
this.lblKpiTrades.Margin = new System.Windows.Forms.Padding(4);
this.lblKpiTrades.MinimumSize = new System.Drawing.Size(130, 56);
this.lblKpiTrades.Name = "lblKpiTrades";
this.lblKpiTrades.Padding = new System.Windows.Forms.Padding(8);
this.lblKpiTrades.Size = new System.Drawing.Size(130, 56);
this.lblKpiTrades.TabIndex = 2;
this.lblKpiTrades.Text = "Trades\n—";
this.lblKpiTrades.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblKpiAvg
//
this.lblKpiAvg.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblKpiAvg.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
this.lblKpiAvg.Margin = new System.Windows.Forms.Padding(4);
this.lblKpiAvg.MinimumSize = new System.Drawing.Size(170, 56);
this.lblKpiAvg.Name = "lblKpiAvg";
this.lblKpiAvg.Padding = new System.Windows.Forms.Padding(8);
this.lblKpiAvg.Size = new System.Drawing.Size(170, 56);
this.lblKpiAvg.TabIndex = 3;
this.lblKpiAvg.Text = "Ø PnL/Trade\n—";
this.lblKpiAvg.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblKpiPf
//
this.lblKpiPf.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblKpiPf.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
this.lblKpiPf.Margin = new System.Windows.Forms.Padding(4);
this.lblKpiPf.MinimumSize = new System.Drawing.Size(150, 56);
this.lblKpiPf.Name = "lblKpiPf";
this.lblKpiPf.Padding = new System.Windows.Forms.Padding(8);
this.lblKpiPf.Size = new System.Drawing.Size(150, 56);
this.lblKpiPf.TabIndex = 4;
this.lblKpiPf.Text = "Profit-Faktor\n—";
this.lblKpiPf.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// tabHistory
//
this.tabHistory.Controls.Add(this.dgvTrades);
this.tabHistory.Controls.Add(this.pnlHistFilters);
this.tabHistory.Location = new System.Drawing.Point(4, 24);
this.tabHistory.Name = "tabHistory";
this.tabHistory.Padding = new System.Windows.Forms.Padding(3);
this.tabHistory.Size = new System.Drawing.Size(1192, 595);
this.tabHistory.TabIndex = 1;
this.tabHistory.Text = "Tradehistorie";
this.tabHistory.UseVisualStyleBackColor = true;
//
// dgvTrades
//
@@ -71,23 +296,14 @@ namespace PolyTraderSharp.Ui.Views
this.dgvTrades.AutoGenerateColumns = false;
this.dgvTrades.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvTrades.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colModule,
this.colAccount,
this.colMarket,
this.colOutcome,
this.colSide,
this.colEntry,
this.colExit,
this.colSize,
this.colPnl,
this.colPnlPct,
this.colClosedAt});
this.colModule, this.colAccount, this.colMarket, this.colOutcome, this.colSide,
this.colEntry, this.colExit, this.colSize, this.colPnl, this.colPnlPct, this.colClosedAt});
this.dgvTrades.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvTrades.Location = new System.Drawing.Point(0, 40);
this.dgvTrades.Location = new System.Drawing.Point(3, 39);
this.dgvTrades.Name = "dgvTrades";
this.dgvTrades.ReadOnly = true;
this.dgvTrades.RowHeadersVisible = false;
this.dgvTrades.Size = new System.Drawing.Size(1100, 560);
this.dgvTrades.Size = new System.Drawing.Size(1186, 553);
this.dgvTrades.TabIndex = 1;
//
// colModule
@@ -167,26 +383,108 @@ namespace PolyTraderSharp.Ui.Views
this.colClosedAt.Name = "colClosedAt";
this.colClosedAt.Width = 140;
//
// pnlHistFilters
//
this.pnlHistFilters.Controls.Add(this.cbWinLoss);
this.pnlHistFilters.Controls.Add(this.lblErgebnis);
this.pnlHistFilters.Controls.Add(this.tbSearch);
this.pnlHistFilters.Controls.Add(this.lblSuche);
this.pnlHistFilters.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlHistFilters.Location = new System.Drawing.Point(3, 3);
this.pnlHistFilters.Name = "pnlHistFilters";
this.pnlHistFilters.Size = new System.Drawing.Size(1186, 36);
this.pnlHistFilters.TabIndex = 0;
//
// lblSuche
//
this.lblSuche.AutoSize = true;
this.lblSuche.Location = new System.Drawing.Point(6, 9);
this.lblSuche.Name = "lblSuche";
this.lblSuche.Size = new System.Drawing.Size(45, 15);
this.lblSuche.TabIndex = 0;
this.lblSuche.Text = "Suche:";
//
// tbSearch
//
this.tbSearch.Location = new System.Drawing.Point(57, 6);
this.tbSearch.Name = "tbSearch";
this.tbSearch.PlaceholderText = "Markt / Outcome …";
this.tbSearch.Size = new System.Drawing.Size(280, 23);
this.tbSearch.TabIndex = 1;
//
// lblErgebnis
//
this.lblErgebnis.AutoSize = true;
this.lblErgebnis.Location = new System.Drawing.Point(360, 9);
this.lblErgebnis.Name = "lblErgebnis";
this.lblErgebnis.Size = new System.Drawing.Size(58, 15);
this.lblErgebnis.TabIndex = 2;
this.lblErgebnis.Text = "Ergebnis:";
//
// cbWinLoss
//
this.cbWinLoss.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbWinLoss.Location = new System.Drawing.Point(424, 6);
this.cbWinLoss.Name = "cbWinLoss";
this.cbWinLoss.Size = new System.Drawing.Size(150, 23);
this.cbWinLoss.TabIndex = 3;
//
// DashboardView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.dgvTrades);
this.Controls.Add(this.pnlTop);
this.Controls.Add(this.tabControlDash);
this.Controls.Add(this.toolStripDash);
this.ClientSize = new System.Drawing.Size(1200, 650);
this.Name = "DashboardView";
this.Text = "Dashboard";
this.pnlTop.ResumeLayout(false);
this.pnlTop.PerformLayout();
this.toolStripDash.ResumeLayout(false);
this.toolStripDash.PerformLayout();
this.tabControlDash.ResumeLayout(false);
this.tabDashboard.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.picEquity)).EndInit();
this.pnlChartsBottom.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.picDay)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.picModule)).EndInit();
this.flpKpis.ResumeLayout(false);
this.tabHistory.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).EndInit();
this.pnlHistFilters.ResumeLayout(false);
this.pnlHistFilters.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel pnlTop;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Label lblSummary;
private System.Windows.Forms.ToolStrip toolStripDash;
private System.Windows.Forms.ToolStripLabel tslKonto;
private System.Windows.Forms.ToolStripComboBox cbAccount;
private System.Windows.Forms.ToolStripLabel tslModul;
private System.Windows.Forms.ToolStripComboBox cbModule;
private System.Windows.Forms.ToolStripLabel tslArt;
private System.Windows.Forms.ToolStripComboBox cbMode;
private System.Windows.Forms.ToolStripLabel tslZeit;
private System.Windows.Forms.ToolStripComboBox cbRange;
private System.Windows.Forms.ToolStripButton tsRefresh;
private System.Windows.Forms.TabControl tabControlDash;
private System.Windows.Forms.TabPage tabDashboard;
private System.Windows.Forms.FlowLayoutPanel flpKpis;
private System.Windows.Forms.Label lblKpiPnl;
private System.Windows.Forms.Label lblKpiWin;
private System.Windows.Forms.Label lblKpiTrades;
private System.Windows.Forms.Label lblKpiAvg;
private System.Windows.Forms.Label lblKpiPf;
private System.Windows.Forms.Panel pnlChartsBottom;
private System.Windows.Forms.PictureBox picModule;
private System.Windows.Forms.PictureBox picDay;
private System.Windows.Forms.PictureBox picEquity;
private System.Windows.Forms.TabPage tabHistory;
private System.Windows.Forms.Panel pnlHistFilters;
private System.Windows.Forms.Label lblSuche;
private System.Windows.Forms.TextBox tbSearch;
private System.Windows.Forms.Label lblErgebnis;
private System.Windows.Forms.ComboBox cbWinLoss;
private System.Windows.Forms.DataGridView dgvTrades;
private System.Windows.Forms.DataGridViewTextBoxColumn colModule;
private System.Windows.Forms.DataGridViewTextBoxColumn colAccount;
+199 -16
View File
@@ -1,45 +1,230 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Core.Analytics;
using PolyTrader.Core.Persistence;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Ui.Views
{
/// <summary>
/// Modulübergreifendes Dashboard: zeigt die letzten Trades ALLER Module aus dem
/// generischen Core-Trade-Log (ITradeLogRepository) und eine Kurzauswertung.
/// Designbar (DashboardView.Designer.cs).
/// Modulübergreifendes Dashboard aus dem generischen Core-Trade-Log (ITradeLogRepository).
/// Tab „Dashboard": KPI-Kacheln + Charts (Equity-Kurve, PnL je Modul, PnL je Tag) für den im
/// ToolStrip gewählten Scope (Konto/Modul/Live-Demo/Zeitraum). Tab „Tradehistorie": gefilterte
/// Trade-Liste. Auswertungslogik pur in <see cref="TradeAnalytics"/>; Charts via ScottPlot
/// (als Bitmap gerendert kein WinForms-GL-Control, saubere Dependencies).
/// </summary>
public partial class DashboardView : Form
{
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
private List<TradeRecord> _allTrades = new(); // zuletzt geladener Roh-Satz
private List<DashboardTradeRow> _historyBase = new(); // Scope-gefiltert, Basis für die Historie-Filter
private bool _loading;
public DashboardView()
{
InitializeComponent();
colEntry.DefaultCellStyle.Format = "F3";
colExit.DefaultCellStyle.Format = "F3";
colSize.DefaultCellStyle.Format = "F2";
colPnl.DefaultCellStyle.Format = "F2";
colPnlPct.DefaultCellStyle.Format = "F1";
colClosedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
btnRefresh.Click += (_, _) => LoadData();
cbMode.Items.AddRange(new object[] { "Alle", "Live", "Demo" });
cbRange.Items.AddRange(new object[] { "7 Tage", "30 Tage", "90 Tage", "Alle" });
cbWinLoss.Items.AddRange(new object[] { "Alle", "Gewinner", "Verlierer" });
tsRefresh.Click += (_, _) => RefreshData();
cbAccount.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
cbModule.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
cbMode.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
cbRange.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
tbSearch.TextChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
cbWinLoss.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
}
public void Initialize(ITradeLogRepository tradeLog, TradingState state)
{
_tradeLog = tradeLog;
_state = state;
LoadData();
RefreshData();
}
private void LoadData()
// ===== Daten laden / Scope =====
private void RefreshData()
{
if (_tradeLog == null) return;
try { _allTrades = _tradeLog.GetRecent(5000); }
catch { _allTrades = new List<TradeRecord>(); } // DB nicht bereit -> leer statt Absturz
var rows = _tradeLog.GetRecent(300).Select(r => new DashboardTradeRow
_loading = true;
PopulateScopeCombos();
_loading = false;
ApplyScope();
}
private void PopulateScopeCombos()
{
// Konten
var selectedAcc = (cbAccount.SelectedItem as FilterAccount)?.Id ?? -1;
cbAccount.Items.Clear();
cbAccount.Items.Add(new FilterAccount(-1, "Alle Konten"));
if (_state != null)
foreach (var a in _state.Accounts.Values.OrderBy(a => a.AccountId))
cbAccount.Items.Add(new FilterAccount(a.AccountId,
(string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : a.Name) + (a.IsDemo ? " (Demo)" : "")));
cbAccount.SelectedIndex = Math.Max(0, IndexOfAccount(selectedAcc));
// Module (aus den vorhandenen Daten)
string selectedMod = cbModule.SelectedItem as string ?? "Alle Module";
cbModule.Items.Clear();
cbModule.Items.Add("Alle Module");
foreach (var m in _allTrades.Select(t => t.ModuleName).Where(m => !string.IsNullOrEmpty(m)).Distinct().OrderBy(m => m))
cbModule.Items.Add(m);
int modIdx = cbModule.Items.IndexOf(selectedMod);
cbModule.SelectedIndex = modIdx >= 0 ? modIdx : 0;
if (cbMode.SelectedIndex < 0) cbMode.SelectedIndex = 0; // Alle
if (cbRange.SelectedIndex < 0) cbRange.SelectedIndex = 1; // 30 Tage
if (cbWinLoss.SelectedIndex < 0) cbWinLoss.SelectedIndex = 0;
}
private int IndexOfAccount(int id)
{
for (int i = 0; i < cbAccount.Items.Count; i++)
if (cbAccount.Items[i] is FilterAccount fa && fa.Id == id) return i;
return 0;
}
private void ApplyScope()
{
int accId = (cbAccount.SelectedItem as FilterAccount)?.Id ?? -1;
string module = cbModule.SelectedItem as string ?? "Alle Module";
string mode = cbMode.SelectedItem as string ?? "Alle";
int? days = (cbRange.SelectedItem as string) switch
{
"7 Tage" => 7,
"30 Tage" => 30,
"90 Tage" => 90,
_ => (int?)null
};
DateTime? since = days.HasValue ? DateTime.UtcNow.AddDays(-days.Value) : null;
var scoped = _allTrades.Where(t =>
(accId < 0 || t.AccountId == accId) &&
(module == "Alle Module" || t.ModuleName == module) &&
(mode == "Alle" || (mode == "Live" && !t.IsDemo) || (mode == "Demo" && t.IsDemo)) &&
(!since.HasValue || t.ClosedAt >= since.Value)).ToList();
UpdateKpis(scoped);
RenderCharts(scoped);
_historyBase = scoped
.OrderByDescending(t => t.ClosedAt)
.Select(ToRow)
.ToList();
ApplyHistoryFilter();
}
// ===== KPIs =====
private void UpdateKpis(List<TradeRecord> scoped)
{
var k = TradeAnalytics.ComputeKpis(scoped);
lblKpiPnl.Text = $"Netto-PnL\n{k.NetPnl:N2} USDC";
lblKpiPnl.ForeColor = k.NetPnl >= 0 ? System.Drawing.Color.ForestGreen : System.Drawing.Color.Firebrick;
lblKpiWin.Text = $"Winrate\n{k.WinRatePct:N1} %";
lblKpiTrades.Text = $"Trades\n{k.TradeCount}";
lblKpiAvg.Text = $"Ø PnL/Trade\n{k.AvgPnlPerTrade:N2}";
lblKpiPf.Text = $"Profit-Faktor\n{(k.ProfitFactor >= TradeAnalytics.NoLossProfitFactor ? "" : k.ProfitFactor.ToString("N2"))}";
}
// ===== Charts (ScottPlot -> Bitmap) =====
private void RenderCharts(List<TradeRecord> scoped)
{
RenderPlot(picEquity, plot =>
{
var curve = TradeAnalytics.EquityCurve(scoped);
plot.Title("Equity-Kurve (kumulierter PnL)");
if (curve.Count == 0) return;
double[] xs = curve.Select(p => p.At.ToOADate()).ToArray();
double[] ys = curve.Select(p => (double)p.Cumulative).ToArray();
plot.Add.Scatter(xs, ys);
plot.Axes.DateTimeTicksBottom();
});
RenderPlot(picModule, plot =>
{
var byMod = TradeAnalytics.PnlByKey(scoped, t => string.IsNullOrEmpty(t.ModuleName) ? "—" : t.ModuleName);
plot.Title("PnL je Modul");
if (byMod.Count == 0) return;
plot.Add.Bars(byMod.Select(x => (double)x.Pnl).ToArray());
SetCategoryTicks(plot, byMod.Select(x => x.Key).ToArray());
});
RenderPlot(picDay, plot =>
{
var byDay = TradeAnalytics.PnlByDay(scoped);
plot.Title("PnL je Tag");
if (byDay.Count == 0) return;
plot.Add.Bars(byDay.Select(x => (double)x.Pnl).ToArray());
SetCategoryTicks(plot, byDay.Select(x => x.Day.ToString("dd.MM")).ToArray());
});
}
private static void SetCategoryTicks(ScottPlot.Plot plot, string[] labels)
{
var ticks = new ScottPlot.TickGenerators.NumericManual();
for (int i = 0; i < labels.Length; i++)
ticks.AddMajor(i, labels[i]);
plot.Axes.Bottom.TickGenerator = ticks;
}
private static void RenderPlot(PictureBox pic, Action<ScottPlot.Plot> build)
{
int w = Math.Max(pic.ClientSize.Width, 300);
int h = Math.Max(pic.ClientSize.Height, 200);
var plot = new ScottPlot.Plot();
try { build(plot); }
catch { /* Chart-Rendering darf die UI nie killen */ }
byte[] png = plot.GetImage(w, h).GetImageBytes();
using var ms = new MemoryStream(png);
using var img = System.Drawing.Image.FromStream(ms);
var old = pic.Image;
pic.Image = new System.Drawing.Bitmap(img);
old?.Dispose();
}
// ===== Tradehistorie =====
private void ApplyHistoryFilter()
{
string search = tbSearch.Text.Trim();
string winLoss = cbWinLoss.SelectedItem as string ?? "Alle";
IEnumerable<DashboardTradeRow> rows = _historyBase;
if (search.Length > 0)
rows = rows.Where(r =>
(r.Market?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false) ||
(r.Outcome?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false));
if (winLoss == "Gewinner") rows = rows.Where(r => r.RealizedPnl > 0m);
else if (winLoss == "Verlierer") rows = rows.Where(r => r.RealizedPnl < 0m);
dgvTrades.DataSource = new BindingList<DashboardTradeRow>(rows.ToList());
}
private DashboardTradeRow ToRow(TradeRecord r) => new()
{
Module = r.ModuleName,
Account = ResolveAccount(r.AccountId, r.IsDemo),
@@ -52,14 +237,7 @@ namespace PolyTraderSharp.Ui.Views
RealizedPnl = r.RealizedPnl,
PnlPercent = r.PnlPercent,
ClosedAt = r.ClosedAt
}).ToList();
dgvTrades.DataSource = new BindingList<DashboardTradeRow>(rows);
decimal totalPnl = rows.Sum(x => x.RealizedPnl);
var perModule = rows.GroupBy(x => x.Module).Select(g => $"{g.Key}: {g.Count()}");
lblSummary.Text = $"{rows.Count} Trades | PnL gesamt: {totalPnl:F2} USDC | {string.Join(" · ", perModule)}";
}
};
private string ResolveAccount(int accountId, bool isDemo)
{
@@ -68,9 +246,14 @@ namespace PolyTraderSharp.Ui.Views
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
private sealed record FilterAccount(int Id, string Label)
{
public override string ToString() => Label;
}
}
/// <summary>Anzeige-Zeile für das Dashboard-Grid (Account bereits zu Name aufgelöst).</summary>
/// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary>
public class DashboardTradeRow
{
public string Module { get; set; } = string.Empty;