diff --git a/Program.cs b/Program.cs index 3fdfaed..9677108 100644 --- a/Program.cs +++ b/Program.cs @@ -265,7 +265,7 @@ internal static class Program var config = viewServices.GetRequiredService(); // Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen. var moduleInfos = allModules - .Select(m => new PolyTraderSharp.Ui.Views.ModuleActivationInfo( + .Select(m => new PolyTrader.Core.Modularity.ModuleActivationInfo( m.Name, modules.Contains(m), m.GetActivationBlocker(config))) .ToList(); view.Initialize( diff --git a/Ui/Views/DashboardView.cs b/Ui/Views/DashboardView.cs index 570d61f..0775c9c 100644 --- a/Ui/Views/DashboardView.cs +++ b/Ui/Views/DashboardView.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Windows.Forms; using PolyTrader.Core.Analytics; +using PolyTrader.Core.Modularity; using PolyTrader.Core.Persistence; using PolyTraderSharp.Models; @@ -388,12 +389,6 @@ namespace PolyTraderSharp.Ui.Views } } - /// - /// Aktivierungs-Info eines Moduls fürs Dashboard: Name, ob es in dieser Session läuft und ein - /// optionaler Grund, warum es nicht aktivierbar ist (z.B. fehlender API-Key). - /// - public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker); - /// Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst). public class DashboardTradeRow { diff --git a/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj index e083f9e..4dd3915 100644 --- a/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj +++ b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj @@ -1,4 +1,4 @@ - + + + + + + + diff --git a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs index f3219e6..ae28174 100644 --- a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs +++ b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs @@ -1,6 +1,10 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Modularity; +using PolyTraderSharp; using PolyTraderSharp.Services; namespace PolyTrader.App.Avalonia.Shell @@ -12,8 +16,25 @@ namespace PolyTrader.App.Avalonia.Shell /// public static class CoreViews { + /// Quelle/Ziel der Modul-Aktivierung – dieselbe Datei wie im Host. + private const string ServerSettingsPath = "server_settings.xml"; + public static void Register(IModuleUiHost host, IServiceProvider services) { + host.RegisterView(new ModuleView + { + Id = "core.dashboard", + Title = "Dashboard", + Group = "Core", + Order = 10, + CreateView = () => new Views.DashboardWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + BuildModuleInfos(services), + ServerSettingsPath) + }); + host.RegisterView(new ModuleView { Id = "core.jobs", @@ -23,9 +44,40 @@ namespace PolyTrader.App.Avalonia.Shell CreateView = () => new Views.JobsWindow(host, services.GetRequiredService()) }); - // TODO Portierung: core.dashboard (Order 10), core.settings (Order 20) und - // core.terminal (Order 40) folgen. Aufbau siehe docs/UI-SPEZIFIKATION-WinForms.md, - // Originalcode im Git-Tag winforms-final. + // TODO Portierung: core.settings (Order 20) und core.terminal (Order 40) folgen. + // Aufbau siehe docs/UI-SPEZIFIKATION-WinForms.md, Originalcode im Git-Tag winforms-final. + } + + /// + /// Baut die Aktivierungs-Übersicht für den Modul-Tab: alle bekannten Module, ob sie in dieser + /// Sitzung laufen, und ein etwaiger Grund gegen die Aktivierung (z.B. fehlender API-Key). + /// + private static IReadOnlyList BuildModuleInfos(IServiceProvider services) + { + var running = services.GetServices().ToList(); + var runningNames = running.Select(m => m.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); + var config = services.GetRequiredService(); + + // Auch die NICHT geladenen Module auflisten – sonst liessen sie sich nie wieder aktivieren. + var all = new IPolyTraderModule[] + { + new PolyTrader.Modules.CopyTrading.CopyTradingModule(), + new PolyTrader.Modules.ResolutionFarming.ResolutionFarmingModule(), + new PolyTrader.Modules.Supervisor.SupervisorModule(), + new PolyTrader.Modules.Accounting.AccountingModule() + }; + + return all.Select(m => new ModuleActivationInfo( + m.Name, + runningNames.Contains(m.Name), + SafeBlocker(m, config))) + .ToList(); + + static string? SafeBlocker(IPolyTraderModule m, IConfiguration config) + { + try { return m.GetActivationBlocker(config); } + catch (Exception ex) { return $"Prüfung fehlgeschlagen: {ex.Message}"; } + } } } } diff --git a/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs new file mode 100644 index 0000000..b10699f --- /dev/null +++ b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using LiveChartsCore; +using LiveChartsCore.Defaults; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; +using PolyTrader.Core.Analytics; +using PolyTraderSharp.Models; +using SkiaSharp; + +namespace PolyTrader.App.Avalonia.ViewModels +{ + /// + /// Datenmodell der drei Dashboard-Diagramme (LiveCharts2). Ersetzt das frühere Rendern nach + /// Bitmap: die Diagramme sind jetzt echte Steuerelemente – interaktiv und ohne System.Drawing. + /// + /// Die Auswertung selbst kommt unverändert aus ; dieses Modell + /// übersetzt deren Ergebnisse nur in Serien und Achsen. Die Trennung war schon vorher da und ist + /// der Grund, warum der Chart-Wechsel die Fachlogik nicht berührt. + /// + public sealed class DashboardChartsModel : INotifyPropertyChanged + { + private static readonly SKColor Accent = new(0x1E, 0x88, 0xE5); + private static readonly SKColor Positive = new(0x2E, 0x7D, 0x32); + private static readonly SKColor Negative = new(0xC6, 0x28, 0x28); + + private ISeries[] _equitySeries = Array.Empty(); + private ISeries[] _moduleSeries = Array.Empty(); + private ISeries[] _daySeries = Array.Empty(); + private Axis[] _equityXAxes = Array.Empty(); + private Axis[] _moduleXAxes = Array.Empty(); + private Axis[] _dayXAxes = Array.Empty(); + + public ISeries[] EquitySeries { get => _equitySeries; private set => Set(ref _equitySeries, value); } + public ISeries[] ModuleSeries { get => _moduleSeries; private set => Set(ref _moduleSeries, value); } + public ISeries[] DaySeries { get => _daySeries; private set => Set(ref _daySeries, value); } + public Axis[] EquityXAxes { get => _equityXAxes; private set => Set(ref _equityXAxes, value); } + public Axis[] ModuleXAxes { get => _moduleXAxes; private set => Set(ref _moduleXAxes, value); } + public Axis[] DayXAxes { get => _dayXAxes; private set => Set(ref _dayXAxes, value); } + + /// Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf. + public void Update(IReadOnlyList scoped) + { + BuildEquity(scoped); + BuildByModule(scoped); + BuildByDay(scoped); + } + + private void BuildEquity(IReadOnlyList scoped) + { + var curve = TradeAnalytics.EquityCurve(scoped); + + EquitySeries = new ISeries[] + { + new LineSeries + { + Name = "Kumulierter PnL", + Values = curve.Select(p => new DateTimePoint(p.At, (double)p.Cumulative)).ToArray(), + Stroke = new SolidColorPaint(Accent, 2), + GeometrySize = 0, // reine Linie – bei vielen Trades sonst unruhig + Fill = null + } + }; + + EquityXAxes = new[] + { + new Axis + { + // Ticks kommen als DateTime-Ticks – zurueckrechnen und kurz formatieren. + Labeler = value => new DateTime((long)value).ToString("dd.MM."), + UnitWidth = TimeSpan.FromDays(1).Ticks, + LabelsRotation = 0 + } + }; + } + + private void BuildByModule(IReadOnlyList scoped) + { + var byMod = TradeAnalytics.PnlByKey(scoped, + t => string.IsNullOrEmpty(t.ModuleName) ? "—" : t.ModuleName); + + ModuleSeries = new ISeries[] + { + new ColumnSeries + { + Name = "PnL", + Values = byMod.Select(x => (double)x.Pnl).ToArray(), + Fill = new SolidColorPaint(Accent) + } + }; + ModuleXAxes = new[] { new Axis { Labels = byMod.Select(x => x.Key).ToArray(), LabelsRotation = 0 } }; + } + + private void BuildByDay(IReadOnlyList scoped) + { + var byDay = TradeAnalytics.PnlByDay(scoped); + + DaySeries = new ISeries[] + { + new ColumnSeries + { + Name = "PnL", + Values = byDay.Select(x => (double)x.Pnl).ToArray(), + // Verlusttage rot, Gewinntage gruen – dieselbe Leseweise wie in den Trade-Listen. + Fill = new SolidColorPaint(byDay.Sum(x => x.Pnl) >= 0m ? Positive : Negative) + } + }; + DayXAxes = new[] + { + new Axis + { + Labels = byDay.Select(x => x.Day.ToString("dd.MM")).ToArray(), + LabelsRotation = byDay.Count > 20 ? 45 : 0 // bei vielen Tagen sonst unleserlich + } + }; + } + + private void Set(ref T field, T value, [CallerMemberName] string? name = null) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + public event PropertyChangedEventHandler? PropertyChanged; + } +} diff --git a/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs b/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs new file mode 100644 index 0000000..a267fa8 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/ViewModels/DashboardRows.cs @@ -0,0 +1,36 @@ +using System; + +namespace PolyTrader.App.Avalonia.ViewModels +{ + /// Anzeige-Zeile des Historie-Grids (Konto bereits zum Namen aufgelöst). + public sealed class DashboardTradeRow + { + public string Module { get; init; } = string.Empty; + public string Account { 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 EntryPrice { get; init; } + public decimal ExitPrice { get; init; } + public decimal Size { get; init; } + public decimal RealizedPnl { get; init; } + public decimal PnlPercent { get; init; } + public DateTime ClosedAt { get; init; } + } + + /// Anzeige-Zeile des Modul-Grids (Tab „Module"). + public sealed class ModuleRow + { + public string Modul { get; init; } = string.Empty; + public string Status { get; init; } = string.Empty; + public string Hinweis { get; init; } = string.Empty; + public string Aktion { get; init; } = string.Empty; + + /// + /// Falsch, wenn das Modul einen Aktivierungs-Blocker hat (z.B. fehlender API-Key). Die + /// WinForms-Fassung zeigte in dem Fall beim Klick einen Hinweisdialog; hier ist die + /// Schaltfläche schlicht deaktiviert – der Grund steht ohnehin in der Spalte „Hinweis". + /// + public bool CanToggle { get; init; } = true; + } +} diff --git a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml new file mode 100644 index 0000000..6ee5ab4 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml @@ -0,0 +1,171 @@ + + + + + + + + +