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:
+11
-1
@@ -1,4 +1,4 @@
|
|||||||
# Roadmap PolyTrader
|
# Roadmap PolyTrader
|
||||||
|
|
||||||
**Stand: 23.08.2026** (A2 pausiert, A4 aufgeteilt) · Das eine Steuerungsdokument. Zusammengeführt aus elf Umsetzungsplänen,
|
**Stand: 23.08.2026** (A2 pausiert, A4 aufgeteilt) · Das eine Steuerungsdokument. Zusammengeführt aus elf Umsetzungsplänen,
|
||||||
drei Konzepten und der Linux-Analyse — diese liegen jetzt unter [`archiv/`](./archiv/) und
|
drei Konzepten und der Linux-Analyse — diese liegen jetzt unter [`archiv/`](./archiv/) und
|
||||||
@@ -85,6 +85,16 @@ acht Stück (Accounting) sinnvoll umbrechen.
|
|||||||
git worktree add ../polytrader-winforms winforms-final
|
git worktree add ../polytrader-winforms winforms-final
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Stand 27.08.2026:** Die Sichtprüfung auf der Linux-VM hat stattgefunden (A4a, Nachträge 2–4)
|
||||||
|
und **fünfzehn Befunde** ergeben. Bis auf drei bewusst zurückgestellte sind alle behoben —
|
||||||
|
darunter B-10 (ohne das DataGrid-Theme rendert keine einzige Tabelle) und ein dabei entdeckter
|
||||||
|
Nebenbefund: **sämtliche Dezimal-Einstellungen waren in der Oberfläche nicht mehr änderbar**,
|
||||||
|
also die komplette Risikokonfiguration. Einzelheiten und Begründungen im Nachtrag 5 von
|
||||||
|
[BEFUND-Linux-Betrieb-2026-08-26.md](./pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md).
|
||||||
|
|
||||||
|
⬜ **Offener Rest:** Gegenprobe am Bild auf der VM (Spaltenbreiten mit echten Daten,
|
||||||
|
Kalendersymbol B-9, Splitter im Copytrading-Detailbereich). Verfahren und Testdaten stehen bereit.
|
||||||
|
|
||||||
**Blockiert nichts** — aber es ist die letzte offene Zusage der UI-Portierung.
|
**Blockiert nichts** — aber es ist die letzte offene Zusage der UI-Portierung.
|
||||||
Regeln für Nacharbeiten: [LEITFADEN-Avalonia-Portierung.md](./LEITFADEN-Avalonia-Portierung.md).
|
Regeln für Nacharbeiten: [LEITFADEN-Avalonia-Portierung.md](./LEITFADEN-Avalonia-Portierung.md).
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Die Rechte enden **an der eigenen Datenbank** — `GRANT ALL ON <projekt>_test.*
|
|||||||
| Zweck | Ort |
|
| Zweck | Ort |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Quellcode | `~/RiderProjects/<projekt>` |
|
| Quellcode | `~/RiderProjects/<projekt>` |
|
||||||
| Publish/Testlauf im Benutzerkontext | `~/<projekt>-test` |
|
| Publish/Testlauf im Benutzerkontext | `~/<projekt>-test` (voller Projektname, keine Kürzel) |
|
||||||
| Installation als Dienst | `/opt/<projekt>` |
|
| Installation als Dienst | `/opt/<projekt>` |
|
||||||
|
|
||||||
### 4.3 systemd-Dienste
|
### 4.3 systemd-Dienste
|
||||||
@@ -141,7 +141,51 @@ mehr zuordnen kann, und der nächste Test misst gegen fremde Last.
|
|||||||
Vor dem Belegen prüfen: `ss -tlnp`. Belegte Ports stehen in Abschnitt 6. Wer einen Port dauerhaft
|
Vor dem Belegen prüfen: `ss -tlnp`. Belegte Ports stehen in Abschnitt 6. Wer einen Port dauerhaft
|
||||||
belegt, trägt ihn dort ein.
|
belegt, trägt ihn dort ein.
|
||||||
|
|
||||||
### 4.5 Erhöhte Rechte
|
### 4.5 X-Displays — feste Nummer je Projekt
|
||||||
|
|
||||||
|
Damit **mehrere Agenten gleichzeitig** Oberflächen testen können, bekommt jedes Projekt ein
|
||||||
|
eigenes virtuelles Display. Zwei Prozesse auf demselben Display sehen die Fenster des jeweils
|
||||||
|
anderen — Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen.
|
||||||
|
|
||||||
|
| Display | Projekt |
|
||||||
|
|---|---|
|
||||||
|
| `:99` | **PolyTrader** |
|
||||||
|
| `:98` | **Predictalytics** |
|
||||||
|
| `:97` | **ClawdDotNet** |
|
||||||
|
| `:96` | **IBKRTrader** |
|
||||||
|
| `:95` und **absteigend** | künftige Projekte (nächstes freies von oben nach unten) |
|
||||||
|
|
||||||
|
Wer ein neues Projekt aufnimmt, trägt seine Nummer in die Belegungsliste (Abschnitt 6) ein.
|
||||||
|
|
||||||
|
**Belegte Displays prüfen:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /tmp/.X11-unix/ # X0 = der echte Desktop, X99 = PolyTrader usw.
|
||||||
|
pgrep -a Xvfb # zeigt, welcher Xvfb auf welchem Display läuft
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwaiste Sockets** — beide Befehle gemeinsam lesen: Steht in `/tmp/.X11-unix/` ein `X98`, zu
|
||||||
|
dem `pgrep` keinen Prozess zeigt, ist das ein Überbleibsel eines abgestürzten oder hart beendeten
|
||||||
|
Xvfb. Die Nummer wirkt dann belegt, obwohl sie frei ist. Aufräumen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm -f /tmp/.X11-unix/X98
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Genau so entstanden: ein `xvfb-run -a`, das sich :98 nahm, weil :99 gerade lief — siehe die
|
||||||
|
Warnung in 4.7.)*
|
||||||
|
|
||||||
|
⚠️ **Drei Regeln, damit der Parallelbetrieb wirklich trägt:**
|
||||||
|
|
||||||
|
1. **`DISPLAY` in jedem Skript setzen** (`export DISPLAY=:98`). Fehlt die Variable, landet man auf
|
||||||
|
`:0` — dem echten Desktop — und stört den Menschen, der dort gerade arbeitet.
|
||||||
|
2. **Nie `pkill Xvfb` ohne Displaynummer.** Das erschlägt die Sitzungen aller anderen Projekte.
|
||||||
|
Immer `pkill -f "Xvfb :98"`. Dasselbe gilt für die eigene Anwendung: `pkill -f` mit einem zu
|
||||||
|
allgemeinen Muster trifft fremde Prozesse — besser die gemerkte PID beenden.
|
||||||
|
3. **Dateien in `/tmp` mit Projektpräfix.** `/tmp/v_1.png` überschreibt sich gegenseitig; nimm
|
||||||
|
`/tmp/<projekt>_v_1.png`. Gleiches gilt für Logdateien und Skripte.
|
||||||
|
|
||||||
|
### 4.6 Erhöhte Rechte
|
||||||
|
|
||||||
`sudo` verlangt normalerweise ein Passwort. Für Testläufe kann es befristet freigeschaltet werden
|
`sudo` verlangt normalerweise ein Passwort. Für Testläufe kann es befristet freigeschaltet werden
|
||||||
(**ein Mensch, einmalig**):
|
(**ein Mensch, einmalig**):
|
||||||
@@ -159,16 +203,21 @@ sudo rm /etc/sudoers.d/99-testing
|
|||||||
Das hebt den Schutz für das Konto vollständig auf — vertretbar auf einer Test-VM hinter VPN, aber
|
Das hebt den Schutz für das Konto vollständig auf — vertretbar auf einer Test-VM hinter VPN, aber
|
||||||
nicht dauerhaft und niemals auf einer Produktivmaschine.
|
nicht dauerhaft und niemals auf einer Produktivmaschine.
|
||||||
|
|
||||||
### 4.6 GUI-Anwendungen ohne Bildschirm testen
|
### 4.7 GUI-Anwendungen ohne Bildschirm testen
|
||||||
|
|
||||||
Braucht eine Anwendung X11, scheitert sie über SSH mit `XOpenDisplay failed`. Für einen einzelnen
|
Braucht eine Anwendung X11, scheitert sie über SSH mit `XOpenDisplay failed`. Für einen einzelnen
|
||||||
Durchlauf genügt:
|
Durchlauf genügt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xvfb-run -a ./MeineAnwendung --irgendein-schalter
|
xvfb-run -n <deine-nummer> ./MeineAnwendung --irgendein-schalter
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.7 Oberflächen selbst begutachten — ohne Bildschirm, ohne fremde Hilfe
|
⚠️ **Nicht `xvfb-run -a` verwenden.** Das `-a` sucht sich selbst ein freies Display und **beginnt
|
||||||
|
dabei bei :99** — also ausgerechnet auf PolyTraders Nummer. Läuft dort gerade nichts, belegt es
|
||||||
|
diese Nummer; startet PolyTrader kurz darauf, kollidieren beide. Mit `-n 98` (bzw. der eigenen
|
||||||
|
Nummer aus 4.5) bleibt die Zuordnung eindeutig.
|
||||||
|
|
||||||
|
### 4.8 Oberflächen selbst begutachten — ohne Bildschirm, ohne fremde Hilfe
|
||||||
|
|
||||||
**Ein Agent kann die Oberfläche eigenständig prüfen.** Kein Mensch muss am Bildschirm sitzen und
|
**Ein Agent kann die Oberfläche eigenständig prüfen.** Kein Mensch muss am Bildschirm sitzen und
|
||||||
Bildschirmfotos schicken. Das Verfahren hat sich am 26.08.2026 bewährt und **sieben Befunde
|
Bildschirmfotos schicken. Das Verfahren hat sich am 26.08.2026 bewährt und **sieben Befunde
|
||||||
@@ -182,33 +231,39 @@ Aufnahmen navigieren lässt:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd ~/<projekt>-test
|
# Kopf anpassen - das ist alles, was sich je Projekt unterscheidet:
|
||||||
export DISPLAY=:99
|
PROJEKT="polytrader"
|
||||||
|
DISP=":99" # eigene Nummer aus Abschnitt 4.5!
|
||||||
|
FENSTER="^PolyTrader$"
|
||||||
|
|
||||||
pkill -f "Xvfb :99" 2>/dev/null; sleep 1
|
cd ~/$PROJEKT-test
|
||||||
Xvfb :99 -screen 0 1600x1000x24 >/dev/null 2>&1 &
|
export DISPLAY=$DISP
|
||||||
|
|
||||||
|
pkill -f "Xvfb $DISP" 2>/dev/null; sleep 1
|
||||||
|
Xvfb $DISP -screen 0 1600x1000x24 >/dev/null 2>&1 &
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
nohup ./MeineAnwendung > /tmp/gui.log 2>&1 &
|
nohup ./MeineAnwendung > /tmp/${PROJEKT}_gui.log 2>&1 &
|
||||||
APP=$!
|
APP=$!
|
||||||
sleep 25 # Startzeit großzügig bemessen
|
sleep 25 # Startzeit großzügig bemessen
|
||||||
|
|
||||||
# Fenster-ID ermitteln (bestaetigt, dass die Oberflaeche ueberhaupt erscheint)
|
# Fenster-ID ermitteln (bestaetigt, dass die Oberflaeche ueberhaupt erscheint)
|
||||||
xdotool search --name "^MeinFenstertitel$" | head -1
|
xdotool search --name "$FENSTER" | head -1
|
||||||
|
|
||||||
# navigieren und aufnehmen
|
# navigieren und aufnehmen - Dateinamen mit Projektpraefix, sonst
|
||||||
shoot() { sleep 5; import -window root /tmp/v_$1.png; }
|
# ueberschreiben sich parallel laufende Projekte gegenseitig
|
||||||
|
shoot() { sleep 5; import -window root /tmp/${PROJEKT}_$1.png; echo " $1"; }
|
||||||
shoot 1_start
|
shoot 1_start
|
||||||
xdotool mousemove 127 205 click 1; shoot 2_zweite_ansicht
|
xdotool mousemove 127 205 click 1; shoot 2_zweite_ansicht
|
||||||
xdotool mousemove 127 237 click 1; shoot 3_dritte_ansicht
|
xdotool mousemove 127 237 click 1; shoot 3_dritte_ansicht
|
||||||
|
|
||||||
kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb :99" 2>/dev/null
|
kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb $DISP" 2>/dev/null
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach die Bilder abholen und ansehen:
|
Danach die Bilder abholen und ansehen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scp testvm:'/tmp/v_*.png' .
|
scp testvm:'/tmp/polytrader_*.png' .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Was sich dabei bewährt hat:**
|
**Was sich dabei bewährt hat:**
|
||||||
@@ -224,9 +279,10 @@ scp testvm:'/tmp/v_*.png' .
|
|||||||
- **Kontraste nachmessen statt schätzen.** Ob Text lesbar ist, lässt sich ausrechnen — Pixelfarben
|
- **Kontraste nachmessen statt schätzen.** Ob Text lesbar ist, lässt sich ausrechnen — Pixelfarben
|
||||||
auslesen und das WCAG-Verhältnis bestimmen (4,5:1 für normalen Text). So wird aus „wirkt blass"
|
auslesen und das WCAG-Verhältnis bestimmen (4,5:1 für normalen Text). So wird aus „wirkt blass"
|
||||||
ein belegter Befund.
|
ein belegter Befund.
|
||||||
- **Nur eine Instanz gleichzeitig.** Läuft der systemd-Dienst noch, schreiben zwei Prozesse in
|
- **Nur eine Instanz des eigenen Projekts gleichzeitig.** Läuft der systemd-Dienst noch, schreiben
|
||||||
dieselbe Datenbank und es entstehen Fehler, die nichts mit der Oberfläche zu tun haben. Vorher
|
zwei Prozesse in dieselbe Datenbank und es entstehen Fehler, die nichts mit der Oberfläche zu tun
|
||||||
`sudo systemctl stop <projekt>`.
|
haben. Vorher `sudo systemctl stop <projekt>`. (Ein **anderes** Projekt auf seinem eigenen
|
||||||
|
Display stört dagegen nicht — dafür ist die Trennung aus 4.5 da.)
|
||||||
- **Auf Zeichen achten, die falsch aussehen.** Fehlt eine Icon-Schriftart, ersetzt Fontconfig sie
|
- **Auf Zeichen achten, die falsch aussehen.** Fehlt eine Icon-Schriftart, ersetzt Fontconfig sie
|
||||||
stillschweigend durch etwas anderes — ein Kalendersymbol wird dann zu einer sinnlosen Zahl. Das
|
stillschweigend durch etwas anderes — ein Kalendersymbol wird dann zu einer sinnlosen Zahl. Das
|
||||||
fällt nur im Bild auf, nie im Log.
|
fällt nur im Bild auf, nie im Log.
|
||||||
@@ -261,6 +317,23 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht.
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `polytrader.service` | `polytrader` | `/opt/polytrader` | PolyTrader | 26.08.2026 |
|
| `polytrader.service` | `polytrader` | `/opt/polytrader` | PolyTrader | 26.08.2026 |
|
||||||
|
|
||||||
|
**Verzeichnisse**
|
||||||
|
|
||||||
|
| Projekt | Publish-Verzeichnis | Dienst |
|
||||||
|
|---|---|---|
|
||||||
|
| PolyTrader | `~/polytrader-test` | `/opt/polytrader` |
|
||||||
|
|
||||||
|
**X-Displays** (Vergabe siehe 4.5)
|
||||||
|
|
||||||
|
| Display | Projekt | Stand |
|
||||||
|
|---|---|---|
|
||||||
|
| `:0` | echter Desktop — **nie belegen** | |
|
||||||
|
| `:99` | PolyTrader | 26.08.2026 |
|
||||||
|
| `:98` | Predictalytics | reserviert |
|
||||||
|
| `:97` | ClawdDotNet | reserviert |
|
||||||
|
| `:96` | IBKRTrader | reserviert |
|
||||||
|
| `:95` abwärts | frei für künftige Projekte | |
|
||||||
|
|
||||||
**Ports** (Stand 26.08.2026)
|
**Ports** (Stand 26.08.2026)
|
||||||
|
|
||||||
| Port | Belegt durch |
|
| Port | Belegt durch |
|
||||||
@@ -279,6 +352,10 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht.
|
|||||||
## 7. Aufräumen nach dem Test
|
## 7. Aufräumen nach dem Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Virtuelles Display beenden (falls noch offen) - NUR das eigene!
|
||||||
|
pkill -f "Xvfb :<deine-nummer>"
|
||||||
|
rm -f /tmp/<projekt>_*.png /tmp/<projekt>_gui.log
|
||||||
|
|
||||||
# Dienst abschalten und entfernen
|
# Dienst abschalten und entfernen
|
||||||
sudo systemctl disable --now <projekt>
|
sudo systemctl disable --now <projekt>
|
||||||
sudo rm -f /etc/systemd/system/<projekt>.service /etc/logrotate.d/<projekt>
|
sudo rm -f /etc/systemd/system/<projekt>.service /etc/logrotate.d/<projekt>
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ einheitlich gelten. Zumal `server_settings.xml` auch den Watchdog-Token trägt.
|
|||||||
**Empfehlung:** Die Sichtprüfung der Modulfenster **mit echten Daten wiederholen**, sobald B-10
|
**Empfehlung:** Die Sichtprüfung der Modulfenster **mit echten Daten wiederholen**, sobald B-10
|
||||||
ausgeliefert ist. Erst dann lassen sich Spaltenbreiten (B-11), Splitter-Positionen und
|
ausgeliefert ist. Erst dann lassen sich Spaltenbreiten (B-11), Splitter-Positionen und
|
||||||
Leerzustände sinnvoll beurteilen. Das Verfahren dafür ist dokumentiert und wiederholbar:
|
Leerzustände sinnvoll beurteilen. Das Verfahren dafür ist dokumentiert und wiederholbar:
|
||||||
[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.7.
|
[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -551,4 +551,88 @@ leer. **Korrektes Verhalten.**
|
|||||||
Die Befunde B-11, B-13 und B-14 sind **Layoutarbeit an einer Stelle** und sollten zusammen
|
Die Befunde B-11, B-13 und B-14 sind **Layoutarbeit an einer Stelle** und sollten zusammen
|
||||||
angegangen werden — am besten mit eingespielten Testdaten, weil sich Spaltenbreiten nur so
|
angegangen werden — am besten mit eingespielten Testdaten, weil sich Spaltenbreiten nur so
|
||||||
sinnvoll festlegen lassen. Das Verfahren ist reproduzierbar:
|
sinnvoll festlegen lassen. Das Verfahren ist reproduzierbar:
|
||||||
[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.7 plus `testdaten-ui.sql`.
|
[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8 plus `testdaten-ui.sql`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nachtrag 5: Behebung der offenen Befunde (27.08.2026)
|
||||||
|
|
||||||
|
Die Nacharbeit ist unter Windows entstanden und dort gebaut und geprüft
|
||||||
|
(`dotnet build -c Release`: 0 Fehler / 15 Warnungen wie zuvor; **484 Tests grün**, 8 davon neu;
|
||||||
|
`--smoke-ui` durchgängig `[OK]`). Die **Gegenprobe am Bild steht aus** — sie gehört auf die
|
||||||
|
Linux-VM, nach dem Verfahren aus [TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8
|
||||||
|
mit `testdaten-ui.sql`.
|
||||||
|
|
||||||
|
## Behoben
|
||||||
|
|
||||||
|
| Befund | Was geändert wurde |
|
||||||
|
|---|---|
|
||||||
|
| **B-2** · zwei Datumsformate | `TerminalLogger` schreibt die `.log`-Dateien jetzt als `yyyy-MM-dd-<Level>.log` — dasselbe Format wie die `.jsonl`. Eine Logrotate-Regel mit **einem** Muster genügt künftig; die alte Regel mit zwei Mustern schadet nicht und darf stehen bleiben, solange noch alte Dateien da sind |
|
||||||
|
| **B-5** · Windows-Pfad unter Linux | `ServerSettings.DefaultMullvadCliPath` ist plattformabhängig (`/usr/bin/mullvad` bzw. der Windows-Pfad). Zusätzlich nennt die Fehlermeldung des `MullvadVpnService` jetzt den auf diesem System erwarteten Pfad — ein aus der Windows-Zeit übernommener Wert in `server_settings.xml` erklärt sich damit selbst |
|
||||||
|
| **B-6** · zu geringer Kontrast | **Es war kein Farbwert-, sondern ein Themenfehler.** Die Kachelfarbe wurde im Code über `ThemeManager.Brush(...)` aufgelöst und danach nie neu ausgewertet — beim Umschalten blieb die *helle* Positiv-Farbe (`#2F6B46`) auf dunklem Grund stehen, exakt die gemessenen 2,33 : 1. Jetzt kommt die Farbe über Klassen (`.positive` / `.negative`) und `DynamicResource`, folgt dem Themenwechsel also von selbst. Die eigentlichen Tokens erfüllen AA: dunkel **5,9 : 1**, hell **5,2 : 1** |
|
||||||
|
| **B-7** · „Gesamt" doppelt | Der Modulname steht nicht mehr im Textblock — die Kachel trägt ihn schon als Überschrift |
|
||||||
|
| **B-8** · zwölfmal „01.01." | Leerzustands-Fehler wie vermutet: ohne Trades nahm LiveCharts einen Standardbereich um 0 an, und Tick 0 ist der 01.01.0001. Die Achse spannt jetzt den **gewählten Zeitraum** auf, wenn keine Daten da sind; der Labeler prüft zusätzlich den gültigen `DateTime`-Bereich (sonst wirft `new DateTime(...)` bei Ticks außerhalb) |
|
||||||
|
| **B-11** · abgeschnittene Überschriften | Systematisch statt Spalte für Spalte: **132** feste Pixelbreiten in allen neun Ansichten sind jetzt `Width="Auto"` — das misst Überschrift *und* Werte. Die 19 `Width="*"`-Spalten haben eine `MinWidth`. Die Regel steht als Kommentar bei der `DataGrid`-Formatvorlage in `App.axaml` |
|
||||||
|
| **B-13** · Name unsichtbar | Im Master-Trader-Grid steht **Name** jetzt direkt hinter `#` mit `MinWidth="180"`; die Wallet-Adresse folgt gekürzt (`0x9e2b…3b95`, neuer `ShortAddressConverter`), die vollständige Adresse im Tooltip |
|
||||||
|
| **B-14** · mehrzeilige Texte | Neuer Feldtyp `MultilineTextSettingsField`; „Beschreibung" und „Begründung" sind am Modell mit `[DataType(DataType.MultilineText)]` ausgezeichnet und erscheinen als sechszeiliges Feld mit Umbruch |
|
||||||
|
| **B-15** · ein Datensatz legt das Modul lahm | Die Wertkonvertierung läuft über `AccountIdSetJson`: JSON-Array wie bisher, eine reine Zahlenliste (`"1,2"`) wird als Altformat noch verstanden, alles andere ergibt eine **leere** Zuordnung. Der Trader wird also geladen und kopiert auf kein Konto — die sichere Richtung. Die Hydration meldet Anzahl und letzten Rohwert als Warnung |
|
||||||
|
| **B-12** · Lizenzschlüssel im Klartext | **Ursache gefunden:** `LicenseCli` läuft vor dem Host-Aufbau und rief damit `SecretProtection.Protect` ohne geladenen Master-Key auf — der reicht den Klartext unverändert durch. Die CLI lädt den Master-Key jetzt selbst. Zusätzlich zieht der Start einen noch im Klartext liegenden Schlüssel einmalig verschlüsselt nach (selbstheilende Migration wie bei den DB-Secrets) |
|
||||||
|
|
||||||
|
## Nebenbefund aus der Nacharbeit — **alle Dezimal-Einstellungen waren nicht änderbar**
|
||||||
|
|
||||||
|
Beim Nachbauen von B-14 fiel auf: `SettingsModelBuilder.Create` kannte `bool`, `int`, `long`,
|
||||||
|
`string` und Enums — **`decimal`, `double` und `float` nicht**. Alle Dezimalwerte fielen still auf
|
||||||
|
`ReadOnlySettingsField` zurück und waren in der Oberfläche damit reine Anzeigen.
|
||||||
|
|
||||||
|
Betroffen war die **komplette Risikokonfiguration**: Positionsgrößen (`PerMarketLimit`,
|
||||||
|
`PerMasterLimit`), Preisgrenzen (`MaxBuyPrice`, `MaxPriceDifference`), SELL-Leiter (`SellFloorPct`,
|
||||||
|
`ProfitTarget`), Spread- und Verkaufsgrenzen sowie im ResolutionFarming `MinPrice`, `MaxPrice`,
|
||||||
|
`MinEdgePct`, `MaxPerMarketUsd` und der Tagesverlust-Kill-Switch. Unter WinForms waren sie im
|
||||||
|
PropertyGrid noch editierbar — die Regression kam mit dem Ersatz des PropertyGrid und ist seither
|
||||||
|
unbemerkt geblieben, weil ein nicht editierbares Feld genauso aussieht wie ein editierbares mit
|
||||||
|
grauem Rahmen.
|
||||||
|
|
||||||
|
Behoben durch `DecimalSettingsField` (nimmt „5,0" und „5.0" an; eine ungültige Eingabe lässt den
|
||||||
|
alten Wert stehen, statt still eine 0 zu schreiben). **Damit das nicht wiederkehrt**, prüft
|
||||||
|
`--smoke-ui` jetzt zusätzlich, dass in `CopyTradingAccountSettings`, `RfSettings` und
|
||||||
|
`ServerSettings` **kein beschreibbares Einzelfeld** als reine Anzeige landet:
|
||||||
|
|
||||||
|
```
|
||||||
|
[OK] Einstellungs-Editor (CopyTradingAccountSettings): alle beschreibbaren Felder sind auch bearbeitbar
|
||||||
|
[OK] Einstellungs-Editor (RfSettings): alle beschreibbaren Felder sind auch bearbeitbar
|
||||||
|
[OK] Einstellungs-Editor (ServerSettings): alle beschreibbaren Felder sind auch bearbeitbar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zusätzlich: Tooltip auf jeder Textzelle
|
||||||
|
|
||||||
|
Auch mit sauberen Breiten bleiben lange Marktfragen und Logmeldungen zwangsläufig abgeschnitten
|
||||||
|
(die Beobachtungen zur Account-Übersicht und zur Spalte „Nachricht" im Dashboard). Eine
|
||||||
|
Formatvorlage in `App.axaml` gibt deshalb **jeder** Textzelle ihren vollen Inhalt als Tooltip —
|
||||||
|
kein Spaltenziehen mehr nötig.
|
||||||
|
|
||||||
|
## Behoben, aber nur am Bild nachweisbar
|
||||||
|
|
||||||
|
**B-9 · Kalendersymbol.** Der wahrscheinliche Grund ist gefunden, aber unter Windows nicht
|
||||||
|
reproduzierbar: In Avalonia stechen die Formatvorlagen der Anwendung die Setter eines
|
||||||
|
`ControlTheme` aus. Unsere `Style Selector="Button"` setzte damit auch auf dem `PART_Button` des
|
||||||
|
`CalendarDatePicker` die Schriftliste „Barlow Condensed, …" — und verdrängte dort die
|
||||||
|
Symbolschrift, aus der das Kalenderzeichen stammt. Unter Windows findet die Schriftsuche das
|
||||||
|
Zeichen trotzdem, unter Linux setzt Fontconfig stillschweigend etwas anderes ein.
|
||||||
|
|
||||||
|
Die Schrift-Zuweisung gilt jetzt als `Button:not(#PART_Button)`; Fläche, Rahmen und Ecken bleiben
|
||||||
|
unverändert für alle Schaltflächen. **Bewusst so eng geschnitten:** Trifft die Vermutung nicht zu,
|
||||||
|
ändert sich nur die Schrift auf Vorlagen-Schaltflächen, nicht das Aussehen der Oberfläche.
|
||||||
|
|
||||||
|
> **Zu prüfen auf der VM:** Zeigt das Accounting-Fenster in den Feldern „Von" und „Bis" jetzt ein
|
||||||
|
> Kalendersymbol? Falls nicht, hilft ein Bildausschnitt der Stelle weiter — das Zeichen lässt sich
|
||||||
|
> aus dem Log nicht ermitteln.
|
||||||
|
|
||||||
|
## Bewusst nicht angefasst
|
||||||
|
|
||||||
|
| Befund | Grund |
|
||||||
|
|---|---|
|
||||||
|
| **B-3** · `--smoke-ui` verlangt eine Datenbank | Eigener Schnitt: Persistenz überspringen oder gegen EF-InMemory bauen. Betrifft fünf DbContexts und hängt daran, ob die CI diesen Modus nutzen soll — siehe A2 |
|
||||||
|
| **B-4** · `--smoke-ui` braucht einen X-Server | Zwei Wege (`xvfb-run` im Build-Image vs. `Avalonia.Headless` im Code). Gehört zu derselben Entscheidung wie B-3 |
|
||||||
|
| **§8** · Logs stempeln UTC, Oberfläche zeigt Ortszeit | Gehört zu **T2** (Umstellung auf `Microsoft.Extensions.Logging`). Ein sauberer Fix braucht `DateTimeOffset` statt `DateTime` in `LogMessageEventArgs` — das berührt jede Logzeile der Anwendung und ist kein Nebenbei-Schnitt |
|
||||||
|
| Leerzustände melden nichts | Betrifft rund vierzehn Grids und ist reine Zutat, kein Fehler. Sinnvoll zusammen mit der nächsten Sichtprüfung zu entscheiden |
|
||||||
|
|||||||
@@ -162,6 +162,16 @@
|
|||||||
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
|
||||||
</Style>
|
</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">
|
<Style Selector="TextBlock.mono, SelectableTextBlock.mono">
|
||||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontMono}" />
|
<Setter Property="FontFamily" Value="{DynamicResource AppFontMono}" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -173,7 +183,6 @@
|
|||||||
|
|
||||||
<Style Selector="Button">
|
<Style Selector="Button">
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
<Setter Property="CornerRadius" Value="0" />
|
||||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontHeading}" />
|
|
||||||
<Setter Property="FontSize" Value="13" />
|
<Setter Property="FontSize" Value="13" />
|
||||||
<Setter Property="Padding" Value="10,5" />
|
<Setter Property="Padding" Value="10,5" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
@@ -181,6 +190,19 @@
|
|||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
</Style>
|
</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.
|
<!-- 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
|
Deshalb müssen Schwebe- und Druckzustand dort gesetzt werden, sonst blitzt beim
|
||||||
Überfahren wieder das Fluent-Grau durch. -->
|
Überfahren wieder das Fluent-Grau durch. -->
|
||||||
@@ -237,7 +259,15 @@
|
|||||||
|
|
||||||
<!-- ═══ Tabellen ══════════════════════════════════════════════════════════════
|
<!-- ═══ Tabellen ══════════════════════════════════════════════════════════════
|
||||||
Kopfzeile klein, gesperrt und gedämpft; Zeilen nur durch eine Haarlinie
|
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">
|
<Style Selector="DataGrid">
|
||||||
<Setter Property="GridLinesVisibility" Value="Horizontal" />
|
<Setter Property="GridLinesVisibility" Value="Horizontal" />
|
||||||
<Setter Property="HeadersVisibility" Value="Column" />
|
<Setter Property="HeadersVisibility" Value="Column" />
|
||||||
@@ -263,6 +293,13 @@
|
|||||||
<Setter Property="Padding" Value="7,5" />
|
<Setter Property="Padding" Value="7,5" />
|
||||||
</Style>
|
</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 ═════════════════════════════════════════════════════════════ -->
|
<!-- ═══ Bausteine ═════════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
<!-- KPI-Kachel: entspricht den zweizeiligen Labels der WinForms-Oberfläche
|
<!-- 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
x:Class="PolyTrader.App.Avalonia.Controls.SettingsEditor">
|
x:Class="PolyTrader.App.Avalonia.Controls.SettingsEditor">
|
||||||
@@ -60,6 +60,25 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</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 -->
|
<!-- Ganzzahl -->
|
||||||
<DataTemplate DataType="vm:IntSettingsField">
|
<DataTemplate DataType="vm:IntSettingsField">
|
||||||
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
|
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
|
||||||
@@ -73,6 +92,19 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</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 -->
|
<!-- Ja/Nein -->
|
||||||
<DataTemplate DataType="vm:BoolSettingsField">
|
<DataTemplate DataType="vm:BoolSettingsField">
|
||||||
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
|
<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 System.Linq;
|
||||||
using Deploymentcenter.Client;
|
using Deploymentcenter.Client;
|
||||||
using PolyTrader.Core;
|
using PolyTrader.Core;
|
||||||
@@ -94,6 +94,12 @@ namespace PolyTrader.App.Avalonia.Licensing
|
|||||||
|
|
||||||
const string settingsPath = "server_settings.xml";
|
const string settingsPath = "server_settings.xml";
|
||||||
var settings = ServerSettings.Load(settingsPath);
|
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.LicenseKey = SecretProtection.Protect(key.Trim());
|
||||||
settings.Save(settingsPath);
|
settings.Save(settingsPath);
|
||||||
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
|
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
|
||||||
@@ -106,6 +112,9 @@ namespace PolyTrader.App.Avalonia.Licensing
|
|||||||
var options = Program.LoadDeploymentcenterOptions();
|
var options = Program.LoadDeploymentcenterOptions();
|
||||||
var settings = ServerSettings.Load("server_settings.xml");
|
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;
|
string key;
|
||||||
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
|
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -190,6 +190,26 @@ namespace PolyTrader.App.Avalonia
|
|||||||
|
|
||||||
ConfigureSecretProtection(logger);
|
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
|
// Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
|
||||||
// fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
|
// fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
|
||||||
AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => logger.Warning(m));
|
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}");
|
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
|
// Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im
|
||||||
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
|
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
|
||||||
string[] themeKeys =
|
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
|
/// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder
|
||||||
/// Credential-Entschlüsselung laufen.
|
/// Credential-Entschlüsselung laufen.
|
||||||
/// </summary>
|
/// </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 keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
|
||||||
string? masterKey = MasterKeyResolver.Resolve(
|
string? masterKey = MasterKeyResolver.Resolve(
|
||||||
keyFile,
|
keyFile,
|
||||||
() => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
|
() => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
|
||||||
onGenerated: _ => logger.Warning(
|
onGenerated: _ => warn(
|
||||||
"🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
|
"🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
|
||||||
"gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
|
"gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
|
||||||
"(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
|
"(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
|
||||||
"Secret unwiederbringlich unlesbar."),
|
"Secret unwiederbringlich unlesbar."),
|
||||||
onWarning: msg => logger.Warning($"⚠️ {msg}"));
|
onWarning: msg => warn($"⚠️ {msg}"));
|
||||||
|
|
||||||
SecretProtection.Configure(masterKey);
|
SecretProtection.Configure(masterKey);
|
||||||
|
|
||||||
if (SecretProtection.IsConfigured)
|
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
|
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[] ModuleXAxes { get => _moduleXAxes; private set => Set(ref _moduleXAxes, value); }
|
||||||
public Axis[] DayXAxes { get => _dayXAxes; private set => Set(ref _dayXAxes, 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>
|
/// <summary>
|
||||||
public void Update(IReadOnlyList<TradeRecord> scoped)
|
/// 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);
|
BuildByModule(scoped);
|
||||||
BuildByDay(scoped);
|
BuildByDay(scoped);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildEquity(IReadOnlyList<TradeRecord> scoped)
|
private void BuildEquity(IReadOnlyList<TradeRecord> scoped, DateTime? since)
|
||||||
{
|
{
|
||||||
var curve = TradeAnalytics.EquityCurve(scoped);
|
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.
|
// Ticks kommen als DateTime-Ticks – zurueckrechnen und kurz formatieren.
|
||||||
Labeler = value => new DateTime((long)value).ToString("dd.MM."),
|
// 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,
|
UnitWidth = TimeSpan.FromDays(1).Ticks,
|
||||||
LabelsRotation = 0
|
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)
|
private void BuildByModule(IReadOnlyList<TradeRecord> scoped)
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using Avalonia.Media;
|
namespace PolyTrader.App.Avalonia.ViewModels
|
||||||
using PolyTrader.App.Avalonia.Shell;
|
|
||||||
|
|
||||||
namespace PolyTrader.App.Avalonia.ViewModels
|
|
||||||
{
|
{
|
||||||
/// <summary>Anzeige-Zeile der Account-Übersicht im Launcher.</summary>
|
/// <summary>Anzeige-Zeile der Account-Übersicht im Launcher.</summary>
|
||||||
public sealed class AccountOverviewRow
|
public sealed class AccountOverviewRow
|
||||||
@@ -39,6 +36,8 @@ namespace PolyTrader.App.Avalonia.ViewModels
|
|||||||
public string ModuleName { get; init; } = string.Empty;
|
public string ModuleName { get; init; } = string.Empty;
|
||||||
public string Text { get; init; } = string.Empty;
|
public string Text { get; init; } = string.Empty;
|
||||||
public bool IsPositive { get; init; } = true;
|
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.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -32,6 +33,9 @@ namespace PolyTrader.App.Avalonia.ViewModels
|
|||||||
/// <summary>Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen).</summary>
|
/// <summary>Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen).</summary>
|
||||||
public bool IsEditable => Property.CanWrite;
|
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) =>
|
protected void Raise([CallerMemberName] string? name = null) =>
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
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>
|
/// <summary>Ja/Nein (bool) – als Kontrollkästchen.</summary>
|
||||||
public sealed class BoolSettingsField : SettingsField
|
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.IsEnum) return new ChoiceSettingsField(target, p, label, description);
|
||||||
if (t == typeof(bool)) return new BoolSettingsField(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(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 –
|
// Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules –
|
||||||
// die pflegt der Modul-Tab des Dashboards).
|
// 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||||
@@ -119,17 +119,17 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridTrades" x:DataType="vm:DashboardTradeRow">
|
<DataGrid Name="gridTrades" x:DataType="vm:DashboardTradeRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding Module}" />
|
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Module}" />
|
||||||
<DataGridTextColumn Header="Konto" Width="130" Binding="{Binding Account}" />
|
<DataGridTextColumn Header="Konto" Width="Auto" Binding="{Binding Account}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
|
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
|
||||||
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
||||||
<DataGridTextColumn Header="Geschlossen" Width="140" Binding="{Binding ClosedAt, 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.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -142,10 +142,10 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridModules" x:DataType="vm:ModuleRow">
|
<DataGrid Name="gridModules" x:DataType="vm:ModuleRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Modul" Width="220" Binding="{Binding Modul}" />
|
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Modul}" />
|
||||||
<DataGridTextColumn Header="Status" Width="260" Binding="{Binding Status}" />
|
<DataGridTextColumn Header="Status" Width="Auto" Binding="{Binding Status}" />
|
||||||
<DataGridTextColumn Header="Hinweis" Width="*" Binding="{Binding Hinweis}" />
|
<DataGridTextColumn Header="Hinweis" Width="*" MinWidth="180" Binding="{Binding Hinweis}" />
|
||||||
<DataGridTemplateColumn Header="Aktion" Width="160">
|
<DataGridTemplateColumn Header="Aktion" Width="Auto">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="vm:ModuleRow">
|
<DataTemplate x:DataType="vm:ModuleRow">
|
||||||
<Button Content="{Binding Aktion}"
|
<Button Content="{Binding Aktion}"
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
(!since.HasValue || t.ClosedAt >= since.Value)).ToList();
|
(!since.HasValue || t.ClosedAt >= since.Value)).ToList();
|
||||||
|
|
||||||
UpdateKpis(scoped);
|
UpdateKpis(scoped);
|
||||||
_charts.Update(scoped);
|
_charts.Update(scoped, since);
|
||||||
|
|
||||||
_historyBase = scoped.OrderByDescending(t => t.ClosedAt).Select(ToRow).ToList();
|
_historyBase = scoped.OrderByDescending(t => t.ClosedAt).Select(ToRow).ToList();
|
||||||
ApplyHistoryFilter();
|
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:models="using:PolyTraderSharp.Models"
|
xmlns:models="using:PolyTraderSharp.Models"
|
||||||
x:Class="PolyTrader.App.Avalonia.Views.JobsWindow">
|
x:Class="PolyTrader.App.Avalonia.Views.JobsWindow">
|
||||||
@@ -19,17 +19,17 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridJobs" x:DataType="models:JobStatusRow">
|
<DataGrid Name="gridJobs" x:DataType="models:JobStatusRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Job Name" Width="150"
|
<DataGridTextColumn Header="Job Name" Width="Auto"
|
||||||
Binding="{Binding JobName}" />
|
Binding="{Binding JobName}" />
|
||||||
<DataGridTextColumn Header="Status" Width="150"
|
<DataGridTextColumn Header="Status" Width="Auto"
|
||||||
Binding="{Binding StatusText}" />
|
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\}}" />
|
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\}}" />
|
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}" />
|
Binding="{Binding IsEnabled}" />
|
||||||
<DataGridTemplateColumn Header="Aktion" Width="90">
|
<DataGridTemplateColumn Header="Aktion" Width="Auto">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="models:JobStatusRow">
|
<DataTemplate x:DataType="models:JobStatusRow">
|
||||||
<Button Content="Run Now"
|
<Button Content="Run Now"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTextColumn Header="Beschreibung" Width="*"
|
<DataGridTextColumn Header="Beschreibung" Width="*" MinWidth="180"
|
||||||
Binding="{Binding Description}" />
|
Binding="{Binding Description}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:shell="using:PolyTrader.App.Avalonia.Shell"
|
xmlns:shell="using:PolyTrader.App.Avalonia.Shell"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
@@ -71,9 +71,9 @@
|
|||||||
<HeaderedContentControl Header="Account-Übersicht">
|
<HeaderedContentControl Header="Account-Übersicht">
|
||||||
<DataGrid Name="gridAccounts" x:DataType="vm:AccountOverviewRow" Height="180">
|
<DataGrid Name="gridAccounts" x:DataType="vm:AccountOverviewRow" Height="180">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Account" Width="180" Binding="{Binding Name}" />
|
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Name}" />
|
||||||
<DataGridTextColumn Header="Module" Width="220" Binding="{Binding Modules}" />
|
<DataGridTextColumn Header="Module" Width="Auto" Binding="{Binding Modules}" />
|
||||||
<DataGridTemplateColumn Header="Polymarket" Width="110">
|
<DataGridTemplateColumn Header="Polymarket" Width="Auto">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="vm:AccountOverviewRow">
|
<DataTemplate x:DataType="vm:AccountOverviewRow">
|
||||||
<Button Content="Profil"
|
<Button Content="Profil"
|
||||||
@@ -83,10 +83,10 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTextColumn Header="Wallet (USDC)" Width="130" Binding="{Binding Balance, StringFormat=\{0:N2\}}" />
|
<DataGridTextColumn Header="Wallet (USDC)" Width="Auto" Binding="{Binding Balance, StringFormat=\{0:N2\}}" />
|
||||||
<DataGridTextColumn Header="3T PnL" Width="120" Binding="{Binding Pnl3d, StringFormat=\{0:N2\}}" />
|
<DataGridTextColumn Header="3T PnL" Width="Auto" Binding="{Binding Pnl3d, StringFormat=\{0:N2\}}" />
|
||||||
<DataGridTextColumn Header="3T Winrate %" Width="120" Binding="{Binding WinRate3d, StringFormat=\{0:N1\}}" />
|
<DataGridTextColumn Header="3T Winrate %" Width="Auto" Binding="{Binding WinRate3d, StringFormat=\{0:N1\}}" />
|
||||||
<DataGridTextColumn Header="Overall P/L" Width="130" Binding="{Binding OverallPnl, StringFormat=\{0:N2\}}" />
|
<DataGridTextColumn Header="Overall P/L" Width="Auto" Binding="{Binding OverallPnl, StringFormat=\{0:N2\}}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
@@ -106,11 +106,19 @@
|
|||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Background="{DynamicResource AppCardBrush}"
|
Background="{DynamicResource AppCardBrush}"
|
||||||
Padding="10,8" Margin="0,0,8,8" Width="250">
|
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">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="{Binding ModuleName}" FontWeight="SemiBold" FontSize="13" />
|
<TextBlock Text="{Binding ModuleName}" FontWeight="SemiBold" FontSize="13" />
|
||||||
<TextBlock Text="{Binding Text}"
|
<TextBlock Text="{Binding Text}"
|
||||||
Foreground="{Binding ForegroundBrush}"
|
Classes.positive="{Binding IsPositive}"
|
||||||
FontFamily="Consolas,Menlo,monospace"
|
Classes.negative="{Binding IsNegative}"
|
||||||
|
FontFamily="{DynamicResource AppFontMono}"
|
||||||
FontSize="11" />
|
FontSize="11" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -125,10 +133,10 @@
|
|||||||
<HeaderedContentControl Grid.Column="0" Header="Auffällige Trades (24h)" Height="250">
|
<HeaderedContentControl Grid.Column="0" Header="Auffällige Trades (24h)" Height="250">
|
||||||
<DataGrid Name="gridNotable" x:DataType="vm:NotableTradeRow">
|
<DataGrid Name="gridNotable" x:DataType="vm:NotableTradeRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding Module}" />
|
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding Module}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
|
||||||
<DataGridTextColumn Header="PnL" Width="85" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL %" Width="75" Binding="{Binding PnlPct, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPct, StringFormat=\{0:F1\}}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
@@ -139,9 +147,9 @@
|
|||||||
<HeaderedContentControl Grid.Column="2" Name="grpAlerts" Header="Warnungen & Fehler (heute)" Height="250">
|
<HeaderedContentControl Grid.Column="2" Name="grpAlerts" Header="Warnungen & Fehler (heute)" Height="250">
|
||||||
<DataGrid Name="gridAlerts" x:DataType="vm:LogAlertRow">
|
<DataGrid Name="gridAlerts" x:DataType="vm:LogAlertRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Zeit" Width="75" Binding="{Binding Time}" />
|
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding Time}" />
|
||||||
<DataGridTextColumn Header="Level" Width="80" Binding="{Binding Level}" />
|
<DataGridTextColumn Header="Level" Width="Auto" Binding="{Binding Level}" />
|
||||||
<DataGridTextColumn Header="Nachricht" Width="*" Binding="{Binding Message}" />
|
<DataGridTextColumn Header="Nachricht" Width="*" MinWidth="180" Binding="{Binding Message}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -398,8 +398,10 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
var k7 = TradeAnalytics.ComputeKpis(moduleTrades.Where(t => t.ClosedAt >= week));
|
var k7 = TradeAnalytics.ComputeKpis(moduleTrades.Where(t => t.ClosedAt >= week));
|
||||||
var k30 = TradeAnalytics.ComputeKpis(moduleTrades);
|
var k30 = TradeAnalytics.ComputeKpis(moduleTrades);
|
||||||
|
|
||||||
string text = $"{module}\n" +
|
// Ohne den Modulnamen: die Kachel traegt ihn bereits als Ueberschrift, im Textblock
|
||||||
$"Heute: {todayPnl:+0.00;-0.00} USDC\n" +
|
// 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" +
|
$"7T: {k7.NetPnl:+0.00;-0.00} · {k7.WinRatePct:0}% · {k7.TradeCount} Tr.\n" +
|
||||||
$"30T: {k30.NetPnl:+0.00;-0.00} · {k30.WinRatePct:0}%";
|
$"30T: {k30.NetPnl:+0.00;-0.00} · {k30.WinRatePct:0}%";
|
||||||
|
|
||||||
@@ -419,7 +421,9 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
string logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
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>();
|
var rows = new List<LogAlertRow>();
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
@@ -504,8 +508,13 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
|
|
||||||
// ===== Helfer =====
|
// ===== 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) =>
|
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) =>
|
private static string OneLine(string s) =>
|
||||||
(s ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim();
|
(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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:acc="using:PolyTrader.Modules.Accounting.Models"
|
xmlns:acc="using:PolyTrader.Modules.Accounting.Models"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
@@ -88,14 +88,14 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridMonthly" Margin="12" x:DataType="vm:MonthlyRow">
|
<DataGrid Name="gridMonthly" Margin="12" x:DataType="vm:MonthlyRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Monat" Width="140" Binding="{Binding Month}" />
|
<DataGridTextColumn Header="Monat" Width="Auto" Binding="{Binding Month}" />
|
||||||
<DataGridTextColumn Header="Handelsergebnis" Width="160" Binding="{Binding Net}" />
|
<DataGridTextColumn Header="Handelsergebnis" Width="Auto" Binding="{Binding Net}" />
|
||||||
<DataGridTextColumn Header="Einzahlungen" Width="140" Binding="{Binding Deposits}" />
|
<DataGridTextColumn Header="Einzahlungen" Width="Auto" Binding="{Binding Deposits}" />
|
||||||
<DataGridTextColumn Header="Auszahlungen" Width="140" Binding="{Binding Withdrawals}" />
|
<DataGridTextColumn Header="Auszahlungen" Width="Auto" Binding="{Binding Withdrawals}" />
|
||||||
<DataGridTextColumn Header="Fees" Width="120" Binding="{Binding Fees}" />
|
<DataGridTextColumn Header="Fees" Width="Auto" Binding="{Binding Fees}" />
|
||||||
<DataGridTextColumn Header="Volumen" Width="140" Binding="{Binding Volume}" />
|
<DataGridTextColumn Header="Volumen" Width="Auto" Binding="{Binding Volume}" />
|
||||||
<DataGridTextColumn Header="Trades" Width="90" Binding="{Binding Trades}" />
|
<DataGridTextColumn Header="Trades" Width="Auto" Binding="{Binding Trades}" />
|
||||||
<DataGridTextColumn Header="Endsaldo" Width="*" Binding="{Binding Closing}" />
|
<DataGridTextColumn Header="Endsaldo" Width="*" MinWidth="180" Binding="{Binding Closing}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -114,17 +114,17 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridLedger" x:DataType="acc:LedgerEntry">
|
<DataGrid Name="gridLedger" x:DataType="acc:LedgerEntry">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Zeitpunkt" Width="150" Binding="{Binding Timestamp, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
<DataGridTextColumn Header="Zeitpunkt" Width="Auto" Binding="{Binding Timestamp, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Typ" Width="120" Binding="{Binding EventType}" />
|
<DataGridTextColumn Header="Typ" Width="Auto" Binding="{Binding EventType}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketSlug}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketSlug}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
|
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
|
||||||
<DataGridTextColumn Header="Size" Width="90" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Preis" Width="80" Binding="{Binding PriceUsdc, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Preis" Width="Auto" Binding="{Binding PriceUsdc, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Brutto" Width="100" Binding="{Binding GrossUsdc, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Brutto" Width="Auto" Binding="{Binding GrossUsdc, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Fee" Width="90" Binding="{Binding FeeUsdc, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Fee" Width="Auto" Binding="{Binding FeeUsdc, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Netto" Width="100" Binding="{Binding NetUsdc, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Netto" Width="Auto" Binding="{Binding NetUsdc, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Quelle" Width="110" Binding="{Binding Source}" />
|
<DataGridTextColumn Header="Quelle" Width="Auto" Binding="{Binding Source}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -144,13 +144,13 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridRuns" x:DataType="acc:IngestRun">
|
<DataGrid Name="gridRuns" x:DataType="acc:IngestRun">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Start" Width="160" Binding="{Binding StartedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
|
<DataGridTextColumn Header="Start" Width="Auto" 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\}}" />
|
<DataGridTextColumn Header="Ende" Width="Auto" Binding="{Binding FinishedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
|
||||||
<DataGridCheckBoxColumn Header="Backfill" Width="80" Binding="{Binding Backfill}" />
|
<DataGridCheckBoxColumn Header="Backfill" Width="Auto" Binding="{Binding Backfill}" />
|
||||||
<DataGridCheckBoxColumn Header="Erfolg" Width="70" Binding="{Binding Success}" />
|
<DataGridCheckBoxColumn Header="Erfolg" Width="Auto" Binding="{Binding Success}" />
|
||||||
<DataGridTextColumn Header="Neu" Width="70" Binding="{Binding NewEntries}" />
|
<DataGridTextColumn Header="Neu" Width="Auto" Binding="{Binding NewEntries}" />
|
||||||
<DataGridTextColumn Header="Duplikate" Width="90" Binding="{Binding DuplicateEntries}" />
|
<DataGridTextColumn Header="Duplikate" Width="Auto" Binding="{Binding DuplicateEntries}" />
|
||||||
<DataGridTextColumn Header="Meldung" Width="*" Binding="{Binding Message}" />
|
<DataGridTextColumn Header="Meldung" Width="*" MinWidth="180" Binding="{Binding Message}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
|
xmlns:conv="using:PolyTrader.App.Avalonia.Converters"
|
||||||
xmlns:ct="using:PolyTraderSharp.Models"
|
xmlns:ct="using:PolyTraderSharp.Models"
|
||||||
x:Class="PolyTrader.App.Avalonia.Views.Modules.CopyTradingWindow">
|
x:Class="PolyTrader.App.Avalonia.Views.Modules.CopyTradingWindow">
|
||||||
|
|
||||||
@@ -29,18 +30,31 @@
|
|||||||
<Grid ColumnDefinitions="*,4,460">
|
<Grid ColumnDefinitions="*,4,460">
|
||||||
<DataGrid Name="gridTraders" Grid.Column="0" x:DataType="ct:TrackedTrader">
|
<DataGrid Name="gridTraders" Grid.Column="0" x:DataType="ct:TrackedTrader">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="#" Width="50" Binding="{Binding Id}" />
|
<!-- Der Name steht vorn und bekommt den freien Platz: er ist die
|
||||||
<DataGridTextColumn Header="Wallet" Width="170" Binding="{Binding WalletAddress}" />
|
wichtigste Information der Zeile. Die Wallet-Adresse folgt
|
||||||
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding DisplayName}" />
|
gekuerzt (vollstaendig im Tooltip) - in voller Laenge drueckte
|
||||||
<DataGridTextColumn Header="Kategorie" Width="110" Binding="{Binding Category}" />
|
sie zusammen mit den festen Breiten der uebrigen Spalten den
|
||||||
<DataGridCheckBoxColumn Header="Aktiv" Width="50" Binding="{Binding IsActive}" />
|
Namen auf wenige Pixel zusammen (Befund B-13). -->
|
||||||
<DataGridTextColumn Header="Trades (7T)" Width="90" Binding="{Binding TotalTrades}" />
|
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding Id}" />
|
||||||
<DataGridTextColumn Header="Winrate %" Width="90" Binding="{Binding Winrate30t, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="Name" Width="*" MinWidth="180" Binding="{Binding DisplayName}" />
|
||||||
<DataGridTextColumn Header="PnL (7T)" Width="100" Binding="{Binding TotalPnl, StringFormat=\{0:F2\}}" />
|
<DataGridTemplateColumn Header="Wallet" Width="Auto">
|
||||||
<DataGridTextColumn Header="Copy-PnL (30T)" Width="110" Binding="{Binding CopyPnl30d, StringFormat=\{0:F2\}}" />
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataGridTextColumn Header="Profit-Faktor" Width="100" Binding="{Binding CopyProfitFactor, StringFormat=\{0:F2\}}" />
|
<DataTemplate x:DataType="ct:TrackedTrader">
|
||||||
<DataGridTextColumn Header="Ø PnL/Trade" Width="100" Binding="{Binding CopyAvgPnlPerTrade, StringFormat=\{0:F2\}}" />
|
<TextBlock Text="{Binding WalletAddress, Converter={x:Static conv:ShortAddressConverter.Instance}}"
|
||||||
<DataGridTextColumn Header="Copy-Trades" Width="90" Binding="{Binding CopyTradeCount30d}" />
|
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.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|
||||||
@@ -78,19 +92,19 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridOpen" x:DataType="vm:CopyOpenTradeRow">
|
<DataGrid Name="gridOpen" x:DataType="vm:CopyOpenTradeRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Account" Width="120" Binding="{Binding Account}" />
|
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Account}" />
|
||||||
<DataGridTextColumn Header="Master-Trader" Width="120" Binding="{Binding Trader}" />
|
<DataGridTextColumn Header="Master-Trader" Width="Auto" Binding="{Binding Trader}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Side" Width="55" Binding="{Binding Side}" />
|
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
|
||||||
<DataGridTextColumn Header="Entry" Width="65" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Aktuell" Width="65" Binding="{Binding Current, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Aktuell" Width="Auto" Binding="{Binding Current, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Einsatz" Width="80" Binding="{Binding Amount, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Einsatz" Width="Auto" Binding="{Binding Amount, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Wert" Width="80" Binding="{Binding Value, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Wert" Width="Auto" Binding="{Binding Value, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Buchgewinn" Width="90" Binding="{Binding Unrealized, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Buchgewinn" Width="Auto" Binding="{Binding Unrealized, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Buchgew. %" Width="80" Binding="{Binding UnrealizedPct, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="Buchgew. %" Width="Auto" Binding="{Binding UnrealizedPct, StringFormat=\{0:F1\}}" />
|
||||||
<DataGridTextColumn Header="Status" Width="90" Binding="{Binding Status}" />
|
<DataGridTextColumn Header="Status" Width="Auto" Binding="{Binding Status}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -120,19 +134,19 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridClosed" x:DataType="vm:CopyClosedTradeRow">
|
<DataGrid Name="gridClosed" x:DataType="vm:CopyClosedTradeRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="#" Width="60" Binding="{Binding TradeId}" />
|
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding TradeId}" />
|
||||||
<DataGridTextColumn Header="Account" Width="130" Binding="{Binding Account}" />
|
<DataGridTextColumn Header="Account" Width="Auto" Binding="{Binding Account}" />
|
||||||
<DataGridTextColumn Header="Master-Trader" Width="130" Binding="{Binding Trader}" />
|
<DataGridTextColumn Header="Master-Trader" Width="Auto" Binding="{Binding Trader}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding Market}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
|
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
|
||||||
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding Exit, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding Exit, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
||||||
<DataGridTextColumn Header="Eröffnet" Width="130" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
<DataGridTextColumn Header="Eröffnet" Width="Auto" 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="Geschlossen" Width="Auto" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
||||||
xmlns:rf="using:PolyTrader.Modules.ResolutionFarming.Models"
|
xmlns:rf="using:PolyTrader.Modules.ResolutionFarming.Models"
|
||||||
@@ -21,14 +21,14 @@
|
|||||||
<TabItem Header="Kandidaten">
|
<TabItem Header="Kandidaten">
|
||||||
<DataGrid Name="gridCandidates" x:DataType="rf:RfCandidate">
|
<DataGrid Name="gridCandidates" x:DataType="rf:RfCandidate">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Gescannt" Width="140" Binding="{Binding ScannedAt, StringFormat=\{0:dd.MM. HH:mm\}}" />
|
<DataGridTextColumn Header="Gescannt" Width="Auto" Binding="{Binding ScannedAt, StringFormat=\{0:dd.MM. HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
|
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
|
||||||
<DataGridTextColumn Header="Ask" Width="70" Binding="{Binding Ask, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Ask" Width="Auto" Binding="{Binding Ask, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Fee (bps)" Width="80" Binding="{Binding FeeBps}" />
|
<DataGridTextColumn Header="Fee (bps)" Width="Auto" Binding="{Binding FeeBps}" />
|
||||||
<DataGridTextColumn Header="Netto-Edge %" Width="110" Binding="{Binding NetEdgePct, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Netto-Edge %" Width="Auto" Binding="{Binding NetEdgePct, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Score" Width="80" Binding="{Binding Score, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Score" Width="Auto" Binding="{Binding Score, StringFormat=\{0:F2\}}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
@@ -40,15 +40,15 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<DataGrid Name="gridPositions" x:DataType="rf:RfPosition">
|
<DataGrid Name="gridPositions" x:DataType="rf:RfPosition">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Eröffnet" Width="140" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
<DataGridTextColumn Header="Eröffnet" Width="Auto" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
|
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
|
||||||
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="Einsatz" Width="90" Binding="{Binding AmountUsd, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Einsatz" Width="Auto" Binding="{Binding AmountUsd, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
|
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
|
||||||
<DataGridTextColumn Header="Auflösung" Width="140" Binding="{Binding EndDate, StringFormat=\{0:dd.MM.yyyy\}}" />
|
<DataGridTextColumn Header="Auflösung" Width="Auto" Binding="{Binding EndDate, StringFormat=\{0:dd.MM.yyyy\}}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -61,15 +61,15 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<DataGrid Name="gridHistory" x:DataType="rf:RfClosedTrade">
|
<DataGrid Name="gridHistory" x:DataType="rf:RfClosedTrade">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
|
||||||
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
|
<DataGridTextColumn Header="Outcome" Width="Auto" Binding="{Binding Outcome}" />
|
||||||
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
|
<DataGridTextColumn Header="Kategorie" Width="Auto" Binding="{Binding Category}" />
|
||||||
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Entry" Width="Auto" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Exit" Width="Auto" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="Size" Width="Auto" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL" Width="90" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
|
<DataGridTextColumn Header="PnL" Width="Auto" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
|
||||||
<DataGridTextColumn Header="PnL %" Width="80" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
<DataGridTextColumn Header="PnL %" Width="Auto" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
|
||||||
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
|
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
xmlns:sup="using:PolyTrader.Modules.Supervisor.Persistence"
|
xmlns:sup="using:PolyTrader.Modules.Supervisor.Persistence"
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
<Grid ColumnDefinitions="*,4,*">
|
<Grid ColumnDefinitions="*,4,*">
|
||||||
<DataGrid Name="gridSignals" Grid.Column="0" x:DataType="svc:SignalSummary">
|
<DataGrid Name="gridSignals" Grid.Column="0" x:DataType="svc:SignalSummary">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Zeit" Width="130" Binding="{Binding FirstSeen, StringFormat=\{0:dd.MM. HH:mm\}}" />
|
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding FirstSeen, StringFormat=\{0:dd.MM. HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding ModuleName}" />
|
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding ModuleName}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
|
||||||
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
|
<DataGridTextColumn Header="Side" Width="Auto" Binding="{Binding Side}" />
|
||||||
<DataGridTextColumn Header="Entscheidung" Width="120" Binding="{Binding LastDecision}" />
|
<DataGridTextColumn Header="Entscheidung" Width="Auto" Binding="{Binding LastDecision}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|
||||||
@@ -89,12 +89,12 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<DataGrid Name="gridReports" x:DataType="sup:SupervisorReport">
|
<DataGrid Name="gridReports" x:DataType="sup:SupervisorReport">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Erstellt" Width="150" Binding="{Binding CreatedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
<DataGridTextColumn Header="Erstellt" Width="Auto" Binding="{Binding CreatedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Profil" Width="140" Binding="{Binding Profile}" />
|
<DataGridTextColumn Header="Profil" Width="Auto" Binding="{Binding Profile}" />
|
||||||
<DataGridTextColumn Header="Modell" Width="200" Binding="{Binding Model}" />
|
<DataGridTextColumn Header="Modell" Width="Auto" Binding="{Binding Model}" />
|
||||||
<DataGridTextColumn Header="Frage" Width="300" Binding="{Binding Question}" />
|
<DataGridTextColumn Header="Frage" Width="Auto" Binding="{Binding Question}" />
|
||||||
<DataGridTextColumn Header="Antwort" Width="*" Binding="{Binding Answer}" />
|
<DataGridTextColumn Header="Antwort" Width="*" MinWidth="180" Binding="{Binding Answer}" />
|
||||||
<DataGridTextColumn Header="Tools" Width="70" Binding="{Binding ToolCallCount}" />
|
<DataGridTextColumn Header="Tools" Width="Auto" Binding="{Binding ToolCallCount}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -107,12 +107,12 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<DataGrid Name="gridCounterfactual" x:DataType="cf:CounterfactualRecord">
|
<DataGrid Name="gridCounterfactual" x:DataType="cf:CounterfactualRecord">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Geprüft" Width="150" Binding="{Binding CheckedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
<DataGridTextColumn Header="Geprüft" Width="Auto" Binding="{Binding CheckedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
|
||||||
<DataGridTextColumn Header="Modul" Width="120" Binding="{Binding ModuleName}" />
|
<DataGridTextColumn Header="Modul" Width="Auto" Binding="{Binding ModuleName}" />
|
||||||
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
|
<DataGridTextColumn Header="Markt" Width="*" MinWidth="180" Binding="{Binding MarketQuestion}" />
|
||||||
<DataGridTextColumn Header="Grund der Ablehnung" Width="260" Binding="{Binding Reason}" />
|
<DataGridTextColumn Header="Grund der Ablehnung" Width="Auto" Binding="{Binding Reason}" />
|
||||||
<DataGridTextColumn Header="Signalpreis" Width="100" Binding="{Binding SignalPrice, StringFormat=\{0:F3\}}" />
|
<DataGridTextColumn Header="Signalpreis" Width="Auto" Binding="{Binding SignalPrice, StringFormat=\{0:F3\}}" />
|
||||||
<DataGridCheckBoxColumn Header="Wäre Gewinner" Width="120" Binding="{Binding IsWinner}" />
|
<DataGridCheckBoxColumn Header="Wäre Gewinner" Width="Auto" Binding="{Binding IsWinner}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
||||||
xmlns:models="using:PolyTraderSharp.Models"
|
xmlns:models="using:PolyTraderSharp.Models"
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
<Grid ColumnDefinitions="380,4,*">
|
<Grid ColumnDefinitions="380,4,*">
|
||||||
<DataGrid Name="gridAccounts" Grid.Column="0" x:DataType="models:AccountState">
|
<DataGrid Name="gridAccounts" Grid.Column="0" x:DataType="models:AccountState">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="#" Width="50" Binding="{Binding AccountId}" />
|
<DataGridTextColumn Header="#" Width="Auto" Binding="{Binding AccountId}" />
|
||||||
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}" />
|
<DataGridTextColumn Header="Name" Width="*" MinWidth="180" Binding="{Binding Name}" />
|
||||||
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
|
<DataGridCheckBoxColumn Header="Demo" Width="Auto" Binding="{Binding IsDemo}" />
|
||||||
<DataGridCheckBoxColumn Header="Aktiv" Width="60" Binding="{Binding IsActive}" />
|
<DataGridCheckBoxColumn Header="Aktiv" Width="Auto" Binding="{Binding IsActive}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
|
||||||
x:Class="PolyTrader.App.Avalonia.Views.TerminalWindow">
|
x:Class="PolyTrader.App.Avalonia.Views.TerminalWindow">
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
|
|
||||||
<DataGrid Name="gridLogs" x:DataType="vm:LogViewerRow">
|
<DataGrid Name="gridLogs" x:DataType="vm:LogViewerRow">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Zeit" Width="90" Binding="{Binding Time}" />
|
<DataGridTextColumn Header="Zeit" Width="Auto" Binding="{Binding Time}" />
|
||||||
<DataGridTextColumn Header="Level" Width="110" Binding="{Binding Level}" />
|
<DataGridTextColumn Header="Level" Width="Auto" Binding="{Binding Level}" />
|
||||||
<DataGridTextColumn Header="CID" Width="160" Binding="{Binding Cid}" />
|
<DataGridTextColumn Header="CID" Width="Auto" Binding="{Binding Cid}" />
|
||||||
<DataGridTextColumn Header="Nachricht" Width="*" Binding="{Binding Message}" />
|
<DataGridTextColumn Header="Nachricht" Width="*" MinWidth="180" Binding="{Binding Message}" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -63,7 +63,7 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
cbLevel.SelectedIndex = 0;
|
cbLevel.SelectedIndex = 0;
|
||||||
cbViewerLevel.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>("btnAutoscroll")!.Click += (_, _) => ToggleAutoscroll();
|
||||||
this.FindControl<Button>("btnClear")!.Click += (_, _) => { _lines.Clear(); UpdateLiveStatus(); };
|
this.FindControl<Button>("btnClear")!.Click += (_, _) => { _lines.Clear(); UpdateLiveStatus(); };
|
||||||
@@ -123,9 +123,9 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
processed++;
|
processed++;
|
||||||
if (filter != "Alle" && e.Level.ToString() != filter) continue;
|
if (filter != "Alle" && e.Level.ToString() != filter) continue;
|
||||||
|
|
||||||
// Anzeige-Zeitzone aus den Server-Settings (siehe AppTimeZone) – nicht mehr fest
|
// Der Zeitstempel steht seit dem 27.08.2026 bereits in der Anzeige-Zeitzone
|
||||||
// gegen eine Windows-Zeitzonen-ID gerechnet.
|
// (siehe AppTimeZone.Now) - hier wird nicht noch einmal umgerechnet.
|
||||||
DateTime t = AppTimeZone.ToDisplay(e.Timestamp);
|
DateTimeOffset t = e.Timestamp;
|
||||||
|
|
||||||
_lines.Add(new LogLine
|
_lines.Add(new LogLine
|
||||||
{
|
{
|
||||||
@@ -187,7 +187,7 @@ namespace PolyTrader.App.Avalonia.Views
|
|||||||
var status = this.FindControl<TextBlock>("lblViewerStatus")!;
|
var status = this.FindControl<TextBlock>("lblViewerStatus")!;
|
||||||
try
|
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");
|
string path = Path.Combine(AppContext.BaseDirectory, "Logs", $"{day:yyyy-MM-dd}.jsonl");
|
||||||
|
|
||||||
_viewerRows.Clear();
|
_viewerRows.Clear();
|
||||||
|
|||||||
@@ -57,8 +57,21 @@ namespace PolyTraderSharp.Models
|
|||||||
|
|
||||||
[Category("Mullvad VPN")]
|
[Category("Mullvad VPN")]
|
||||||
[DisplayName("Mullvad CLI Path")]
|
[DisplayName("Mullvad CLI Path")]
|
||||||
[Description("Path to mullvad.exe")]
|
[Description("Pfad zum Mullvad-Kommandozeilenwerkzeug. Der Standard richtet sich nach dem " +
|
||||||
public string MullvadCliPath { get; set; } = @"C:\Program Files\Mullvad VPN\resources\mullvad.exe";
|
"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")]
|
[Category("Blockchain Listener")]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace PolyTraderSharp.Services
|
namespace PolyTraderSharp.Services
|
||||||
{
|
{
|
||||||
@@ -69,6 +69,21 @@ namespace PolyTraderSharp.Services
|
|||||||
$"„{TimeZoneInfo.Local.Id}\" verwendet. Empfohlen ist die IANA-Schreibweise, z.B. „{DefaultId}\".");
|
$"„{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>
|
/// <summary>Rechnet einen Zeitstempel in die Anzeige-Zeitzone um (UTC-Kennzeichnung wird beachtet).</summary>
|
||||||
public static DateTime ToDisplay(DateTime value) =>
|
public static DateTime ToDisplay(DateTime value) =>
|
||||||
value.Kind == DateTimeKind.Utc
|
value.Kind == DateTimeKind.Utc
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -130,7 +130,13 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
if (!File.Exists(_settings.MullvadCliPath))
|
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;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
@@ -11,7 +11,17 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
public string Message { get; }
|
public string Message { get; }
|
||||||
public LogLevel Level { 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>
|
/// <summary>Korrelations-ID (z. B. SignalId) für die Log-Forensik; leer wenn ohne Kontext.</summary>
|
||||||
public string CorrelationId { get; }
|
public string CorrelationId { get; }
|
||||||
@@ -20,7 +30,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
Message = message;
|
Message = message;
|
||||||
Level = level;
|
Level = level;
|
||||||
Timestamp = DateTime.Now;
|
Timestamp = AppTimeZone.Now;
|
||||||
CorrelationId = correlationId ?? "";
|
CorrelationId = correlationId ?? "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +115,12 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
try
|
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 fileName = $"{dateStr}-{e.Level}.log";
|
||||||
string fullPath = Path.Combine(_logsDirectory, fileName);
|
string fullPath = Path.Combine(_logsDirectory, fileName);
|
||||||
|
|
||||||
@@ -167,7 +182,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.Now - maxAge;
|
var cutoff = DateTimeOffset.UtcNow - maxAge;
|
||||||
return _history.Where(x => x.Timestamp >= cutoff).ToList();
|
return _history.Where(x => x.Timestamp >= cutoff).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace PolyTraderSharp.Models
|
namespace PolyTraderSharp.Models
|
||||||
@@ -33,12 +34,14 @@ namespace PolyTraderSharp.Models
|
|||||||
[Category("02. Categorization")]
|
[Category("02. Categorization")]
|
||||||
[DisplayName("Beschreibung")]
|
[DisplayName("Beschreibung")]
|
||||||
[Description("Freitext-Notiz zum Master (z. B. Herkunft, beobachtete Stärken/Schwächen).")]
|
[Description("Freitext-Notiz zum Master (z. B. Herkunft, beobachtete Stärken/Schwächen).")]
|
||||||
|
[DataType(DataType.MultilineText)]
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Category("02. Categorization")]
|
[Category("02. Categorization")]
|
||||||
[DisplayName("Begründung / Reasoning")]
|
[DisplayName("Begründung / Reasoning")]
|
||||||
[Description("Warum kopieren wir diesen Master? Wird auch vom Auto-Kill-Switch mit Zeitstempel befüllt, wenn er " +
|
[Description("Warum kopieren wir diesen Master? Wird auch vom Auto-Kill-Switch mit Zeitstempel befüllt, wenn er " +
|
||||||
"automatisch pausiert wird (Phase 3.3).")]
|
"automatisch pausiert wird (Phase 3.3).")]
|
||||||
|
[DataType(DataType.MultilineText)]
|
||||||
public string Reasoning { get; set; } = string.Empty;
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Category("03. General")]
|
[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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
using PolyTraderSharp.Models;
|
using PolyTraderSharp.Models;
|
||||||
@@ -67,10 +66,12 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
|
|||||||
v => v.Aggregate(0, (h, i) => HashCode.Combine(h, i)),
|
v => v.Aggregate(0, (h, i) => HashCode.Combine(h, i)),
|
||||||
v => new HashSet<int>(v));
|
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)
|
e.Property(x => x.AssignedAccountIds)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
|
v => AccountIdSetJson.Serialize(v),
|
||||||
v => string.IsNullOrEmpty(v) ? new HashSet<int>() : (JsonSerializer.Deserialize<HashSet<int>>(v, (JsonSerializerOptions?)null) ?? new HashSet<int>()))
|
v => AccountIdSetJson.Deserialize(v))
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
e.Property(x => x.AssignedAccountIds).Metadata.SetValueComparer(comparer);
|
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.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -42,10 +42,21 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
HydrateAccountSettings(_state.Accounts.Values.ToList());
|
HydrateAccountSettings(_state.Accounts.Values.ToList());
|
||||||
|
|
||||||
|
PolyTrader.Modules.CopyTrading.Persistence.Ef.AccountIdSetJson.ResetDiagnostics();
|
||||||
|
|
||||||
foreach (var trd in _traderRepo.GetAll())
|
foreach (var trd in _traderRepo.GetAll())
|
||||||
_copyState.Traders[trd.Id] = trd;
|
_copyState.Traders[trd.Id] = trd;
|
||||||
|
|
||||||
_logger.Info($"Copytrading-Hydration abgeschlossen: {_copyState.Traders.Count} Trader geladen.");
|
_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)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -57,13 +57,18 @@ namespace PolyTrader.Modules.Supervisor.Services
|
|||||||
return;
|
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)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var now = AppTimeZone.Now;
|
||||||
var next = now.Date.AddHours(hour);
|
var next = now.Date.AddHours(hour) - now.Offset;
|
||||||
if (next <= now) next = next.AddDays(1);
|
var nextUtc = new DateTimeOffset(next, TimeSpan.Zero);
|
||||||
try { await Task.Delay(next - now, stoppingToken); }
|
if (nextUtc <= now) nextUtc = nextUtc.AddDays(1);
|
||||||
|
try { await Task.Delay(nextUtc - now, stoppingToken); }
|
||||||
catch (OperationCanceledException) { break; }
|
catch (OperationCanceledException) { break; }
|
||||||
|
|
||||||
try { await RunOnceAsync(stoppingToken); }
|
try { await RunOnceAsync(stoppingToken); }
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sichert die Befunde des Linux-Sichttests vom 26.08.2026 ab
|
||||||
|
/// (docs/pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md), damit sie nicht zurückkehren.
|
||||||
|
/// </summary>
|
||||||
|
public class LinuxFindingsTests
|
||||||
|
{
|
||||||
|
// ── B-5: Windows-Pfad als Standardwert unter Linux ──────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Mullvad_default_path_matches_the_platform()
|
||||||
|
{
|
||||||
|
string expected = System.OperatingSystem.IsWindows()
|
||||||
|
? @"C:\Program Files\Mullvad VPN\resources\mullvad.exe"
|
||||||
|
: "/usr/bin/mullvad";
|
||||||
|
|
||||||
|
Assert.Equal(expected, ServerSettings.DefaultMullvadCliPath);
|
||||||
|
Assert.Equal(expected, new ServerSettings().MullvadCliPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── B-14: mehrzeilige Texte in einzeiligen Feldern ──────────────────────────────
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(nameof(TrackedTrader.Description))]
|
||||||
|
[InlineData(nameof(TrackedTrader.Reasoning))]
|
||||||
|
public void Long_free_text_properties_are_marked_multiline(string propertyName)
|
||||||
|
{
|
||||||
|
var p = typeof(TrackedTrader).GetProperty(propertyName)!;
|
||||||
|
var attr = p.GetCustomAttribute<DataTypeAttribute>();
|
||||||
|
|
||||||
|
Assert.NotNull(attr);
|
||||||
|
Assert.Equal(DataType.MultilineText, attr!.DataType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── B-15: ein fehlerhafter Datensatz legt die Modul-Hydration lahm ──────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Assigned_accounts_round_trip_through_json()
|
||||||
|
{
|
||||||
|
var set = new System.Collections.Generic.HashSet<int> { 1, 2 };
|
||||||
|
Assert.Equal(set, AccountIdSetJson.Deserialize(AccountIdSetJson.Serialize(set)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_column_becomes_an_empty_assignment()
|
||||||
|
{
|
||||||
|
Assert.Empty(AccountIdSetJson.Deserialize(null));
|
||||||
|
Assert.Empty(AccountIdSetJson.Deserialize(""));
|
||||||
|
Assert.Empty(AccountIdSetJson.Deserialize("[]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Plain_number_list_is_still_understood()
|
||||||
|
{
|
||||||
|
AccountIdSetJson.ResetDiagnostics();
|
||||||
|
|
||||||
|
Assert.Equal(new System.Collections.Generic.HashSet<int> { 1, 2 },
|
||||||
|
AccountIdSetJson.Deserialize("1,2"));
|
||||||
|
Assert.Equal(0, AccountIdSetJson.MalformedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Unreadable_value_yields_an_empty_set_and_is_counted()
|
||||||
|
{
|
||||||
|
AccountIdSetJson.ResetDiagnostics();
|
||||||
|
|
||||||
|
// Der Fall aus dem Test: kaputter Wert statt JSON-Array. Frueher warf die
|
||||||
|
// EF-Wertkonvertierung hier und es wurde KEIN einziger Trader geladen.
|
||||||
|
Assert.Empty(AccountIdSetJson.Deserialize("{kaputt"));
|
||||||
|
|
||||||
|
Assert.Equal(1, AccountIdSetJson.MalformedCount);
|
||||||
|
Assert.Equal("{kaputt", AccountIdSetJson.LastMalformedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Text_that_is_not_a_number_list_is_not_silently_accepted()
|
||||||
|
{
|
||||||
|
AccountIdSetJson.ResetDiagnostics();
|
||||||
|
|
||||||
|
Assert.Empty(AccountIdSetJson.Deserialize("1,zwei"));
|
||||||
|
Assert.Equal(1, AccountIdSetJson.MalformedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user