From 669676ac6064f8924d73f5daa58d8fe78ebf31b5 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 6 Aug 2026 22:19:42 +0200 Subject: [PATCH] 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 --- src/PolyTrader.App.Avalonia/App.axaml.cs | 1 + src/PolyTrader.App.Avalonia/Program.cs | 1 + .../Shell/ModuleViews.cs | 91 +++++ .../ViewModels/CopyTradingRows.cs | 78 +++++ .../ViewModels/DashboardRows.cs | 18 +- .../ViewModels/LogRows.cs | 10 +- .../Views/Modules/AccountingWindow.axaml | 167 +++++++++ .../Views/Modules/AccountingWindow.axaml.cs | 253 ++++++++++++++ .../Views/Modules/CopyTradingWindow.axaml | 161 +++++++++ .../Views/Modules/CopyTradingWindow.axaml.cs | 317 ++++++++++++++++++ .../Views/Modules/README.md | 18 + .../Modules/ResolutionFarmingWindow.axaml | 97 ++++++ .../Modules/ResolutionFarmingWindow.axaml.cs | 127 +++++++ .../Views/Modules/SupervisorWindow.axaml | 129 +++++++ .../Views/Modules/SupervisorWindow.axaml.cs | 192 +++++++++++ 15 files changed, 1658 insertions(+), 2 deletions(-) create mode 100644 src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs create mode 100644 src/PolyTrader.App.Avalonia/ViewModels/CopyTradingRows.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/README.md create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/SupervisorWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/Modules/SupervisorWindow.axaml.cs diff --git a/src/PolyTrader.App.Avalonia/App.axaml.cs b/src/PolyTrader.App.Avalonia/App.axaml.cs index a3fd30b..3274a6d 100644 --- a/src/PolyTrader.App.Avalonia/App.axaml.cs +++ b/src/PolyTrader.App.Avalonia/App.axaml.cs @@ -33,6 +33,7 @@ namespace PolyTrader.App.Avalonia CoreViews.Register(uiHost, Services); foreach (var module in Services.GetServices()) module.RegisterUi(uiHost, Services); + ModuleViews.Register(uiHost, Services); ViewIcons.AssignDefaults(uiHost); var launcher = new LauncherWindow(uiHost, Services); diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs index e8da7b8..a865fde 100644 --- a/src/PolyTrader.App.Avalonia/Program.cs +++ b/src/PolyTrader.App.Avalonia/Program.cs @@ -175,6 +175,7 @@ namespace PolyTrader.App.Avalonia CoreViews.Register(uiHost, host.Services); foreach (var module in host.Services.GetServices()) module.RegisterUi(uiHost, host.Services); + ModuleViews.Register(uiHost, host.Services); ViewIcons.AssignDefaults(uiHost); int failures = 0; diff --git a/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs b/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs new file mode 100644 index 0000000..b318e76 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PolyTrader.Core.Modularity; + +namespace PolyTrader.App.Avalonia.Shell +{ + /// + /// Registriert die Fenster der Module. Liegt bewusst in der App und nicht in den Modulen – + /// Begründung in Views/Modules/README.md: die Modulprojekte bleiben dadurch frei von + /// Avalonia, was für den kopflosen Linux-Betrieb (--headless) den Ausschlag gibt. + /// + /// Registriert wird nur, was auch geladen ist: ist ein Modul über + /// DisabledModules abgeschaltet, fehlen seine Dienste im Container – dann entfällt auch + /// sein Fenster, und der Launcher zeigt es gar nicht erst an. + /// + public static class ModuleViews + { + public static void Register(IModuleUiHost host, IServiceProvider services) + { + RegisterIfLoaded(services, "CopyTrading", () => host.RegisterView(new ModuleView + { + Id = "copytrading.main", + Title = "Copytrading", + Group = "CopyTrading", + Order = 100, + CreateView = () => new Views.Modules.CopyTradingWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + })); + + RegisterIfLoaded(services, "ResolutionFarming", () => host.RegisterView(new ModuleView + { + Id = "resolutionfarming.main", + Title = "ResolutionFarming", + Group = "ResolutionFarming", + Order = 200, + CreateView = () => new Views.Modules.ResolutionFarmingWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + })); + + RegisterIfLoaded(services, "Supervisor", () => host.RegisterView(new ModuleView + { + Id = "supervisor.main", + Title = "Supervisor", + Group = "Supervisor", + Order = 300, + CreateView = () => new Views.Modules.SupervisorWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + })); + + RegisterIfLoaded(services, "Accounting", () => host.RegisterView(new ModuleView + { + Id = "accounting.main", + Title = "Accounting", + Group = "Accounting", + Order = 400, + CreateView = () => new Views.Modules.AccountingWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService()) + })); + } + + /// Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist. + private static void RegisterIfLoaded(IServiceProvider services, string moduleName, Action register) + { + bool loaded = services.GetServices() + .Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase)); + if (loaded) register(); + } + } +} diff --git a/src/PolyTrader.App.Avalonia/ViewModels/CopyTradingRows.cs b/src/PolyTrader.App.Avalonia/ViewModels/CopyTradingRows.cs new file mode 100644 index 0000000..304ff22 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/ViewModels/CopyTradingRows.cs @@ -0,0 +1,78 @@ +using System; +using Avalonia.Media; +using PolyTrader.Modules.CopyTrading.Logic; + +namespace PolyTrader.App.Avalonia.ViewModels +{ + /// + /// Avalonia-Farbpalette zu den -Kategorien. Die Schwellenlogik liegt + /// (getestet) in ; hier steht nur, wie die Kategorien aussehen. + /// Werte unverändert aus der WinForms-Oberfläche übernommen. + /// + public static class TradeRowPalette + { + public static readonly IBrush Loss = new SolidColorBrush(Color.FromRgb(245, 200, 200)); // rot + public static readonly IBrush SmallWin = new SolidColorBrush(Color.FromRgb(212, 240, 212)); // hellgrün + public static readonly IBrush BigWin = new SolidColorBrush(Color.FromRgb(140, 214, 140)); // grün + + public static IBrush ForPnlPercent(decimal pnlPercent) => TradeRowColoring.ForPnlPercent(pnlPercent) switch + { + TradeRowTint.Loss => Loss, + TradeRowTint.BigWin => BigWin, + _ => SmallWin + }; + } + + /// Zeile des Grids „Offene Trades" (aufgeloester Kontoname + Zeilenfarbe). + public sealed class CopyOpenTradeRow + { + public string Account { get; init; } = string.Empty; + public string Trader { get; init; } = string.Empty; + public string Market { get; init; } = string.Empty; + public string Outcome { get; init; } = string.Empty; + public string Side { get; init; } = string.Empty; + public decimal Entry { get; init; } + public decimal Current { get; init; } + public decimal Size { get; init; } + public decimal Amount { get; init; } + public decimal Value { get; init; } + public decimal Unrealized { get; init; } + public decimal UnrealizedPct { get; init; } + public string Status { get; init; } = string.Empty; + + /// Zeilenhintergrund nach Buchgewinn – wie zuvor die DataBindingComplete-Färbung. + public IBrush RowBrush => TradeRowPalette.ForPnlPercent(UnrealizedPct); + } + + /// + /// Zeile des Grids „Geschlossene Trades". Bewusst eigener Typ und nicht das Modell + /// PolyTraderSharp.Models.ClosedTradeRow: hier stehen der aufgeloeste Master-Trader-Name + /// und die Zeilenfarbe, die es dort nicht gibt. + /// + public sealed class CopyClosedTradeRow + { + public int TradeId { get; init; } + public string Account { get; init; } = string.Empty; + public string Trader { get; init; } = string.Empty; + public string Market { get; init; } = string.Empty; + public string Outcome { get; init; } = string.Empty; + public string Side { get; init; } = string.Empty; + public decimal Entry { get; init; } + public decimal Exit { get; init; } + public decimal Size { get; init; } + public decimal Pnl { get; init; } + public decimal PnlPercent { get; init; } + public DateTime OpenedAt { get; init; } + public DateTime ClosedAt { get; init; } + + public IBrush RowBrush => TradeRowPalette.ForPnlPercent(PnlPercent); + } + + /// Eintrag der Account-Zuweisung eines Master-Traders (Mehrfachauswahl). + public sealed class AccountAssignment + { + public int AccountId { get; init; } + public string Label { get; init; } = string.Empty; + public bool IsAssigned { get; set; } + } +} diff --git a/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs b/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs index a267fa8..07ba8a8 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace PolyTrader.App.Avalonia.ViewModels { @@ -33,4 +33,20 @@ namespace PolyTrader.App.Avalonia.ViewModels /// public bool CanToggle { get; init; } = true; } + + /// + /// Monatszeile der BWA-Übersicht. Die Werte sind bereits in die gewählte Währung umgerechnet + /// und als Text formatiert – die Umrechnung passiert einmal beim Aufbau, nicht je Bindung. + /// + public sealed class MonthlyRow + { + public string Month { get; init; } = string.Empty; + public string Net { get; init; } = string.Empty; + public string Deposits { get; init; } = string.Empty; + public string Withdrawals { get; init; } = string.Empty; + public string Fees { get; init; } = string.Empty; + public string Volume { get; init; } = string.Empty; + public int Trades { get; init; } + public string Closing { get; init; } = string.Empty; + } } diff --git a/src/PolyTrader.App.Avalonia/ViewModels/LogRows.cs b/src/PolyTrader.App.Avalonia/ViewModels/LogRows.cs index 9d36cee..078d3e5 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/LogRows.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/LogRows.cs @@ -1,4 +1,4 @@ -using Avalonia.Media; +using Avalonia.Media; namespace PolyTrader.App.Avalonia.ViewModels { @@ -20,4 +20,12 @@ namespace PolyTrader.App.Avalonia.ViewModels public string Cid { get; init; } = string.Empty; public string Message { get; init; } = string.Empty; } + + /// Ein Eintrag im Supervisor-Analyseverlauf (Frage oder Antwort). + public sealed class ChatEntry + { + public string Header { get; init; } = string.Empty; + public string Text { get; init; } = string.Empty; + public IBrush HeaderBrush { get; init; } = Brushes.SteelBlue; + } } diff --git a/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml b/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml new file mode 100644 index 0000000..a9bf0f7 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + +