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; }
}
}
}