Files
PolyTraderSharp/src/PolyTrader.App.Avalonia/Views/Modules/SupervisorWindow.axaml.cs
T
RichardandClaude Opus 5 669676ac60 Modul-Fenster nach Avalonia - UI-Portierung inhaltlich abgeschlossen
Alle acht Fenster laufen jetzt unter Avalonia: Launcher, Dashboard, Settings, Terminal,
Server Jobs, Copytrading, ResolutionFarming, Supervisor, Accounting (+ Shutdown- und
Allzweck-Dialog).

- Copytrading: vier Tabs (Master-Trader mit Account-Zuweisung, Offene Trades, Geschlossene
  Trades mit Filterleiste, Account-Einstellungen). Zeilenfaerbung nach PnL laeuft ueber
  DataGrid.LoadingRow - greift damit auch bei virtualisierten Zeilen, anders als das
  fruehere Faerben in DataBindingComplete.
- ResolutionFarming: Kandidaten, Positionen, Historie, Settings - je Konto.
- Supervisor: Analyse-Chat gegen den Agenten (Tool-Fortschritt in der Statuszeile),
  Dossiers mit Markdown-Ansicht, Berichte, Counterfactuals.
- Accounting: KPI-Kacheln, Monats-BWA, Ledger, Abruf/Status, PDF- und CSV-Export ueber
  den plattformneutralen Datei-Dialog.

Der SettingsEditor traegt wie erwartet die drei restlichen PropertyGrid-Stellen
(Master-Trader, Account-Einstellungen, ResolutionFarming) mit je einem Aufruf.

ENTSCHEIDUNG - Modul-Fenster liegen in der App, nicht in den Modulen
(begruendet in Views/Modules/README.md): Die App ist der Kompositionswurzel und
referenziert ohnehin alle Module. So bleiben die Modulprojekte FREI VON AVALONIA, was fuer
den kopflosen Linux-Betrieb den Ausschlag gibt - der Daemon soll keine GUI-Bibliothek
mitschleppen. Verifiziert: kein Modul zieht Avalonia. Registrierung in Shell/ModuleViews.cs,
und zwar nur fuer tatsaechlich geladene Module - ein per DisabledModules abgeschaltetes
Modul bekommt gar kein Fenster.

Nebenbei: eigene View-Model-Typen CopyOpenTradeRow/CopyClosedTradeRow statt des
Modul-Modells ClosedTradeRow - sie tragen den aufgeloesten Master-Trader-Namen und die
Zeilenfarbe, die es dort nicht gibt (und vermeiden die Namensmehrdeutigkeit).

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (alle 8 Fenster + Launcher +
Dialog + Editor-Pruefung), App laeuft real mit allen Trading-Diensten, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:19:42 +02:00

193 lines
7.6 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Analytics;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Counterfactual;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTrader.Modules.Supervisor.Services;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// Supervisor: Analyse-Chat gegen den Agenten, Dossiers je Signal, gespeicherte Berichte und
/// Counterfactuals. Layout vollständig in SupervisorWindow.axaml.
/// </summary>
public partial class SupervisorWindow : Window
{
private readonly ObservableCollection<ChatEntry> _chat = new();
private readonly ObservableCollection<SignalSummary> _signals = new();
private readonly ObservableCollection<SupervisorReport> _reports = new();
private readonly ObservableCollection<CounterfactualRecord> _counterfactuals = new();
private readonly SupervisorAgent _agent = null!;
private readonly DossierService _dossiers = null!;
private readonly ISupervisorReportRepository _reportRepo = null!;
private readonly ISupervisorCounterfactualRepository _cfRepo = null!;
private readonly TerminalLogger _logger = null!;
public SupervisorWindow() => AvaloniaXamlLoader.Load(this);
public SupervisorWindow(IModuleUiHost host, SupervisorAgent agent, DossierService dossiers,
ISupervisorReportRepository reportRepo, ISupervisorCounterfactualRepository cfRepo,
TerminalLogger logger) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "supervisor.main", this);
_agent = agent;
_dossiers = dossiers;
_reportRepo = reportRepo;
_cfRepo = cfRepo;
_logger = logger;
this.FindControl<ItemsControl>("chatLines")!.ItemsSource = _chat;
this.FindControl<DataGrid>("gridSignals")!.ItemsSource = _signals;
this.FindControl<DataGrid>("gridReports")!.ItemsSource = _reports;
this.FindControl<DataGrid>("gridCounterfactual")!.ItemsSource = _counterfactuals;
var cbProfile = this.FindControl<ComboBox>("cbProfile")!;
cbProfile.ItemsSource = SupervisorProfiles.All.ToList();
cbProfile.SelectedIndex = 0;
this.FindControl<Button>("btnSend")!.Click += async (_, _) => await AskAsync();
this.FindControl<Button>("btnClearChat")!.Click += (_, _) => _chat.Clear();
this.FindControl<Button>("btnDossierRefresh")!.Click += (_, _) => LoadSignals();
this.FindControl<Button>("btnDossierOpen")!.Click += (_, _) => OpenDossier();
this.FindControl<Button>("btnReportsRefresh")!.Click += (_, _) => LoadReports();
this.FindControl<Button>("btnCfRefresh")!.Click += (_, _) => LoadCounterfactuals();
// Auswahl im Signal-Grid übernimmt die SignalId ins Eingabefeld.
this.FindControl<DataGrid>("gridSignals")!.SelectionChanged += (_, _) =>
{
if (this.FindControl<DataGrid>("gridSignals")!.SelectedItem is SignalSummary s)
this.FindControl<TextBox>("tbSignalId")!.Text = s.SignalId;
};
LoadSignals();
LoadReports();
LoadCounterfactuals();
}
// ===== Analyse =====
private async System.Threading.Tasks.Task AskAsync()
{
var tbQuestion = this.FindControl<TextBox>("tbQuestion")!;
string question = tbQuestion.Text?.Trim() ?? string.Empty;
if (question.Length == 0) return;
var btn = this.FindControl<Button>("btnSend")!;
var status = this.FindControl<TextBlock>("lblChatStatus")!;
AddChat("Du", question, Brushes.SteelBlue);
tbQuestion.Text = string.Empty;
btn.IsEnabled = false;
status.Text = "Der Supervisor denkt nach …";
try
{
var profile = this.FindControl<ComboBox>("cbProfile")!.SelectedItem as SupervisorProfile;
string? model = this.FindControl<TextBox>("tbModel")!.Text?.Trim();
if (string.IsNullOrWhiteSpace(model)) model = null;
// Zwischenmeldungen des Agenten (Tool-Aufrufe) laufen über die Statuszeile.
var progress = new Progress<string>(m => Dispatcher.UIThread.Post(() => status.Text = m));
var result = await _agent.AskAsync(question, model, progress, profile);
AddChat($"Supervisor ({profile?.Name ?? "Allgemein"})", result.Answer, Brushes.SeaGreen);
status.Text = $"{result.ToolInvocations.Count} Tool-Aufruf(e) · " +
$"{result.PromptTokens + result.CompletionTokens} Token.";
}
catch (Exception ex)
{
AddChat("Fehler", ex.Message, Brushes.Firebrick);
status.Text = "Anfrage fehlgeschlagen.";
_logger.Error($"Supervisor-Anfrage fehlgeschlagen: {ex.Message}");
}
finally
{
btn.IsEnabled = true;
LoadReports(); // die Anfrage wurde als Bericht abgelegt
}
}
private void AddChat(string who, string text, IBrush brush)
{
_chat.Add(new ChatEntry { Header = $"[{DateTime.Now:HH:mm:ss}] {who}", Text = text, HeaderBrush = brush });
this.FindControl<ScrollViewer>("chatScroll")!.ScrollToEnd();
}
// ===== Dossiers =====
private void LoadSignals()
{
try
{
_signals.Clear();
foreach (var s in _dossiers.RecentSignals(200)) _signals.Add(s);
}
catch (Exception ex)
{
_logger.Error($"Signale konnten nicht geladen werden: {ex.Message}");
}
}
private void OpenDossier()
{
string signalId = this.FindControl<TextBox>("tbSignalId")!.Text?.Trim() ?? string.Empty;
var target = this.FindControl<SelectableTextBlock>("txtDossier")!;
if (signalId.Length == 0)
{
target.Text = "Bitte eine SignalId auswählen oder eingeben.";
return;
}
try
{
var dossier = _dossiers.BuildForSignal(signalId);
target.Text = DossierBuilder.ToMarkdown(dossier);
}
catch (Exception ex)
{
target.Text = $"Dossier konnte nicht erstellt werden: {ex.Message}";
}
}
// ===== Berichte / Counterfactual =====
private void LoadReports()
{
try
{
_reports.Clear();
foreach (var r in _reportRepo.GetRecent(200)) _reports.Add(r);
}
catch (Exception ex)
{
_logger.Error($"Berichte konnten nicht geladen werden: {ex.Message}");
}
}
private void LoadCounterfactuals()
{
try
{
_counterfactuals.Clear();
foreach (var c in _cfRepo.GetRecent(500)) _counterfactuals.Add(c);
}
catch (Exception ex)
{
_logger.Error($"Counterfactuals konnten nicht geladen werden: {ex.Message}");
}
}
}
}