diff --git a/src/PolyTrader.App.Avalonia/App.axaml b/src/PolyTrader.App.Avalonia/App.axaml
index fdc3c0a..c2b51f0 100644
--- a/src/PolyTrader.App.Avalonia/App.axaml
+++ b/src/PolyTrader.App.Avalonia/App.axaml
@@ -409,11 +409,21 @@
+
+
+
+
diff --git a/src/PolyTrader.App.Avalonia/Shell/ThemeManager.cs b/src/PolyTrader.App.Avalonia/Shell/ThemeManager.cs
index b7f509f..a82965f 100644
--- a/src/PolyTrader.App.Avalonia/Shell/ThemeManager.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/ThemeManager.cs
@@ -2,7 +2,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
+using Avalonia.Platform;
using Avalonia.Styling;
+using Avalonia.Threading;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
@@ -23,13 +25,37 @@ namespace PolyTrader.App.Avalonia.Shell
private static ServerSettings? _settings;
private static string? _settingsPath;
private static TerminalLogger? _logger;
+ private static bool _colorHookAttached;
/// Feuert nach jedem Wechsel – Fenster zeichnen daraufhin ihre Code-Farben neu.
public static event Action? ThemeChanged;
/// Ist gerade das dunkle Schema aktiv (auch wenn es über „System" kam)?
- public static bool IsDark =>
- global::Avalonia.Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
+ ///
+ /// Gelesen wird die angeforderte Variante, nicht ActualThemeVariant der Anwendung:
+ /// Bei „System" steht dort – die Fenster lösen das
+ /// selbst nach hell oder dunkel auf, die Anwendung erfährt davon nichts. Der Vergleich mit
+ /// lieferte deshalb im Auslieferungszustand immer
+ /// false, obwohl dunkel gezeichnet wurde: Beschriftung, Symbol und
+ /// sagten geschlossen das Gegenteil dessen, was zu sehen war.
+ ///
+ public static bool IsDark
+ {
+ get
+ {
+ var app = global::Avalonia.Application.Current;
+ if (app == null) return false;
+
+ // Feste Wahl: die Antwort steht bereits in der angeforderten Variante.
+ var requested = app.RequestedThemeVariant;
+ if (requested == ThemeVariant.Dark) return true;
+ if (requested == ThemeVariant.Light) return false;
+
+ // „System": Die Betriebssystem-Einstellung ist die Quelle, aus der auch die Fenster
+ // auflösen – also dort nachsehen statt auf Anwendungsebene zu raten.
+ return app.PlatformSettings?.GetColorValues().ThemeVariant == PlatformThemeVariant.Dark;
+ }
+ }
///
/// Einmalig beim Start aufrufen: übernimmt das gespeicherte Schema und merkt sich, wohin
@@ -41,6 +67,28 @@ namespace PolyTrader.App.Avalonia.Shell
_settingsPath = settingsPath;
_logger = logger;
ApplyVariant(settings.Theme);
+ HookSystemColorChanges();
+ }
+
+ ///
+ /// Bei „System" wechselt das Schema, ohne dass jemand bei uns klickt. Avalonia stellt die
+ /// Standard-Steuerelemente dann selbst um – die im Code gesetzten Farben und die Anzeige in
+ /// der Kopf- und Statusleiste bleiben aber stehen, bis feuert.
+ ///
+ private static void HookSystemColorChanges()
+ {
+ if (_colorHookAttached) return;
+
+ var platform = global::Avalonia.Application.Current?.PlatformSettings;
+ if (platform == null) return;
+
+ platform.ColorValuesChanged += (_, _) =>
+ {
+ // Nur wer dem Betriebssystem folgt, ist von dessen Umstellung betroffen.
+ if (_settings?.Theme != AppThemeVariant.System) return;
+ Dispatcher.UIThread.Post(() => ThemeChanged?.Invoke());
+ };
+ _colorHookAttached = true;
}
/// Wechselt hell ↔ dunkel und speichert die Wahl. „System" wird dabei zu einer festen Wahl.
@@ -92,7 +140,12 @@ namespace PolyTrader.App.Avalonia.Shell
public static IBrush Brush(string key)
{
var app = global::Avalonia.Application.Current;
- if (app != null && app.TryFindResource(key, app.ActualThemeVariant, out object? value) && value is IBrush b)
+
+ // Nicht ActualThemeVariant: bei „System" ist das Default, und die Ressourcensuche
+ // liefert dann die hellen Farben, während die Fenster dunkel zeichnen. IsDark kennt
+ // die tatsächlich gezeichnete Variante – dieselbe Quelle wie für Symbol und Beschriftung.
+ var variant = IsDark ? ThemeVariant.Dark : ThemeVariant.Light;
+ if (app != null && app.TryFindResource(key, variant, out object? value) && value is IBrush b)
return b;
// Unbekannter Schlüssel ist ein Programmierfehler, darf die Oberfläche aber nicht kippen.
diff --git a/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs
index 8dae7a9..3aa05dd 100644
--- a/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs
+++ b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs
@@ -71,29 +71,46 @@ namespace PolyTrader.App.Avalonia.ViewModels
}
};
+ // Ohne Trades hat die Achse keine Werte. LiveCharts nahm dann einen Standardbereich um
+ // 0 an - und weil Tick 0 dem 01.01.0001 entspricht, stand an der Achse zwoelfmal
+ // "01.01." (Befund B-8 des Linux-Sichttests). Jetzt spannt sie im Leerzustand den
+ // tatsaechlich gewaehlten Zeitraum auf.
+ DateTime? emptyFrom = null, emptyTo = null;
+ if (curve.Count == 0)
+ {
+ emptyTo = DateTime.UtcNow.Date.AddDays(1);
+ emptyFrom = (since ?? emptyTo.Value.AddDays(-30)).Date;
+ if (emptyFrom >= emptyTo) emptyFrom = emptyTo.Value.AddDays(-1);
+ }
+
+ // LiveCharts setzt rund zehn Teilstriche, gleich wie kurz der Zeitraum ist. Bei sieben
+ // Trades an zwei Tagen stand deshalb fuenfmal "25.08." und fuenfmal "26.08." an der
+ // Achse - fachlich richtig, gelesen aber als Fehler (Befund #386). Zwei Wege zu einem
+ // Ziel: keine zwei Teilstriche mit derselben Beschriftung.
+ TimeSpan span = curve.Count > 0
+ ? curve[^1].At - curve[0].At
+ : emptyTo!.Value - emptyFrom!.Value;
+ bool shortSpan = span < TimeSpan.FromDays(3);
+
var axis = new Axis
{
// Ticks kommen als DateTime-Ticks – zurueckrechnen und kurz formatieren.
// Die Bereichspruefung ist Pflicht: LiveCharts fragt auch Werte ausserhalb des
// DateTime-Bereichs ab, und new DateTime(...) wuerde dort werfen.
Labeler = value => value >= 0 && value <= DateTime.MaxValue.Ticks
- ? new DateTime((long)value).ToString("dd.MM.")
+ ? new DateTime((long)value).ToString(shortSpan ? "dd.MM. HH:mm" : "dd.MM.")
: string.Empty,
UnitWidth = TimeSpan.FromDays(1).Ticks,
- LabelsRotation = 0
+ // Kurzer Zeitraum: die Uhrzeit unterscheidet die Teilstriche. Laengerer Zeitraum:
+ // das Datum genuegt, dann aber hoechstens ein Teilstrich je Tag.
+ MinStep = shortSpan ? 0d : TimeSpan.FromDays(1).Ticks,
+ LabelsRotation = shortSpan ? 45 : 0
};
- // Ohne Trades hat die Achse keine Werte. LiveCharts nahm dann einen Standardbereich um
- // 0 an - und weil Tick 0 dem 01.01.0001 entspricht, stand an der Achse zwoelfmal
- // "01.01." (Befund B-8 des Linux-Sichttests). Jetzt spannt sie im Leerzustand den
- // tatsaechlich gewaehlten Zeitraum auf.
- if (curve.Count == 0)
+ if (emptyFrom.HasValue)
{
- DateTime end = DateTime.UtcNow.Date.AddDays(1);
- DateTime start = (since ?? end.AddDays(-30)).Date;
- if (start >= end) start = end.AddDays(-1);
- axis.MinLimit = start.Ticks;
- axis.MaxLimit = end.Ticks;
+ axis.MinLimit = emptyFrom.Value.Ticks;
+ axis.MaxLimit = emptyTo!.Value.Ticks;
}
EquityXAxes = new[] { axis };
@@ -104,15 +121,7 @@ namespace PolyTrader.App.Avalonia.ViewModels
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)
- }
- };
+ ModuleSeries = SignedColumns(byMod.Select(x => x.Pnl).ToArray());
ModuleXAxes = new[] { new Axis { Labels = byMod.Select(x => x.Key).ToArray(), LabelsRotation = 0 } };
}
@@ -120,16 +129,7 @@ namespace PolyTrader.App.Avalonia.ViewModels
{
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)
- }
- };
+ DaySeries = SignedColumns(byDay.Select(x => x.Pnl).ToArray());
DayXAxes = new[]
{
new Axis
@@ -140,6 +140,45 @@ namespace PolyTrader.App.Avalonia.ViewModels
};
}
+ ///
+ /// Balken nach ihrem Vorzeichen faerben: Gewinn gruen, Verlust rot – dieselbe Leseweise wie
+ /// in den Trade-Listen.
+ ///
+ ///
+ /// Eine ColumnSeries hat genau ein Fill fuer alle ihre Balken; je Balken faerben
+ /// geht damit nicht. Frueher stand hier deshalb ein Ausdruck ueber die SUMME aller Werte –
+ /// der faerbte im Verlustfall auch die Gewinntage rot und sagte beim Ueberfliegen das
+ /// Gegenteil der Zahl darunter (Befund #385).
+ ///
+ /// Stattdessen zwei Reihen an denselben Positionen: die eine traegt die Gewinne, die andere
+ /// die Verluste, die jeweils andere laesst dort eine Luecke (null, wird nicht
+ /// gezeichnet). IgnoresBarPosition haelt sie uebereinander statt nebeneinander.
+ ///
+ private static ISeries[] SignedColumns(IReadOnlyList values)
+ {
+ double?[] OnlyWhere(bool positive) => values
+ .Select(v => positive == (v >= 0m) ? (double?)v : null)
+ .ToArray();
+
+ return new ISeries[]
+ {
+ new ColumnSeries
+ {
+ Name = "Gewinn",
+ Values = OnlyWhere(true),
+ Fill = new SolidColorPaint(Positive),
+ IgnoresBarPosition = true
+ },
+ new ColumnSeries
+ {
+ Name = "Verlust",
+ Values = OnlyWhere(false),
+ Fill = new SolidColorPaint(Negative),
+ IgnoresBarPosition = true
+ }
+ };
+ }
+
private void Set(ref T field, T value, [CallerMemberName] string? name = null)
{
field = value;
diff --git a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs
index ae19b38..9046c13 100644
--- a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs
+++ b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs
@@ -186,6 +186,9 @@ namespace PolyTrader.App.Avalonia.ViewModels
/// Nur-Lese-Anzeige (Eigenschaft ohne Setter, oder nicht editierbarer Typ).
public sealed class ReadOnlySettingsField : SettingsField
{
+ /// Steht für „hier steht nichts" – dieselbe Schreibweise wie in der Account-Übersicht.
+ public const string EmptyPlaceholder = "—";
+
public ReadOnlySettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public string Value
@@ -198,6 +201,25 @@ namespace PolyTrader.App.Avalonia.ViewModels
return raw?.ToString() ?? string.Empty;
}
}
+
+ ///
+ /// Der Wert für die Oberfläche: leer wird zum Platzhalter.
+ ///
+ ///
+ /// „Deaktivierte Module" ist im Auslieferungszustand eine leere Liste. Ein leeres Nur-Lese-Feld
+ /// ist von einem fehlenden Bedienelement nicht zu unterscheiden – in der Zeile stand links die
+ /// Beschriftung, rechts der Hilfetext und dazwischen nichts, während jede andere Zeile des
+ /// Dialogs dort ein Feld hat (Befund #386). Der Platzhalter beantwortet die Frage „ist hier
+ /// etwas kaputt?" ohne Blick in den Quelltext.
+ ///
+ public string DisplayValue
+ {
+ get
+ {
+ string v = Value;
+ return string.IsNullOrWhiteSpace(v) ? EmptyPlaceholder : v;
+ }
+ }
}
/// Eine Kategorie mit ihren Feldern – entspricht einer Überschrift im Dialog.
diff --git a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
index a092ccd..c150af6 100644
--- a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
@@ -121,7 +121,7 @@
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
index 53c50bb..ff5a955 100644
--- a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
@@ -40,7 +40,7 @@
-
diff --git a/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml b/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml
index 3d2d500..435aafc 100644
--- a/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/Modules/AccountingWindow.axaml
@@ -116,7 +116,7 @@
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml b/src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml
index 42ce71d..bfaa195 100644
--- a/src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml
@@ -103,7 +103,7 @@
-
+
@@ -146,7 +146,7 @@
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml b/src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml
index 4891d81..48234d0 100644
--- a/src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/Modules/ResolutionFarmingWindow.axaml
@@ -22,7 +22,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
@@ -61,7 +61,7 @@
-
+