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>
193 lines
7.6 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|