using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Analytics; using PolyTrader.Core.Persistence; using PolyTrader.Modules.Supervisor.Persistence; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTraderSharp.Ui { /// /// Live-Überblick-Widgets für den Launcher (Slice 4): Modul-PnL/Winrate-Kacheln, Supervisor-KI- /// Kurzfassung, Warnungen/Fehler aus den Logs und auffällige Trades. Isoliert als UserControl, /// damit der Launcher-Designer schlank bleibt. Alle Datenzugriffe defensiv (Widgets dürfen den /// Launcher nie brechen). Layout im Designer (LauncherWidgetsPanel.Designer.cs). /// public partial class LauncherWidgetsPanel : UserControl { private ITradeLogRepository? _tradeLog; private ISupervisorReportRepository? _reports; private readonly string _logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); public LauncherWidgetsPanel() { InitializeComponent(); colNotPnl.DefaultCellStyle.Format = "F2"; colNotPnlPct.DefaultCellStyle.Format = "F1"; } public void Initialize(IServiceProvider services) { _tradeLog = services.GetService(); _reports = services.GetService(); RefreshData(); } /// Aktualisiert alle Widgets. Wird vom Launcher periodisch aufgerufen. public void RefreshData() { if (IsDisposed) return; UpdateModuleKpis(); UpdateAlerts(); UpdateNotable(); UpdateSupervisor(); } // ----- Modul-PnL/Winrate-Kacheln ----- private void UpdateModuleKpis() { flpKpis.SuspendLayout(); flpKpis.Controls.Clear(); try { var since = DateTime.UtcNow.AddDays(-30); List all = _tradeLog?.Find(t => t.ClosedAt >= since) ?? new List(); DateTime today = DateTime.UtcNow.Date; DateTime week = DateTime.UtcNow.AddDays(-7); foreach (var module in all.Select(t => t.ModuleName).Where(m => !string.IsNullOrEmpty(m)).Distinct().OrderBy(m => m)) flpKpis.Controls.Add(BuildTile(module, all.Where(t => t.ModuleName == module).ToList(), today, week)); flpKpis.Controls.Add(BuildTile("Gesamt", all, today, week)); if (flpKpis.Controls.Count == 1) // nur "Gesamt", keine Trades flpKpis.Controls.Add(new Label { AutoSize = true, Margin = new Padding(6), ForeColor = Color.Gray, Text = "Noch keine abgeschlossenen Trades." }); } catch (Exception ex) { flpKpis.Controls.Add(new Label { AutoSize = true, ForeColor = Color.Firebrick, Text = $"KPIs n/v: {ex.Message}" }); } flpKpis.ResumeLayout(); } private static Label BuildTile(string module, List moduleTrades, DateTime today, DateTime week) { decimal todayPnl = moduleTrades.Where(t => t.ClosedAt >= today).Sum(t => t.RealizedPnl); var k7 = TradeAnalytics.ComputeKpis(moduleTrades.Where(t => t.ClosedAt >= week)); var k30 = TradeAnalytics.ComputeKpis(moduleTrades); var tile = new Label { AutoSize = false, Width = 252, Height = 92, BorderStyle = BorderStyle.FixedSingle, Margin = new Padding(3), Padding = new Padding(7), TextAlign = ContentAlignment.TopLeft, Font = new Font("Segoe UI", 9F), Text = $"{module}\n" + $"Heute: {todayPnl:+0.00;-0.00} USDC\n" + $"7T: {k7.NetPnl:+0.00;-0.00} · {k7.WinRatePct:0}% · {k7.TradeCount} Tr.\n" + $"30T: {k30.NetPnl:+0.00;-0.00} · {k30.WinRatePct:0}%" }; tile.ForeColor = k30.NetPnl >= 0 ? Color.ForestGreen : Color.Firebrick; return tile; } // ----- Warnungen & Fehler (heute, JSONL) ----- private void UpdateAlerts() { try { string path = Path.Combine(_logsDir, $"{DateTime.Now:yyyy-MM-dd}.jsonl"); var rows = new List(); if (File.Exists(path)) { foreach (var line in File.ReadLines(path)) { var p = LogJson.ParseLine(line); if (p == null) continue; if (p.Level != "Error" && p.Level != "Warning") continue; rows.Add(new AlertRow { Time = ShortTime(p.Time), Level = p.Level, Message = OneLine(p.Message) }); } } rows.Reverse(); // neueste zuerst dgvAlerts.DataSource = rows.Take(200).ToList(); grpAlerts.Text = $"Warnungen & Fehler (heute): {rows.Count}"; } catch (Exception ex) { grpAlerts.Text = $"Warnungen & Fehler – n/v ({ex.Message})"; } } // ----- Auffällige Trades (24h) ----- private void UpdateNotable() { try { var since = DateTime.UtcNow.AddDays(-1); var rows = (_tradeLog?.Find(t => t.ClosedAt >= since) ?? new List()) .OrderByDescending(t => Math.Abs(t.RealizedPnl)) .Take(30) .Select(t => new NotableRow { Module = t.ModuleName, Market = t.MarketQuestion, Pnl = t.RealizedPnl, PnlPct = t.PnlPercent }) .ToList(); dgvNotable.DataSource = rows; } catch (Exception ex) { grpNotable.Text = $"Auffällige Trades – n/v ({ex.Message})"; } } // ----- Supervisor-KI-Kurzfassung ----- private void UpdateSupervisor() { if (_reports == null) { rtbSupervisor.Text = "Supervisor-Modul nicht verfügbar."; return; } try { var last = _reports.GetRecent(1).FirstOrDefault(); rtbSupervisor.Text = last == null ? "Noch kein Supervisor-Bericht. (OpenRouter-Key setzen und im Supervisor-Fenster eine Analyse starten.)" : $"[{last.CreatedAt:dd.MM. HH:mm}] {last.Profile} · {last.Model}\n\n{last.Answer}"; } catch (Exception ex) { rtbSupervisor.Text = $"Supervisor-Berichte n/v: {ex.Message}"; } } // ----- Helfer ----- private static string ShortTime(string iso) => DateTime.TryParse(iso, out var dt) ? dt.ToString("HH:mm:ss") : iso; private static string OneLine(string s) => (s ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim(); private sealed class AlertRow { public string Time { get; set; } = string.Empty; public string Level { get; set; } = string.Empty; public string Message { get; set; } = string.Empty; } private sealed class NotableRow { public string Module { get; set; } = string.Empty; public string Market { get; set; } = string.Empty; public decimal Pnl { get; set; } public decimal PnlPct { get; set; } } } }