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
{
///
/// 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 ; Charts via ScottPlot
/// (als Bitmap gerendert – kein WinForms-GL-Control, saubere Dependencies).
///
public partial class DashboardView : Form
{
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
private List _allTrades = new(); // zuletzt geladener Roh-Satz
private List _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";
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;
RefreshData();
}
// ===== Daten laden / Scope =====
private void RefreshData()
{
if (_tradeLog == null) return;
try { _allTrades = _tradeLog.GetRecent(5000); }
catch { _allTrades = new List(); } // DB nicht bereit -> leer statt Absturz
_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 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 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 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 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(rows.ToList());
}
private DashboardTradeRow ToRow(TradeRecord r) => new()
{
Module = r.ModuleName,
Account = ResolveAccount(r.AccountId, r.IsDemo),
Market = r.MarketQuestion,
Outcome = r.Outcome,
Side = r.Side,
EntryPrice = r.EntryPrice,
ExitPrice = r.ExitPrice,
Size = r.Size,
RealizedPnl = r.RealizedPnl,
PnlPercent = r.PnlPercent,
ClosedAt = r.ClosedAt
};
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 sealed record FilterAccount(int Id, string Label)
{
public override string ToString() => Label;
}
}
/// Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).
public class DashboardTradeRow
{
public string Module { get; set; } = string.Empty;
public string Account { get; set; } = string.Empty;
public string Market { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Side { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal ExitPrice { get; set; }
public decimal Size { get; set; }
public decimal RealizedPnl { get; set; }
public decimal PnlPercent { get; set; }
public DateTime ClosedAt { get; set; }
}
}