Testanleitung: eigenes X-Display je Projekt fuer Parallelbetrieb
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s

Damit mehrere Agenten gleichzeitig Oberflaechen testen koennen, bekommt jedes
Projekt eine feste Displaynummer (Vorgabe Richard):

  :99 PolyTrader, :98 Predictalytics, :97 ClawdDotNet, :96 IBKRTrader,
  :95 und absteigend fuer kuenftige Projekte.

Zwei Prozesse auf demselben Display sehen die Fenster des jeweils anderen -
Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen.

Beim Durcharbeiten fielen zwei weitere Kollisionspunkte auf, die zum
Parallelbetrieb dazugehoeren und mit aufgenommen wurden:

- Screenshot- und Logdateien in /tmp brauchen ein Projektpraefix. /tmp/v_1.png
  ueberschreiben sich parallel laufende Projekte gegenseitig.
- "xvfb-run -a" darf NICHT mehr verwendet werden: Das -a sucht sich selbst ein
  freies Display und beginnt dabei bei :99 - ausgerechnet PolyTraders Nummer.
  Stattdessen "xvfb-run -n <nummer>".

Die Warnung ist nicht theoretisch: Auf der VM lag ein verwaister X98-Socket,
entstanden durch genau so einen "xvfb-run -a"-Aufruf beim smoke-ui-Test, als
:99 belegt war. Er blockierte Predictalytics' reservierte Nummer, ohne dass ein
Prozess lief. Entfernt, und die Anleitung erklaert jetzt, wie man verwaiste
Sockets erkennt (ls /tmp/.X11-unix/ gegen pgrep -a Xvfb lesen) und aufraeumt.

Weiter ergaenzt:
- pkill nie ohne Displaynummer - "pkill Xvfb" erschlaegt die Sitzungen aller
  anderen Projekte.
- DISPLAY in jedem Skript setzen, sonst landet man auf :0, dem echten Desktop.
- Die Skriptvorlage hat jetzt einen Kopf mit PROJEKT/DISP/FENSTER; das ist
  alles, was sich je Projekt unterscheidet. Praktisch gegengetestet.
- Aufraeumabschnitt und Belegungsliste um Displays und Verzeichnisse erweitert.

Nebenbei aufgefallen: Das PolyTrader-Publish lag unter ~/pt-test statt
~/polytrader-test und verletzte damit die eigene Verzeichniskonvention.
Umbenannt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-27 20:49:28 +02:00
co-authored by Claude Opus 5
parent 62570cfdd8
commit 9b2294cb4c
33 changed files with 979 additions and 244 deletions
+39 -2
View File
@@ -162,6 +162,16 @@
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
</Style>
<!-- Gewinn/Verlust als Klasse statt als im Code aufgelöster Pinsel. Über DynamicResource
folgt die Farbe dem Themenwechsel von selbst; ein im Code geholter Pinsel blieb dagegen
auf der Farbe stehen, die beim Erzeugen der Zeile galt (Befund B-6). -->
<Style Selector="TextBlock.positive, SelectableTextBlock.positive">
<Setter Property="Foreground" Value="{DynamicResource AppPositiveBrush}" />
</Style>
<Style Selector="TextBlock.negative, SelectableTextBlock.negative">
<Setter Property="Foreground" Value="{DynamicResource AppNegativeBrush}" />
</Style>
<Style Selector="TextBlock.mono, SelectableTextBlock.mono">
<Setter Property="FontFamily" Value="{DynamicResource AppFontMono}" />
</Style>
@@ -173,7 +183,6 @@
<Style Selector="Button">
<Setter Property="CornerRadius" Value="0" />
<Setter Property="FontFamily" Value="{DynamicResource AppFontHeading}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="10,5" />
<Setter Property="Background" Value="Transparent" />
@@ -181,6 +190,19 @@
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- Die Schriftart bewusst OHNE die Schaltflaechen, die zur Vorlage eines anderen
Steuerelements gehoeren (#PART_Button - z.B. der Kalenderknopf des
CalendarDatePicker). In Avalonia stechen die Styles der Anwendung die Setter eines
ControlTheme aus; unsere Schriftliste landete damit auch auf dem Kalenderknopf und
verdraengte die Symbolschrift, aus der sein Zeichen stammt. Unter Windows faellt das
nicht auf, unter Linux setzt Fontconfig stillschweigend irgendeine andere Schrift
ein, in der derselbe Zeichencode etwas voellig anderes bedeutet - im Test stand dort
statt eines Kalenders eine sinnlose "26" (Befund B-9 des Linux-Sichttests).
ACHTUNG: nur am Bild zu pruefen, nie im Log - Gegenprobe auf der Linux-VM noetig. -->
<Style Selector="Button:not(#PART_Button)">
<Setter Property="FontFamily" Value="{DynamicResource AppFontHeading}" />
</Style>
<!-- Fluent malt Füllung und Rahmen im Vorlagenteil und überschreibt sie je Zustand.
Deshalb müssen Schwebe- und Druckzustand dort gesetzt werden, sonst blitzt beim
Überfahren wieder das Fluent-Grau durch. -->
@@ -237,7 +259,15 @@
<!-- ═══ Tabellen ══════════════════════════════════════════════════════════════
Kopfzeile klein, gesperrt und gedämpft; Zeilen nur durch eine Haarlinie
getrennt (keine Gitterkästen). -->
getrennt (keine Gitterkästen).
SPALTENBREITEN – Regel für alle Grids: feste Pixelbreiten nur, wo sie wirklich
gebraucht werden. Der Normalfall ist Width="Auto" (misst Überschrift UND Werte)
plus genau eine Width="*"-Spalte mit MinWidth für den Fließtext. Die aus WinForms
übernommenen festen Breiten waren auf die Datenwerte ausgelegt, nicht auf die
Überschriften – in der breiteren Avalonia-Standardschrift standen dann „Trades (7",
„Winrate '" und „Fee (bp" in den Köpfen, während die Werte darunter mühelos passten
(Befunde B-11 und B-13 des Linux-Sichttests). -->
<Style Selector="DataGrid">
<Setter Property="GridLinesVisibility" Value="Horizontal" />
<Setter Property="HeadersVisibility" Value="Column" />
@@ -263,6 +293,13 @@
<Setter Property="Padding" Value="7,5" />
</Style>
<!-- Jede Textzelle zeigt ihren vollen Inhalt im Tooltip. Auch mit sauberen Breiten
bleiben lange Marktfragen und Meldungen zwangsläufig abgeschnitten; so kommt man
ohne Spaltenziehen an den ganzen Text. -->
<Style Selector="DataGridCell TextBlock">
<Setter Property="ToolTip.Tip" Value="{Binding $self.Text}" />
</Style>
<!-- ═══ Bausteine ═════════════════════════════════════════════════════════════ -->
<!-- KPI-Kachel: entspricht den zweizeiligen Labels der WinForms-Oberfläche
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
x:Class="PolyTrader.App.Avalonia.Controls.SettingsEditor">
@@ -60,6 +60,25 @@
</Grid>
</DataTemplate>
<!-- Freitext ueber mehrere Zeilen (Beschreibung, Begruendung).
Fest sechs Zeilen hoch und mit Umbruch - einzeilig zeigte das
Feld von 1000 moeglichen Zeichen nur einen Bruchteil (B-14). -->
<DataTemplate DataType="vm:MultilineTextSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel"
Text="{Binding Label}" VerticalAlignment="Top" Margin="0,6,12,0" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding Value, Mode=TwoWay}" IsEnabled="{Binding IsEditable}"
AcceptsReturn="True" TextWrapping="Wrap"
MinHeight="96" MaxHeight="200"
VerticalContentAlignment="Top"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Ganzzahl -->
<DataTemplate DataType="vm:IntSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
@@ -73,6 +92,19 @@
</Grid>
</DataTemplate>
<!-- Dezimalzahl (Betraege, Prozentschwellen, Preisgrenzen) -->
<DataTemplate DataType="vm:DecimalSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding Value, Mode=TwoWay}" IsEnabled="{Binding IsEditable}"
MaxWidth="160" HorizontalAlignment="Left" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Ja/Nein -->
<DataTemplate DataType="vm:BoolSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
@@ -0,0 +1,42 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace PolyTrader.App.Avalonia.Converters
{
/// <summary>
/// Kürzt eine Wallet-Adresse auf die Form <c>0x9e2b…3b95</c>.
///
/// <para>Eine Polygon-Adresse ist 42 Zeichen lang und passt in keine sinnvolle Spaltenbreite.
/// Im Master-Trader-Grid drückte sie zusammen mit den übrigen festen Breiten die Namensspalte
/// auf wenige Pixel zusammen – die Anzeigenamen, die wichtigste Information der Zeile, waren
/// nirgends zu sehen (Befund B-13 des Linux-Sichttests). Die vollständige Adresse steht
/// weiterhin im Tooltip der Zelle.</para>
/// </summary>
public sealed class ShortAddressConverter : IValueConverter
{
public static readonly ShortAddressConverter Instance = new();
/// <summary>Zeichen am Anfang (einschließlich „0x") und am Ende.</summary>
private const int Head = 6;
private const int Tail = 4;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
string s = value as string ?? string.Empty;
return Shorten(s);
}
public static string Shorten(string address)
{
if (string.IsNullOrWhiteSpace(address)) return string.Empty;
address = address.Trim();
return address.Length <= Head + Tail + 1
? address
: $"{address.Substring(0, Head)}…{address.Substring(address.Length - Tail)}";
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException("Die gekürzte Adresse ist reine Anzeige.");
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Deploymentcenter.Client;
using PolyTrader.Core;
@@ -94,6 +94,12 @@ namespace PolyTrader.App.Avalonia.Licensing
const string settingsPath = "server_settings.xml";
var settings = ServerSettings.Load(settingsPath);
// Master-Key laden, BEVOR der Schlüssel abgelegt wird. Ohne diesen Aufruf reicht
// SecretProtection.Protect den Klartext unveraendert durch - genau das war der Grund,
// warum --license-status "gesetzt (Klartext)" meldete (Befund B-12).
Program.ConfigureSecretProtection(Console.Error.WriteLine);
settings.LicenseKey = SecretProtection.Protect(key.Trim());
settings.Save(settingsPath);
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
@@ -106,6 +112,9 @@ namespace PolyTrader.App.Avalonia.Licensing
var options = Program.LoadDeploymentcenterOptions();
var settings = ServerSettings.Load("server_settings.xml");
// Ein verschluesselt abgelegter Schluessel (B-12) ist ohne Master-Key nicht lesbar.
Program.ConfigureSecretProtection(Console.Error.WriteLine);
string key;
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
catch (Exception ex)
+80 -5
View File
@@ -190,6 +190,26 @@ namespace PolyTrader.App.Avalonia
ConfigureSecretProtection(logger);
// Selbstheilende Migration (B-12): ein noch im Klartext abgelegter Lizenzschluessel
// wird beim ersten Start mit Master-Key verschluesselt nachgezogen. server_settings.xml
// traegt neben ihm auch den Watchdog-Token - die Konvention "Secrets nur verschluesselt
// at-rest" soll dort genauso gelten wie in der Datenbank.
if (SecretProtection.IsConfigured
&& !string.IsNullOrWhiteSpace(serverSettings.LicenseKey)
&& !SecretProtection.IsEncrypted(serverSettings.LicenseKey))
{
try
{
serverSettings.LicenseKey = SecretProtection.Protect(serverSettings.LicenseKey);
serverSettings.Save(ServerSettingsPath);
logger.Info("🔐 Lizenzschlüssel lag im Klartext vor und wurde verschlüsselt nachgezogen.");
}
catch (Exception ex)
{
logger.Warning($"⚠️ Lizenzschlüssel konnte nicht verschlüsselt nachgezogen werden: {ex.Message}");
}
}
// Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
// fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => logger.Warning(m));
@@ -373,6 +393,51 @@ namespace PolyTrader.App.Avalonia
Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}");
}
// Der Editor waehlt den Feldtyp nach dem Eigenschaftstyp. Fehlt ein Typ in dieser
// Zuordnung, faellt er still auf "nur Anzeige" zurueck - die Einstellung ist dann in
// der Oberflaeche nicht mehr aenderbar, ohne dass irgendwo ein Fehler auftaucht.
// Genau so waren saemtliche Dezimal-Einstellungen (Positionsgroessen, Preisgrenzen,
// Kill-Switch-Schwellen) unbemerkt zu reinen Anzeigen geworden. Deshalb hier gegen die
// Modelle mit den meisten Zahlenfeldern pruefen.
foreach (object model in new object[]
{
new PolyTraderSharp.Models.CopyTradingAccountSettings(),
new PolyTrader.Modules.ResolutionFarming.Models.RfSettings(),
new ServerSettings()
})
{
try
{
// Listen (z.B. "Deaktivierte Module") stehen bewusst nur zur Anzeige - die
// pflegt der Modul-Tab des Dashboards. Geprueft werden Einzelwerte.
var notEditable = ViewModels.SettingsModelBuilder.Build(model)
.SelectMany(sec => sec.Fields)
.Where(f => f.IsEditable
&& f is ViewModels.ReadOnlySettingsField
&& !typeof(System.Collections.IEnumerable).IsAssignableFrom(f.ValueType))
.Select(f => f.Label)
.ToList();
if (notEditable.Count > 0)
{
failures++;
Console.WriteLine($"[FEHLER] Einstellungs-Editor ({model.GetType().Name}): " +
$"{notEditable.Count} beschreibbare Felder werden nur angezeigt: " +
string.Join(", ", notEditable));
}
else
{
Console.WriteLine($"[OK] Einstellungs-Editor ({model.GetType().Name}): " +
"alle beschreibbaren Felder sind auch bearbeitbar");
}
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] Einstellungs-Editor ({model.GetType().Name}): {ex.GetType().Name}: {ex.Message}");
}
}
// Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
string[] themeKeys =
@@ -427,25 +492,35 @@ namespace PolyTrader.App.Avalonia
/// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder
/// Credential-Entschlüsselung laufen.
/// </summary>
private static void ConfigureSecretProtection(TerminalLogger logger)
private static void ConfigureSecretProtection(TerminalLogger logger) =>
ConfigureSecretProtection(msg => logger.Warning(msg), msg => logger.Info(msg));
/// <summary>
/// Wie oben, nur ohne <see cref="TerminalLogger"/>: die Lizenz-CLI läuft VOR dem
/// Host-Aufbau und hat noch keinen. Bis zum 26.08.2026 fehlte dieser Aufruf dort ganz –
/// <c>--license-set-key</c> legte den Schlüssel deshalb im Klartext ab, obwohl der Code
/// <see cref="SecretProtection.Protect"/> aufrief: ohne Master-Key reicht der Aufruf den
/// Klartext unverändert durch (Befund B-12 des Linux-Sichttests).
/// </summary>
internal static void ConfigureSecretProtection(Action<string> warn, Action<string>? info = null)
{
string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
string? masterKey = MasterKeyResolver.Resolve(
keyFile,
() => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
onGenerated: _ => logger.Warning(
onGenerated: _ => warn(
"🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
"gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
"(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
"Secret unwiederbringlich unlesbar."),
onWarning: msg => logger.Warning($"⚠️ {msg}"));
onWarning: msg => warn($"⚠️ {msg}"));
SecretProtection.Configure(masterKey);
if (SecretProtection.IsConfigured)
logger.Info("🔐 Secret-Verschlüsselung aktiv – Account-Credentials werden at-rest verschlüsselt (AES-256-GCM).");
info?.Invoke("🔐 Secret-Verschlüsselung aktiv – Account-Credentials werden at-rest verschlüsselt (AES-256-GCM).");
else
logger.Warning("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt – Account-Credentials liegen UNVERSCHLÜSSELT in der DB.");
warn("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt – Account-Credentials liegen UNVERSCHLÜSSELT in der DB.");
}
}
}
@@ -41,15 +41,21 @@ namespace PolyTrader.App.Avalonia.ViewModels
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)
/// <summary>
/// Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf.
/// </summary>
/// <param name="since">
/// Beginn des gewaehlten Zeitraums (null = "Alles"). Wird nur fuer die Datumsachse der
/// Equity-Kurve gebraucht: ohne Trades hat die Achse sonst keinen Bezugspunkt.
/// </param>
public void Update(IReadOnlyList<TradeRecord> scoped, DateTime? since = null)
{
BuildEquity(scoped);
BuildEquity(scoped, since);
BuildByModule(scoped);
BuildByDay(scoped);
}
private void BuildEquity(IReadOnlyList<TradeRecord> scoped)
private void BuildEquity(IReadOnlyList<TradeRecord> scoped, DateTime? since)
{
var curve = TradeAnalytics.EquityCurve(scoped);
@@ -65,16 +71,32 @@ namespace PolyTrader.App.Avalonia.ViewModels
}
};
EquityXAxes = new[]
var axis = new Axis
{
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
}
// 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.")
: string.Empty,
UnitWidth = TimeSpan.FromDays(1).Ticks,
LabelsRotation = 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)
{
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;
}
EquityXAxes = new[] { axis };
}
private void BuildByModule(IReadOnlyList<TradeRecord> scoped)
@@ -1,7 +1,4 @@
using Avalonia.Media;
using PolyTrader.App.Avalonia.Shell;
namespace PolyTrader.App.Avalonia.ViewModels
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>Anzeige-Zeile der Account-Übersicht im Launcher.</summary>
public sealed class AccountOverviewRow
@@ -39,6 +36,8 @@ namespace PolyTrader.App.Avalonia.ViewModels
public string ModuleName { get; init; } = string.Empty;
public string Text { get; init; } = string.Empty;
public bool IsPositive { get; init; } = true;
public IBrush ForegroundBrush => ThemeManager.Brush(IsPositive ? "AppPositiveBrush" : "AppNegativeBrush");
/// <summary>Gegenstueck zu <see cref="IsPositive"/> - fuer die Klassenbindung im XAML.</summary>
public bool IsNegative => !IsPositive;
}
}
@@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
@@ -32,6 +33,9 @@ namespace PolyTrader.App.Avalonia.ViewModels
/// <summary>Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen).</summary>
public bool IsEditable => Property.CanWrite;
/// <summary>Typ der dahinterliegenden Eigenschaft – die Konstruktionsprüfung schaut darauf.</summary>
public Type ValueType => Property.PropertyType;
protected void Raise([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
@@ -50,6 +54,79 @@ namespace PolyTrader.App.Avalonia.ViewModels
}
}
/// <summary>
/// Freitext über mehrere Zeilen. Gleiche Bindung wie <see cref="TextSettingsField"/>, nur mit
/// eigener Darstellung: Bis zum 26.08.2026 standen auch „Beschreibung" und „Begründung"
/// (je bis 1000 Zeichen) in einzeiligen Feldern, in denen nur ein Bruchteil sichtbar war
/// (Befund B-14 des Linux-Sichttests). Ausgezeichnet wird das am Modell mit
/// <c>[DataType(DataType.MultilineText)]</c>.
/// </summary>
public sealed class MultilineTextSettingsField : SettingsField
{
public MultilineTextSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public string Value
{
get => Property.GetValue(Target) as string ?? string.Empty;
set { if (IsEditable) { Property.SetValue(Target, value ?? string.Empty); Raise(); } }
}
}
/// <summary>
/// Dezimalzahl (decimal/double/float) – wie <see cref="IntSettingsField"/> bewusst als Text:
/// eine ungültige Eingabe lässt den bisherigen Wert stehen, statt still eine 0 zu schreiben.
///
/// <para>Vor dem 27.08.2026 fehlte dieser Feldtyp: alle Dezimal-Einstellungen fielen auf
/// <see cref="ReadOnlySettingsField"/> zurück und waren damit in der Oberfläche gar nicht mehr
/// änderbar – betroffen war die komplette Risikokonfiguration (Positionsgrößen, Preisgrenzen,
/// Kill-Switch-Schwellen), die unter WinForms im PropertyGrid noch editierbar war.</para>
///
/// <para>Beim Lesen wird die Anzeigekultur verwendet (de-DE: „5,0"), beim Schreiben werden
/// Komma und Punkt akzeptiert – sonst scheitert ein aus der Dokumentation kopierter Wert.</para>
/// </summary>
public sealed class DecimalSettingsField : SettingsField
{
public DecimalSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public string Value
{
get
{
object? raw = Property.GetValue(Target);
return raw switch
{
decimal m => m.ToString("0.############", CultureInfo.CurrentCulture),
double db => db.ToString("0.############", CultureInfo.CurrentCulture),
float f => f.ToString("0.############", CultureInfo.CurrentCulture),
_ => string.Empty
};
}
set
{
if (!IsEditable) return;
if (TryParse(value, out decimal parsed))
{
Type t = Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType;
if (t == typeof(decimal)) Property.SetValue(Target, parsed);
else if (t == typeof(double)) Property.SetValue(Target, (double)parsed);
else if (t == typeof(float)) Property.SetValue(Target, (float)parsed);
}
Raise();
}
}
/// <summary>Nimmt „5,0" und „5.0" an – unabhängig von der eingestellten Kultur.</summary>
internal static bool TryParse(string? text, out decimal value)
{
value = 0m;
if (string.IsNullOrWhiteSpace(text)) return false;
const NumberStyles styles = NumberStyles.Float;
return decimal.TryParse(text, styles, CultureInfo.CurrentCulture, out value)
|| decimal.TryParse(text, styles, CultureInfo.InvariantCulture, out value)
|| decimal.TryParse(text.Replace(',', '.'), styles, CultureInfo.InvariantCulture, out value);
}
}
/// <summary>Ja/Nein (bool) – als Kontrollkästchen.</summary>
public sealed class BoolSettingsField : SettingsField
{
@@ -178,7 +255,15 @@ namespace PolyTrader.App.Avalonia.ViewModels
if (t.IsEnum) return new ChoiceSettingsField(target, p, label, description);
if (t == typeof(bool)) return new BoolSettingsField(target, p, label, description);
if (t == typeof(int) || t == typeof(long)) return new IntSettingsField(target, p, label, description);
if (t == typeof(string)) return new TextSettingsField(target, p, label, description);
if (t == typeof(decimal) || t == typeof(double) || t == typeof(float))
return new DecimalSettingsField(target, p, label, description);
if (t == typeof(string))
{
bool multiline = p.GetCustomAttribute<DataTypeAttribute>()?.DataType == DataType.MultilineText;
return multiline
? new MultilineTextSettingsField(target, p, label, description)
: new TextSettingsField(target, p, label, description);
}
// Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules –
// die pflegt der Modul-Tab des Dashboards).
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
@@ -119,17 +119,17 @@
<DataGrid Name="gridTrades" x:DataType="vm:DashboardTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding Module}" />
<DataGridTextColumn Header="Konto" Width="130" Binding="{Binding Account}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Geschlossen" Width="140" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Module}" />
<DataGridTextColumn Header="Konto" Width="Auto" Binding="{Binding Account}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Geschlossen" Width="Auto" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -142,10 +142,10 @@
<DataGrid Name="gridModules" x:DataType="vm:ModuleRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Modul" Width="220" Binding="{Binding Modul}" />
<DataGridTextColumn Header="Status" Width="260" Binding="{Binding Status}" />
<DataGridTextColumn Header="Hinweis" Width="*" Binding="{Binding Hinweis}" />
<DataGridTemplateColumn Header="Aktion" Width="160">
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Modul}" />
<DataGridTextColumn Header="Status" Width="Auto" Binding="{Binding Status}" />
<DataGridTextColumn Header="Hinweis" Width="*" MinWidth="180" Binding="{Binding Hinweis}" />
<DataGridTemplateColumn Header="Aktion" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ModuleRow">
<Button Content="{Binding Aktion}"
@@ -140,7 +140,7 @@ namespace PolyTrader.App.Avalonia.Views
(!since.HasValue || t.ClosedAt >= since.Value)).ToList();
UpdateKpis(scoped);
_charts.Update(scoped);
_charts.Update(scoped, since);
_historyBase = scoped.OrderByDescending(t => t.ClosedAt).Select(ToRow).ToList();
ApplyHistoryFilter();
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="using:PolyTraderSharp.Models"
x:Class="PolyTrader.App.Avalonia.Views.JobsWindow">
@@ -19,17 +19,17 @@
<DataGrid Name="gridJobs" x:DataType="models:JobStatusRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Job Name" Width="150"
<DataGridTextColumn Header="Job Name" Width="Auto"
Binding="{Binding JobName}" />
<DataGridTextColumn Header="Status" Width="150"
<DataGridTextColumn Header="Status" Width="Auto"
Binding="{Binding StatusText}" />
<DataGridTextColumn Header="Zuletzt ausgeführt" Width="130"
<DataGridTextColumn Header="Zuletzt ausgeführt" Width="Auto"
Binding="{Binding LastRun, StringFormat=\{0:HH:mm:ss\}}" />
<DataGridTextColumn Header="Nächster Lauf" Width="130"
<DataGridTextColumn Header="Nächster Lauf" Width="Auto"
Binding="{Binding NextRun, StringFormat=\{0:HH:mm:ss\}}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="60" IsReadOnly="False"
<DataGridCheckBoxColumn Header="Aktiv" Width="Auto" IsReadOnly="False"
Binding="{Binding IsEnabled}" />
<DataGridTemplateColumn Header="Aktion" Width="90">
<DataGridTemplateColumn Header="Aktion" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="models:JobStatusRow">
<Button Content="Run Now"
@@ -40,7 +40,7 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Beschreibung" Width="*"
<DataGridTextColumn Header="Beschreibung" Width="*" MinWidth="180"
Binding="{Binding Description}" />
</DataGrid.Columns>
</DataGrid>
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="using:PolyTrader.App.Avalonia.Shell"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
@@ -71,9 +71,9 @@
<HeaderedContentControl Header="Account-Übersicht">
<DataGrid Name="gridAccounts" x:DataType="vm:AccountOverviewRow" Height="180">
<DataGrid.Columns>
<DataGridTextColumn Header="Account" Width="180" Binding="{Binding Name}" />
<DataGridTextColumn Header="Module" Width="220" Binding="{Binding Modules}" />
<DataGridTemplateColumn Header="Polymarket" Width="110">
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Name}" />
<DataGridTextColumn Header="Module" Width="Auto" Binding="{Binding Modules}" />
<DataGridTemplateColumn Header="Polymarket" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:AccountOverviewRow">
<Button Content="Profil"
@@ -83,10 +83,10 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Wallet (USDC)" Width="130" Binding="{Binding Balance, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="3T PnL" Width="120" Binding="{Binding Pnl3d, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="3T Winrate %" Width="120" Binding="{Binding WinRate3d, StringFormat=\{0:N1\}}" />
<DataGridTextColumn Header="Overall P/L" Width="130" Binding="{Binding OverallPnl, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="Wallet (USDC)" Width="Auto" Binding="{Binding Balance, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="3T PnL" Width="Auto" Binding="{Binding Pnl3d, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="3T Winrate %" Width="Auto" Binding="{Binding WinRate3d, StringFormat=\{0:N1\}}" />
<DataGridTextColumn Header="Overall P/L" Width="Auto" Binding="{Binding OverallPnl, StringFormat=\{0:N2\}}" />
</DataGrid.Columns>
</DataGrid>
</HeaderedContentControl>
@@ -106,11 +106,19 @@
CornerRadius="4"
Background="{DynamicResource AppCardBrush}"
Padding="10,8" Margin="0,0,8,8" Width="250">
<!--
Die Vorzeichenfarbe kommt ueber Klassen + DynamicResource, nicht
mehr als im Code aufgeloester Pinsel: der wurde beim Themenwechsel
nicht neu ausgewertet und blieb dann auf der Hell-Farbe stehen -
dunkelgruen auf dunklem Grund, 2,3:1 statt der geforderten 4,5:1
(Befund B-6 des Linux-Sichttests).
-->
<StackPanel Spacing="4">
<TextBlock Text="{Binding ModuleName}" FontWeight="SemiBold" FontSize="13" />
<TextBlock Text="{Binding Text}"
Foreground="{Binding ForegroundBrush}"
FontFamily="Consolas,Menlo,monospace"
Classes.positive="{Binding IsPositive}"
Classes.negative="{Binding IsNegative}"
FontFamily="{DynamicResource AppFontMono}"
FontSize="11" />
</StackPanel>
</Border>
@@ -125,10 +133,10 @@
<HeaderedContentControl Grid.Column="0" Header="Auffällige Trades (24h)" Height="250">
<DataGrid Name="gridNotable" x:DataType="vm:NotableTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding Module}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="PnL" Width="85" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="75" Binding="{Binding PnlPct, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Module}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPct, StringFormat=\{0:F1\}}" />
</DataGrid.Columns>
</DataGrid>
</HeaderedContentControl>
@@ -139,9 +147,9 @@
<HeaderedContentControl Grid.Column="2" Name="grpAlerts" Header="Warnungen &amp; Fehler (heute)" Height="250">
<DataGrid Name="gridAlerts" x:DataType="vm:LogAlertRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeit" Width="75" Binding="{Binding Time}" />
<DataGridTextColumn Header="Level" Width="80" Binding="{Binding Level}" />
<DataGridTextColumn Header="Nachricht" Width="*" Binding="{Binding Message}" />
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding Time}" />
<DataGridTextColumn Header="Level" Width="Auto" Binding="{Binding Level}" />
<DataGridTextColumn Header="Nachricht" Width="*" MinWidth="180" Binding="{Binding Message}" />
</DataGrid.Columns>
</DataGrid>
</HeaderedContentControl>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
@@ -398,8 +398,10 @@ namespace PolyTrader.App.Avalonia.Views
var k7 = TradeAnalytics.ComputeKpis(moduleTrades.Where(t => t.ClosedAt >= week));
var k30 = TradeAnalytics.ComputeKpis(moduleTrades);
string text = $"{module}\n" +
$"Heute: {todayPnl:+0.00;-0.00} USDC\n" +
// Ohne den Modulnamen: die Kachel traegt ihn bereits als Ueberschrift, im Textblock
// stand er ein zweites Mal ("CopyTrading / CopyTrading / Heute: ...") und sah aus
// wie ein Darstellungsfehler (Befund B-7 des Linux-Sichttests).
string text = $"Heute: {todayPnl:+0.00;-0.00} USDC\n" +
$"7T: {k7.NetPnl:+0.00;-0.00} · {k7.WinRatePct:0}% · {k7.TradeCount} Tr.\n" +
$"30T: {k30.NetPnl:+0.00;-0.00} · {k30.WinRatePct:0}%";
@@ -419,7 +421,9 @@ namespace PolyTrader.App.Avalonia.Views
try
{
string logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
string path = Path.Combine(logsDir, $"{DateTime.Now:yyyy-MM-dd}.jsonl");
// Tagesdatei nach der Anzeige-Zeitzone, nicht nach der Systemzeit - sonst greift
// die Startseite auf einem UTC-Server abends in die Datei des Vortags (§8).
string path = Path.Combine(logsDir, $"{AppTimeZone.Today:yyyy-MM-dd}.jsonl");
var rows = new List<LogAlertRow>();
if (File.Exists(path))
{
@@ -504,8 +508,13 @@ namespace PolyTrader.App.Avalonia.Views
// ===== Helfer =====
// Als DateTimeOffset lesen und die Uhrzeit SO anzeigen, wie sie in der Datei steht:
// DateTime.TryParse haette den Zeitstempel zusaetzlich in die Systemzeit gerueckt.
private static string ShortTime(string iso) =>
DateTime.TryParse(iso, out var dt) ? dt.ToString("HH:mm:ss") : iso;
DateTimeOffset.TryParse(iso, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var dt)
? dt.ToString("HH:mm:ss")
: iso;
private static string OneLine(string s) =>
(s ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim();
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:acc="using:PolyTrader.Modules.Accounting.Models"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
@@ -88,14 +88,14 @@
<DataGrid Name="gridMonthly" Margin="12" x:DataType="vm:MonthlyRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Monat" Width="140" Binding="{Binding Month}" />
<DataGridTextColumn Header="Handelsergebnis" Width="160" Binding="{Binding Net}" />
<DataGridTextColumn Header="Einzahlungen" Width="140" Binding="{Binding Deposits}" />
<DataGridTextColumn Header="Auszahlungen" Width="140" Binding="{Binding Withdrawals}" />
<DataGridTextColumn Header="Fees" Width="120" Binding="{Binding Fees}" />
<DataGridTextColumn Header="Volumen" Width="140" Binding="{Binding Volume}" />
<DataGridTextColumn Header="Trades" Width="90" Binding="{Binding Trades}" />
<DataGridTextColumn Header="Endsaldo" Width="*" Binding="{Binding Closing}" />
<DataGridTextColumn Header="Monat" Width="Auto" Binding="{Binding Month}" />
<DataGridTextColumn Header="Handelsergebnis" Width="Auto" Binding="{Binding Net}" />
<DataGridTextColumn Header="Einzahlungen" Width="Auto" Binding="{Binding Deposits}" />
<DataGridTextColumn Header="Auszahlungen" Width="Auto" Binding="{Binding Withdrawals}" />
<DataGridTextColumn Header="Fees" Width="Auto" Binding="{Binding Fees}" />
<DataGridTextColumn Header="Volumen" Width="Auto" Binding="{Binding Volume}" />
<DataGridTextColumn Header="Trades" Width="Auto" Binding="{Binding Trades}" />
<DataGridTextColumn Header="Endsaldo" Width="*" MinWidth="180" Binding="{Binding Closing}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -114,17 +114,17 @@
<DataGrid Name="gridLedger" x:DataType="acc:LedgerEntry">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeitpunkt" Width="150" Binding="{Binding Timestamp, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Typ" Width="120" Binding="{Binding EventType}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketSlug}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Size" Width="90" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Preis" Width="80" Binding="{Binding PriceUsdc, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Brutto" Width="100" Binding="{Binding GrossUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Fee" Width="90" Binding="{Binding FeeUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Netto" Width="100" Binding="{Binding NetUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Quelle" Width="110" Binding="{Binding Source}" />
<DataGridTextColumn Header="Zeitpunkt" Width="Auto" Binding="{Binding Timestamp, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Typ" Width="Auto" Binding="{Binding EventType}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketSlug}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Preis" Width="Auto" Binding="{Binding PriceUsdc, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Brutto" Width="Auto" Binding="{Binding GrossUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Fee" Width="Auto" Binding="{Binding FeeUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Netto" Width="Auto" Binding="{Binding NetUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Quelle" Width="Auto" Binding="{Binding Source}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -144,13 +144,13 @@
<DataGrid Name="gridRuns" x:DataType="acc:IngestRun">
<DataGrid.Columns>
<DataGridTextColumn Header="Start" Width="160" Binding="{Binding StartedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridTextColumn Header="Ende" Width="160" Binding="{Binding FinishedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridCheckBoxColumn Header="Backfill" Width="80" Binding="{Binding Backfill}" />
<DataGridCheckBoxColumn Header="Erfolg" Width="70" Binding="{Binding Success}" />
<DataGridTextColumn Header="Neu" Width="70" Binding="{Binding NewEntries}" />
<DataGridTextColumn Header="Duplikate" Width="90" Binding="{Binding DuplicateEntries}" />
<DataGridTextColumn Header="Meldung" Width="*" Binding="{Binding Message}" />
<DataGridTextColumn Header="Start" Width="Auto" Binding="{Binding StartedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridTextColumn Header="Ende" Width="Auto" Binding="{Binding FinishedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridCheckBoxColumn Header="Backfill" Width="Auto" Binding="{Binding Backfill}" />
<DataGridCheckBoxColumn Header="Erfolg" Width="Auto" Binding="{Binding Success}" />
<DataGridTextColumn Header="Neu" Width="Auto" Binding="{Binding NewEntries}" />
<DataGridTextColumn Header="Duplikate" Width="Auto" Binding="{Binding DuplicateEntries}" />
<DataGridTextColumn Header="Meldung" Width="*" MinWidth="180" Binding="{Binding Message}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,7 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:conv="using:PolyTrader.App.Avalonia.Converters"
xmlns:ct="using:PolyTraderSharp.Models"
x:Class="PolyTrader.App.Avalonia.Views.Modules.CopyTradingWindow">
@@ -29,18 +30,31 @@
<Grid ColumnDefinitions="*,4,460">
<DataGrid Name="gridTraders" Grid.Column="0" x:DataType="ct:TrackedTrader">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="50" Binding="{Binding Id}" />
<DataGridTextColumn Header="Wallet" Width="170" Binding="{Binding WalletAddress}" />
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding DisplayName}" />
<DataGridTextColumn Header="Kategorie" Width="110" Binding="{Binding Category}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="50" Binding="{Binding IsActive}" />
<DataGridTextColumn Header="Trades (7T)" Width="90" Binding="{Binding TotalTrades}" />
<DataGridTextColumn Header="Winrate %" Width="90" Binding="{Binding Winrate30t, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="PnL (7T)" Width="100" Binding="{Binding TotalPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-PnL (30T)" Width="110" Binding="{Binding CopyPnl30d, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Profit-Faktor" Width="100" Binding="{Binding CopyProfitFactor, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Ø PnL/Trade" Width="100" Binding="{Binding CopyAvgPnlPerTrade, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-Trades" Width="90" Binding="{Binding CopyTradeCount30d}" />
<!-- Der Name steht vorn und bekommt den freien Platz: er ist die
wichtigste Information der Zeile. Die Wallet-Adresse folgt
gekuerzt (vollstaendig im Tooltip) - in voller Laenge drueckte
sie zusammen mit den festen Breiten der uebrigen Spalten den
Namen auf wenige Pixel zusammen (Befund B-13). -->
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding Id}" />
<DataGridTextColumn Header="Name" Width="*" MinWidth="180" Binding="{Binding DisplayName}" />
<DataGridTemplateColumn Header="Wallet" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="ct:TrackedTrader">
<TextBlock Text="{Binding WalletAddress, Converter={x:Static conv:ShortAddressConverter.Instance}}"
ToolTip.Tip="{Binding WalletAddress}"
VerticalAlignment="Center" Margin="7,0" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="Auto" Binding="{Binding IsActive}" />
<DataGridTextColumn Header="Trades (7T)" Width="Auto" Binding="{Binding TotalTrades}" />
<DataGridTextColumn Header="Winrate %" Width="Auto" Binding="{Binding Winrate30t, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="PnL (7T)" Width="Auto" Binding="{Binding TotalPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-PnL (30T)" Width="Auto" Binding="{Binding CopyPnl30d, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Profit-Faktor" Width="Auto" Binding="{Binding CopyProfitFactor, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Ø PnL/Trade" Width="Auto" Binding="{Binding CopyAvgPnlPerTrade, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-Trades" Width="Auto" Binding="{Binding CopyTradeCount30d}" />
</DataGrid.Columns>
</DataGrid>
@@ -78,19 +92,19 @@
<DataGrid Name="gridOpen" x:DataType="vm:CopyOpenTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Account" Width="120" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="120" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="55" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="65" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Aktuell" Width="65" Binding="{Binding Current, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="80" Binding="{Binding Amount, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Wert" Width="80" Binding="{Binding Value, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgewinn" Width="90" Binding="{Binding Unrealized, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgew. %" Width="80" Binding="{Binding UnrealizedPct, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Status" Width="90" Binding="{Binding Status}" />
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="Auto" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Aktuell" Width="Auto" Binding="{Binding Current, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="Auto" Binding="{Binding Amount, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Wert" Width="Auto" Binding="{Binding Value, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgewinn" Width="Auto" Binding="{Binding Unrealized, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgew. %" Width="Auto" Binding="{Binding UnrealizedPct, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Status" Width="Auto" Binding="{Binding Status}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -120,19 +134,19 @@
<DataGrid Name="gridClosed" x:DataType="vm:CopyClosedTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="60" Binding="{Binding TradeId}" />
<DataGridTextColumn Header="Account" Width="130" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="130" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding Exit, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Eröffnet" Width="130" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Geschlossen" Width="130" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding TradeId}" />
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="Auto" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding Exit, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Eröffnet" Width="Auto" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Geschlossen" Width="Auto" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:rf="using:PolyTrader.Modules.ResolutionFarming.Models"
@@ -21,14 +21,14 @@
<TabItem Header="Kandidaten">
<DataGrid Name="gridCandidates" x:DataType="rf:RfCandidate">
<DataGrid.Columns>
<DataGridTextColumn Header="Gescannt" Width="140" Binding="{Binding ScannedAt, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Ask" Width="70" Binding="{Binding Ask, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Fee (bps)" Width="80" Binding="{Binding FeeBps}" />
<DataGridTextColumn Header="Netto-Edge %" Width="110" Binding="{Binding NetEdgePct, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Score" Width="80" Binding="{Binding Score, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Gescannt" Width="Auto" Binding="{Binding ScannedAt, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
<DataGridTextColumn Header="Ask" Width="Auto" Binding="{Binding Ask, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Fee (bps)" Width="Auto" Binding="{Binding FeeBps}" />
<DataGridTextColumn Header="Netto-Edge %" Width="Auto" Binding="{Binding NetEdgePct, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Score" Width="Auto" Binding="{Binding Score, StringFormat=\{0:F2\}}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
@@ -40,15 +40,15 @@
</Border>
<DataGrid Name="gridPositions" x:DataType="rf:RfPosition">
<DataGrid.Columns>
<DataGridTextColumn Header="Eröffnet" Width="140" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="90" Binding="{Binding AmountUsd, StringFormat=\{0:F2\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
<DataGridTextColumn Header="Auflösung" Width="140" Binding="{Binding EndDate, StringFormat=\{0:dd.MM.yyyy\}}" />
<DataGridTextColumn Header="Eröffnet" Width="Auto" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="Auto" Binding="{Binding AmountUsd, StringFormat=\{0:F2\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
<DataGridTextColumn Header="Auflösung" Width="Auto" Binding="{Binding EndDate, StringFormat=\{0:dd.MM.yyyy\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -61,15 +61,15 @@
</Border>
<DataGrid Name="gridHistory" x:DataType="rf:RfClosedTrade">
<DataGrid.Columns>
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="90" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="80" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:sup="using:PolyTrader.Modules.Supervisor.Persistence"
@@ -63,11 +63,11 @@
<Grid ColumnDefinitions="*,4,*">
<DataGrid Name="gridSignals" Grid.Column="0" x:DataType="svc:SignalSummary">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeit" Width="130" Binding="{Binding FirstSeen, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entscheidung" Width="120" Binding="{Binding LastDecision}" />
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding FirstSeen, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entscheidung" Width="Auto" Binding="{Binding LastDecision}" />
</DataGrid.Columns>
</DataGrid>
@@ -89,12 +89,12 @@
</Border>
<DataGrid Name="gridReports" x:DataType="sup:SupervisorReport">
<DataGrid.Columns>
<DataGridTextColumn Header="Erstellt" Width="150" Binding="{Binding CreatedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Profil" Width="140" Binding="{Binding Profile}" />
<DataGridTextColumn Header="Modell" Width="200" Binding="{Binding Model}" />
<DataGridTextColumn Header="Frage" Width="300" Binding="{Binding Question}" />
<DataGridTextColumn Header="Antwort" Width="*" Binding="{Binding Answer}" />
<DataGridTextColumn Header="Tools" Width="70" Binding="{Binding ToolCallCount}" />
<DataGridTextColumn Header="Erstellt" Width="Auto" Binding="{Binding CreatedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Profil" Width="Auto" Binding="{Binding Profile}" />
<DataGridTextColumn Header="Modell" Width="Auto" Binding="{Binding Model}" />
<DataGridTextColumn Header="Frage" Width="Auto" Binding="{Binding Question}" />
<DataGridTextColumn Header="Antwort" Width="*" MinWidth="180" Binding="{Binding Answer}" />
<DataGridTextColumn Header="Tools" Width="Auto" Binding="{Binding ToolCallCount}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -107,12 +107,12 @@
</Border>
<DataGrid Name="gridCounterfactual" x:DataType="cf:CounterfactualRecord">
<DataGrid.Columns>
<DataGridTextColumn Header="Geprüft" Width="150" Binding="{Binding CheckedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="120" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Grund der Ablehnung" Width="260" Binding="{Binding Reason}" />
<DataGridTextColumn Header="Signalpreis" Width="100" Binding="{Binding SignalPrice, StringFormat=\{0:F3\}}" />
<DataGridCheckBoxColumn Header="Wäre Gewinner" Width="120" Binding="{Binding IsWinner}" />
<DataGridTextColumn Header="Geprüft" Width="Auto" Binding="{Binding CheckedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Grund der Ablehnung" Width="Auto" Binding="{Binding Reason}" />
<DataGridTextColumn Header="Signalpreis" Width="Auto" Binding="{Binding SignalPrice, StringFormat=\{0:F3\}}" />
<DataGridCheckBoxColumn Header="Wäre Gewinner" Width="Auto" Binding="{Binding IsWinner}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:models="using:PolyTraderSharp.Models"
@@ -56,10 +56,10 @@
<Grid ColumnDefinitions="380,4,*">
<DataGrid Name="gridAccounts" Grid.Column="0" x:DataType="models:AccountState">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="50" Binding="{Binding AccountId}" />
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="60" Binding="{Binding IsActive}" />
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding AccountId}" />
<DataGridTextColumn Header="Name" Width="*" MinWidth="180" Binding="{Binding Name}" />
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="Auto" Binding="{Binding IsActive}" />
</DataGrid.Columns>
</DataGrid>
@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
x:Class="PolyTrader.App.Avalonia.Views.TerminalWindow">
@@ -77,10 +77,10 @@
<DataGrid Name="gridLogs" x:DataType="vm:LogViewerRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeit" Width="90" Binding="{Binding Time}" />
<DataGridTextColumn Header="Level" Width="110" Binding="{Binding Level}" />
<DataGridTextColumn Header="CID" Width="160" Binding="{Binding Cid}" />
<DataGridTextColumn Header="Nachricht" Width="*" Binding="{Binding Message}" />
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding Time}" />
<DataGridTextColumn Header="Level" Width="Auto" Binding="{Binding Level}" />
<DataGridTextColumn Header="CID" Width="Auto" Binding="{Binding Cid}" />
<DataGridTextColumn Header="Nachricht" Width="*" MinWidth="180" Binding="{Binding Message}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -63,7 +63,7 @@ namespace PolyTrader.App.Avalonia.Views
cbLevel.SelectedIndex = 0;
cbViewerLevel.SelectedIndex = 0;
this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate = DateTime.Now;
this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate = AppTimeZone.Today;
this.FindControl<Button>("btnAutoscroll")!.Click += (_, _) => ToggleAutoscroll();
this.FindControl<Button>("btnClear")!.Click += (_, _) => { _lines.Clear(); UpdateLiveStatus(); };
@@ -123,9 +123,9 @@ namespace PolyTrader.App.Avalonia.Views
processed++;
if (filter != "Alle" && e.Level.ToString() != filter) continue;
// Anzeige-Zeitzone aus den Server-Settings (siehe AppTimeZone) – nicht mehr fest
// gegen eine Windows-Zeitzonen-ID gerechnet.
DateTime t = AppTimeZone.ToDisplay(e.Timestamp);
// Der Zeitstempel steht seit dem 27.08.2026 bereits in der Anzeige-Zeitzone
// (siehe AppTimeZone.Now) - hier wird nicht noch einmal umgerechnet.
DateTimeOffset t = e.Timestamp;
_lines.Add(new LogLine
{
@@ -187,7 +187,7 @@ namespace PolyTrader.App.Avalonia.Views
var status = this.FindControl<TextBlock>("lblViewerStatus")!;
try
{
DateTime day = this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate ?? DateTime.Now;
DateTime day = this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate ?? AppTimeZone.Today;
string path = Path.Combine(AppContext.BaseDirectory, "Logs", $"{day:yyyy-MM-dd}.jsonl");
_viewerRows.Clear();
+15 -2
View File
@@ -57,8 +57,21 @@ namespace PolyTraderSharp.Models
[Category("Mullvad VPN")]
[DisplayName("Mullvad CLI Path")]
[Description("Path to mullvad.exe")]
public string MullvadCliPath { get; set; } = @"C:\Program Files\Mullvad VPN\resources\mullvad.exe";
[Description("Pfad zum Mullvad-Kommandozeilenwerkzeug. Der Standard richtet sich nach dem " +
"Betriebssystem: unter Linux /usr/bin/mullvad, unter Windows die mullvad.exe im " +
"Installationsverzeichnis.")]
public string MullvadCliPath { get; set; } = DefaultMullvadCliPath;
/// <summary>
/// Standardpfad des Mullvad-CLI je Plattform. Bis zum 26.08.2026 stand hier fest der
/// Windows-Pfad - auf dem Linux-Zielserver war der Standardwert damit sinnlos und die
/// VPN-Rotation scheiterte mit einer Meldung, der man die Ursache nicht ansieht
/// (Befund B-5 des Linux-Sichttests).
/// </summary>
public static string DefaultMullvadCliPath =>
System.OperatingSystem.IsWindows()
? @"C:\Program Files\Mullvad VPN\resources\mullvad.exe"
: "/usr/bin/mullvad";
[Category("Blockchain Listener")]
+16 -1
View File
@@ -1,4 +1,4 @@
using System;
using System;
namespace PolyTraderSharp.Services
{
@@ -69,6 +69,21 @@ namespace PolyTraderSharp.Services
$"„{TimeZoneInfo.Local.Id}\" verwendet. Empfohlen ist die IANA-Schreibweise, z.B. „{DefaultId}\".");
}
/// <summary>
/// Jetzt – in der Anzeige-Zeitzone, mit dem dazu passenden Offset.
///
/// <para>Bewusst <see cref="DateTimeOffset"/> und nicht <see cref="DateTime"/>: Ein
/// umgerechneter <c>DateTime</c> trägt seine Zone nicht mit sich, und beim Formatieren mit
/// <c>zzz</c> setzt .NET dann den Offset des <i>Systems</i> ein. Genau daran lag der
/// Widerspruch, den der Linux-Lauf gezeigt hat: Auf einem UTC-Server meldete die Anwendung
/// „Anzeige-Zeitzone: Europe/Berlin" und stempelte in derselben Zeile mit UTC (§8 des
/// Befundberichts vom 26.08.2026).</para>
/// </summary>
public static DateTimeOffset Now => TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, Current);
/// <summary>Der heutige Tag in der Anzeige-Zeitzone – für Logdatei-Tagesgrenzen.</summary>
public static DateTime Today => Now.Date;
/// <summary>Rechnet einen Zeitstempel in die Anzeige-Zeitzone um (UTC-Kennzeichnung wird beachtet).</summary>
public static DateTime ToDisplay(DateTime value) =>
value.Kind == DateTimeKind.Utc
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -130,7 +130,13 @@ namespace PolyTraderSharp.Services
{
if (!File.Exists(_settings.MullvadCliPath))
{
_logger.Error($"Mullvad CLI not found at: {_settings.MullvadCliPath}");
// Nennt gleich den plattformueblichen Pfad mit: Ein aus der Windows-Zeit
// uebernommener Wert in server_settings.xml ist auf dem Linux-Server der
// haeufigste Grund, und ohne diesen Hinweis sieht man ihm das nicht an
// (Befund B-5 des Linux-Sichttests).
_logger.Error($"Mullvad CLI nicht gefunden: \"{_settings.MullvadCliPath}\". " +
$"Auf diesem System wird ueblicherweise \"{ServerSettings.DefaultMullvadCliPath}\" erwartet " +
$"(Einstellung \"Mullvad CLI Path\").");
return string.Empty;
}
+20 -5
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading.Channels;
@@ -11,7 +11,17 @@ namespace PolyTraderSharp.Services
{
public string Message { get; }
public LogLevel Level { get; }
public DateTime Timestamp { get; }
/// <summary>
/// Zeitpunkt in der Anzeige-Zeitzone der Anwendung, mit passendem Offset.
///
/// <para>Bis zum 27.08.2026 stand hier <c>DateTime.Now</c> – die Zeit des <i>Systems</i>.
/// Auf einem UTC-Server (dem Normalfall) stempelten die Logs damit UTC, während die
/// Oberfläche Ortszeit anzeigte: Wer einen Vorfall von „22:30" suchte, fand ihn unter
/// 20:30 – zwischen Mitternacht und 2 Uhr sogar in der Logdatei des Vortags
/// (§8 des Befundberichts vom 26.08.2026).</para>
/// </summary>
public DateTimeOffset Timestamp { get; }
/// <summary>Korrelations-ID (z. B. SignalId) für die Log-Forensik; leer wenn ohne Kontext.</summary>
public string CorrelationId { get; }
@@ -20,7 +30,7 @@ namespace PolyTraderSharp.Services
{
Message = message;
Level = level;
Timestamp = DateTime.Now;
Timestamp = AppTimeZone.Now;
CorrelationId = correlationId ?? "";
}
}
@@ -105,7 +115,12 @@ namespace PolyTraderSharp.Services
{
try
{
string dateStr = e.Timestamp.ToString("dd-MM-yyyy");
// ISO-Datum wie bei der JSONL-Datei. Bis zum 26.08.2026 stand hier
// "dd-MM-yyyy" - im selben Logs-Ordner lagen damit zwei Namenskonventionen
// nebeneinander: eine Logrotate-Regel brauchte zwei Muster, und im
// Dateimanager sortierten die .log-Dateien nach Tag statt nach Datum
// (Befund B-2 des Linux-Laufs).
string dateStr = e.Timestamp.ToString("yyyy-MM-dd");
string fileName = $"{dateStr}-{e.Level}.log";
string fullPath = Path.Combine(_logsDirectory, fileName);
@@ -167,7 +182,7 @@ namespace PolyTraderSharp.Services
{
lock (_lock)
{
var cutoff = DateTime.Now - maxAge;
var cutoff = DateTimeOffset.UtcNow - maxAge;
return _history.Where(x => x.Timestamp >= cutoff).ToList();
}
}
@@ -1,5 +1,6 @@
using System;
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
namespace PolyTraderSharp.Models
@@ -33,12 +34,14 @@ namespace PolyTraderSharp.Models
[Category("02. Categorization")]
[DisplayName("Beschreibung")]
[Description("Freitext-Notiz zum Master (z. B. Herkunft, beobachtete Stärken/Schwächen).")]
[DataType(DataType.MultilineText)]
public string Description { get; set; } = string.Empty;
[Category("02. Categorization")]
[DisplayName("Begründung / Reasoning")]
[Description("Warum kopieren wir diesen Master? Wird auch vom Auto-Kill-Switch mit Zeitstempel befüllt, wenn er " +
"automatisch pausiert wird (Phase 3.3).")]
[DataType(DataType.MultilineText)]
public string Reasoning { get; set; } = string.Empty;
[Category("03. General")]
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
{
/// <summary>
/// Wandelt die Spalte <c>AssignedAccountIds</c> zwischen JSON-Text und
/// <see cref="HashSet{T}"/> um – tolerant gegenüber beschädigten Werten.
///
/// <para><b>Warum das nötig ist:</b> Bis zum 26.08.2026 rief die EF-Wertkonvertierung direkt
/// <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions?)"/> auf. Ein
/// einziger unlesbarer Wert (im Test ein <c>"1,2"</c> statt <c>"[1,2]"</c>) ließ damit die
/// gesamte Abfrage werfen: es wurde <i>kein einziger</i> Master-Trader geladen und das ganze
/// Modul blieb leer. Im Betrieb hieße das, ein beschädigter Datensatz legt das Modul still
/// (Befund B-15 des Linux-Sichttests).</para>
///
/// <para>Jetzt gilt: JSON-Array wie bisher; eine reine Zahlenliste (<c>"1,2"</c>) wird als
/// Altformat noch verstanden; alles andere ergibt eine leere Zuordnung – der Trader wird
/// geladen, kopiert aber auf kein Konto (die sichere Richtung). Wie oft das passiert ist,
/// steht in <see cref="MalformedCount"/>; die Hydration meldet es beim Start.</para>
/// </summary>
public static class AccountIdSetJson
{
private static int _malformedCount;
private static string _lastMalformedValue = string.Empty;
/// <summary>Anzahl der bisher nicht lesbaren Werte seit Programmstart.</summary>
public static int MalformedCount => Volatile.Read(ref _malformedCount);
/// <summary>Der zuletzt nicht lesbare Rohwert – für die Meldung im Log.</summary>
public static string LastMalformedValue => Volatile.Read(ref _lastMalformedValue);
/// <summary>Setzt die Zählung zurück (Tests).</summary>
public static void ResetDiagnostics()
{
Interlocked.Exchange(ref _malformedCount, 0);
Interlocked.Exchange(ref _lastMalformedValue, string.Empty);
}
public static string Serialize(HashSet<int>? value) =>
JsonSerializer.Serialize(value ?? new HashSet<int>(), (JsonSerializerOptions?)null);
public static HashSet<int> Deserialize(string? stored)
{
if (string.IsNullOrWhiteSpace(stored)) return new HashSet<int>();
try
{
var parsed = JsonSerializer.Deserialize<HashSet<int>>(stored, (JsonSerializerOptions?)null);
if (parsed != null) return parsed;
}
catch (JsonException)
{
// Fällt unten auf das Altformat bzw. auf "leer" zurück.
}
// Altformat/Handeingabe: "1,2" oder "1;2". Nur akzeptieren, wenn ALLE Teile Zahlen
// sind – sonst wüsste man nicht, was man da still übernimmt.
var parts = stored.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0 && parts.All(p => int.TryParse(p.Trim(), out _)))
return parts.Select(p => int.Parse(p.Trim())).ToHashSet();
Interlocked.Increment(ref _malformedCount);
Interlocked.Exchange(ref _lastMalformedValue, stored);
return new HashSet<int>();
}
}
}
@@ -1,7 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using PolyTraderSharp.Models;
@@ -67,10 +66,12 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
v => v.Aggregate(0, (h, i) => HashCode.Combine(h, i)),
v => new HashSet<int>(v));
// Lesen bewusst ueber AccountIdSetJson: ein beschaedigter Wert darf nur DIESE
// Zuordnung leeren, nicht die ganze Abfrage werfen (Befund B-15).
e.Property(x => x.AssignedAccountIds)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => string.IsNullOrEmpty(v) ? new HashSet<int>() : (JsonSerializer.Deserialize<HashSet<int>>(v, (JsonSerializerOptions?)null) ?? new HashSet<int>()))
v => AccountIdSetJson.Serialize(v),
v => AccountIdSetJson.Deserialize(v))
.HasColumnType("text");
e.Property(x => x.AssignedAccountIds).Metadata.SetValueComparer(comparer);
});
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -42,10 +42,21 @@ namespace PolyTraderSharp.Services
{
HydrateAccountSettings(_state.Accounts.Values.ToList());
PolyTrader.Modules.CopyTrading.Persistence.Ef.AccountIdSetJson.ResetDiagnostics();
foreach (var trd in _traderRepo.GetAll())
_copyState.Traders[trd.Id] = trd;
_logger.Info($"Copytrading-Hydration abgeschlossen: {_copyState.Traders.Count} Trader geladen.");
// Beschaedigte Account-Zuordnungen brechen das Laden nicht mehr ab (Befund B-15),
// duerfen aber auch nicht still bleiben: ein Trader ohne Zuordnung kopiert nirgends.
int malformed = PolyTrader.Modules.CopyTrading.Persistence.Ef.AccountIdSetJson.MalformedCount;
if (malformed > 0)
_logger.Warning($"⚠️ {malformed} Master-Trader haben unlesbare Account-Zuordnungen " +
$"(zuletzt: \"{PolyTrader.Modules.CopyTrading.Persistence.Ef.AccountIdSetJson.LastMalformedValue}\"). " +
"Sie wurden ohne Zuordnung geladen und kopieren damit auf KEIN Konto. " +
"Erwartet wird ein JSON-Array, z.B. [1,2].");
}
catch (System.Exception ex)
{
@@ -57,13 +57,18 @@ namespace PolyTrader.Modules.Supervisor.Services
return;
}
_logger.Info($"Supervisor-Tagesbericht aktiv: täglich um {hour:D2}:00 (lokal).");
// Die Uhrzeit gilt in der Anzeige-Zeitzone der Anwendung, nicht in der des Systems:
// Auf einem UTC-Server waere der "18-Uhr-Bericht" sonst um 20 Uhr Ortszeit gekommen
// und haette einen anderen Tag umfasst als die Oberflaeche zeigt (§8 des
// Befundberichts vom 26.08.2026).
_logger.Info($"Supervisor-Tagesbericht aktiv: täglich um {hour:D2}:00 ({AppTimeZone.CurrentId}).");
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
var next = now.Date.AddHours(hour);
if (next <= now) next = next.AddDays(1);
try { await Task.Delay(next - now, stoppingToken); }
var now = AppTimeZone.Now;
var next = now.Date.AddHours(hour) - now.Offset;
var nextUtc = new DateTimeOffset(next, TimeSpan.Zero);
if (nextUtc <= now) nextUtc = nextUtc.AddDays(1);
try { await Task.Delay(nextUtc - now, stoppingToken); }
catch (OperationCanceledException) { break; }
try { await RunOnceAsync(stoppingToken); }