Dashboard nach Avalonia portiert - LiveCharts2 laeuft
Drittes Fenster: KPI-Kacheln, drei Diagramme, Tradehistorie mit Suche/Filter und Modul-Aktivierung. Aufbau nach docs/UI-SPEZIFIKATION-WinForms.md. - Diagramme sind jetzt echte Steuerelemente (LiveCharts2) statt nach Bitmap gerenderter ScottPlot-Bilder: interaktiv (Tooltips/Zoom) und ohne System.Drawing. Die Auswertung kommt unveraendert aus TradeAnalytics - der Chart-Wechsel beruehrte keine Fachlogik. Das war der Zweck der bestehenden Trennung und hat sich hier ausgezahlt. - ModuleActivationInfo vom UI-Typ in den Core verschoben (PolyTrader.Core.Modularity): Aussage ueber die Modularitaet, keine Darstellungsfrage - beide Shells brauchen sie. - Modul-Tab listet auch NICHT geladene Module, sonst liessen sie sich nie reaktivieren. Blockierte Module haben eine deaktivierte Schaltflaeche statt eines Hinweisdialogs; der Grund steht ohnehin in der Spalte 'Hinweis'. WICHTIG - Avalonia 12 -> 11.3.19 zurueckgenommen: Der erste Wurf zog per Version='*' Avalonia 12.1.1. LiveCharts2 2.0.5 (die aktuellste Version) ist gegen Avalonia 11 gebaut und bricht dort zur Laufzeit: MissingFieldException 'Avalonia.Input.Gestures.PinchEvent'. Avalonia 12 ist dem Chart-Oekosystem voraus. Jetzt 11.3.19 (DataGrid folgt eigener Reihe: 11.3.13). Erst wieder anheben, wenn LiveCharts2 Avalonia 12 unterstuetzt. Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (alle 4 Fenster), App laeuft real mit allen Trading-Diensten, publisht fuer linux-x64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Datenmodell der drei Dashboard-Diagramme (LiveCharts2). Ersetzt das frühere Rendern nach
|
||||
/// Bitmap: die Diagramme sind jetzt echte Steuerelemente – interaktiv und ohne System.Drawing.
|
||||
///
|
||||
/// <para>Die Auswertung selbst kommt unverändert aus <see cref="TradeAnalytics"/>; 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.</para>
|
||||
/// </summary>
|
||||
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<ISeries>();
|
||||
private ISeries[] _moduleSeries = Array.Empty<ISeries>();
|
||||
private ISeries[] _daySeries = Array.Empty<ISeries>();
|
||||
private Axis[] _equityXAxes = Array.Empty<Axis>();
|
||||
private Axis[] _moduleXAxes = Array.Empty<Axis>();
|
||||
private Axis[] _dayXAxes = Array.Empty<Axis>();
|
||||
|
||||
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); }
|
||||
|
||||
/// <summary>Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf.</summary>
|
||||
public void Update(IReadOnlyList<TradeRecord> scoped)
|
||||
{
|
||||
BuildEquity(scoped);
|
||||
BuildByModule(scoped);
|
||||
BuildByDay(scoped);
|
||||
}
|
||||
|
||||
private void BuildEquity(IReadOnlyList<TradeRecord> scoped)
|
||||
{
|
||||
var curve = TradeAnalytics.EquityCurve(scoped);
|
||||
|
||||
EquitySeries = new ISeries[]
|
||||
{
|
||||
new LineSeries<DateTimePoint>
|
||||
{
|
||||
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<TradeRecord> scoped)
|
||||
{
|
||||
var byMod = TradeAnalytics.PnlByKey(scoped,
|
||||
t => string.IsNullOrEmpty(t.ModuleName) ? "—" : t.ModuleName);
|
||||
|
||||
ModuleSeries = new ISeries[]
|
||||
{
|
||||
new ColumnSeries<double>
|
||||
{
|
||||
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<TradeRecord> scoped)
|
||||
{
|
||||
var byDay = TradeAnalytics.PnlByDay(scoped);
|
||||
|
||||
DaySeries = new ISeries[]
|
||||
{
|
||||
new ColumnSeries<double>
|
||||
{
|
||||
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<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace PolyTrader.App.Avalonia.ViewModels
|
||||
{
|
||||
/// <summary>Anzeige-Zeile des Historie-Grids (Konto bereits zum Namen aufgelöst).</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Anzeige-Zeile des Modul-Grids (Tab „Module").</summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// </summary>
|
||||
public bool CanToggle { get; init; } = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user