Testanleitung: eigenes X-Display je Projekt fuer Parallelbetrieb
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:
@@ -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)
|
||||
|
||||
@@ -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 & 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();
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user