Avalonia-Portierung verbleibender UI-Elemente (A1 bis A4) umgesetzt

Paritaet mit dem WinForms-Referenzstand (Tag winforms-final) hergestellt:

- A1 & A2 (Launcher): Account-Uebersicht DataGrid (inkl. Polymarket-Profil-Button via Launcher.LaunchUriAsync), Modul-KPI-Kacheln (7T/30T PnL & Winrate) und 3-Spalten-Live-Ueberblick (Auffaellige Trades 24h, Warnungen & Fehler heute aus den JSONL-Logs, Supervisor-KI Bericht). Periodische Aktualisierung alle 30 s eingebaut.
- A3 (Copytrading): Master-Trader Werkzeugleiste um 'Neu' (AddNewTrader) und 'Loeschen' (DeleteSelectedTrader mit DialogWindow.Confirm) erweitert.
- A4 (Terminal): Log-Kontextmenue fuer 'Kopieren' und 'Alles auswaehlen' an die Live-Log-Ausgabe angefuegt.

Verifiziert: Solution baut fehlerfrei, 450 Tests gruen, --smoke-ui test erfolgreich.

Co-Authored-By: Antigravity AI <antigravity@google.com>
This commit is contained in:
Richard
2026-08-10 10:48:27 +02:00
co-authored by Antigravity AI
parent 29ea62fc7f
commit 7f0b05e9ba
7 changed files with 454 additions and 19 deletions
@@ -1,20 +1,29 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Analytics;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// „Startleiste" der Anwendung – Gegenstück zur bisherigen LauncherForm. Öffnet die Fenster,
/// spiegelt deren Offen-Status und zeigt die Kernkennzahlen in der Statusleiste.
/// spiegelt deren Offen-Status, zeigt Kernkennzahlen und beinhaltet die Live-Überblick-Widgets.
/// Layout vollständig in LauncherWindow.axaml.
/// </summary>
public partial class LauncherWindow : Window
@@ -24,6 +33,12 @@ namespace PolyTrader.App.Avalonia.Views
private readonly TradingState _state;
private readonly DispatcherTimer _statusTimer = new() { Interval = TimeSpan.FromSeconds(1) };
private readonly ObservableCollection<WindowMenuEntry> _windowButtons = new();
private readonly ObservableCollection<AccountOverviewRow> _accountRows = new();
private readonly ObservableCollection<ModuleKpiTile> _kpiTiles = new();
private readonly ObservableCollection<NotableTradeRow> _notableRows = new();
private readonly ObservableCollection<LogAlertRow> _alertRows = new();
private int _statusTicks;
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
@@ -37,6 +52,11 @@ namespace PolyTrader.App.Avalonia.Views
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(uiHost, null, this);
this.FindControl<ItemsControl>("windowButtons")!.ItemsSource = _windowButtons;
this.FindControl<DataGrid>("gridAccounts")!.ItemsSource = _accountRows;
this.FindControl<ItemsControl>("kpiTiles")!.ItemsSource = _kpiTiles;
this.FindControl<DataGrid>("gridNotable")!.ItemsSource = _notableRows;
this.FindControl<DataGrid>("gridAlerts")!.ItemsSource = _alertRows;
RebuildWindowButtons();
_uiHost.OpenStateChanged += OnOpenStateChanged;
@@ -54,6 +74,7 @@ namespace PolyTrader.App.Avalonia.Views
_statusTimer.Tick += (_, _) => UpdateStatus();
_statusTimer.Start();
UpdateStatus();
RefreshData();
// Schliessen des Launchers laeuft ueber dieselbe Sicherheitsabfrage wie "Beenden".
Closing += (_, e) =>
@@ -110,8 +131,6 @@ namespace PolyTrader.App.Avalonia.Views
Apply(this.FindControl<Button>("btnLiveTrading")!, "LiveTrading", _state.LiveTradingMode);
Apply(this.FindControl<Button>("btnDemoTrading")!, "DemoTrading", _state.DemoTradingMode);
// Aussage wie in der WinForms-Oberflaeche (gruen/orange/rot); die konkreten Toene
// kommen aus dem Thema, damit sie auf hellem wie dunklem Grund lesbar bleiben.
static void Apply(Button btn, string label, TradingMode mode)
{
(string text, string brushKey) = mode switch
@@ -139,6 +158,245 @@ namespace PolyTrader.App.Avalonia.Views
$"API: {(_state.IsAlchemyHealthy ? "WSS aktiv" : "Polling")}";
UpdateTradingToggles();
// Account-Übersicht + Widgets alle 30 s aktualisieren
if (_statusTicks++ % 30 == 0)
{
RefreshData();
}
}
private void RefreshData()
{
LoadAccountOverview();
UpdateModuleKpis();
UpdateAlerts();
UpdateNotable();
UpdateSupervisor();
}
// ===== Account-Übersicht =====
private void LoadAccountOverview()
{
var tradeLog = _services.GetService<ITradeLogRepository>();
DateTime since3d = DateTime.UtcNow.AddDays(-3);
_accountRows.Clear();
if (_state?.Accounts == null) return;
foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId))
{
List<TradeRecord> trades;
try { trades = tradeLog?.Find(t => t.AccountId == acc.AccountId) ?? new List<TradeRecord>(); }
catch { trades = new List<TradeRecord>(); }
var (pnl3d, win3d, _) = TradeAnalytics.WindowSummary(trades.Where(t => t.ClosedAt >= since3d));
string modules = trades
.Select(t => t.ModuleName)
.Where(m => !string.IsNullOrEmpty(m))
.Distinct().OrderBy(m => m)
.DefaultIfEmpty("—")
.Aggregate((a, b) => a + ", " + b);
_accountRows.Add(new AccountOverviewRow
{
AccountId = acc.AccountId,
Name = (string.IsNullOrEmpty(acc.Name) ? $"#{acc.AccountId}" : acc.Name) + (acc.IsDemo ? " (Demo)" : ""),
Modules = modules,
WalletAddress = acc.WalletAddress ?? string.Empty,
Balance = acc.TotalBalance,
Pnl3d = pnl3d,
WinRate3d = win3d,
OverallPnl = trades.Sum(t => t.RealizedPnl)
});
}
}
public async void OnOpenPolymarketProfileClick(object? sender, RoutedEventArgs e)
{
if ((sender as Button)?.DataContext is AccountOverviewRow row)
{
if (string.IsNullOrWhiteSpace(row.WalletAddress))
{
await DialogWindow.Info(this, "Polymarket", "Für diesen Account ist keine Wallet-Adresse hinterlegt.");
return;
}
try
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
await topLevel.Launcher.LaunchUriAsync(new Uri($"https://polymarket.com/profile/{row.WalletAddress}"));
}
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Konnte Polymarket nicht öffnen: {ex.Message}");
}
}
}
// ===== Modul-KPI-Kacheln =====
private void UpdateModuleKpis()
{
_kpiTiles.Clear();
var tradeLog = _services.GetService<ITradeLogRepository>();
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);
var modules = all.Select(t => t.ModuleName).Where(m => !string.IsNullOrEmpty(m)).Distinct().OrderBy(m => m).ToList();
foreach (var module in modules)
{
var moduleTrades = all.Where(t => t.ModuleName == module).ToList();
_kpiTiles.Add(BuildTile(module, moduleTrades, today, week));
}
_kpiTiles.Add(BuildTile("Gesamt", all, today, week));
if (_kpiTiles.Count == 1 && all.Count == 0)
{
_kpiTiles.Add(new ModuleKpiTile
{
ModuleName = "Hinweis",
Text = "Noch keine abgeschlossenen Trades.",
IsPositive = true
});
}
}
catch (Exception ex)
{
_kpiTiles.Add(new ModuleKpiTile
{
ModuleName = "Fehler",
Text = $"KPIs n/v: {ex.Message}",
IsPositive = false
});
}
}
private static ModuleKpiTile 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);
string 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}%";
return new ModuleKpiTile
{
ModuleName = module,
Text = text,
IsPositive = k30.NetPnl >= 0
};
}
// ===== Warnungen & Fehler =====
private void UpdateAlerts()
{
var grpAlerts = this.FindControl<HeaderedContentControl>("grpAlerts");
try
{
string logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
string path = Path.Combine(logsDir, $"{DateTime.Now:yyyy-MM-dd}.jsonl");
var rows = new List<LogAlertRow>();
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 LogAlertRow
{
Time = ShortTime(p.Time),
Level = p.Level,
Message = OneLine(p.Message)
});
}
}
rows.Reverse(); // neueste zuerst
_alertRows.Clear();
foreach (var r in rows.Take(200)) _alertRows.Add(r);
if (grpAlerts != null) grpAlerts.Header = $"Warnungen & Fehler (heute): {rows.Count}";
}
catch (Exception ex)
{
if (grpAlerts != null) grpAlerts.Header = $"Warnungen & Fehler – n/v ({ex.Message})";
}
}
// ===== Auffällige Trades (24h) =====
private void UpdateNotable()
{
try
{
var tradeLog = _services.GetService<ITradeLogRepository>();
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 NotableTradeRow
{
Module = t.ModuleName ?? string.Empty,
Market = t.MarketQuestion ?? string.Empty,
Pnl = t.RealizedPnl,
PnlPct = t.PnlPercent
})
.ToList();
_notableRows.Clear();
foreach (var r in rows) _notableRows.Add(r);
}
catch
{
_notableRows.Clear();
}
}
// ===== Supervisor-KI-Kurzfassung =====
private void UpdateSupervisor()
{
var txtSupervisor = this.FindControl<SelectableTextBlock>("txtSupervisor");
if (txtSupervisor == null) return;
var reports = _services.GetService<ISupervisorReportRepository>();
if (reports == null)
{
txtSupervisor.Text = "Supervisor-Modul nicht verfügbar.";
return;
}
try
{
var last = reports.GetRecent(1).FirstOrDefault();
txtSupervisor.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)
{
txtSupervisor.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();
}
}