Files
PolyTraderSharp/Ui/LauncherWidgetsPanel.cs
T
RichardandClaude Opus 4.8 3c8ea3534e UI Slice 4: Launcher-Live-Widgets + VS-Re-Serialisierungs-Regressionen behoben
Slice 4 - Launcher-Live-Ueberblick (LauncherWidgetsPanel, isoliertes UserControl, rechts angedockt,
30s-Refresh; minimaler Eingriff ins von Richard bearbeitete Launcher-Designer):
- Modul-PnL/Winrate-Kacheln (je Modul + Gesamt: Heute/7T/30T, gruen/rot) via TradeAnalytics.
- Supervisor-KI-Kurzfassung (letzter sup_report).
- Warnungen & Fehler (heutige JSONL-Logs, Error/Warning).
- Auffaellige Trades (24h, nach |PnL| sortiert).

Regressionen aus VS-Re-Serialisierung behoben (VS liess hand-erstellte DataGridView-Spalten fallen
-> col* null -> NRE beim Oeffnen):
- DashboardView (dgvTrades): Spalten-Instanziierung + AutoGenerateColumns=false + Columns.AddRange
  + Spalten-Konfig wiederhergestellt.
- JobsView (dgvJobs): dito (nur Button-Spalte hatte ueberlebt).
- Smoke-UI dauerhaft um JobsView/TerminalView/SettingsView erweitert -> faengt diese Regressionsklasse
  kuenftig ab.

Enthaelt ausserdem Richards zwischenzeitliche UI-Arbeit (Launcher-Icons cross_reference/emotion_batman/
file_start_workflow, Designer-Re-Serialisierungen, .ico-Sammlung, Modul-Form-.resx). Persoenliche
Notizdatei bewusst NICHT committet.

Build 0 Fehler, 396 Tests gruen, --smoke-ui alle 9 Views/Forms gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:31:59 +02:00

206 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 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).
/// </summary>
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<ITradeLogRepository>();
_reports = services.GetService<ISupervisorReportRepository>();
RefreshData();
}
/// <summary>Aktualisiert alle Widgets. Wird vom Launcher periodisch aufgerufen.</summary>
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<TradeRecord> all = _tradeLog?.Find(t => t.ClosedAt >= since) ?? new List<TradeRecord>();
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<TradeRecord> 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<AlertRow>();
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<TradeRecord>())
.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; }
}
}
}