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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user