Compare commits

..
Author SHA1 Message Date
RichardandClaude Opus 5 487ea4466d Dunkelmodus mit Umschalter in der Fensterleiste
Farbschema hell / dunkel / dem System folgen, umschaltbar per Klick, Stand wird in
server_settings.xml gemerkt (ServerSettings.Theme). Der Editor zeigt die Einstellung
automatisch als Auswahlfeld - kein Zusatzcode, weil sie ein Enum ist.

- App.axaml: 18 Farb-Token je Variante (Flaechen, Rahmen, Texte, Bedeutungsfarben,
  Zeilenfaerbung, Trading-Umschalter, Chat-Rollen). Die Standard-Steuerelemente stellt
  das FluentTheme selbst um.
- 22 fest verdrahtete Farben in 11 XAML-Dateien auf DynamicResource umgestellt - die
  wechseln damit von selbst mit.
- Umschalter (Sonne/Mond) sitzt in der gemeinsamen Fensterleiste, also auf JEDEM Fenster.

Der eigentliche Knackpunkt waren die Farben, die im Code gesetzt werden - die folgen dem
Thema NICHT von selbst:
- TradeRowPalette liefert jetzt Eigenschaften statt static readonly, loest also bei jedem
  Zugriff neu auf. Im Dunkeln gedaempfte Toene statt der hellen Pastelltoene, die dort
  blenden und den Text unlesbar machen wuerden.
- ChatEntry speichert die ROLLE statt eines fertigen Brush - dadurch stimmt der Verlauf
  nach dem Umschalten ohne Neuaufbau der Liste.
- Launcher-Umschalter, Dashboard-KPI und die Grid-Zeilenfarben zeichnen sich ueber
  ThemeManager.ThemeChanged neu.
- Das Terminal bleibt in beiden Schemata dunkel (Konsolen sind konventionell dunkel).

Nebenbei die Anzeige-Kultur gepinnt (stand ohnehin auf der Linux-Liste): auf Linux richtet
sie sich sonst nach LANG/LC_ALL, das unter systemd oft nicht gesetzt ist - dann faellt .NET
auf Invariant zurueck und aus '1.234,56 USDC' wird '1,234.56 USDC'. Maschinen-I/O laeuft
davon unabhaengig weiter invariant.

Smoke-UI prueft jetzt zusaetzlich, dass ALLE 18 Farben in BEIDEN Varianten aufloesen -
ein vertippter Ressourcenschluessel wuerde sonst still grau werden.

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (10 Fenster + Editor +
beide Farbschemata), App startet im gespeicherten Schema, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:41:25 +02:00
RichardandClaude Opus 5 669676ac60 Modul-Fenster nach Avalonia - UI-Portierung inhaltlich abgeschlossen
Alle acht Fenster laufen jetzt unter Avalonia: Launcher, Dashboard, Settings, Terminal,
Server Jobs, Copytrading, ResolutionFarming, Supervisor, Accounting (+ Shutdown- und
Allzweck-Dialog).

- Copytrading: vier Tabs (Master-Trader mit Account-Zuweisung, Offene Trades, Geschlossene
  Trades mit Filterleiste, Account-Einstellungen). Zeilenfaerbung nach PnL laeuft ueber
  DataGrid.LoadingRow - greift damit auch bei virtualisierten Zeilen, anders als das
  fruehere Faerben in DataBindingComplete.
- ResolutionFarming: Kandidaten, Positionen, Historie, Settings - je Konto.
- Supervisor: Analyse-Chat gegen den Agenten (Tool-Fortschritt in der Statuszeile),
  Dossiers mit Markdown-Ansicht, Berichte, Counterfactuals.
- Accounting: KPI-Kacheln, Monats-BWA, Ledger, Abruf/Status, PDF- und CSV-Export ueber
  den plattformneutralen Datei-Dialog.

Der SettingsEditor traegt wie erwartet die drei restlichen PropertyGrid-Stellen
(Master-Trader, Account-Einstellungen, ResolutionFarming) mit je einem Aufruf.

ENTSCHEIDUNG - Modul-Fenster liegen in der App, nicht in den Modulen
(begruendet in Views/Modules/README.md): Die App ist der Kompositionswurzel und
referenziert ohnehin alle Module. So bleiben die Modulprojekte FREI VON AVALONIA, was fuer
den kopflosen Linux-Betrieb den Ausschlag gibt - der Daemon soll keine GUI-Bibliothek
mitschleppen. Verifiziert: kein Modul zieht Avalonia. Registrierung in Shell/ModuleViews.cs,
und zwar nur fuer tatsaechlich geladene Module - ein per DisabledModules abgeschaltetes
Modul bekommt gar kein Fenster.

Nebenbei: eigene View-Model-Typen CopyOpenTradeRow/CopyClosedTradeRow statt des
Modul-Modells ClosedTradeRow - sie tragen den aufgeloesten Master-Trader-Namen und die
Zeilenfarbe, die es dort nicht gibt (und vermeiden die Namensmehrdeutigkeit).

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (alle 8 Fenster + Launcher +
Dialog + Editor-Pruefung), App laeuft real mit allen Trading-Diensten, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:19:42 +02:00
RichardandClaude Opus 5 929fa20ee0 Terminal nach Avalonia + konfigurierbare Zeitzone (P2-Kern)
Alle vier Core-Fenster sind damit portiert.

TERMINAL:
- Live-Ausgabe als virtualisiertes ItemsControl ueber eine begrenzte Zeilenliste statt
  RichTextBox. Damit entfaellt das Auto-Clear der WinForms-Fassung, das bei Erreichen der
  Zeichengrenze den GESAMTEN Verlauf verwarf - jetzt werden nur die aeltesten Zeilen
  verdraengt (Ringpuffer, 5000 Zeilen), der juengste Verlauf bleibt immer sichtbar.
  Die Zeilenzahl steht in der Statuszeile.
- Log-Viewer unveraendert im Funktionsumfang: JSONL-Tagesdateien, Filter nach Datum, Level,
  CID und Volltext, Doppelklick uebernimmt die CID (Signal-Kette verfolgen).

ZEITZONE (Befund aus der Linux-Analyse, hier faellig geworden):
Die Terminal-Ansicht rechnete hart gegen die WINDOWS-ID 'W. Europe Standard Time'. Auf Linux
traegt die nur ueber die ICU-Zuordnung und faellt ganz aus, wenn ICU fehlt oder
InvariantGlobalization gesetzt ist - das Fenster haette beim Oeffnen geworfen.

Neu: PolyTraderSharp.Services.AppTimeZone + ServerSettings.ApplicationTimeZoneId
(Default 'Europe/Berlin', IANA-Schreibweise). Aufloesung versucht die ID direkt, dann die
jeweils andere Schreibweise (IANA<->Windows), zuletzt die Systemzeitzone - ein unbekannter
Wert ist damit nie fatal, sondern erzeugt nur eine Warnung. Beide Programm-Einstiege setzen
sie einmalig beim Start; laut Vorgabe wird sie bei der Installation festgelegt und nicht im
laufenden Betrieb gewechselt (Aenderung verschiebt Logdatei-Tagesgrenzen).

8 neue Tests (AppTimeZoneTests) halten fest: IANA- UND Windows-ID liefern denselben
UTC-Versatz (Winter +1, Sommer +2), unbekannte IDs fallen mit Warnung auf die Systemzeitzone
zurueck, leere Angabe = Systemzeitzone, UTC wird korrekt umgerechnet.

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (6 Fenster + Editor-Pruefung,
Zeitzone loest als Europe/Berlin auf), App laeuft real, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:06:24 +02:00
RichardandClaude Opus 5 4ba8149e64 Settings nach Avalonia - PropertyGrid durch kategorisierten Editor ersetzt
Der PropertyGrid-Ersatz ist EIN wiederverwendbares Steuerelement statt Handarbeit je Feld:
Controls/SettingsEditor zeigt ein beliebiges Einstellungsobjekt nach Richards Vorlage -
Kategorie-Ueberschrift, Beschriftung links, Feld rechts, Erklaerung klein darunter.

- SettingsModelBuilder liest die Attribute, die fuers PropertyGrid ohnehin gepflegt waren:
  [Category] gruppiert, [DisplayName] beschriftet, [Description] wird zum Hinweistext,
  [Browsable(false)] blendet aus (Watchdog-Token, Lizenzschluessel bleiben unsichtbar).
  Ergebnis fuer ServerSettings: 6 Abschnitte, 15 Felder - ohne eine Zeile Feld-Code.
- Layout-Regel gewahrt: WELCHE Felder es gibt, kommt als Daten; WIE ein Feld aussieht,
  steht deklarativ als DataTemplate je Feldtyp (Text/Zahl/Ja-Nein/Auswahl/Nur-Lese).
- Damit sind auch die drei restlichen PropertyGrid-Stellen (Account-Einstellungen,
  Master-Trader, ResolutionFarming) mit je einem Aufruf erledigt.

SettingsWindow: Server-Settings + Polymarket-Accounts, Master-Key erzeugen,
OpenRouter-Key/Watchdog-Token setzen, Test-Heartbeat. Rueckmeldungen laufen ueber die
Statuszeile statt ueber Dialoge - nur echte Entscheidungen bekommen einen Dialog
(neu: Views/DialogWindow fuer Hinweis/Rueckfrage/maskierte Eingabe, Avalonia hat keine MessageBox).

Nebenbei einen offenen Punkt der Linux-Analyse erledigt: Schluesseldateien (master.key,
openrouter.key) werden jetzt per File.SetUnixFileMode auf 600 gesetzt. Auf Linux legt
File.WriteAllText sonst mit ueblicher umask 644 an - weltweit lesbar.

Lizenzdialog bewusst NICHT portiert: haengt am alten LicenseLabrador-SDK, das mit der
Deploymentcenter-Anbindung (P3c) ohnehin ersetzt wird - waere Wegwerfarbeit.

Smoke-UI prueft jetzt zusaetzlich die Feldzahl des Editors: ein Fenster kann fehlerfrei
konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen.

Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (5 Fenster + Editor-Pruefung),
Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:55:47 +02:00
RichardandClaude Opus 5 0b8728b25f Dashboard nach Avalonia portiert - LiveCharts2 laeuft
Drittes Fenster: KPI-Kacheln, drei Diagramme, Tradehistorie mit Suche/Filter und
Modul-Aktivierung. Aufbau nach docs/UI-SPEZIFIKATION-WinForms.md.

- Diagramme sind jetzt echte Steuerelemente (LiveCharts2) statt nach Bitmap gerenderter
  ScottPlot-Bilder: interaktiv (Tooltips/Zoom) und ohne System.Drawing. Die Auswertung
  kommt unveraendert aus TradeAnalytics - der Chart-Wechsel beruehrte keine Fachlogik.
  Das war der Zweck der bestehenden Trennung und hat sich hier ausgezahlt.
- ModuleActivationInfo vom UI-Typ in den Core verschoben (PolyTrader.Core.Modularity):
  Aussage ueber die Modularitaet, keine Darstellungsfrage - beide Shells brauchen sie.
- Modul-Tab listet auch NICHT geladene Module, sonst liessen sie sich nie reaktivieren.
  Blockierte Module haben eine deaktivierte Schaltflaeche statt eines Hinweisdialogs;
  der Grund steht ohnehin in der Spalte 'Hinweis'.

WICHTIG - Avalonia 12 -> 11.3.19 zurueckgenommen:
Der erste Wurf zog per Version='*' Avalonia 12.1.1. LiveCharts2 2.0.5 (die aktuellste
Version) ist gegen Avalonia 11 gebaut und bricht dort zur Laufzeit:
MissingFieldException 'Avalonia.Input.Gestures.PinchEvent'. Avalonia 12 ist dem
Chart-Oekosystem voraus. Jetzt 11.3.19 (DataGrid folgt eigener Reihe: 11.3.13).
Erst wieder anheben, wenn LiveCharts2 Avalonia 12 unterstuetzt.

Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (alle 4 Fenster),
App laeuft real mit allen Trading-Diensten, publisht fuer linux-x64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:45:07 +02:00
RichardandClaude Opus 5 cd59e5c0a5 Avalonia-Grundgeruest: plattformneutrale App laeuft (Shell + erstes Fenster)
Neues Projekt src/PolyTrader.App.Avalonia (net10.0, Avalonia 12.1.1, LiveCharts2 2.0.5)
- laeuft unter Windows und Linux aus derselben Quelle.

Enthalten:
- Program.cs mit bewusst getrenntem Aufbau: BuildHost() stellt Persistenz, Dienste und
  Module ohne jeden UI-Bezug zusammen, erst Main haengt Avalonia daran. Damit ist der
  kopflose Linux-Betrieb (--headless, Stufe L2) ohne Umbau erreichbar - der Schalter ist
  bereits drin.
- AvaloniaUiHost als IModuleUiHost: gleiche Semantik wie die WinForms-Shell (ein Fenster
  je View, offene nach vorn holen, alles maximiert).
- Fenster-Menueleiste vollstaendig DEKLARATIV (Controls/WindowMenuBar.axaml + ItemsSource
  auf WindowMenuModel.Entries). Loest die alte Fassung ab, die menu.Items zur Laufzeit
  leerte und neu befuellte - genau der Punkt, den die neue Layout-Regel verbietet.
- ViewIcons fuer Avalonia: dieselben Schluessel und dieselben PNGs wie zuvor, Core und
  Module bleiben unveraendert.
- LauncherWindow, JobsWindow, ShutdownConfirmWindow (inkl. der 10-Sekunden-Sperre).
- --smoke-ui als Nachfolger der WinForms-Konstruktionspruefung; startet den Host bewusst
  NICHT, damit ein reiner UI-Test nicht die Trading-Engine gegen echte Endpunkte anwirft.

Dabei aufgeraeumt:
- JobManager.Jobs: BindingList -> ObservableCollection. BindingList implementiert kein
  INotifyCollectionChanged; neu registrierte Jobs waeren in Avalonia unsichtbar geblieben.
- StartupHydrationService aufgeteilt in CoreStateHydrationService (Core: Accounts +
  Demo-Positionen) und CopyTradingHydrationService (Modul: Settings + Trader). Behebt einen
  latenten Fehler: bei deaktiviertem Copytrading-Modul waeren die Accounts gar nicht mehr
  hydriert worden, obwohl sie zum Core gehoeren.

Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen, die App laeuft real mit
Fenster und allen Trading-Diensten (Market-Sync, Master-Trader-Analyse, RF-Scanner),
und publisht fuer linux-x64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:55:05 +02:00
RichardandClaude Opus 5 a2b1c18ee3 Umstieg auf .NET 10 + transitive Newtonsoft-Luecke geschlossen
Alle sieben Projekte auf net10.0 (App: net10.0-windows). Runtime 10.0.10 ist
installiert, 442 Tests gruen, Linux-Publish aller Nicht-App-Projekte laeuft.

EF Core 8 / Pomelo 8 bleiben bewusst stehen: net10.0 konsumiert net8.0-Bibliotheken
problemlos, und ein Provider-Wechsel hat wegen der Migrations-Implikationen eine
eigene Risikoflaeche - das gehoert in einen separaten, verifizierten Durchgang.

Sicherheitsfund nebenbei: Ab .NET 9 prueft NuGet standardmaessig auch transitive
Pakete. Damit wurde sichtbar, dass Nethereum 6.1.0 Newtonsoft.Json [11.0.2, 14.0.0)
zulaesst und ohne Pinnung auf 11.0.2 aufloest - bekannte Luecke hoher Schwere
(GHSA-5crp-9r3c-p9vr). Die App pinnte laengst 13.0.4, Core und Module nicht.
Jetzt im Core gepinnt, damit jeder Consumer sie bekommt (auch der kuenftige
Linux-Daemon). Danach 0 NU1903-Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:42:24 +02:00
RichardandClaude Opus 5 f8aa90882b Analyse auf Revision 4: P1/P3b/P4 als erledigt vermerkt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:00 +02:00
RichardandClaude Opus 5 53546ceeeb P1c+P4: Module und Tests auf net8.0 - WinForms nur noch in PolyTrader.App
Variante B (Entscheidung Richard): Modul-UI entfernt statt in Zwischenprojekte
ausgelagert. Avalonia ist plattformuebergreifend, die neuen Ansichten kommen spaeter
direkt in die Modul-Projekte zurueck - kein Zwischenschritt, keine Wegwerfarbeit.

- 23 WinForms-Dateien aus den 4 Modulen entfernt (Spezifikation steht in
  docs/UI-SPEZIFIKATION-WinForms.md, Originalcode im Tag winforms-final).
- RegisterUi ist jetzt je Modul ein dokumentierter No-Op: View-ID, Titel, Gruppe,
  Order und der Tab-Aufbau stehen als XML-Doku drin, damit der Avalonia-Nachbau
  die stabilen IDs und die Struktur uebernimmt.
- Alle 4 Modulprojekte + Testprojekt: net8.0 statt net8.0-windows, UseWindowsForms raus.
- P4 vorgezogen (war durch den Testprojekt-Wechsel faellig): PDFsharp-MigraDoc-GDI
  -> PDFsharp-MigraDoc (Core-Build). Der Core-Build findet keine Systemschriften,
  daher neu Logic/PdfFontResolver.cs: durchsucht die Schriftverzeichnisse des OS nach
  Segoe UI/DejaVu/Liberation/Noto/Arial/FreeSans. Keine Schriftdateien im Repo noetig;
  fehlt auf Linux alles, kommt eine klare Meldung mit apt-Hinweis statt eines
  kryptischen Renderer-Fehlers.

Verifiziert: Core, alle 4 Module und das Testprojekt publishen fuer linux-x64, und
zwar ohne ein einziges Windows-spezifisches Paket in den deps.json. 442 Tests gruen
(inkl. PDF-Rendering) auf net8.0. Windows-App laeuft weiter mit den Core-Fenstern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:32:12 +02:00
RichardandClaude Opus 5 0a26b8563d UI-Spezifikation der WinForms-Oberflaeche gesichert (Rekonstruktionsvorlage)
Vor dem Entfernen der WinForms-UI: maschinell aus allen .Designer.cs extrahierte
Spezifikation aller 16 Fenster - Groessen, Tabs, Beschriftungen, Schaltflaechen und
saemtliche Grid-Spalten mit Reihenfolge, Format und Breite. Dazu uebergreifende
Gestaltungsregeln, Farbwerte der PnL-Zeilenfaerbung, Symbol-Schluessel je Fenster
und Verhaltensnotizen aus dem Code-Behind.

Damit laesst sich die Avalonia-UI moeglichst 1:1 nachbauen. Der Originalcode bleibt
zusaetzlich im Tag winforms-final; das Dokument empfiehlt, dort einmalig Screenshots
mit echten Daten zu ziehen (optischer Gesamteindruck, den kein Text ersetzt).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:27:42 +02:00
RichardandClaude Opus 5 5320d25d6d P1b: System.Drawing aus der Fachlogik entfernt
- TradeRowColoring liefert jetzt eine TradeRowTint-Kategorie (Loss/SmallWin/BigWin)
  statt einer System.Drawing.Color. Die Schwellenlogik bleibt getestet, die konkrete
  Farbe legt die UI fest (neu: Ui/TradeRowPalette.cs im CopyTrading-Modul).
  Bessere Schichtung und Voraussetzung dafuer, dass das Modul spaeter net8.0 wird.
- TradeRowColoringTests prueft die Kategorie statt der Farbe (gleiche Abdeckung).
- Models/DashboardRow.cs: verwaistes using System.Drawing entfernt.

System.Drawing liegt damit ausschliesslich noch in UI-Ordnern - die Fachlogik in
Core und Modulen ist frei davon.

442 Tests gruen, --smoke-ui gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:13:14 +02:00
RichardandClaude Opus 5 ad1593bfc1 P1a: PolyTrader.Core auf net8.0 - UI-Contract toolkit-neutral
Der Core traegt keine WinForms-/System.Drawing-Abhaengigkeit mehr und baut als
plattformneutrales net8.0 (verifiziert: publish -r linux-x64 erfolgreich).

- ModuleView.CreateForm (Func<Form>) -> CreateView (Func<object>): die Shell kennt
  ihr Toolkit und castet, der Core nicht. Avalonia kann denselben Contract nutzen.
- ModuleView.Icon (System.Drawing.Image, seit .NET 7 Windows-only) -> IconKey (string).
  Aufloesung Schluessel->Bildressource neu in Ui/ViewIcons.cs, ersetzt Program.AssignMenuIcons.
- WindowMenu.cs (reine WinForms-Logik) aus dem Core nach Ui/ verschoben.
- WindowMenuTests entfernt: testet die eingefrorene WinForms-Menuelogik, die das
  Testprojekt nach dem Core-Schnitt nicht mehr erreicht. Im Tag winforms-final erhalten;
  das Avalonia-Gegenstueck bekommt eigene Tests (P6/P9).

Module und App bleiben vorerst net8.0-windows - ihre UI zieht erst mit der
Avalonia-Portierung um. Windows-App unveraendert lauffaehig (--smoke-ui gruen).

442 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:10:41 +02:00
102 changed files with 6284 additions and 5418 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
using System.ComponentModel; using System.ComponentModel;
using System.Drawing;
namespace PolyTraderSharp.Models namespace PolyTraderSharp.Models
{ {
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows7.0</TargetFramework> <TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>favicon.ico</ApplicationIcon> <ApplicationIcon>favicon.ico</ApplicationIcon>
+15
View File
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.Supervis
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.Accounting", "src\PolyTrader.Modules.Accounting\PolyTrader.Modules.Accounting.csproj", "{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.Accounting", "src\PolyTrader.Modules.Accounting\PolyTrader.Modules.Accounting.csproj", "{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.App.Avalonia", "src\PolyTrader.App.Avalonia\PolyTrader.App.Avalonia.csproj", "{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,18 @@ Global
{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x64.Build.0 = Release|Any CPU {A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x64.Build.0 = Release|Any CPU
{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x86.ActiveCfg = Release|Any CPU {A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x86.ActiveCfg = Release|Any CPU
{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x86.Build.0 = Release|Any CPU {A74EB53F-08E7-43A5-A664-A49E9AB50AEE}.Release|x86.Build.0 = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|x64.ActiveCfg = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|x64.Build.0 = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|x86.ActiveCfg = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Debug|x86.Build.0 = Debug|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|Any CPU.Build.0 = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|x64.ActiveCfg = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|x64.Build.0 = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|x86.ActiveCfg = Release|Any CPU
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -126,6 +140,7 @@ Global
{98C70A7B-DC3D-48E3-BE5D-03F867E9BFA8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {98C70A7B-DC3D-48E3-BE5D-03F867E9BFA8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{63F8B9B4-6F56-4A6B-BEBE-53DE49F913D1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {63F8B9B4-6F56-4A6B-BEBE-53DE49F913D1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A74EB53F-08E7-43A5-A664-A49E9AB50AEE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {A74EB53F-08E7-43A5-A664-A49E9AB50AEE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {60AA6BCF-B17E-4D52-A290-14154A3E97CF} SolutionGuid = {60AA6BCF-B17E-4D52-A290-14154A3E97CF}
+21 -27
View File
@@ -77,6 +77,11 @@ internal static class Program
// Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht. // Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht.
ConfigureSecretProtection(bootLog); ConfigureSecretProtection(bootLog);
// Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
// fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => bootLog.Warning(m));
bootLog.Info($"Anzeige-Zeitzone: {AppTimeZone.CurrentId}");
// Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules). // Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
var allModules = new System.Collections.Generic.List<IPolyTraderModule> var allModules = new System.Collections.Generic.List<IPolyTraderModule>
{ {
@@ -159,7 +164,7 @@ internal static class Program
// MUSS als erster HostedService laufen: hydriert den State, bevor die // MUSS als erster HostedService laufen: hydriert den State, bevor die
// Trading-Services gegen einen leeren State anlaufen. // Trading-Services gegen einen leeren State anlaufen.
services.AddHostedService<StartupHydrationService>(); services.AddHostedService<CoreStateHydrationService>();
// Module registrieren ihre eigenen Services/Channels/State selbst. // Module registrieren ihre eigenen Services/Channels/State selbst.
foreach (var module in modules) foreach (var module in modules)
@@ -215,7 +220,7 @@ internal static class Program
Title = "Terminal", Title = "Terminal",
Group = "Core", Group = "Core",
Order = 10, Order = 10,
CreateForm = () => CreateView = () =>
{ {
var view = new PolyTraderSharp.Ui.Views.TerminalView(); var view = new PolyTraderSharp.Ui.Views.TerminalView();
view.Initialize(viewServices.GetRequiredService<TerminalLogger>()); view.Initialize(viewServices.GetRequiredService<TerminalLogger>());
@@ -228,7 +233,7 @@ internal static class Program
Title = "Server Settings", Title = "Server Settings",
Group = "Core", Group = "Core",
Order = 20, Order = 20,
CreateForm = () => CreateView = () =>
{ {
var view = new PolyTraderSharp.Ui.Views.SettingsView(); var view = new PolyTraderSharp.Ui.Views.SettingsView();
view.Initialize( view.Initialize(
@@ -246,7 +251,7 @@ internal static class Program
Title = "Server Jobs", Title = "Server Jobs",
Group = "Core", Group = "Core",
Order = 30, Order = 30,
CreateForm = () => CreateView = () =>
{ {
var view = new PolyTraderSharp.Ui.Views.JobsView(); var view = new PolyTraderSharp.Ui.Views.JobsView();
view.Initialize(viewServices.GetRequiredService<JobManager>()); view.Initialize(viewServices.GetRequiredService<JobManager>());
@@ -259,13 +264,13 @@ internal static class Program
Title = "Dashboard", Title = "Dashboard",
Group = "Core", Group = "Core",
Order = 5, Order = 5,
CreateForm = () => CreateView = () =>
{ {
var view = new PolyTraderSharp.Ui.Views.DashboardView(); var view = new PolyTraderSharp.Ui.Views.DashboardView();
var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>(); var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
// Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen. // Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
var moduleInfos = allModules var moduleInfos = allModules
.Select(m => new PolyTraderSharp.Ui.Views.ModuleActivationInfo( .Select(m => new PolyTrader.Core.Modularity.ModuleActivationInfo(
m.Name, modules.Contains(m), m.GetActivationBlocker(config))) m.Name, modules.Contains(m), m.GetActivationBlocker(config)))
.ToList(); .ToList();
view.Initialize( view.Initialize(
@@ -292,26 +297,12 @@ internal static class Program
} }
/// <summary> /// <summary>
/// Weist den registrierten Views (Core + Module) ihr Menü-Icon aus den App-Ressourcen zu /// Weist den registrierten Views (Core + Module) ihren Standard-Symbolschlüssel zu.
/// gemappt über die stabile View-ID. Bereits gesetzte Icons bleiben erhalten. /// Die Auflösung Schlüssel → Bildressource liegt in <see cref="PolyTraderSharp.Ui.ViewIcons"/>;
/// Core und Module kennen nur noch den Schlüssel (toolkit-neutral, siehe ModuleView.IconKey).
/// </summary> /// </summary>
private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost) private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
{ => PolyTraderSharp.Ui.ViewIcons.AssignDefaults(uiHost);
var map = new System.Collections.Generic.Dictionary<string, System.Drawing.Image>
{
["core.dashboard"] = Properties.Resources.dashboard,
["core.settings"] = Properties.Resources.setting_tools,
["core.terminal"] = Properties.Resources.error_log,
["core.jobs"] = Properties.Resources.system_time,
["accounting.main"] = Properties.Resources.coins_in_hand,
["copytrading.main"] = Properties.Resources.cross_reference,
["resolutionfarming.main"] = Properties.Resources.file_start_workflow,
["supervisor.main"] = Properties.Resources.emotion_batman,
};
foreach (var view in uiHost.Views)
if (view.Icon == null && map.TryGetValue(view.Id, out var img))
view.Icon = img;
}
private static void RunConfigMigrationFromJson(string folder) private static void RunConfigMigrationFromJson(string folder)
{ {
@@ -474,7 +465,8 @@ internal static class Program
services.AddCorePersistence(databaseOptions); services.AddCorePersistence(databaseOptions);
services.AddSingleton<TerminalLogger>(); services.AddSingleton<TerminalLogger>();
services.AddSingleton<TradingState>(); services.AddSingleton<TradingState>();
services.AddSingleton<StartupHydrationService>(); services.AddSingleton<CoreStateHydrationService>();
services.AddSingleton<CopyTradingHydrationService>();
foreach (var module in modules) foreach (var module in modules)
{ {
@@ -492,7 +484,9 @@ internal static class Program
// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten. // State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
try try
{ {
host.Services.GetRequiredService<StartupHydrationService>() host.Services.GetRequiredService<CoreStateHydrationService>()
.StartAsync(default).GetAwaiter().GetResult();
host.Services.GetRequiredService<CopyTradingHydrationService>()
.StartAsync(default).GetAwaiter().GetResult(); .StartAsync(default).GetAwaiter().GetResult();
} }
catch (Exception ex) catch (Exception ex)
@@ -510,7 +504,7 @@ internal static class Program
{ {
try try
{ {
using var form = view.CreateForm(); using var form = (System.Windows.Forms.Form)view.CreateView();
Console.WriteLine($"[OK] {view.Id} ({view.Title})"); Console.WriteLine($"[OK] {view.Id} ({view.Title})");
} }
catch (Exception ex) catch (Exception ex)
+2 -2
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -62,7 +62,7 @@ namespace PolyTraderSharp.Ui
// Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das // Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
// aktuelle Fenster → currentViewId null). // aktuelle Fenster → currentViewId null).
PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null); WindowMenu.Wire(menuStrip, _uiHost, null);
btn_liveTrading.Click += (_, _) => CycleLiveTrading(); btn_liveTrading.Click += (_, _) => CycleLiveTrading();
btn_demoTrading.Click += (_, _) => CycleDemoTrading(); btn_demoTrading.Click += (_, _) => CycleDemoTrading();
+6 -3
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
@@ -43,7 +43,7 @@ namespace PolyTraderSharp.Ui
private void AttachWindowMenu(Form form, string currentViewId) private void AttachWindowMenu(Form form, string currentViewId)
{ {
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) }; var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) };
PolyTrader.Core.Modularity.WindowMenu.Wire(menu, this, currentViewId); WindowMenu.Wire(menu, this, currentViewId);
form.Controls.Add(menu); form.Controls.Add(menu);
form.MainMenuStrip = menu; form.MainMenuStrip = menu;
} }
@@ -103,7 +103,10 @@ namespace PolyTraderSharp.Ui
return; return;
} }
var form = view.CreateForm(); // CreateView liefert bewusst object (toolkit-neutraler Core-Contract). Die WinForms-Shell
// kennt ihr Toolkit und castet hier; ein falscher Typ ist ein Programmierfehler und soll
// laut knallen, nicht still ein leeres Fenster erzeugen.
var form = (Form)view.CreateView();
if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name) if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name)
form.Text = view.Title; form.Text = view.Title;
+61
View File
@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Drawing;
using PolyTrader.Core.Modularity;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Löst die toolkit-neutralen <see cref="ModuleView.IconKey"/>-Schlüssel gegen die WinForms-
/// Bildressourcen der App auf. Diese Zuordnung ist die einzige Stelle, an der Views und
/// Bilddaten zusammenkommen Core und Module kennen nur noch den Schlüssel als Zeichenkette.
///
/// Die Avalonia-Shell bekommt später ein eigenes Gegenstück mit denselben Schlüsseln; die
/// Registrierungen in Core und Modulen bleiben dabei unverändert.
/// </summary>
internal static class ViewIcons
{
/// <summary>Symbol-Schlüssel → Bildressource. Unbekannte Schlüssel liefern <c>null</c> (Text-only-Menüeintrag).</summary>
private static readonly Dictionary<string, Image> Map = new()
{
["dashboard"] = Properties.Resources.dashboard,
["settings"] = Properties.Resources.setting_tools,
["terminal"] = Properties.Resources.error_log,
["jobs"] = Properties.Resources.system_time,
["accounting"] = Properties.Resources.coins_in_hand,
["copytrading"] = Properties.Resources.cross_reference,
["resolutionfarming"] = Properties.Resources.file_start_workflow,
["supervisor"] = Properties.Resources.emotion_batman,
};
/// <summary>
/// Standard-Symbolschlüssel je View-ID. Greift für Views, die selbst keinen
/// <see cref="ModuleView.IconKey"/> setzen u.a. die Modul-Fenster, die die App-Ressourcen
/// nicht kennen. Ersetzt die frühere Icon-Zuweisung in <c>Program.AssignMenuIcons</c>.
/// </summary>
private static readonly Dictionary<string, string> DefaultKeyByViewId = new()
{
["core.dashboard"] = "dashboard",
["core.settings"] = "settings",
["core.terminal"] = "terminal",
["core.jobs"] = "jobs",
["accounting.main"] = "accounting",
["copytrading.main"] = "copytrading",
["resolutionfarming.main"] = "resolutionfarming",
["supervisor.main"] = "supervisor",
};
/// <summary>Bildressource zum Schlüssel, oder <c>null</c> (kein Symbol / unbekannter Schlüssel).</summary>
public static Image? Resolve(string? iconKey) =>
iconKey != null && Map.TryGetValue(iconKey, out var img) ? img : null;
/// <summary>
/// Setzt bei allen registrierten Views den Standard-Symbolschlüssel, sofern noch keiner gesetzt ist.
/// </summary>
public static void AssignDefaults(IModuleUiHost uiHost)
{
foreach (var view in uiHost.Views)
if (view.IconKey == null && DefaultKeyByViewId.TryGetValue(view.Id, out var key))
view.IconKey = key;
}
}
}
+2 -7
View File
@@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using PolyTrader.Core.Analytics; using PolyTrader.Core.Analytics;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence; using PolyTrader.Core.Persistence;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
@@ -388,12 +389,6 @@ namespace PolyTraderSharp.Ui.Views
} }
} }
/// <summary>
/// Aktivierungs-Info eines Moduls fürs Dashboard: Name, ob es in dieser Session läuft und ein
/// optionaler Grund, warum es nicht aktivierbar ist (z.B. fehlender API-Key).
/// </summary>
public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker);
/// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary> /// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary>
public class DashboardTradeRow public class DashboardTradeRow
{ {
+2 -2
View File
@@ -1,4 +1,4 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
using PolyTraderSharp.Services; using PolyTraderSharp.Services;
@@ -23,7 +23,7 @@ namespace PolyTraderSharp.Ui.Views
public void Initialize(JobManager jobManager) public void Initialize(JobManager jobManager)
{ {
dgvJobs.DataSource = jobManager.Jobs; dgvJobs.DataSource = new System.ComponentModel.BindingList<JobStatusRow>(jobManager.Jobs);
} }
private void DgvJobs_CellContentClick(object? sender, DataGridViewCellEventArgs e) private void DgvJobs_CellContentClick(object? sender, DataGridViewCellEventArgs e)
@@ -1,11 +1,16 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using PolyTrader.Core.Modularity;
namespace PolyTrader.Core.Modularity namespace PolyTraderSharp.Ui
{ {
/// <summary> /// <summary>
/// WinForms-Umsetzung des gemeinsamen Fenster-Menüs. Liegt bewusst in der App und nicht mehr
/// im Core: der Core ist seit der Linux-Portierung toolkit-neutral (net8.0) und darf keine
/// WinForms-Typen mehr kennen. Die Avalonia-Shell bekommt später ihr eigenes Gegenstück.
///
/// Baut das gemeinsame Fenster-Menü, das auf JEDEM PolyTrader-Fenster erscheint und das Wechseln /// Baut das gemeinsame Fenster-Menü, das auf JEDEM PolyTrader-Fenster erscheint und das Wechseln
/// zwischen allen Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract /// zwischen allen Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract
/// <see cref="IModuleUiHost"/> nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen). /// <see cref="IModuleUiHost"/> nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
@@ -57,11 +62,12 @@ namespace PolyTrader.Core.Modularity
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title)) foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
{ {
bool isCurrent = view.Id == currentViewId; bool isCurrent = view.Id == currentViewId;
var image = ViewIcons.Resolve(view.IconKey);
var item = new ToolStripMenuItem(view.Title) var item = new ToolStripMenuItem(view.Title)
{ {
Image = view.Icon, Image = image,
ImageScaling = ToolStripItemImageScaling.SizeToFit, ImageScaling = ToolStripItemImageScaling.SizeToFit,
DisplayStyle = view.Icon != null DisplayStyle = image != null
? ToolStripItemDisplayStyle.ImageAndText ? ToolStripItemDisplayStyle.ImageAndText
: ToolStripItemDisplayStyle.Text, : ToolStripItemDisplayStyle.Text,
Checked = isCurrent || host.IsOpen(view.Id) Checked = isCurrent || host.IsOpen(view.Id)
+5 -4
View File
@@ -1,6 +1,6 @@
# Analyse & Umsetzungsrahmen: Linux-Fähigkeit von PolyTrader # Analyse & Umsetzungsrahmen: Linux-Fähigkeit von PolyTrader
**Stand:** 06.08.2026 · **Revision 3** (Entscheidungen Richard eingearbeitet, P3b umgesetzt) **Stand:** 06.08.2026 · **Revision 4** (P1 + P3b + P4 umgesetzt nur noch PolyTrader.App ist Windows-gebunden)
**Basis:** 7 Projekte, ~24.250 LOC Produktivcode (ohne Tests/EF-Migrationen), 448 Tests **Basis:** 7 Projekte, ~24.250 LOC Produktivcode (ohne Tests/EF-Migrationen), 448 Tests
--- ---
@@ -11,7 +11,7 @@
|---|---|---|---| |---|---|---|---|
| 1 | Kultur-Bug | **Sofort gefixt** ✅ | Erledigt, siehe [Abschnitt 4.1](#41-erledigt--der-aktive-bug-ist-behoben) | | 1 | Kultur-Bug | **Sofort gefixt** ✅ | Erledigt, siehe [Abschnitt 4.1](#41-erledigt--der-aktive-bug-ist-behoben) |
| 2 | Designer-Regel | **Neue Formulierung übernommen:** Layout deklarativ in `.axaml`, nicht zur Laufzeit im Code | ersetzt die WinForms-Designer-Regel | | 2 | Designer-Regel | **Neue Formulierung übernommen:** Layout deklarativ in `.axaml`, nicht zur Laufzeit im Code | ersetzt die WinForms-Designer-Regel |
| 3 | Windows-UI | **Variante 2**: auf heutigem Stand eingefroren; soll später **komplett aus dem Build verschwinden** | Strategie siehe [5.5](#55-windows-ui-als-fallback-variante-2) | | 3 | Windows-UI | **Variante B**: Modul-UI **entfernt** ✅, Core-UI folgt; Fallback ist der Tag `winforms-final` | umgesetzt 06.08.2026 |
| 4 | `PropertyGrid` | wird durch **normale Steuerelemente** ersetzt | +5 PT ggü. Fremdbibliothek, dafür bessere UX | | 4 | `PropertyGrid` | wird durch **normale Steuerelemente** ersetzt | +5 PT ggü. Fremdbibliothek, dafür bessere UX |
| 5 | Lizenz/Watchdog | laufen jetzt über das **Deploymentcenter**, nicht mehr über Einzeldienste | **Linux-Blocker entfällt**, siehe [5.2](#52-lizenz--watchdog--jetzt-über-das-deploymentcenter) | | 5 | Lizenz/Watchdog | laufen jetzt über das **Deploymentcenter**, nicht mehr über Einzeldienste | **Linux-Blocker entfällt**, siehe [5.2](#52-lizenz--watchdog--jetzt-über-das-deploymentcenter) |
| 6 | Zeitzone | **einmalig bei der Installation** festgelegt, nicht im laufenden Betrieb gewechselt | vereinfacht die Umsetzung, siehe [3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6) | | 6 | Zeitzone | **einmalig bei der Installation** festgelegt, nicht im laufenden Betrieb gewechselt | vereinfacht die Umsetzung, siehe [3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6) |
@@ -30,7 +30,7 @@
| Was ist der Löwenanteil? | Die UI. ~8.300 LOC (34 % des Produktivcodes) hängen an WinForms. | | Was ist der Löwenanteil? | Die UI. ~8.300 LOC (34 % des Produktivcodes) hängen an WinForms. |
| Was ist überraschend gut? | Die **gesamte Trading-Kernlogik ist bereits portabel**: kein einziger `DllImport`, keine Registry, keine WMI, keine DPAPI, keine `SpecialFolder`, saubere `Path.Combine`-Nutzung. Nethereum, Pomelo/EF, `HttpClient`, `ClientWebSocket`, AES-GCM laufen unverändert. | | Was ist überraschend gut? | Die **gesamte Trading-Kernlogik ist bereits portabel**: kein einziger `DllImport`, keine Registry, keine WMI, keine DPAPI, keine `SpecialFolder`, saubere `Path.Combine`-Nutzung. Nethereum, Pomelo/EF, `HttpClient`, `ClientWebSocket`, AES-GCM laufen unverändert. |
| Was ist der teuerste Einzelpunkt? | Der **`PropertyGrid`-Ersatz** durch handgebaute Steuerelemente (6 Instanzen in 4 Fenstern). | | Was ist der teuerste Einzelpunkt? | Der **`PropertyGrid`-Ersatz** durch handgebaute Steuerelemente (6 Instanzen in 4 Fenstern). |
| Gesamtaufwand | **4161 Personentage** (davon 2 PT bereits erledigt) | | Gesamtaufwand | **~3351 Personentage offen** (P1, P3b, P4 und der Kultur-Fix sind erledigt) |
| Wichtigste Erkenntnis | **Für ~15 PT (≈ 25 %) bekommst du 80 % des Nutzens**: Der Trading-Kern läuft headless auf Linux, die WinForms-UI bleibt als eingefrorener Fallback. Siehe [Abschnitt 7](#7-fahrplan). | | Wichtigste Erkenntnis | **Für ~15 PT (≈ 25 %) bekommst du 80 % des Nutzens**: Der Trading-Kern läuft headless auf Linux, die WinForms-UI bleibt als eingefrorener Fallback. Siehe [Abschnitt 7](#7-fahrplan). |
--- ---
@@ -448,7 +448,8 @@ Heute laufen Generic Host und `Application.Run` im **selben Prozess**; darunter
|---|---|---:|---| |---|---|---:|---|
| ~~**L0**~~ | ~~Kultur-Bug~~ | — | ✅ **erledigt** Faktor-100-Preisfehler behoben, 10 Regressionstests | | ~~**L0**~~ | ~~Kultur-Bug~~ | — | ✅ **erledigt** Faktor-100-Preisfehler behoben, 10 Regressionstests |
| ~~**L0b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | — | ✅ **erledigt** 36 Dateien raus, 448 Tests grün, 8 Warnungen weniger | | ~~**L0b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | — | ✅ **erledigt** 36 Dateien raus, 448 Tests grün, 8 Warnungen weniger |
| **L1** | Tag `winforms-final` setzen → dann P0 + P1 + P2 + P3 + P3c + P4 | 1115 PT | Core/Module sind `net8.0` und plattformneutral; Zeitzone bei Installation festlegbar; Deploymentcenter angebunden; Tests laufen auf Linux | | ~~**L1a**~~ | ~~Tag `winforms-final` + P1 (Core/Module entkoppeln) + P4 (PDF)~~ | — | ✅ **erledigt** Core, 4 Module und Testprojekt sind `net8.0`; alle sechs publishen für linux-x64 ohne Windows-Pakete; 442 Tests grün |
| **L1b** | P0 (Avalonia-Spike) + P2 (Zeitzone/Kultur) + P3 (Mullvad, Dateirechte) + P3c (Deploymentcenter) | 69 PT | Zeitzone bei Installation festlegbar; Deploymentcenter angebunden; Linux-Kleinkram erledigt |
| **L2** | Headless-Einstiegspunkt + systemd + Feldtest im Zielland | 35 PT | **Trading läuft headless auf Linux.** ⭐ Meilenstein | | **L2** | Headless-Einstiegspunkt + systemd + Feldtest im Zielland | 35 PT | **Trading läuft headless auf Linux.** ⭐ Meilenstein |
| **L3** | P5 + P6 + P7 (Charts, Shell, Core-Views) | 1824 PT | Avalonia-Grundgerüst steht; Dashboard, Settings, Terminal nutzbar | | **L3** | P5 + P6 + P7 (Charts, Shell, Core-Views) | 1824 PT | Avalonia-Grundgerüst steht; Dashboard, Settings, Terminal nutzbar |
| **L4** | P8 + P9 (Modul-Fenster, Tests, CI) | 1724 PT | WinForms funktional abgelöst | | **L4** | P8 + P9 (Modul-Fenster, Tests, CI) | 1724 PT | WinForms funktional abgelöst |
+461
View File
@@ -0,0 +1,461 @@
# UI-Spezifikation der WinForms-Oberfläche (Rekonstruktionsvorlage für Avalonia)
**Stand:** 06.08.2026 · **Zweck:** Die WinForms-UI wird im Zuge der Linux-Portierung entfernt.
Dieses Dokument hält fest, **wie sie aussah und was sie konnte**, damit die Avalonia-Umsetzung
so nah wie möglich am Original bleibt.
> **Der Originalcode ist nicht verloren.** Der Git-Tag **`winforms-final`** enthält den vollständigen
> letzten Stand. Auschecken und ansehen:
>
> ```bash
> git checkout winforms-final
> ```
>
> Dieses Dokument ist die *lesbare Zusammenfassung* — für Details (Pixelabstände, Andockung,
> Farbwerte) immer in den Tag schauen. Die Angaben unten sind maschinell aus den
> `.Designer.cs`-Dateien extrahiert, also exakt und nicht aus dem Gedächtnis rekonstruiert.
## Screenshots — bitte einmal selbst anlegen
Was dieses Dokument **nicht** leisten kann, ist der optische Gesamteindruck: Proportionen,
Abstände, Farbwirkung, wie voll die Fenster im Alltag aussehen. Dafür lohnt sich einmalig:
```bash
git checkout winforms-final
```
Dann die App starten, jedes Fenster **mit echten Daten** öffnen und einen Screenshot ablegen
(Vorschlag: `docs/ui-referenz/<Fenstername>.png`). Zehn Minuten Arbeit, die beim Nachbau mehr
wert sind als jede Beschreibung. Danach mit `git checkout main` zurück.
---
## Übergreifende Gestaltungsregeln
Diese Muster ziehen sich durch alle Fenster und sollten in Avalonia erhalten bleiben:
| Regel | Umsetzung in WinForms | Entsprechung in Avalonia |
|---|---|---|
| **Alle Fenster maximiert öffnen** | `WindowState = Maximized`, `StartPosition = CenterScreen` (`ShellUiHost.OpenView`) | `WindowState="Maximized"` |
| **Gemeinsame Fenster-Menüleiste** auf JEDEM Fenster | `WindowMenu.Wire()` hängt oben einen `MenuStrip` an; alle Fenster nebeneinander mit Icon, aktuelles fett + angehakt | `Menu` mit `ItemsSource`-Bindung (deklarativ!) |
| **Rechte Menüaktion kontextabhängig** | Launcher → „Beenden" (mit Sicherheitsabfrage); alle anderen Fenster → „Fenster schließen" | `DockPanel` — Avalonia kennt kein `ToolStripItemAlignment.Right` |
| **Ein Fenster je View** | `ShellUiHost` hält höchstens eine Instanz, holt sie sonst nach vorn | gleiche Logik; der Contract ist bereits toolkit-neutral |
| **Zahlenformate in Tabellen** | `DefaultCellStyle.Format` (`N2`, `N1`, …) | `Binding StringFormat` |
| **Schriftart** | „Segoe UI" (21×), Terminal „Consolas" (1×) | Systemschrift + Monospace-Fallback — Segoe UI gibt es auf Linux nicht |
| **Zeilenfärbung nach PnL** | `TradeRowColoring` lieferte die Farbe direkt in `DefaultCellStyle.BackColor` | bereits umgestellt: Logik liefert `TradeRowTint`, die UI mappt auf Farbe |
**Farbwerte der Zeilenfärbung** (aus `TradeRowPalette` — unverändert übernehmen):
| Kategorie | RGB | Bedeutung |
|---|---|---|
| `Loss` | 245, 200, 200 | < 0 % — rot |
| `SmallWin` | 212, 240, 212 | 010 % — hellgrün |
| `BigWin` | 140, 214, 140 | > 10 % — grün |
**Symbol-Schlüssel je Fenster** (siehe `Ui/ViewIcons.cs` — die Schlüssel bleiben gültig):
| View-ID | Schlüssel | Bildressource |
|---|---|---|
| `core.dashboard` | `dashboard` | dashboard.png |
| `core.settings` | `settings` | setting_tools.png |
| `core.terminal` | `terminal` | error_log.png |
| `core.jobs` | `jobs` | system_time.png |
| `copytrading.main` | `copytrading` | cross_reference.png |
| `resolutionfarming.main` | `resolutionfarming` | file_start_workflow.png |
| `supervisor.main` | `supervisor` | emotion_batman.png |
| `accounting.main` | `accounting` | coins_in_hand.png |
Die PNG-Dateien liegen weiterhin in `Resources/` und können unverändert weiterverwendet werden.
---
## A. Modul-Fenster (in diesem Schritt entfernt)
### CopyTradingMainForm — Hauptfenster Copytrading
**Fenstergröße:** 1714 × 1167 px
**Tabs (Reihenfolge):** Master-Trader | Offene Trades | Geschlossene Trades | Account-Einstellungen
**Steuerelemente:** TabPage ×4, TabControl ×1, MasterTradersView ×1, OpenTradesView ×1, ClosedTradesView ×1, AccountSettingsView ×1
### MasterTradersView — Tab „Master-Trader“
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | # | `colId` | — | 50 px |
| 2 | Wallet | `colWallet` | — | 170 px |
| 3 | Name | `colName` | — | Rest/Auto |
| 4 | Kategorie | `colCategory` | — | 110 px |
| 5 | Aktiv | `colActive` | — | 50 px |
| 6 | Trades (7T) | `colTrades` | — | 90 px |
| 7 | Winrate % | `colWinrate` | — | 90 px |
| 8 | PnL (7T) | `colPnl` | — | 100 px |
| 9 | Copy-PnL (30T) | `colCopyPnl` | — | 110 px |
| 10 | Profit-Faktor | `colCopyPf` | — | 100 px |
| 11 | Ø PnL/Trade | `colCopyAvg` | — | 100 px |
| 12 | Copy-Trades | `colCopyCount` | — | 90 px |
**Steuerelemente:** DataGridViewTextBoxColumn ×11, ToolStripButton ×4, Panel ×2, ToolStrip ×1, ToolStripSeparator ×1, DataGridView ×1, DataGridViewCheckBoxColumn ×1, Splitter ×1, PropertyGrid ×1, GroupBox ×1, CheckedListBox ×1, Label ×1
### OpenTradesView — Tab „Offene Trades“
**Beschriftungen:**
- `lblSummary` = „—"
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | Account | `colAccount` | — | Fill 120 |
| 2 | Master-Trader | `colTrader` | — | Fill 120 |
| 3 | Markt | `colMarket` | — | Fill 240 |
| 4 | Outcome | `colOutcome` | — | Fill 80 |
| 5 | Side | `colSide` | — | Fill 55 |
| 6 | Entry | `colEntry` | — | Fill 65 |
| 7 | Aktuell | `colCurrent` | — | Fill 65 |
| 8 | Size | `colSize` | — | Fill 70 |
| 9 | Einsatz | `colAmount` | — | Fill 80 |
| 10 | Wert | `colValue` | — | Fill 80 |
| 11 | Buchgewinn | `colUnrealized` | — | Fill 90 |
| 12 | Buchgew. % | `colUnrealizedPct` | — | Fill 80 |
| 13 | Status | `colStatus` | — | Fill 90 |
**Steuerelemente:** DataGridViewTextBoxColumn ×13, ToolStrip ×1, ToolStripButton ×1, DataGridView ×1, Panel ×1, Label ×1
### ClosedTradesView — Tab „Geschlossene Trades“
**Beschriftungen:**
- `lblSummary` = „—"
- `lblMarkt` = „Markt:"
- `lblMaster` = „Master:"
- `lblErgebnis` = „Ergebnis:"
- `lblVon` = „Von:"
- `lblBis` = „Bis:"
**Schaltflächen:** „Zurücksetzen"
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | # | `colTradeId` | — | 60 px |
| 2 | Account | `colAccount` | — | 130 px |
| 3 | Master-Trader | `colTrader` | — | 130 px |
| 4 | Markt | `colMarket` | — | Rest/Auto |
| 5 | Outcome | `colOutcome` | — | 80 px |
| 6 | Side | `colSide` | — | 60 px |
| 7 | Entry | `colEntry` | — | 70 px |
| 8 | Exit | `colExit` | — | 70 px |
| 9 | Size | `colSize` | — | 70 px |
| 10 | PnL | `colPnl` | — | 80 px |
| 11 | PnL % | `colPnlPct` | — | 70 px |
| 12 | Eröffnet | `colOpenedAt` | — | 130 px |
| 13 | Geschlossen | `colClosedAt` | — | 130 px |
**Steuerelemente:** DataGridViewTextBoxColumn ×13, Label ×6, Panel ×2, ComboBox ×2, DateTimePicker ×2, ToolStrip ×1, ToolStripButton ×1, DataGridView ×1, TextBox ×1, Button ×1
### AccountSettingsView — Tab „Account-Einstellungen“
**Steuerelemente:** ToolStrip ×1, ToolStripLabel ×1, ToolStripComboBox ×1, ToolStripSeparator ×1, ToolStripButton ×1, PropertyGrid ×1, Panel ×1, Label ×1
### ResolutionFarmingMainForm
**Fenstergröße:** 1429 × 1030 px
**Tabs (Reihenfolge):** Kandidaten | Positionen | Historie / Statistik | Settings
**Beschriftungen:**
- `lblCandKonto` = „Konto:"
- `lblSetKonto` = „Konto:"
**Schaltflächen:** „Aktualisieren", „Speichern"
**Steuerelemente:** TabPage ×4, Panel ×4, Label ×4, Button ×4, DataGridView ×3, ComboBox ×2, TabControl ×1, PropertyGrid ×1
### SupervisorMainForm
**Fenstergröße:** 1786 × 1163 px
**Tabs (Reihenfolge):** Analyse | Dossiers | Berichte | Counterfactual
**Beschriftungen:**
- `lblProfil` = „Profil:"
- `lblModel` = „Modell:"
- `lblSignal` = „SignalId:"
**Schaltflächen:** „Aktualisieren", „Dossier öffnen", „Senden", „Verlauf leeren"
**Steuerelemente:** ToolStripButton ×5, TabPage ×4, ToolStrip ×4, ToolStripLabel ×3, TextBox ×3, DataGridView ×3, ToolStripTextBox ×2, ToolStripSeparator ×2, SplitContainer ×2, TabControl ×1, ToolStripComboBox ×1, RichTextBox ×1, Panel ×1, Button ×1, Label ×1
### AccountingMainForm
**Fenstergröße:** 1571 × 1067 px
**Tabs (Reihenfolge):** Übersicht / BWA | Ledger | Abruf / Status
**Beschriftungen:**
- `lblKpiNet` = „Netto-Handelsergebnis ↵ —"
- `lblKpiClosing` = „Endsaldo ↵ —"
- `lblKpiDeposits` = „Einzahlungen ↵ —"
- `lblKpiWithdrawals` = „Auszahlungen ↵ —"
- `lblKpiFees` = „Fees ↵ —"
- `lblKpiRewards` = „Rewards ↵ —"
- `lblKpiVolume` = „Handelsvolumen ↵ —"
- `lblKpiTrades` = „Trades ↵ —"
- `lblWaehrung` = „Währung:"
- `lblBis` = „Bis:"
- `lblVon` = „Von:"
- `lblOvKonto` = „Konto:"
- `lblLedgerKonto` = „Konto:"
- `lblStatusKonto` = „Konto:"
**Schaltflächen:** „Aktualisieren", „Backfill (volle Historie)", „Berechnen", „CSV-Export", „Inkrement jetzt", „PDF-Export"
**Steuerelemente:** Label ×13, ToolStripButton ×4, TabPage ×3, Button ×3, DataGridView ×3, ToolStripSeparator ×3, ComboBox ×2, DateTimePicker ×2, ToolStrip ×2, ToolStripLabel ×2, ToolStripComboBox ×2, TabControl ×1, Panel ×1, FlowLayoutPanel ×1
---
## B. Core-/Shell-Fenster (bleiben vorerst bestehen)
### LauncherForm — Startleiste
**Fenstergröße:** 2599 × 669 px
**Beschriftungen:**
- `lbl_trading` = „Trading: —"
- `lbl_modules` = „Module: —"
- `lbl_ratelimit` = „API: —"
**Schaltflächen:** „API: —", „Accounting", „CopyTrading", „Dashboard", „DemoTrading (—)", „LiveTrading (—)", „ResolutionFarming", „Server Jobs", „Settings", „Supervisor", „Terminal / Logs"
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | Account | `colAccName` | — | 113 px |
| 2 | Module | `colAccModules` | — | 109 px |
| 3 | Polymarket | `colAccPoly` | — | 106 px |
| 4 | Wallet (USDC) | `colAccBalance` | — | 157 px |
| 5 | 3T PnL | `colAccPnl3d` | — | Rest/Auto |
| 6 | 3T Winrate % | `colAccWin3d` | — | 153 px |
| 7 | Overall P/L | `colAccOverall` | — | 133 px |
**Steuerelemente:** ToolStripButton ×10, DataGridViewTextBoxColumn ×6, ToolStripStatusLabel ×3, ToolStrip ×2, ToolStripSeparator ×2, MenuStrip ×1, StatusStrip ×1, DataGridView ×1, DataGridViewButtonColumn ×1, LauncherWidgetsPanel ×1
### LauncherWidgetsPanel — Live-Überblick
**Tabs (Reihenfolge):** Auffällige Trades (24h)
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | Zeit | `colAlertTime` | — | 70 px |
| 2 | Level | `colAlertLevel` | — | 70 px |
| 3 | Nachricht | `colAlertMsg` | — | Rest/Auto |
| 4 | Modul | `colNotModule` | — | 100 px |
| 5 | Markt | `colNotMarket` | — | Rest/Auto |
| 6 | PnL | `colNotPnl` | — | 80 px |
| 7 | PnL % | `colNotPnlPct` | — | 70 px |
**Steuerelemente:** DataGridViewTextBoxColumn ×7, GroupBox ×3, DataGridView ×2, TableLayoutPanel ×1, FlowLayoutPanel ×1, RichTextBox ×1
### DashboardView
**Fenstergröße:** 1714 × 1083 px
**Tabs (Reihenfolge):** Dashboard | Tradehistorie | Module
**Beschriftungen:**
- `lblKpiPnl` = „Netto-PnL ↵ —"
- `lblKpiWin` = „Winrate ↵ —"
- `lblKpiTrades` = „Trades ↵ —"
- `lblKpiAvg` = „Ø PnL/Trade ↵ —"
- `lblKpiPf` = „Profit-Faktor ↵ —"
- `lblErgebnis` = „Ergebnis:"
- `lblSuche` = „Suche:"
- `lblModulesInfo` = „Module aktivieren/deaktivieren. Änderungen greifen nach dem nächsten Neustart von PolyTrader."
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | Modul | `colModule` | — | 110 px |
| 2 | Konto | `colAccount` | — | 130 px |
| 3 | Markt | `colMarket` | — | Rest/Auto |
| 4 | Outcome | `colOutcome` | — | 80 px |
| 5 | Side | `colSide` | — | 60 px |
| 6 | Entry | `colEntry` | — | 70 px |
| 7 | Exit | `colExit` | — | 70 px |
| 8 | Size | `colSize` | — | 70 px |
| 9 | PnL | `colPnl` | — | 80 px |
| 10 | PnL % | `colPnlPct` | — | 70 px |
| 11 | Geschlossen | `colClosedAt` | — | 140 px |
| 12 | Modul | `colModName` | — | 220 px |
| 13 | Status | `colModStatus` | — | 260 px |
| 14 | Hinweis | `colModHint` | — | Rest/Auto |
| 15 | Aktion | `colModAction` | — | 160 px |
**Steuerelemente:** DataGridViewTextBoxColumn ×14, Label ×8, ToolStripLabel ×4, ToolStripComboBox ×4, TabPage ×3, PictureBox ×3, Panel ×2, DataGridView ×2, ToolStrip ×1, ToolStripButton ×1, TabControl ×1, FlowLayoutPanel ×1, TextBox ×1, ComboBox ×1, DataGridViewButtonColumn ×1
### TerminalView
**Fenstergröße:** 1429 × 1083 px
**Tabs (Reihenfolge):** Live | Log Viewer
**Beschriftungen:**
- `lblFilter` = „Level:"
- `lblViewerStatus` = „—"
- `lblVText` = „Text:"
- `lblVCid` = „CID:"
- `lblVLevel` = „Level:"
- `lblVDatum` = „Datum:"
**Schaltflächen:** „Alles auswählen", „Alles kopieren", „Kopieren", „Laden", „Stop Autoscroll", „Terminal leeren"
**Steuerelemente:** Label ×6, ToolStripMenuItem ×4, DataGridViewTextBoxColumn ×4, TabPage ×2, Panel ×2, ComboBox ×2, Button ×2, TextBox ×2, TabControl ×1, RichTextBox ×1, ContextMenuStrip ×1, ToolStripSeparator ×1, DateTimePicker ×1, DataGridView ×1
### SettingsView
**Fenstergröße:** 1185 × 820 px
**Tabs (Reihenfolge):** General Settings | Polymarket Accounts
**Schaltflächen:** „Lizenz prüfen / setzen …", „Load Settings from File", „Löschen", „Master-Key erzeugen", „Neuer Account", „OpenRouter-Key setzen …", „Save Changes", „Test-Heartbeat senden", „Watchdog-Token setzen …"
**Steuerelemente:** ToolStripButton ×9, PropertyGrid ×2, TabPage ×2, ToolStrip ×2, DataGridViewTextBoxColumn ×2, DataGridViewCheckBoxColumn ×2, TabControl ×1, DataGridView ×1
### JobsView
**Fenstergröße:** 1429 × 833 px
**Tabellenspalten** (Reihenfolge = Anzeigereihenfolge):
| # | Spaltenkopf | Feldname | Format | Breite |
|--:|---|---|---|---|
| 1 | Job Name | `colJobName` | — | 150 px |
| 2 | Status | `colStatus` | — | 150 px |
| 3 | Zuletzt ausgeführt | `colLastRun` | — | 130 px |
| 4 | Nächster Lauf | `colNextRun` | — | 130 px |
| 5 | Aktiv | `col_dgv_jobs_Enabled` | — | 60 px |
| 6 | Aktion | `col_dgv_jobs_Run` | — | 90 px |
| 7 | Beschreibung | `colDescription` | — | Rest/Auto |
**Steuerelemente:** DataGridViewTextBoxColumn ×5, DataGridView ×1, DataGridViewCheckBoxColumn ×1, DataGridViewButtonColumn ×1, ToolStrip ×1, ToolStripButton ×1
### LicenseDialog
**Beschriftungen:**
- `lblHeadline` = „Für PolyTrader wird eine gültige Lizenz benötigt."
- `lblStatus` = „Status:"
- `lblHwCaption` = „Hardware-ID dieses Rechners (für die Aktivierung):"
- `lblKeyCaption` = „Lizenzschlüssel:"
**Schaltflächen:** „Kopieren", „Ohne Lizenz starten (eingeschränkt)", „Validieren"
**Steuerelemente:** Label ×5, Button ×3, TextBox ×2
### ShutdownConfirmDialog
**Beschriftungen:**
- `lblTitle` = „PolyTrader sicher beenden?"
- `lblCountdown` = „Beenden möglich in 10 s …"
**Schaltflächen:** „Abbrechen", „Beenden (10 s)"
**Steuerelemente:** Label ×3, Button ×2, Timer ×1
---
## Verhaltensnotizen aus dem Code-Behind
Was die Designer-Dateien nicht zeigen — Verhalten, das beim Nachbau leicht untergeht:
### Copytrading
- **MasterTradersView**: Links das Grid, rechts ein `PropertyGrid` für den gewählten Trader, darunter
eine `CheckedListBox` „Zugewiesene Accounts". Getrennt durch einen `Splitter`. Nach dem Speichern
erscheint ein Hinweistext mit Uhrzeit: „Gespeichert: #ID Name (n Account(s)) um HH:mm:ss".
- **OpenTradesView / ClosedTradesView**: Die Zeilenfärbung wird bei `DataBindingComplete` neu gesetzt.
Unter dem Grid zeigt eine Zusammenfassungszeile (`lblSummary`) die Summen.
- **ClosedTradesView**: Filterleiste mit Markt-Suche, Master-Auswahl, Ergebnis-Auswahl
(Alle / Gewinner / Verlierer) und Datumsbereich; dazu „Zurücksetzen".
- **AccountSettingsView**: Konto-Auswahl in der Toolbar, darunter ein `PropertyGrid` auf
`CopyTradingAccountSettings`. **Die `[Description]`-Attribute der Modellklasse sind die
Feldbeschreibungen — beim Nachbau als Tooltips oder Hilfetexte übernehmen.**
### ResolutionFarming
Vier Tabs (Kandidaten / Positionen / Historie + Statistik / Settings), je ein Grid mit einer
„Aktualisieren"-Schaltfläche und Konto-Auswahl oben. Der Settings-Tab zeigt ein `PropertyGrid`
plus „Speichern" mit Bestätigungstext samt Uhrzeit.
### Supervisor
- **Analyse-Tab**: chat-artig — `RichTextBox` für den Verlauf (je Eintrag Zeitstempel + Sprecher),
Eingabefeld, „Senden", „Verlauf leeren"; Profil- und Modellauswahl in der Toolbar.
- **Dossiers-Tab**: Grid plus „Dossier öffnen".
- **Berichte / Counterfactual**: Grid plus „Aktualisieren", Filterung über `SignalId`.
### Accounting
- Drei Tabs: Übersicht/BWA, Ledger, Abruf/Status.
- **KPI-Kacheln** in einem `FlowLayoutPanel`: Netto-Handelsergebnis, Endsaldo, Einzahlungen,
Auszahlungen, Fees, Rewards, Handelsvolumen, Trades. Format je Kachel zweizeilig:
`"<Titel>\n<Wert:N2> <Währung>"` — der Wert steht unter dem Titel.
- Zeitraumwahl über zwei `DateTimePicker` (Von/Bis), dazu Währungs- und Kontoauswahl.
- Export über „PDF-Export" und „CSV-Export"; im Status-Tab „Inkrement jetzt" und
„Backfill (volle Historie)".
### Shell
- **LauncherForm**: Startleiste mit einem Button je Fenster (angehakt, wenn das Fenster offen ist),
Schnellaktionen „LiveTrading" und „DemoTrading" (dreistufig: Inactive → SellOnly → Active),
Statuszeile mit Trading-Status, Modul-Status und API-Ratelimit. Darunter die Account-Übersicht
und der Live-Überblick (`LauncherWidgetsPanel`).
- **Das Schließen-X ist abgefangen** und läuft über dieselbe Sicherheitsabfrage wie „Beenden".
- **ShutdownConfirmDialog**: Die Beenden-Schaltfläche ist **10 Sekunden gesperrt**, mit Countdown im
Text („Beenden möglich in 10 s"); Abbrechen ist jederzeit möglich.
- **TerminalView**: Live-Tab mit farbigen Log-Zeilen und abschaltbarem Autoscroll, Kontextmenü
(Alles auswählen / Alles kopieren), Auto-Clear bei Erreichen der RAM-Grenze. Log-Viewer-Tab mit
Filtern über Datum, Level, CID und Volltext.
- **DashboardView**: KPI-Kacheln (Netto-PnL, Winrate, Trades, Ø PnL/Trade, Profit-Faktor), drei
Diagramme (Equity-Kurve, PnL je Modul, PnL je Tag), Tradehistorie mit Suche und
Gewinner/Verlierer-Filter sowie ein Modul-Tab zum Aktivieren/Deaktivieren (wirkt nach Neustart).
**Der Profit-Faktor zeigt „∞", wenn es keine Verluste gab.**
---
## Was beim Nachbau bewusst *anders* werden darf
Nicht alles ist erhaltenswert — diese Punkte sind Altlasten der WinForms-Umsetzung:
1. **Die `PropertyGrid`-Fenster** (Settings, Account-Einstellungen, Master-Trader,
ResolutionFarming): statt einer alphabetischen Reflection-Liste künftig fachlich gruppierte
Formulare mit Validierung, Einheiten am Feld und abhängigen Feldern. Die `[Description]`-Attribute
liefern die Texte, die `[Browsable(false)]`-Markierungen die auszublendenden Felder.
2. **Menüaufbau zur Laufzeit** (`WindowMenu.Populate` leert und füllt `menu.Items`): verstößt gegen
die neue Layout-Regel. In Avalonia ein deklaratives `Menu` mit `ItemsSource`-Bindung.
3. **Diagramme als Bitmap** (ScottPlot → PNG → `PictureBox`): entfällt mit LiveCharts2.
4. **Das `RichTextBox`-Terminal**: als virtualisiertes `ItemsControl` über eine
`ObservableCollection<LogLine>` neu bauen — das löst nebenbei das RAM-Problem, wegen dem heute
ein Auto-Clear nötig ist.
5. **Feste Pixelbreiten** in den Grids: in Avalonia besser mit `*`-Sternbreiten arbeiten. Die Werte
in den Tabellen oben geben die *Proportionen* vor, nicht die absoluten Zahlen.
+6
View File
@@ -0,0 +1,6 @@
bin/
obj/
appsettings.Local.json
server_settings.xml
master.key
openrouter.key
+138
View File
@@ -0,0 +1,138 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PolyTrader.App.Avalonia.App">
<!--
Farbschema. RequestedThemeVariant wird NICHT hier festgelegt, sondern beim Start vom
ThemeManager aus den Server-Settings gesetzt (hell / dunkel / dem System folgen).
Alle projekteigenen Farben stehen als Token in beiden Varianten. Im XAML werden sie über
DynamicResource gebunden dadurch wechseln sie beim Umschalten von selbst. Nur wenige
Stellen setzen Farben im Code (Grid-Zeilen, Umschalter); die holen sie über
ThemeManager.Brush(...) und zeichnen sich bei ThemeChanged neu.
-->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<!-- Flächen und Rahmen -->
<SolidColorBrush x:Key="AppSurfaceBrush" Color="#FAFAFA" />
<SolidColorBrush x:Key="AppSurfaceAltBrush" Color="#F3F3F3" />
<SolidColorBrush x:Key="AppCardBrush" Color="#F5F5F5" />
<SolidColorBrush x:Key="AppBorderBrush" Color="#DDDDDD" />
<!-- Texte -->
<SolidColorBrush x:Key="AppMutedTextBrush" Color="#777777" />
<SolidColorBrush x:Key="AppCaptionTextBrush" Color="#666666" />
<SolidColorBrush x:Key="AppReadOnlyTextBrush" Color="#444444" />
<!-- Bedeutungsfarben -->
<SolidColorBrush x:Key="AppPositiveBrush" Color="#2E7D32" />
<SolidColorBrush x:Key="AppNegativeBrush" Color="#C62828" />
<SolidColorBrush x:Key="AppWarningBrush" Color="#996600" />
<!-- Zeilenfärbung der Trade-Listen (Pastell auf hellem Grund) -->
<SolidColorBrush x:Key="AppTradeLossBrush" Color="#F5C8C8" />
<SolidColorBrush x:Key="AppTradeSmallWinBrush" Color="#D4F0D4" />
<SolidColorBrush x:Key="AppTradeBigWinBrush" Color="#8CD68C" />
<!-- Trading-Umschalter im Launcher -->
<SolidColorBrush x:Key="AppToggleActiveBrush" Color="#90EE90" />
<SolidColorBrush x:Key="AppToggleSellOnlyBrush" Color="#FFA500" />
<SolidColorBrush x:Key="AppToggleInactiveBrush" Color="#CD5C5C" />
<!-- Supervisor-Analyseverlauf -->
<SolidColorBrush x:Key="AppChatUserBrush" Color="#4682B4" />
<SolidColorBrush x:Key="AppChatAgentBrush" Color="#2E8B57" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<!-- Flächen und Rahmen -->
<SolidColorBrush x:Key="AppSurfaceBrush" Color="#2B2B2B" />
<SolidColorBrush x:Key="AppSurfaceAltBrush" Color="#333333" />
<SolidColorBrush x:Key="AppCardBrush" Color="#2F2F2F" />
<SolidColorBrush x:Key="AppBorderBrush" Color="#484848" />
<!-- Texte: heller, aber nicht reinweiß sonst flimmert es auf Dunkel -->
<SolidColorBrush x:Key="AppMutedTextBrush" Color="#9A9A9A" />
<SolidColorBrush x:Key="AppCaptionTextBrush" Color="#AAAAAA" />
<SolidColorBrush x:Key="AppReadOnlyTextBrush" Color="#CFCFCF" />
<!-- Bedeutungsfarben: gleiche Aussage, aufgehellt für Kontrast auf Dunkel -->
<SolidColorBrush x:Key="AppPositiveBrush" Color="#81C784" />
<SolidColorBrush x:Key="AppNegativeBrush" Color="#EF5350" />
<SolidColorBrush x:Key="AppWarningBrush" Color="#FFB74D" />
<!-- Zeilenfärbung: gedämpft statt pastell die hellen Töne würden auf Dunkel
blenden und den Text unlesbar machen. Reihenfolge der Aussage bleibt. -->
<SolidColorBrush x:Key="AppTradeLossBrush" Color="#4E2A2A" />
<SolidColorBrush x:Key="AppTradeSmallWinBrush" Color="#2C3F2C" />
<SolidColorBrush x:Key="AppTradeBigWinBrush" Color="#3D6B3D" />
<!-- Trading-Umschalter -->
<SolidColorBrush x:Key="AppToggleActiveBrush" Color="#3F7A3F" />
<SolidColorBrush x:Key="AppToggleSellOnlyBrush" Color="#A76B1F" />
<SolidColorBrush x:Key="AppToggleInactiveBrush" Color="#8E3B3B" />
<!-- Supervisor-Analyseverlauf -->
<SolidColorBrush x:Key="AppChatUserBrush" Color="#7FB3D8" />
<SolidColorBrush x:Key="AppChatAgentBrush" Color="#68B68A" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<!-- Projektweite Grundgestaltung. Bewusst hier zentral statt je Fenster wiederholt,
damit alle Fenster gleich aussehen (wie zuvor die einheitliche Segoe-UI-Vorgabe). -->
<Style Selector="DataGrid">
<Setter Property="GridLinesVisibility" Value="Horizontal" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="CanUserResizeColumns" Value="True" />
<Setter Property="CanUserSortColumns" Value="True" />
<Setter Property="RowHeight" Value="24" />
<Setter Property="FontSize" Value="12" />
</Style>
<!-- KPI-Kachel: entspricht den zweizeiligen Labels der WinForms-Oberfläche
("<Titel> \n <Wert>"), siehe docs/UI-SPEZIFIKATION-WinForms.md. -->
<Style Selector="Border.kpi">
<Setter Property="Background" Value="{DynamicResource AppCardBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="Margin" Value="0,0,8,8" />
<Setter Property="MinWidth" Value="150" />
</Style>
<Style Selector="Border.kpi > StackPanel > TextBlock.caption">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="{DynamicResource AppCaptionTextBrush}" />
</Style>
<Style Selector="Border.kpi > StackPanel > TextBlock.value">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- Werkzeugleiste oben in jedem Fenster (Ersatz für ToolStrip). -->
<Style Selector="Border.toolbar">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="Padding" Value="6,4" />
</Style>
<Style Selector="Border.toolbar Button">
<Setter Property="Margin" Value="0,0,6,0" />
<Setter Property="Padding" Value="10,4" />
</Style>
<!-- Statusleiste unten (Ersatz für StatusStrip). -->
<Style Selector="Border.statusbar">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="Padding" Value="8,4" />
</Style>
<!-- Gedämpfte Hinweistexte (Statuszeilen, Erklärungen unter Feldern). -->
<Style Selector="TextBlock.muted">
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
</Style>
</Application.Styles>
</Application>
+56
View File
@@ -0,0 +1,56 @@
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.App.Avalonia.Views;
namespace PolyTrader.App.Avalonia
{
/// <summary>
/// Avalonia-Anwendungsobjekt. Hält die Verbindung zwischen dem bereits laufenden Generic Host
/// (Trading-Services, Module, Persistenz) und der Oberfläche: beim Start wird der Launcher
/// erzeugt und die Views von Core und Modulen werden bei der Shell registriert.
/// </summary>
public partial class App : global::Avalonia.Application
{
/// <summary>
/// Wird von <see cref="Program"/> vor <c>StartWithClassicDesktopLifetime</c> gesetzt.
/// Bewusst statisch: Avalonia erzeugt die Application-Instanz selbst, ein Konstruktor-
/// Parameter ist daher nicht möglich.
/// </summary>
public static IServiceProvider? Services { get; set; }
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && Services != null)
{
// Farbschema aus den Server-Settings übernehmen, BEVOR das erste Fenster entsteht
// sonst blitzt kurz das helle Standardschema auf.
var settings = Services.GetRequiredService<PolyTraderSharp.Models.ServerSettings>();
Shell.ThemeManager.Initialize(settings, "server_settings.xml",
Services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>());
var uiHost = Services.GetRequiredService<AvaloniaUiHost>();
CoreViews.Register(uiHost, Services);
foreach (var module in Services.GetServices<Core.Modularity.IPolyTraderModule>())
module.RegisterUi(uiHost, Services);
ModuleViews.Register(uiHost, Services);
ViewIcons.AssignDefaults(uiHost);
var launcher = new LauncherWindow(uiHost, Services);
uiHost.SetMainWindow(launcher);
desktop.MainWindow = launcher;
// Schliessen des Launchers laeuft ueber die Sicherheitsabfrage (wie bisher das X).
desktop.ShutdownMode = global::Avalonia.Controls.ShutdownMode.OnMainWindowClose;
}
base.OnFrameworkInitializationCompleted();
}
}
}
@@ -0,0 +1,124 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
x:Class="PolyTrader.App.Avalonia.Controls.SettingsEditor">
<!--
Ersatz für das WinForms-PropertyGrid. Aufbau nach Vorgabe:
Kategorie-Überschrift (fett), darunter je Feld eine Zeile aus Beschriftung links und
Steuerelement rechts; ein etwaiger Erklärungstext steht klein unter dem Feld.
Vollständig deklarativ: WELCHE Felder es gibt, liefert das Datenmodell
(SettingsModelBuilder liest Category/DisplayName/Description/Browsable vom Einstellungs-
objekt). WIE ein Feld aussieht, steht hier — je Feldtyp ein DataTemplate. Damit bleibt die
Layout-Regel gewahrt und alle Einstellungsfenster sehen automatisch gleich aus.
-->
<UserControl.Resources>
<!-- Einheitliche Beschriftungsspalte: sorgt für die saubere Flucht aller Felder. -->
<x:Double x:Key="LabelWidth">210</x:Double>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="TextBlock.sectionTitle">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Margin" Value="0,16,0,8" />
</Style>
<Style Selector="TextBlock.fieldLabel">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,0,12,0" />
</Style>
<Style Selector="TextBlock.fieldHint">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Margin" Value="0,2,0,0" />
</Style>
<Style Selector="Grid.fieldRow">
<Setter Property="Margin" Value="0,0,0,8" />
</Style>
</UserControl.Styles>
<ScrollViewer>
<ItemsControl Name="sections" Margin="16,0,16,16">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SettingsSection">
<StackPanel>
<TextBlock Classes="sectionTitle" Text="{Binding Title}" />
<ItemsControl ItemsSource="{Binding Fields}">
<ItemsControl.DataTemplates>
<!-- Freitext -->
<DataTemplate DataType="vm:TextSettingsField">
<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}" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Ganzzahl -->
<DataTemplate DataType="vm:IntSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<TextBox Text="{Binding Value, Mode=TwoWay}" IsEnabled="{Binding IsEditable}"
MaxWidth="160" HorizontalAlignment="Left" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Ja/Nein -->
<DataTemplate DataType="vm:BoolSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<CheckBox IsChecked="{Binding Value, Mode=TwoWay}" IsEnabled="{Binding IsEditable}" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Auswahl -->
<DataTemplate DataType="vm:ChoiceSettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<ComboBox ItemsSource="{Binding Options}"
SelectedItem="{Binding Value, Mode=TwoWay}"
IsEnabled="{Binding IsEditable}"
HorizontalAlignment="Stretch" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
<!-- Reine Anzeige -->
<DataTemplate DataType="vm:ReadOnlySettingsField">
<Grid Classes="fieldRow" ColumnDefinitions="210,*">
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<SelectableTextBlock Text="{Binding Value}" VerticalAlignment="Center"
Foreground="{DynamicResource AppReadOnlyTextBrush}" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,26 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.ViewModels;
namespace PolyTrader.App.Avalonia.Controls
{
/// <summary>
/// Zeigt ein beliebiges Einstellungsobjekt als kategorisiertes Formular. Ersetzt das
/// WinForms-PropertyGrid an allen vier Stellen (Server-Settings, Account-Einstellungen,
/// Master-Trader, ResolutionFarming) ein Aufruf je Fenster, kein Feld-Code.
///
/// Änderungen wirken direkt auf dem übergebenen Objekt; das Speichern (Datei/DB) bleibt
/// Sache des jeweiligen Fensters genau wie zuvor beim PropertyGrid.
/// </summary>
public partial class SettingsEditor : UserControl
{
public SettingsEditor() => AvaloniaXamlLoader.Load(this);
/// <summary>Setzt das anzuzeigende Objekt (oder <c>null</c>, um zu leeren).</summary>
public void Show(object? target)
{
var list = this.FindControl<ItemsControl>("sections")!;
list.ItemsSource = target == null ? null : SettingsModelBuilder.Build(target);
}
}
}
@@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="using:PolyTrader.App.Avalonia.Shell"
x:Class="PolyTrader.App.Avalonia.Controls.WindowMenuBar"
x:DataType="shell:WindowMenuModel">
<!--
Gemeinsame Fenster-Menüleiste, die auf JEDEM PolyTrader-Fenster oben sitzt.
Alle Fenster stehen nebeneinander (nicht in einem Untermenü versteckt), das aktuelle
fett, offene mit Haken. Die rechte Aktion ist kontextabhängig.
Vollständig deklarativ: die Einträge kommen als Datenmodell aus WindowMenuModel.Entries,
hier wird nur beschrieben, wie ein Eintrag aussieht. Kein Aufbau zur Laufzeit im Code.
(Avalonia kennt kein ToolStripItemAlignment.Right — die rechte Ausrichtung entsteht
dadurch, dass der letzte Eintrag im DockPanel rechts angedockt wird.)
-->
<Border Background="{DynamicResource AppSurfaceAltBrush}" BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="0,0,0,1" Padding="4,2">
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="shell:WindowMenuEntry">
<Button Command="{Binding Command}"
ToolTip.Tip="{Binding ToolTip}"
Background="Transparent"
BorderThickness="0"
Padding="8,4"
Margin="0,0,2,0">
<StackPanel Orientation="Horizontal" Spacing="6">
<Image Source="{Binding Icon}"
Width="20" Height="20"
VerticalAlignment="Center"
IsVisible="{Binding Icon, Converter={x:Static ObjectConverters.IsNotNull}}" />
<TextBlock Text="{Binding CheckMark}"
VerticalAlignment="Center"
Foreground="{DynamicResource AppPositiveBrush}" />
<TextBlock Text="{Binding Title}"
VerticalAlignment="Center"
FontWeight="{Binding FontWeight}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
@@ -0,0 +1,30 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Controls
{
/// <summary>
/// Code-Behind der Fenster-Menüleiste: nur Verdrahtung, kein Layout (siehe Layout-Regel).
/// Jedes Fenster ruft <see cref="Attach"/> auf und bekommt damit dieselbe Leiste.
/// </summary>
public partial class WindowMenuBar : UserControl
{
private WindowMenuModel? _model;
public WindowMenuBar() => AvaloniaXamlLoader.Load(this);
/// <summary>
/// Verbindet die Leiste mit der Shell. <paramref name="currentViewId"/> ist <c>null</c> für den
/// Launcher (rechte Aktion „Beenden"), sonst die View-ID (rechte Aktion „Fenster schließen").
/// Die Event-Registrierung wird beim Entladen wieder gelöst kein Leak.
/// </summary>
public void Attach(IModuleUiHost host, string? currentViewId, Window owner)
{
_model = new WindowMenuModel(host, currentViewId, owner.Close);
DataContext = _model;
owner.Closed += (_, _) => _model?.Detach();
}
}
}
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Plattformneutral: dieselbe Anwendung laeuft unter Windows und Linux. Loest schrittweise
PolyTrader.App (WinForms, net10.0-windows) ab; der letzte WinForms-Stand liegt im
Git-Tag winforms-final, die UI-Beschreibung in docs/UI-SPEZIFIKATION-WinForms.md. -->
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>PolyTrader.App.Avalonia</AssemblyName>
<RootNamespace>PolyTrader.App.Avalonia</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!-- Avalonia-Previewer und XAML-Compiler: Layout wird deklarativ in .axaml gebaut,
nicht zur Laufzeit im Code (Layout-Regel des Projekts). -->
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<!-- Bewusst die 11er-Linie: LiveCharts2 2.0.5 (aktuellste Version) ist gegen Avalonia 11
gebaut und bricht unter 12 (Avalonia.Input.Gestures.PinchEvent existiert dort nicht mehr).
Avalonia 12 ist dem Chart-Oekosystem voraus; 11.3.x ist die reife Linie mit vollstaendiger
Paketunterstuetzung. Erst wieder anheben, wenn LiveCharts2 Avalonia 12 unterstuetzt. -->
<PackageReference Include="Avalonia" Version="11.3.19" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.19" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.19" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.19" />
<!-- DataGrid folgt einer eigenen Versionsreihe und endet in der 11er-Linie bei 11.3.13. -->
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<!-- Diagramme (Entscheidung: LiveCharts2 statt ScottPlot, aus optischen Gruenden). -->
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
<ProjectReference Include="..\PolyTrader.Modules.CopyTrading\PolyTrader.Modules.CopyTrading.csproj" />
<ProjectReference Include="..\PolyTrader.Modules.ResolutionFarming\PolyTrader.Modules.ResolutionFarming.csproj" />
<ProjectReference Include="..\PolyTrader.Modules.Supervisor\PolyTrader.Modules.Supervisor.csproj" />
<ProjectReference Include="..\PolyTrader.Modules.Accounting\PolyTrader.Modules.Accounting.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Symbole der bisherigen UI unveraendert weiterverwenden (siehe Shell/ViewIcons.cs). -->
<AvaloniaResource Include="..\..\Resources\*.png" Link="Assets\%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="appsettings.Local.json" Condition="Exists('appsettings.Local.json')" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+320
View File
@@ -0,0 +1,320 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Notifications;
using PolyTrader.Core.Security;
using PolyTrader.Modules.Accounting;
using PolyTrader.Modules.CopyTrading;
using PolyTrader.Modules.ResolutionFarming;
using PolyTrader.Modules.Supervisor;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia
{
/// <summary>
/// Einstiegspunkt der plattformneutralen PolyTrader-Anwendung.
///
/// <para>Der Aufbau ist bewusst zweigeteilt: <see cref="BuildHost"/> stellt den Generic Host mit
/// Persistenz, Diensten und Modulen zusammen ohne jeden Bezug zur Oberfläche. Erst
/// <see cref="Main"/> hängt Avalonia daran. Damit lässt sich derselbe Host später ohne Umbau
/// für den kopflosen Linux-Betrieb starten (Schalter <c>--headless</c>, Stufe L2).</para>
/// </summary>
internal static class Program
{
private const string ServerSettingsPath = "server_settings.xml";
[STAThread]
public static int Main(string[] args)
{
bool headless = args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase));
bool smoke = args.Any(a => string.Equals(a, "--smoke-ui", StringComparison.OrdinalIgnoreCase));
var host = BuildHost(out var bootLog);
// Konstruktionspruefung der Fenster: Host NICHT starten. Sonst laufen Trading-Engine,
// WSS-Clients und Jobs gegen die echten Boersen-Endpunkte an - fuer einen reinen
// UI-Test unerwuenscht (und auf einem Build-Server schlicht falsch).
if (smoke)
{
try { return RunSmokeUi(host, bootLog); }
finally { host.Dispose(); }
}
host.Start();
try
{
if (headless)
{
bootLog.Info("Kopfloser Betrieb: keine Oberfläche, Trading-Dienste laufen. Beenden mit Strg+C.");
host.WaitForShutdown();
return 0;
}
App.Services = host.Services;
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return 0;
}
finally
{
// Geordnetes Herunterfahren der Module erst nach dem Ende der Oberfläche.
host.StopAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult();
host.Dispose();
}
}
/// <summary>Von Avalonia (auch vom XAML-Previewer) erwartete Fabrikmethode.</summary>
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
/// <summary>
/// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die
/// aktiven Module. Kennt die Oberfläche nicht.
/// </summary>
private static IHost BuildHost(out TerminalLogger bootLog)
{
var serverSettings = ServerSettings.Load(ServerSettingsPath);
var logger = new TerminalLogger();
bootLog = logger;
// Anzeige-Kultur festnageln: Auf Linux richtet sich die Kultur sonst nach LANG/LC_ALL,
// das unter systemd oft gar nicht gesetzt ist - dann faellt .NET auf Invariant zurueck
// und aus "1.234,56 USDC" wird "1,234.56 USDC". Maschinen-I/O (API, Logs, CSV) laeuft
// unabhaengig davon ohnehin invariant.
var uiCulture = new System.Globalization.CultureInfo("de-DE");
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = uiCulture;
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = uiCulture;
ConfigureSecretProtection(logger);
// Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
// fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt.
AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => logger.Warning(m));
logger.Info($"Anzeige-Zeitzone: {AppTimeZone.CurrentId}");
var allModules = new List<IPolyTraderModule>
{
new CopyTradingModule(),
new ResolutionFarmingModule(),
new SupervisorModule(),
new AccountingModule()
};
var disabled = new HashSet<string>(serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
var modules = allModules.Where(m => !disabled.Contains(m.Name)).ToList();
return Host.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureAppConfiguration((_, config) =>
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
var databaseOptions = new DatabaseOptions
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
};
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton(serverSettings);
services.AddSingleton(logger);
services.AddSingleton<TradingState>();
services.AddSingleton<JobManager>();
services.AddSingleton<MullvadVpnService>();
services.AddHttpClient();
// Polymarket-Zugriffe teilen sich einen langlebigen Verbindungspool.
services.AddSingleton(sp => new PolymarketApiService(
sp.GetRequiredService<TerminalLogger>(), CreatePolymarketHttpClient()));
services.AddSingleton(sp => new PolymarketClobClient(
sp.GetRequiredService<TerminalLogger>(), CreatePolymarketHttpClient()));
// Benachrichtigungen: neutrale Senke, bis RocketChat/Telegram angebunden sind.
services.AddSingleton<INotificationSink>(
sp => new LogNotificationSink(sp.GetRequiredService<TerminalLogger>()));
services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>()));
// MUSS vor den Modulen registriert werden: hydriert die Accounts, bevor die
// Trading-Dienste anlaufen (Reihenfolge = Registrierungsreihenfolge).
services.AddHostedService<CoreStateHydrationService>();
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddHostedService<MarketSyncService>();
services.AddHostedService(sp => sp.GetRequiredService<MullvadVpnService>());
services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
services.AddSingleton<AvaloniaUiHost>();
})
.Build();
}
/// <summary>
/// Kopflose Konstruktionsprüfung aller registrierten Fenster Nachfolger des
/// <c>--smoke-ui</c>-Laufs der WinForms-App, der Konstruktionsfehler zuverlässig gefangen hat.
/// Läuft ohne Fenster und ohne Message-Loop.
/// </summary>
private static int RunSmokeUi(IHost host, TerminalLogger logger)
{
// Avalonia muss initialisiert sein, damit Fenster konstruiert werden koennen.
BuildAvaloniaApp().SetupWithoutStarting();
App.Services = host.Services;
ThemeManager.Initialize(host.Services.GetRequiredService<ServerSettings>(), ServerSettingsPath, logger);
var uiHost = host.Services.GetRequiredService<AvaloniaUiHost>();
CoreViews.Register(uiHost, host.Services);
foreach (var module in host.Services.GetServices<IPolyTraderModule>())
module.RegisterUi(uiHost, host.Services);
ModuleViews.Register(uiHost, host.Services);
ViewIcons.AssignDefaults(uiHost);
int failures = 0;
Console.WriteLine("=== Smoke-UI: Fenster-Konstruktion (Avalonia) ===");
foreach (var view in uiHost.Views)
{
try
{
_ = view.CreateView();
Console.WriteLine($"[OK] {view.Id} ({view.Title})");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}");
}
}
try
{
_ = new Views.LauncherWindow(uiHost, host.Services);
Console.WriteLine("[OK] LauncherWindow konstruiert");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] LauncherWindow: {ex.GetType().Name}: {ex.Message}");
}
try
{
_ = new Views.ShutdownConfirmWindow();
Console.WriteLine("[OK] ShutdownConfirmWindow konstruiert");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] ShutdownConfirmWindow: {ex.GetType().Name}: {ex.Message}");
}
// Der Einstellungs-Editor wird aus den Attributen des Modells aufgebaut. Ein Fenster kann
// fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen -
// deshalb hier gegen die tatsaechliche Feldzahl pruefen.
try
{
var sections = ViewModels.SettingsModelBuilder.Build(new ServerSettings());
int fieldCount = sections.Sum(x => x.Fields.Count);
if (sections.Count == 0 || fieldCount == 0)
{
failures++;
Console.WriteLine("[FEHLER] Einstellungs-Editor: keine Felder aus ServerSettings ermittelt " +
"(Category-/DisplayName-Attribute verloren?).");
}
else
{
Console.WriteLine($"[OK] Einstellungs-Editor: {sections.Count} Abschnitte, {fieldCount} Felder " +
$"({string.Join(", ", sections.Select(x => x.Title))})");
}
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}");
}
// Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
string[] themeKeys =
{
"AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush", "AppBorderBrush",
"AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
"AppPositiveBrush", "AppNegativeBrush", "AppWarningBrush",
"AppTradeLossBrush", "AppTradeSmallWinBrush", "AppTradeBigWinBrush",
"AppToggleActiveBrush", "AppToggleSellOnlyBrush", "AppToggleInactiveBrush",
"AppChatUserBrush", "AppChatAgentBrush"
};
foreach (var variant in new[] { global::Avalonia.Styling.ThemeVariant.Light,
global::Avalonia.Styling.ThemeVariant.Dark })
{
var missing = themeKeys.Where(k =>
!(global::Avalonia.Application.Current!.TryFindResource(k, variant, out object? v) && v is global::Avalonia.Media.IBrush))
.ToList();
if (missing.Count > 0)
{
failures++;
Console.WriteLine($"[FEHLER] Farbschema „{variant}“: fehlende Farben: {string.Join(", ", missing)}");
}
else
{
Console.WriteLine($"[OK] Farbschema „{variant}“: alle {themeKeys.Length} Farben vorhanden");
}
}
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures == 0 ? 0 : 1;
}
/// <summary>
/// HttpClient für die Polymarket-APIs: langlebiger Verbindungspool und die Header, die die
/// Gegenstelle erwartet (identisch zur bisherigen WinForms-Zusammenstellung).
/// </summary>
private static System.Net.Http.HttpClient CreatePolymarketHttpClient() =>
new(new System.Net.Http.SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 100
})
{
DefaultRequestHeaders = { { "User-Agent", "py_clob_client" }, { "Accept", "*/*" } }
};
/// <summary>
/// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte <c>master.key</c>) und aktiviert
/// die at-rest-Verschlüsselung. Muss VOR jeder Credential-Entschlüsselung laufen.
/// </summary>
private static void ConfigureSecretProtection(TerminalLogger logger)
{
string? masterKey = Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY");
if (string.IsNullOrWhiteSpace(masterKey))
{
string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim();
}
SecretProtection.Configure(masterKey);
if (SecretProtection.IsConfigured)
logger.Info("🔐 Secret-Verschlüsselung aktiv Account-Credentials werden at-rest verschlüsselt (AES-256-GCM).");
else
logger.Warning("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt Account-Credentials liegen UNVERSCHLÜSSELT in der DB.");
}
}
}
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Avalonia-Umsetzung von <see cref="IModuleUiHost"/>. Verhält sich wie der frühere
/// WinForms-<c>ShellUiHost</c>: je View höchstens ein Fenster, ein bereits offenes wird nach vorn
/// geholt, alle Fenster öffnen maximiert.
///
/// <para>Der Core-Contract ist toolkit-neutral (<see cref="ModuleView.CreateView"/> liefert
/// <see cref="object"/>) hier wird auf <see cref="Window"/> gecastet. Ein falscher Typ ist ein
/// Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster erzeugen.</para>
/// </summary>
public sealed class AvaloniaUiHost : IModuleUiHost
{
private readonly List<ModuleView> _views = new();
private readonly Dictionary<string, Window> _open = new();
private Window? _mainWindow;
private bool _shutdownDialogOpen;
/// <summary>True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde.</summary>
public bool ShutdownConfirmed { get; private set; }
public event Action? OpenStateChanged;
public IReadOnlyList<ModuleView> Views => _views;
public void RegisterView(ModuleView view) => _views.Add(view);
/// <summary>Setzt das Hauptfenster (Launcher) Ziel für <see cref="ActivateMain"/>.</summary>
public void SetMainWindow(Window main) => _mainWindow = main;
public bool IsOpen(string viewId) => _open.ContainsKey(viewId);
public void ActivateMain()
{
if (_mainWindow == null) return;
if (_mainWindow.WindowState == WindowState.Minimized)
_mainWindow.WindowState = WindowState.Maximized;
_mainWindow.Activate();
}
public void OpenView(string viewId)
{
var view = _views.FirstOrDefault(v => v.Id == viewId);
if (view != null) OpenView(view);
}
public void OpenView(ModuleView view)
{
if (_open.TryGetValue(view.Id, out var existing))
{
if (existing.WindowState == WindowState.Minimized)
existing.WindowState = WindowState.Normal;
existing.Activate();
return;
}
var window = (Window)view.CreateView();
if (string.IsNullOrEmpty(window.Title)) window.Title = view.Title;
// Vorgabe aus der WinForms-Oberflaeche uebernommen: alle Fenster maximiert (Full-HD-Ziel).
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.WindowState = WindowState.Maximized;
_open[view.Id] = window;
window.Closed += (_, _) =>
{
_open.Remove(view.Id);
OpenStateChanged?.Invoke();
};
window.Show();
OpenStateChanged?.Invoke();
}
/// <summary>
/// Zeigt die Sicherheitsabfrage (Beenden-Schaltfläche 10 s gesperrt) und fährt bei Bestätigung
/// herunter. Aus jedem Fenster aufrufbar auch aus Modul-Fenstern, die die App nicht kennen.
/// </summary>
public async void RequestShutdown()
{
if (ShutdownConfirmed || _shutdownDialogOpen) return;
_shutdownDialogOpen = true;
try
{
var dlg = new Views.ShutdownConfirmWindow();
bool confirmed = _mainWindow != null
? await dlg.ShowDialog<bool>(_mainWindow)
: await dlg.ShowDialog<bool>(new Window());
if (!confirmed) return;
ShutdownConfirmed = true;
if (global::Avalonia.Application.Current?.ApplicationLifetime
is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.Shutdown();
}
}
finally
{
_shutdownDialogOpen = false;
}
}
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTraderSharp;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Registriert die Fenster, die der Core selbst beisteuert (Dashboard, Einstellungen,
/// Terminal, Jobs). Gegenstück zur entsprechenden Registrierung im WinForms-Program.cs
/// die View-IDs sind bewusst identisch, damit Symbole und Reihenfolge unverändert greifen.
/// </summary>
public static class CoreViews
{
/// <summary>Quelle/Ziel der Modul-Aktivierung dieselbe Datei wie im Host.</summary>
private const string ServerSettingsPath = "server_settings.xml";
public static void Register(IModuleUiHost host, IServiceProvider services)
{
host.RegisterView(new ModuleView
{
Id = "core.dashboard",
Title = "Dashboard",
Group = "Core",
Order = 10,
CreateView = () => new Views.DashboardWindow(
host,
services.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
services.GetRequiredService<TradingState>(),
BuildModuleInfos(services),
ServerSettingsPath)
});
host.RegisterView(new ModuleView
{
Id = "core.settings",
Title = "Server Settings",
Group = "Core",
Order = 20,
CreateView = () => new Views.SettingsWindow(
host,
services.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
services.GetRequiredService<TradingState>(),
services.GetRequiredService<TerminalLogger>(),
services.GetService<WatchdogHeartbeatService>(),
services.GetService<MullvadVpnService>())
});
host.RegisterView(new ModuleView
{
Id = "core.jobs",
Title = "Server Jobs",
Group = "Core",
Order = 30,
CreateView = () => new Views.JobsWindow(host, services.GetRequiredService<JobManager>())
});
host.RegisterView(new ModuleView
{
Id = "core.terminal",
Title = "Terminal / Logs",
Group = "Core",
Order = 40,
CreateView = () => new Views.TerminalWindow(host, services.GetRequiredService<TerminalLogger>())
});
}
/// <summary>
/// Baut die Aktivierungs-Übersicht für den Modul-Tab: alle bekannten Module, ob sie in dieser
/// Sitzung laufen, und ein etwaiger Grund gegen die Aktivierung (z.B. fehlender API-Key).
/// </summary>
private static IReadOnlyList<ModuleActivationInfo> BuildModuleInfos(IServiceProvider services)
{
var running = services.GetServices<IPolyTraderModule>().ToList();
var runningNames = running.Select(m => m.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
var config = services.GetRequiredService<IConfiguration>();
// Auch die NICHT geladenen Module auflisten sonst liessen sie sich nie wieder aktivieren.
var all = new IPolyTraderModule[]
{
new PolyTrader.Modules.CopyTrading.CopyTradingModule(),
new PolyTrader.Modules.ResolutionFarming.ResolutionFarmingModule(),
new PolyTrader.Modules.Supervisor.SupervisorModule(),
new PolyTrader.Modules.Accounting.AccountingModule()
};
return all.Select(m => new ModuleActivationInfo(
m.Name,
runningNames.Contains(m.Name),
SafeBlocker(m, config)))
.ToList();
static string? SafeBlocker(IPolyTraderModule m, IConfiguration config)
{
try { return m.GetActivationBlocker(config); }
catch (Exception ex) { return $"Prüfung fehlgeschlagen: {ex.Message}"; }
}
}
}
}
@@ -0,0 +1,91 @@
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Registriert die Fenster der Module. Liegt bewusst in der App und nicht in den Modulen
/// Begründung in <c>Views/Modules/README.md</c>: die Modulprojekte bleiben dadurch frei von
/// Avalonia, was für den kopflosen Linux-Betrieb (<c>--headless</c>) den Ausschlag gibt.
///
/// <para>Registriert wird nur, was auch geladen ist: ist ein Modul über
/// <c>DisabledModules</c> abgeschaltet, fehlen seine Dienste im Container dann entfällt auch
/// sein Fenster, und der Launcher zeigt es gar nicht erst an.</para>
/// </summary>
public static class ModuleViews
{
public static void Register(IModuleUiHost host, IServiceProvider services)
{
RegisterIfLoaded(services, "CopyTrading", () => host.RegisterView(new ModuleView
{
Id = "copytrading.main",
Title = "Copytrading",
Group = "CopyTrading",
Order = 100,
CreateView = () => new Views.Modules.CopyTradingWindow(
host,
services.GetRequiredService<PolyTraderSharp.TradingState>(),
services.GetRequiredService<PolyTraderSharp.CopyTradingState>(),
services.GetRequiredService<Modules.CopyTrading.Persistence.ITrackedTraderRepository>(),
services.GetRequiredService<Modules.CopyTrading.Persistence.ICopyTradingAccountSettingsRepository>(),
services.GetRequiredService<Modules.CopyTrading.Persistence.ICopyTradeLogRepository>())
}));
RegisterIfLoaded(services, "ResolutionFarming", () => host.RegisterView(new ModuleView
{
Id = "resolutionfarming.main",
Title = "ResolutionFarming",
Group = "ResolutionFarming",
Order = 200,
CreateView = () => new Views.Modules.ResolutionFarmingWindow(
host,
services.GetRequiredService<PolyTraderSharp.TradingState>(),
services.GetRequiredService<Modules.ResolutionFarming.Persistence.IRfCandidateRepository>(),
services.GetRequiredService<Modules.ResolutionFarming.Persistence.IRfPositionRepository>(),
services.GetRequiredService<Modules.ResolutionFarming.Persistence.IRfClosedTradeRepository>(),
services.GetRequiredService<Modules.ResolutionFarming.Persistence.IRfSettingsRepository>())
}));
RegisterIfLoaded(services, "Supervisor", () => host.RegisterView(new ModuleView
{
Id = "supervisor.main",
Title = "Supervisor",
Group = "Supervisor",
Order = 300,
CreateView = () => new Views.Modules.SupervisorWindow(
host,
services.GetRequiredService<Modules.Supervisor.Agent.SupervisorAgent>(),
services.GetRequiredService<Modules.Supervisor.Services.DossierService>(),
services.GetRequiredService<Modules.Supervisor.Persistence.ISupervisorReportRepository>(),
services.GetRequiredService<Modules.Supervisor.Persistence.ISupervisorCounterfactualRepository>(),
services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>())
}));
RegisterIfLoaded(services, "Accounting", () => host.RegisterView(new ModuleView
{
Id = "accounting.main",
Title = "Accounting",
Group = "Accounting",
Order = 400,
CreateView = () => new Views.Modules.AccountingWindow(
host,
services.GetRequiredService<PolyTraderSharp.TradingState>(),
services.GetRequiredService<Modules.Accounting.Services.AccountingReportService>(),
services.GetRequiredService<Modules.Accounting.Persistence.ILedgerRepository>(),
services.GetRequiredService<Modules.Accounting.Persistence.IIngestRunRepository>(),
services.GetRequiredService<Modules.Accounting.Services.AccountingIngestService>(),
services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>())
}));
}
/// <summary>Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist.</summary>
private static void RegisterIfLoaded(IServiceProvider services, string moduleName, Action register)
{
bool loaded = services.GetServices<IPolyTraderModule>()
.Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase));
if (loaded) register();
}
}
}
@@ -0,0 +1,102 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Verwaltet das Farbschema (hell/dunkel/System) und macht es persistent.
///
/// <para>Die Standard-Steuerelemente stellt Avalonia selbst um, sobald
/// <see cref="global::Avalonia.Application.RequestedThemeVariant"/> wechselt für alles, was per
/// <c>DynamicResource</c> gebunden ist, gilt dasselbe. Farben, die im Code gesetzt werden
/// (Zeilenhintergründe, Umschalter-Flächen, KPI-Vorzeichen), aktualisieren sich dagegen NICHT von
/// selbst. Fenster mit solchen Stellen hängen sich deshalb an <see cref="ThemeChanged"/> und
/// zeichnen sie dort neu.</para>
/// </summary>
public static class ThemeManager
{
private static ServerSettings? _settings;
private static string? _settingsPath;
private static TerminalLogger? _logger;
/// <summary>Feuert nach jedem Wechsel Fenster zeichnen daraufhin ihre Code-Farben neu.</summary>
public static event Action? ThemeChanged;
/// <summary>Ist gerade das dunkle Schema aktiv (auch wenn es über „System" kam)?</summary>
public static bool IsDark =>
global::Avalonia.Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
/// <summary>
/// Einmalig beim Start aufrufen: übernimmt das gespeicherte Schema und merkt sich, wohin
/// spätere Änderungen geschrieben werden.
/// </summary>
public static void Initialize(ServerSettings settings, string settingsPath, TerminalLogger? logger = null)
{
_settings = settings;
_settingsPath = settingsPath;
_logger = logger;
ApplyVariant(settings.Theme);
}
/// <summary>Wechselt hell ↔ dunkel und speichert die Wahl. „System" wird dabei zu einer festen Wahl.</summary>
public static void Toggle()
{
Set(IsDark ? AppThemeVariant.Hell : AppThemeVariant.Dunkel);
}
/// <summary>Setzt das Schema, speichert es und benachrichtigt die Fenster.</summary>
public static void Set(AppThemeVariant variant)
{
ApplyVariant(variant);
if (_settings != null && _settingsPath != null)
{
_settings.Theme = variant;
try
{
_settings.Save(_settingsPath);
}
catch (Exception ex)
{
// Ein fehlgeschlagenes Speichern darf die Umschaltung nicht verhindern
// sie gilt dann nur für diese Sitzung.
_logger?.Warning($"Farbschema konnte nicht gespeichert werden: {ex.Message}");
}
}
ThemeChanged?.Invoke();
}
private static void ApplyVariant(AppThemeVariant variant)
{
var app = global::Avalonia.Application.Current;
if (app == null) return;
app.RequestedThemeVariant = variant switch
{
AppThemeVariant.Hell => ThemeVariant.Light,
AppThemeVariant.Dunkel => ThemeVariant.Dark,
_ => ThemeVariant.Default // folgt dem Betriebssystem
};
}
/// <summary>
/// Löst eine Farbe aus den Themen-Ressourcen auf. Für Stellen, die ihre Farbe im Code setzen
/// müssen (Grid-Zeilen, Umschalter) im XAML nimmt man stattdessen <c>DynamicResource</c>.
/// </summary>
public static IBrush Brush(string key)
{
var app = global::Avalonia.Application.Current;
if (app != null && app.TryFindResource(key, app.ActualThemeVariant, out object? value) && value is IBrush b)
return b;
// Unbekannter Schlüssel ist ein Programmierfehler, darf die Oberfläche aber nicht kippen.
return Brushes.Gray;
}
}
}
@@ -0,0 +1,74 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Löst die toolkit-neutralen <see cref="ModuleView.IconKey"/>-Schlüssel gegen die
/// Avalonia-Bildressourcen auf. Gegenstück zum gleichnamigen Helfer der WinForms-Shell
/// <b>dieselben Schlüssel, dieselben PNG-Dateien</b>, sodass Core und Module unverändert bleiben.
/// </summary>
public static class ViewIcons
{
/// <summary>Symbol-Schlüssel → Dateiname in <c>Resources/</c> (als Avalonia-Asset eingebettet).</summary>
private static readonly Dictionary<string, string> FileByKey = new()
{
["dashboard"] = "dashboard.png",
["settings"] = "setting_tools.png",
["terminal"] = "error_log.png",
["jobs"] = "system_time.png",
["accounting"] = "coins_in_hand.png",
["copytrading"] = "cross_reference.png",
["resolutionfarming"] = "file_start_workflow.png",
["supervisor"] = "emotion_batman.png",
};
/// <summary>Standard-Symbolschlüssel je View-ID identisch zur WinForms-Shell.</summary>
private static readonly Dictionary<string, string> DefaultKeyByViewId = new()
{
["core.dashboard"] = "dashboard",
["core.settings"] = "settings",
["core.terminal"] = "terminal",
["core.jobs"] = "jobs",
["accounting.main"] = "accounting",
["copytrading.main"] = "copytrading",
["resolutionfarming.main"] = "resolutionfarming",
["supervisor.main"] = "supervisor",
};
private static readonly ConcurrentDictionary<string, Bitmap?> Cache = new();
/// <summary>Bild zum Schlüssel, oder <c>null</c> (kein Symbol / Datei fehlt).</summary>
public static Bitmap? Resolve(string? iconKey)
{
if (iconKey == null || !FileByKey.TryGetValue(iconKey, out var file)) return null;
return Cache.GetOrAdd(iconKey, _ =>
{
try
{
var uri = new Uri($"avares://PolyTrader.App.Avalonia/Assets/{file}");
using var stream = AssetLoader.Open(uri);
return new Bitmap(stream);
}
catch
{
// Ein fehlendes Symbol darf die Oberflaeche nie aufhalten Menueeintrag bleibt Text-only.
return null;
}
});
}
/// <summary>Setzt bei allen registrierten Views den Standard-Symbolschlüssel, falls noch keiner gesetzt ist.</summary>
public static void AssignDefaults(IModuleUiHost uiHost)
{
foreach (var view in uiHost.Views)
if (view.IconKey == null && DefaultKeyByViewId.TryGetValue(view.Id, out var key))
view.IconKey = key;
}
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>Schlanker Befehl ohne MVVM-Fremdbibliothek.</summary>
public sealed class RelayCommand : ICommand
{
private readonly Action _execute;
public RelayCommand(Action execute) => _execute = execute;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged { add { } remove { } }
}
/// <summary>Ein Eintrag der Fenster-Menüleiste (Launcher, eine View, oder die rechte Aktion).</summary>
public sealed class WindowMenuEntry
{
public string Title { get; init; } = "";
public string? ToolTip { get; init; }
public Bitmap? Icon { get; init; }
/// <summary>Aktuelles Fenster wird fett dargestellt.</summary>
public bool IsCurrent { get; init; }
/// <summary>Fenster ist offen (oder das aktuelle) bekommt einen Haken.</summary>
public bool IsChecked { get; init; }
public ICommand Command { get; init; } = new RelayCommand(() => { });
// Von den Bindungen im XAML genutzt (kein Konverter nötig).
public string FontWeight => IsCurrent ? "Bold" : "Normal";
public string CheckMark => IsChecked ? "✓ " : "";
}
/// <summary>
/// Datenmodell der gemeinsamen Fenster-Menüleiste, die auf JEDEM PolyTrader-Fenster erscheint.
///
/// <para><b>Umsetzung der neuen Layout-Regel:</b> Die WinForms-Fassung baute das Menü zur Laufzeit
/// auf (<c>menu.Items.Clear()</c> + dynamisch erzeugte <c>ToolStripMenuItem</c>). Hier liefert das
/// Modell nur noch <see cref="Entries"/>; das Layout steht deklarativ in
/// <c>Controls/WindowMenuBar.axaml</c>.</para>
/// </summary>
public sealed class WindowMenuModel : INotifyPropertyChanged
{
private readonly IModuleUiHost _host;
private readonly string? _currentViewId;
private readonly Action _closeThisWindow;
public ObservableCollection<WindowMenuEntry> Entries { get; } = new();
/// <param name="currentViewId">
/// <c>null</c> für den Launcher (dort heisst die rechte Aktion „Beenden"), sonst die View-ID
/// des Fensters (dann „Fenster schließen").
/// </param>
public WindowMenuModel(IModuleUiHost host, string? currentViewId, Action closeThisWindow)
{
_host = host;
_currentViewId = currentViewId;
_closeThisWindow = closeThisWindow;
Rebuild();
_host.OpenStateChanged += OnOpenStateChanged;
ThemeManager.ThemeChanged += OnOpenStateChanged; // Symbol des Umschalters dreht sich
}
/// <summary>Registrierung lösen, wenn das Fenster geht kein Event-Leak über die Host-Lebensdauer.</summary>
public void Detach()
{
_host.OpenStateChanged -= OnOpenStateChanged;
ThemeManager.ThemeChanged -= OnOpenStateChanged;
}
private void OnOpenStateChanged()
{
if (Dispatcher.UIThread.CheckAccess()) Rebuild();
else Dispatcher.UIThread.Post(Rebuild);
}
private void Rebuild()
{
Entries.Clear();
Entries.Add(new WindowMenuEntry
{
Title = "Launcher",
IsCurrent = _currentViewId == null,
IsChecked = _currentViewId == null,
Command = new RelayCommand(_host.ActivateMain)
});
foreach (var view in _host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
{
string id = view.Id;
bool isCurrent = id == _currentViewId;
Entries.Add(new WindowMenuEntry
{
Title = view.Title,
Icon = ViewIcons.Resolve(view.IconKey),
IsCurrent = isCurrent,
IsChecked = isCurrent || _host.IsOpen(id),
Command = new RelayCommand(() => _host.OpenView(id))
});
}
// Farbschema-Umschalter: sitzt auf JEDEM Fenster, damit er ueberall erreichbar ist.
// Sonne = ins Helle wechseln, Mond = ins Dunkle.
Entries.Add(new WindowMenuEntry
{
Title = ThemeManager.IsDark ? "☀" : "🌙",
ToolTip = ThemeManager.IsDark ? "Zum hellen Farbschema wechseln" : "Zum dunklen Farbschema wechseln",
Command = new RelayCommand(ThemeManager.Toggle)
});
// Kontextabhaengige rechte Aktion wie in der WinForms-Fassung: nur der Launcher darf die
// App beenden (mit Sicherheitsabfrage), jedes andere Fenster schliesst nur sich selbst.
Entries.Add(_currentViewId == null
? new WindowMenuEntry { Title = "Beenden", Command = new RelayCommand(_host.RequestShutdown) }
: new WindowMenuEntry { Title = "Fenster schließen", Command = new RelayCommand(_closeThisWindow) });
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Entries)));
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -0,0 +1,83 @@
using System;
using Avalonia.Media;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Modules.CopyTrading.Logic;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>
/// Farbpalette zu den <see cref="TradeRowTint"/>-Kategorien. Die Schwellenlogik liegt
/// (getestet) in <see cref="TradeRowColoring"/>; hier steht nur, wie die Kategorien aussehen.
///
/// <para>Die Farben kommen aus den Themen-Ressourcen und werden bei JEDEM Zugriff aufgelöst
/// deshalb Eigenschaften statt <c>static readonly</c>. Nach einem Themenwechsel liefern sie
/// sofort die passende Variante; im hellen Schema die Pastelltöne der bisherigen Oberfläche,
/// im dunklen gedämpfte Entsprechungen (helle Pastelltöne würden dort blenden).</para>
/// </summary>
public static class TradeRowPalette
{
public static IBrush Loss => ThemeManager.Brush("AppTradeLossBrush");
public static IBrush SmallWin => ThemeManager.Brush("AppTradeSmallWinBrush");
public static IBrush BigWin => ThemeManager.Brush("AppTradeBigWinBrush");
public static IBrush ForPnlPercent(decimal pnlPercent) => TradeRowColoring.ForPnlPercent(pnlPercent) switch
{
TradeRowTint.Loss => Loss,
TradeRowTint.BigWin => BigWin,
_ => SmallWin
};
}
/// <summary>Zeile des Grids „Offene Trades" (aufgeloester Kontoname + Zeilenfarbe).</summary>
public sealed class CopyOpenTradeRow
{
public string Account { get; init; } = string.Empty;
public string Trader { get; init; } = string.Empty;
public string Market { get; init; } = string.Empty;
public string Outcome { get; init; } = string.Empty;
public string Side { get; init; } = string.Empty;
public decimal Entry { get; init; }
public decimal Current { get; init; }
public decimal Size { get; init; }
public decimal Amount { get; init; }
public decimal Value { get; init; }
public decimal Unrealized { get; init; }
public decimal UnrealizedPct { get; init; }
public string Status { get; init; } = string.Empty;
/// <summary>Zeilenhintergrund nach Buchgewinn wie zuvor die DataBindingComplete-Färbung.</summary>
public IBrush RowBrush => TradeRowPalette.ForPnlPercent(UnrealizedPct);
}
/// <summary>
/// Zeile des Grids „Geschlossene Trades". Bewusst eigener Typ und nicht das Modell
/// <c>PolyTraderSharp.Models.ClosedTradeRow</c>: hier stehen der aufgeloeste Master-Trader-Name
/// und die Zeilenfarbe, die es dort nicht gibt.
/// </summary>
public sealed class CopyClosedTradeRow
{
public int TradeId { get; init; }
public string Account { get; init; } = string.Empty;
public string Trader { get; init; } = string.Empty;
public string Market { get; init; } = string.Empty;
public string Outcome { get; init; } = string.Empty;
public string Side { get; init; } = string.Empty;
public decimal Entry { get; init; }
public decimal Exit { get; init; }
public decimal Size { get; init; }
public decimal Pnl { get; init; }
public decimal PnlPercent { get; init; }
public DateTime OpenedAt { get; init; }
public DateTime ClosedAt { get; init; }
public IBrush RowBrush => TradeRowPalette.ForPnlPercent(PnlPercent);
}
/// <summary>Eintrag der Account-Zuweisung eines Master-Traders (Mehrfachauswahl).</summary>
public sealed class AccountAssignment
{
public int AccountId { get; init; }
public string Label { get; init; } = string.Empty;
public bool IsAssigned { get; set; }
}
}
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using PolyTrader.Core.Analytics;
using PolyTraderSharp.Models;
using SkiaSharp;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>
/// Datenmodell der drei Dashboard-Diagramme (LiveCharts2). Ersetzt das frühere Rendern nach
/// Bitmap: die Diagramme sind jetzt echte Steuerelemente interaktiv und ohne System.Drawing.
///
/// <para>Die Auswertung selbst kommt unverändert aus <see cref="TradeAnalytics"/>; dieses Modell
/// übersetzt deren Ergebnisse nur in Serien und Achsen. Die Trennung war schon vorher da und ist
/// der Grund, warum der Chart-Wechsel die Fachlogik nicht berührt.</para>
/// </summary>
public sealed class DashboardChartsModel : INotifyPropertyChanged
{
private static readonly SKColor Accent = new(0x1E, 0x88, 0xE5);
private static readonly SKColor Positive = new(0x2E, 0x7D, 0x32);
private static readonly SKColor Negative = new(0xC6, 0x28, 0x28);
private ISeries[] _equitySeries = Array.Empty<ISeries>();
private ISeries[] _moduleSeries = Array.Empty<ISeries>();
private ISeries[] _daySeries = Array.Empty<ISeries>();
private Axis[] _equityXAxes = Array.Empty<Axis>();
private Axis[] _moduleXAxes = Array.Empty<Axis>();
private Axis[] _dayXAxes = Array.Empty<Axis>();
public ISeries[] EquitySeries { get => _equitySeries; private set => Set(ref _equitySeries, value); }
public ISeries[] ModuleSeries { get => _moduleSeries; private set => Set(ref _moduleSeries, value); }
public ISeries[] DaySeries { get => _daySeries; private set => Set(ref _daySeries, value); }
public Axis[] EquityXAxes { get => _equityXAxes; private set => Set(ref _equityXAxes, value); }
public Axis[] ModuleXAxes { get => _moduleXAxes; private set => Set(ref _moduleXAxes, value); }
public Axis[] DayXAxes { get => _dayXAxes; private set => Set(ref _dayXAxes, value); }
/// <summary>Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf.</summary>
public void Update(IReadOnlyList<TradeRecord> scoped)
{
BuildEquity(scoped);
BuildByModule(scoped);
BuildByDay(scoped);
}
private void BuildEquity(IReadOnlyList<TradeRecord> scoped)
{
var curve = TradeAnalytics.EquityCurve(scoped);
EquitySeries = new ISeries[]
{
new LineSeries<DateTimePoint>
{
Name = "Kumulierter PnL",
Values = curve.Select(p => new DateTimePoint(p.At, (double)p.Cumulative)).ToArray(),
Stroke = new SolidColorPaint(Accent, 2),
GeometrySize = 0, // reine Linie bei vielen Trades sonst unruhig
Fill = null
}
};
EquityXAxes = new[]
{
new Axis
{
// Ticks kommen als DateTime-Ticks zurueckrechnen und kurz formatieren.
Labeler = value => new DateTime((long)value).ToString("dd.MM."),
UnitWidth = TimeSpan.FromDays(1).Ticks,
LabelsRotation = 0
}
};
}
private void BuildByModule(IReadOnlyList<TradeRecord> scoped)
{
var byMod = TradeAnalytics.PnlByKey(scoped,
t => string.IsNullOrEmpty(t.ModuleName) ? "—" : t.ModuleName);
ModuleSeries = new ISeries[]
{
new ColumnSeries<double>
{
Name = "PnL",
Values = byMod.Select(x => (double)x.Pnl).ToArray(),
Fill = new SolidColorPaint(Accent)
}
};
ModuleXAxes = new[] { new Axis { Labels = byMod.Select(x => x.Key).ToArray(), LabelsRotation = 0 } };
}
private void BuildByDay(IReadOnlyList<TradeRecord> scoped)
{
var byDay = TradeAnalytics.PnlByDay(scoped);
DaySeries = new ISeries[]
{
new ColumnSeries<double>
{
Name = "PnL",
Values = byDay.Select(x => (double)x.Pnl).ToArray(),
// Verlusttage rot, Gewinntage gruen dieselbe Leseweise wie in den Trade-Listen.
Fill = new SolidColorPaint(byDay.Sum(x => x.Pnl) >= 0m ? Positive : Negative)
}
};
DayXAxes = new[]
{
new Axis
{
Labels = byDay.Select(x => x.Day.ToString("dd.MM")).ToArray(),
LabelsRotation = byDay.Count > 20 ? 45 : 0 // bei vielen Tagen sonst unleserlich
}
};
}
private void Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -0,0 +1,52 @@
using System;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>Anzeige-Zeile des Historie-Grids (Konto bereits zum Namen aufgelöst).</summary>
public sealed class DashboardTradeRow
{
public string Module { get; init; } = string.Empty;
public string Account { get; init; } = string.Empty;
public string Market { get; init; } = string.Empty;
public string Outcome { get; init; } = string.Empty;
public string Side { get; init; } = string.Empty;
public decimal EntryPrice { get; init; }
public decimal ExitPrice { get; init; }
public decimal Size { get; init; }
public decimal RealizedPnl { get; init; }
public decimal PnlPercent { get; init; }
public DateTime ClosedAt { get; init; }
}
/// <summary>Anzeige-Zeile des Modul-Grids (Tab „Module").</summary>
public sealed class ModuleRow
{
public string Modul { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public string Hinweis { get; init; } = string.Empty;
public string Aktion { get; init; } = string.Empty;
/// <summary>
/// Falsch, wenn das Modul einen Aktivierungs-Blocker hat (z.B. fehlender API-Key). Die
/// WinForms-Fassung zeigte in dem Fall beim Klick einen Hinweisdialog; hier ist die
/// Schaltfläche schlicht deaktiviert der Grund steht ohnehin in der Spalte „Hinweis".
/// </summary>
public bool CanToggle { get; init; } = true;
}
/// <summary>
/// Monatszeile der BWA-Übersicht. Die Werte sind bereits in die gewählte Währung umgerechnet
/// und als Text formatiert die Umrechnung passiert einmal beim Aufbau, nicht je Bindung.
/// </summary>
public sealed class MonthlyRow
{
public string Month { get; init; } = string.Empty;
public string Net { get; init; } = string.Empty;
public string Deposits { get; init; } = string.Empty;
public string Withdrawals { get; init; } = string.Empty;
public string Fees { get; init; } = string.Empty;
public string Volume { get; init; } = string.Empty;
public int Trades { get; init; }
public string Closing { get; init; } = string.Empty;
}
}
@@ -0,0 +1,45 @@
using Avalonia.Media;
using PolyTrader.App.Avalonia.Shell;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>
/// Eine Zeile der Live-Ausgabe. Farbe wird beim Erzeugen festgelegt die Zeile ist danach
/// unveränderlich, was die Virtualisierung billig macht (kein Change-Tracking je Zeile).
/// </summary>
public sealed class LogLine
{
public string Text { get; init; } = string.Empty;
public IBrush Brush { get; init; } = Brushes.White;
}
/// <summary>Anzeige-Zeile des Log Viewers (JSONL-Tagesdatei).</summary>
public sealed class LogViewerRow
{
public string Time { get; init; } = string.Empty;
public string Level { get; init; } = string.Empty;
public string Cid { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
}
/// <summary>Wer im Supervisor-Verlauf spricht bestimmt die Farbe der Kopfzeile.</summary>
public enum ChatRole { User, Agent, Error }
/// <summary>
/// Ein Eintrag im Supervisor-Analyseverlauf. Speichert die ROLLE, nicht die Farbe dadurch
/// stimmt die Darstellung nach einem Themenwechsel ohne Neuaufbau der Liste.
/// </summary>
public sealed class ChatEntry
{
public string Header { get; init; } = string.Empty;
public string Text { get; init; } = string.Empty;
public ChatRole Role { get; init; } = ChatRole.User;
public IBrush HeaderBrush => Role switch
{
ChatRole.Agent => ThemeManager.Brush("AppChatAgentBrush"),
ChatRole.Error => ThemeManager.Brush("AppNegativeBrush"),
_ => ThemeManager.Brush("AppChatUserBrush")
};
}
}
@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>Ein Eingabefeld der Einstellungen. Schreibt Änderungen direkt in das Zielobjekt.</summary>
public abstract class SettingsField : INotifyPropertyChanged
{
protected readonly object Target;
protected readonly PropertyInfo Property;
protected SettingsField(object target, PropertyInfo property, string label, string? description)
{
Target = target;
Property = property;
Label = label;
Description = description ?? string.Empty;
}
public string Label { get; }
/// <summary>Erklärungstext aus dem <see cref="DescriptionAttribute"/> erscheint klein unter dem Feld.</summary>
public string Description { get; }
public bool HasDescription => Description.Length > 0;
/// <summary>Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen).</summary>
public bool IsEditable => Property.CanWrite;
protected void Raise([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public event PropertyChangedEventHandler? PropertyChanged;
}
/// <summary>Freitext (string).</summary>
public sealed class TextSettingsField : SettingsField
{
public TextSettingsField(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>Ja/Nein (bool) als Kontrollkästchen.</summary>
public sealed class BoolSettingsField : SettingsField
{
public BoolSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public bool Value
{
get => Property.GetValue(Target) is true;
set { if (IsEditable) { Property.SetValue(Target, value); Raise(); } }
}
}
/// <summary>
/// Ganzzahl. Bewusst als Text mit invarianter Umwandlung: eine ungültige Eingabe lässt den
/// bisherigen Wert stehen, statt still eine 0 zu schreiben.
/// </summary>
public sealed class IntSettingsField : SettingsField
{
public IntSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public string Value
{
get => Convert.ToInt32(Property.GetValue(Target) ?? 0).ToString(CultureInfo.InvariantCulture);
set
{
if (!IsEditable) return;
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
Property.SetValue(Target, parsed);
Raise();
}
}
}
/// <summary>Auswahl aus festen Werten (Enum) als Aufklappliste.</summary>
public sealed class ChoiceSettingsField : SettingsField
{
public ChoiceSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d)
{
Options = Enum.GetNames(Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType);
}
public IReadOnlyList<string> Options { get; }
public string? Value
{
get => Property.GetValue(Target)?.ToString();
set
{
if (!IsEditable || value == null) return;
Property.SetValue(Target, Enum.Parse(
Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType, value));
Raise();
}
}
}
/// <summary>Nur-Lese-Anzeige (Eigenschaft ohne Setter, oder nicht editierbarer Typ).</summary>
public sealed class ReadOnlySettingsField : SettingsField
{
public ReadOnlySettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { }
public string Value
{
get
{
object? raw = Property.GetValue(Target);
if (raw is System.Collections.IEnumerable seq and not string)
return string.Join(", ", seq.Cast<object?>().Select(o => o?.ToString()));
return raw?.ToString() ?? string.Empty;
}
}
}
/// <summary>Eine Kategorie mit ihren Feldern entspricht einer Überschrift im Dialog.</summary>
public sealed class SettingsSection
{
public string Title { get; init; } = string.Empty;
public IReadOnlyList<SettingsField> Fields { get; init; } = Array.Empty<SettingsField>();
}
/// <summary>
/// Baut aus einem Einstellungsobjekt die Abschnitte für den <c>SettingsEditor</c>.
///
/// <para>Nutzt die Attribute, die für das frühere <c>PropertyGrid</c> ohnehin schon gepflegt
/// wurden: <see cref="CategoryAttribute"/> gruppiert, <see cref="DisplayNameAttribute"/> liefert
/// die Beschriftung, <see cref="DescriptionAttribute"/> den Hinweistext unter dem Feld, und
/// <c>[Browsable(false)]</c> blendet aus (z.B. Watchdog-Token und Lizenzschlüssel die gehören
/// nicht offen in die Oberfläche).</para>
///
/// <para>Damit ist der Ersatz des PropertyGrid keine Fleißarbeit je Feld: die Beschreibung der
/// Felder steht weiterhin am Modell, nur die Darstellung ist jetzt eine eigene, gestaltbare
/// Oberfläche statt einer generischen Reflection-Liste.</para>
/// </summary>
public static class SettingsModelBuilder
{
public static IReadOnlyList<SettingsSection> Build(object target)
{
var fields = target.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetIndexParameters().Length == 0)
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
.Select(p => new
{
Property = p,
Category = p.GetCustomAttribute<CategoryAttribute>()?.Category ?? "Allgemein",
Label = p.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? p.Name,
Description = p.GetCustomAttribute<DescriptionAttribute>()?.Description
})
.ToList();
return fields
.GroupBy(f => f.Category)
.Select(g => new SettingsSection
{
Title = g.Key,
Fields = g.Select(f => Create(target, f.Property, f.Label, f.Description)).ToList()
})
.ToList();
}
private static SettingsField Create(object target, PropertyInfo p, string label, string? description)
{
Type t = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType;
if (!p.CanWrite) return new ReadOnlySettingsField(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(int) || t == typeof(long)) return new IntSettingsField(target, p, label, description);
if (t == typeof(string)) return new TextSettingsField(target, p, label, description);
// Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules
// die pflegt der Modul-Tab des Dashboards).
return new ReadOnlySettingsField(target, p, label, description);
}
}
}
@@ -0,0 +1,171 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
x:Class="PolyTrader.App.Avalonia.Views.DashboardWindow"
x:DataType="vm:DashboardChartsModel"
Title="Dashboard"
Width="1714" Height="1083">
<!--
Modulübergreifendes Dashboard aus dem Core-Trade-Log.
Aufbau übernommen aus der WinForms-Fassung (docs/UI-SPEZIFIKATION-WinForms.md):
Tab „Dashboard" mit KPI-Kacheln und drei Diagrammen, Tab „Tradehistorie" mit
gefilterter Liste, Tab „Module" zum Aktivieren/Deaktivieren (wirkt nach Neustart).
Die Diagramme sind jetzt echte Steuerelemente (LiveCharts2) statt gerenderter Bitmaps —
damit interaktiv (Tooltips, Zoom) und ohne System.Drawing.
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnRefresh" Content="Aktualisieren" />
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbAccount" MinWidth="180" />
<TextBlock Text="Modul:" VerticalAlignment="Center" />
<ComboBox Name="cbModule" MinWidth="150" />
<TextBlock Text="Modus:" VerticalAlignment="Center" />
<ComboBox Name="cbMode" MinWidth="100" />
<TextBlock Text="Zeitraum:" VerticalAlignment="Center" />
<ComboBox Name="cbRange" MinWidth="110" />
</StackPanel>
</Border>
<TabControl Name="tabs">
<TabItem Header="Dashboard">
<Grid RowDefinitions="Auto,*" Margin="10">
<!-- KPI-Kacheln: zweizeilig (Titel oben, Wert darunter) wie zuvor. -->
<WrapPanel Grid.Row="0" Orientation="Horizontal">
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Netto-PnL" />
<TextBlock Name="kpiPnl" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Winrate" />
<TextBlock Name="kpiWin" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Trades" />
<TextBlock Name="kpiTrades" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Ø PnL/Trade" />
<TextBlock Name="kpiAvg" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Profit-Faktor" />
<TextBlock Name="kpiPf" Classes="value" Text="—" />
</StackPanel>
</Border>
</WrapPanel>
<Grid Grid.Row="1" RowDefinitions="*,*" ColumnDefinitions="*,*" Margin="0,12,0,0">
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="0,0,0,8">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="Equity-Kurve (kumulierter PnL)" />
<lvc:CartesianChart Name="chartEquity"
Series="{Binding EquitySeries}"
XAxes="{Binding EquityXAxes}"
TooltipPosition="Top" />
</DockPanel>
</Border>
<Border Grid.Row="1" Grid.Column="0"
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="0,0,4,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="PnL je Modul" />
<lvc:CartesianChart Name="chartModule"
Series="{Binding ModuleSeries}"
XAxes="{Binding ModuleXAxes}" />
</DockPanel>
</Border>
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="4,0,0,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="PnL je Tag" />
<lvc:CartesianChart Name="chartDay"
Series="{Binding DaySeries}"
XAxes="{Binding DayXAxes}" />
</DockPanel>
</Border>
</Grid>
</Grid>
</TabItem>
<TabItem Header="Tradehistorie">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Suche:" VerticalAlignment="Center" />
<TextBox Name="tbSearch" MinWidth="260" Watermark="Markt oder Outcome …" />
<TextBlock Text="Ergebnis:" VerticalAlignment="Center" />
<ComboBox Name="cbWinLoss" MinWidth="130" />
</StackPanel>
</Border>
<DataGrid Name="gridTrades" x:DataType="vm:DashboardTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding Module}" />
<DataGridTextColumn Header="Konto" Width="130" Binding="{Binding Account}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Geschlossen" Width="140" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Name="tabModules" Header="Module">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="10" TextWrapping="Wrap"
Text="Module aktivieren/deaktivieren. Änderungen greifen nach dem nächsten Neustart von PolyTrader." />
<DataGrid Name="gridModules" x:DataType="vm:ModuleRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Modul" Width="220" Binding="{Binding Modul}" />
<DataGridTextColumn Header="Status" Width="260" Binding="{Binding Status}" />
<DataGridTextColumn Header="Hinweis" Width="*" Binding="{Binding Hinweis}" />
<DataGridTemplateColumn Header="Aktion" Width="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:ModuleRow">
<Button Content="{Binding Aktion}"
Margin="2" Padding="8,2"
Click="OnModuleActionClick"
IsEnabled="{Binding CanToggle}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Modulübergreifendes Dashboard aus dem Core-Trade-Log. Aufbau und Verhalten entsprechen der
/// bisherigen WinForms-Ansicht; die Auswertung kommt unverändert aus <c>TradeAnalytics</c>,
/// die Diagramme jetzt aus <see cref="DashboardChartsModel"/> (LiveCharts2).
/// Layout vollständig in DashboardWindow.axaml.
/// </summary>
public partial class DashboardWindow : Window
{
private readonly DashboardChartsModel _charts = new();
private readonly ObservableCollection<DashboardTradeRow> _history = new();
private readonly ObservableCollection<ModuleRow> _modules = new();
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
private IReadOnlyList<ModuleActivationInfo>? _moduleInfos;
private string? _settingsPath;
private List<TradeRecord> _allTrades = new();
private List<DashboardTradeRow> _historyBase = new();
private bool _loading;
public DashboardWindow() => AvaloniaXamlLoader.Load(this);
/// <param name="moduleInfos">
/// Alle bekannten Module (auch deaktivierte) für den Tab „Module". <c>null</c> = kein
/// Modul-Management (Tab wird ausgeblendet, z.B. im Smoke-Test).
/// </param>
public DashboardWindow(IModuleUiHost host, ITradeLogRepository tradeLog, TradingState state,
IReadOnlyList<ModuleActivationInfo>? moduleInfos = null, string? settingsPath = null) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.dashboard", this);
_tradeLog = tradeLog;
_state = state;
_moduleInfos = moduleInfos;
_settingsPath = settingsPath;
DataContext = _charts;
this.FindControl<DataGrid>("gridTrades")!.ItemsSource = _history;
this.FindControl<DataGrid>("gridModules")!.ItemsSource = _modules;
var cbMode = this.FindControl<ComboBox>("cbMode")!;
var cbRange = this.FindControl<ComboBox>("cbRange")!;
var cbWinLoss = this.FindControl<ComboBox>("cbWinLoss")!;
cbMode.ItemsSource = new[] { "Alle", "Live", "Demo" };
cbRange.ItemsSource = new[] { "7 Tage", "30 Tage", "90 Tage", "Alle" };
cbWinLoss.ItemsSource = new[] { "Alle", "Gewinner", "Verlierer" };
cbMode.SelectedIndex = 0;
cbRange.SelectedIndex = 1; // 30 Tage, wie bisher
cbWinLoss.SelectedIndex = 0;
this.FindControl<Button>("btnRefresh")!.Click += (_, _) => RefreshData();
foreach (var name in new[] { "cbAccount", "cbModule", "cbMode", "cbRange" })
this.FindControl<ComboBox>(name)!.SelectionChanged += (_, _) => { if (!_loading) ApplyScope(); };
this.FindControl<TextBox>("tbSearch")!.TextChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
cbWinLoss.SelectionChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
RefreshData();
InitModulesTab();
// Die KPI-Farbe wird im Code gesetzt und wechselt daher nicht von selbst mit dem Thema.
void OnTheme() => ApplyScope();
Shell.ThemeManager.ThemeChanged += OnTheme;
Closed += (_, _) => Shell.ThemeManager.ThemeChanged -= OnTheme;
}
// ===== Daten laden / Scope =====
private void RefreshData()
{
if (_tradeLog == null) return;
try { _allTrades = _tradeLog.GetRecent(5000); }
catch { _allTrades = new List<TradeRecord>(); } // DB nicht bereit -> leer statt Absturz
_loading = true;
PopulateScopeCombos();
_loading = false;
ApplyScope();
}
private void PopulateScopeCombos()
{
var cbAccount = this.FindControl<ComboBox>("cbAccount")!;
var cbModule = this.FindControl<ComboBox>("cbModule")!;
int selectedAcc = (cbAccount.SelectedItem as FilterAccount)?.Id ?? -1;
var accounts = new List<FilterAccount> { new(-1, "Alle Konten") };
if (_state != null)
accounts.AddRange(_state.Accounts.Values.OrderBy(a => a.AccountId)
.Select(a => new FilterAccount(a.AccountId,
(string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : a.Name) + (a.IsDemo ? " (Demo)" : ""))));
cbAccount.ItemsSource = accounts;
int accIdx = accounts.FindIndex(a => a.Id == selectedAcc);
cbAccount.SelectedIndex = accIdx >= 0 ? accIdx : 0;
string selectedMod = cbModule.SelectedItem as string ?? "Alle Module";
var mods = new List<string> { "Alle Module" };
mods.AddRange(_allTrades.Select(t => t.ModuleName)
.Where(m => !string.IsNullOrEmpty(m))
.Distinct().OrderBy(m => m));
cbModule.ItemsSource = mods;
int modIdx = mods.IndexOf(selectedMod);
cbModule.SelectedIndex = modIdx >= 0 ? modIdx : 0;
}
private void ApplyScope()
{
int accId = (this.FindControl<ComboBox>("cbAccount")!.SelectedItem as FilterAccount)?.Id ?? -1;
string module = this.FindControl<ComboBox>("cbModule")!.SelectedItem as string ?? "Alle Module";
string mode = this.FindControl<ComboBox>("cbMode")!.SelectedItem as string ?? "Alle";
int? days = this.FindControl<ComboBox>("cbRange")!.SelectedItem as string switch
{
"7 Tage" => 7,
"30 Tage" => 30,
"90 Tage" => 90,
_ => (int?)null
};
DateTime? since = days.HasValue ? DateTime.UtcNow.AddDays(-days.Value) : null;
var scoped = _allTrades.Where(t =>
(accId < 0 || t.AccountId == accId) &&
(module == "Alle Module" || t.ModuleName == module) &&
(mode == "Alle" || (mode == "Live" && !t.IsDemo) || (mode == "Demo" && t.IsDemo)) &&
(!since.HasValue || t.ClosedAt >= since.Value)).ToList();
UpdateKpis(scoped);
_charts.Update(scoped);
_historyBase = scoped.OrderByDescending(t => t.ClosedAt).Select(ToRow).ToList();
ApplyHistoryFilter();
}
// ===== KPIs =====
private void UpdateKpis(List<TradeRecord> scoped)
{
var k = Core.Analytics.TradeAnalytics.ComputeKpis(scoped);
var pnl = this.FindControl<TextBlock>("kpiPnl")!;
pnl.Text = $"{k.NetPnl:N2} USDC";
pnl.Foreground = Shell.ThemeManager.Brush(k.NetPnl >= 0 ? "AppPositiveBrush" : "AppNegativeBrush");
this.FindControl<TextBlock>("kpiWin")!.Text = $"{k.WinRatePct:N1} %";
this.FindControl<TextBlock>("kpiTrades")!.Text = k.TradeCount.ToString();
this.FindControl<TextBlock>("kpiAvg")!.Text = $"{k.AvgPnlPerTrade:N2}";
// Verlustfreies Set wird als ∞ dargestellt (wie bisher), nicht als 999.
this.FindControl<TextBlock>("kpiPf")!.Text =
k.ProfitFactor >= Core.Analytics.TradeAnalytics.NoLossProfitFactor
? "∞"
: k.ProfitFactor.ToString("N2");
}
// ===== Tradehistorie =====
private void ApplyHistoryFilter()
{
string search = this.FindControl<TextBox>("tbSearch")!.Text?.Trim() ?? string.Empty;
string winLoss = this.FindControl<ComboBox>("cbWinLoss")!.SelectedItem as string ?? "Alle";
IEnumerable<DashboardTradeRow> rows = _historyBase;
if (search.Length > 0)
rows = rows.Where(r =>
(r.Market?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false) ||
(r.Outcome?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false));
if (winLoss == "Gewinner") rows = rows.Where(r => r.RealizedPnl > 0m);
else if (winLoss == "Verlierer") rows = rows.Where(r => r.RealizedPnl < 0m);
_history.Clear();
foreach (var r in rows) _history.Add(r);
}
private DashboardTradeRow ToRow(TradeRecord r) => new()
{
Module = r.ModuleName,
Account = ResolveAccount(r.AccountId, r.IsDemo),
Market = r.MarketQuestion,
Outcome = r.Outcome,
Side = r.Side,
EntryPrice = r.EntryPrice,
ExitPrice = r.ExitPrice,
Size = r.Size,
RealizedPnl = r.RealizedPnl,
PnlPercent = r.PnlPercent,
ClosedAt = r.ClosedAt
};
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
private sealed record FilterAccount(int Id, string Label)
{
public override string ToString() => Label;
}
// ===== Module aktivieren/deaktivieren (neustart-basiert) =====
private void InitModulesTab()
{
var tab = this.FindControl<TabItem>("tabModules")!;
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath))
{
tab.IsVisible = false; // kein Modul-Management (z.B. Smoke-Test)
return;
}
PopulateModules();
}
private HashSet<string> LoadDisabledModules()
{
try
{
var s = ServerSettings.Load(_settingsPath!);
return new HashSet<string>(s.DisabledModules, StringComparer.OrdinalIgnoreCase);
}
catch
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
}
private void PopulateModules()
{
if (_moduleInfos == null) return;
var disabled = LoadDisabledModules();
_modules.Clear();
foreach (var m in _moduleInfos)
{
bool blocked = !string.IsNullOrEmpty(m.Blocker);
bool desiredEnabled = !disabled.Contains(m.Name);
string status, hint, action;
if (blocked)
{
status = "⚠ Nicht aktivierbar"; hint = m.Blocker!; action = "—";
}
else if (m.IsRunning && desiredEnabled)
{
status = "Aktiv"; hint = ""; action = "Deaktivieren";
}
else if (m.IsRunning && !desiredEnabled)
{
status = "Aktiv stoppt nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Aktivieren";
}
else if (!m.IsRunning && desiredEnabled)
{
status = "Startet nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Deaktivieren";
}
else
{
status = "Deaktiviert"; hint = ""; action = "Aktivieren";
}
_modules.Add(new ModuleRow
{
Modul = m.Name, Status = status, Hinweis = hint, Aktion = action, CanToggle = !blocked
});
}
}
private void OnModuleActionClick(object? sender, RoutedEventArgs e)
{
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath)) return;
if (sender is not Button { DataContext: ModuleRow row }) return;
var info = _moduleInfos.FirstOrDefault(m => m.Name == row.Modul);
if (info == null || !string.IsNullOrEmpty(info.Blocker)) return;
try
{
// Frisch aus der Datei lesen, damit parallele Änderungen nicht überschrieben werden.
var settings = ServerSettings.Load(_settingsPath!);
var set = new HashSet<string>(settings.DisabledModules, StringComparer.OrdinalIgnoreCase);
if (set.Contains(info.Name)) set.Remove(info.Name);
else set.Add(info.Name);
settings.DisabledModules = set.OrderBy(x => x).ToList();
settings.Save(_settingsPath!);
PopulateModules();
}
catch (Exception ex)
{
// Fehler landet im Status der Zeile statt in einem Dialog die Ansicht bleibt bedienbar.
_modules.Clear();
_modules.Add(new ModuleRow
{
Modul = info.Name,
Status = "Speichern fehlgeschlagen",
Hinweis = ex.Message,
Aktion = "—",
CanToggle = false
});
}
}
}
}
@@ -0,0 +1,26 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PolyTrader.App.Avalonia.Views.DialogWindow"
Width="520" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<!--
Ein Dialog für alle drei Fälle, die die WinForms-Oberfläche über MessageBox/Prompt löste:
Hinweis, Ja/Nein-Rückfrage und (maskierte) Eingabe. Avalonia bringt keine MessageBox mit;
ein eigener Dialog ist der ehrlichere Weg als eine Fremdbibliothek für drei Anwendungsfälle.
-->
<StackPanel Margin="20" Spacing="12">
<TextBlock Name="lblTitle" FontSize="15" FontWeight="SemiBold" />
<TextBlock Name="lblMessage" TextWrapping="Wrap" />
<TextBox Name="txtInput" IsVisible="False" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button Name="btnCancel" Content="Abbrechen" Padding="16,6" IsCancel="True" />
<Button Name="btnOk" Content="OK" Padding="16,6" IsDefault="True" />
</StackPanel>
</StackPanel>
</Window>
@@ -0,0 +1,66 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Kleiner Allzweck-Dialog: Hinweis, Ja/Nein-Rückfrage und (maskierte) Eingabe.
/// Ersetzt die MessageBox-/Prompt-Aufrufe der WinForms-Oberfläche.
/// </summary>
public partial class DialogWindow : Window
{
public DialogWindow() => AvaloniaXamlLoader.Load(this);
private void Setup(string title, string message, bool withInput, bool masked, string okText, bool withCancel)
{
Title = title;
this.FindControl<TextBlock>("lblTitle")!.Text = title;
this.FindControl<TextBlock>("lblMessage")!.Text = message;
var input = this.FindControl<TextBox>("txtInput")!;
input.IsVisible = withInput;
if (withInput)
{
input.PasswordChar = masked ? '•' : '\0';
input.Text = string.Empty;
}
var ok = this.FindControl<Button>("btnOk")!;
var cancel = this.FindControl<Button>("btnCancel")!;
ok.Content = okText;
cancel.IsVisible = withCancel;
ok.Click += (_, _) => Close(true);
cancel.Click += (_, _) => Close(false);
}
/// <summary>Reiner Hinweis mit OK.</summary>
public static async Task Info(Window owner, string title, string message)
{
var dlg = new DialogWindow();
dlg.Setup(title, message, withInput: false, masked: false, okText: "OK", withCancel: false);
await dlg.ShowDialog(owner);
}
/// <summary>Ja/Nein-Rückfrage. Liefert <c>true</c> bei Bestätigung.</summary>
public static async Task<bool> Confirm(Window owner, string title, string message, string okText = "Ja")
{
var dlg = new DialogWindow();
dlg.Setup(title, message, withInput: false, masked: false, okText: okText, withCancel: true);
return await dlg.ShowDialog<bool>(owner);
}
/// <summary>
/// Eingabe. Liefert <c>null</c> bei Abbruch leerer String bedeutet bewusst „Wert entfernen"
/// (dieselbe Unterscheidung wie in der bisherigen Oberfläche).
/// </summary>
public static async Task<string?> Prompt(Window owner, string title, string message, bool masked = true)
{
var dlg = new DialogWindow();
dlg.Setup(title, message, withInput: true, masked: masked, okText: "OK", withCancel: true);
bool ok = await dlg.ShowDialog<bool>(owner);
return ok ? dlg.FindControl<TextBox>("txtInput")!.Text ?? string.Empty : null;
}
}
}
@@ -0,0 +1,54 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:models="using:PolyTraderSharp.Models"
x:Class="PolyTrader.App.Avalonia.Views.JobsWindow"
Title="Server Jobs"
Width="1429" Height="833">
<!--
Ansicht der Hintergrund-Jobs (JobManager).
Spaltenreihenfolge, Überschriften, Breiten und Zeitformat entsprechen der bisherigen
WinForms-Ansicht (docs/UI-SPEZIFIKATION-WinForms.md, Abschnitt JobsView).
Feste Pixelbreiten wurden dabei absichtlich in Sternbreiten übersetzt, damit das Fenster
auf anderen Auflösungen mitwächst — die Proportionen bleiben erhalten.
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Button Name="btnRefresh" Content="Aktualisieren" />
</StackPanel>
</Border>
<DataGrid Name="gridJobs" x:DataType="models:JobStatusRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Job Name" Width="150"
Binding="{Binding JobName}" />
<DataGridTextColumn Header="Status" Width="150"
Binding="{Binding StatusText}" />
<DataGridTextColumn Header="Zuletzt ausgeführt" Width="130"
Binding="{Binding LastRun, StringFormat=\{0:HH:mm:ss\}}" />
<DataGridTextColumn Header="Nächster Lauf" Width="130"
Binding="{Binding NextRun, StringFormat=\{0:HH:mm:ss\}}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="60" IsReadOnly="False"
Binding="{Binding IsEnabled}" />
<DataGridTemplateColumn Header="Aktion" Width="90">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="models:JobStatusRow">
<Button Content="Run Now"
Margin="2"
Padding="6,2"
Click="OnRunNowClick"
IsEnabled="{Binding ManualTriggerAction, Converter={x:Static ObjectConverters.IsNotNull}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Beschreibung" Width="*"
Binding="{Binding Description}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Window>
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using PolyTrader.Core.Modularity;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Hintergrund-Jobs (JobManager). Verhalten wie bisher: „Run Now" löst den ManualTrigger aus,
/// „Aktiv" schaltet den Job über die Bindung. Layout vollständig in JobsWindow.axaml.
/// </summary>
public partial class JobsWindow : Window
{
public JobsWindow() => AvaloniaXamlLoader.Load(this);
public JobsWindow(IModuleUiHost host, JobManager jobManager) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.jobs", this);
var grid = this.FindControl<DataGrid>("gridJobs")!;
grid.ItemsSource = jobManager.Jobs;
this.FindControl<Button>("btnRefresh")!.Click += (_, _) =>
{
// Die Zeilen aktualisieren sich über INotifyPropertyChanged von selbst; der Knopf
// erzwingt lediglich ein Neuzeichnen (wie das frühere dgvJobs.Refresh()).
grid.InvalidateVisual();
};
}
/// <summary>Manuellen Lauf anstoßen bewusst im Hintergrund, damit die Oberfläche nicht blockiert.</summary>
private void OnRunNowClick(object? sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: JobStatusRow job } && job.ManualTriggerAction != null)
_ = Task.Run(() => job.ManualTriggerAction.Invoke());
}
}
}
@@ -0,0 +1,76 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:shell="using:PolyTrader.App.Avalonia.Shell"
x:Class="PolyTrader.App.Avalonia.Views.LauncherWindow"
Title="PolyTrader"
Width="1600" Height="700"
WindowState="Maximized">
<!--
„Startleiste" der Anwendung. Entspricht der bisherigen LauncherForm:
oben die gemeinsame Fenster-Menüleiste, darunter die Schnellaktionen (Trading-Modi),
dann die Fenster-Buttons, unten die Statusleiste.
Die Fenster-Buttons sind bewusst datengebunden (ItemsControl über die registrierten Views)
statt einzeln im Layout festgeschrieben: die WinForms-Fassung hatte je Modul einen fest
verdrahteten Button, der bei fehlendem Modul deaktiviert wurde. Über die Bindung erscheinen
genau die Fenster, die es wirklich gibt — das ist robuster und bleibt trotzdem deklarativ.
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="4">
<Button Name="btnLiveTrading" Content="LiveTrading (—)" MinWidth="170" />
<Button Name="btnDemoTrading" Content="DemoTrading (—)" MinWidth="170" />
</StackPanel>
</Border>
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<StackPanel Orientation="Horizontal" Spacing="24">
<TextBlock Name="lblTrading" Text="Trading: —" />
<TextBlock Name="lblModules" Text="Module: —" />
<TextBlock Name="lblRatelimit" Text="API: —" />
</StackPanel>
</Border>
<ScrollViewer>
<StackPanel Margin="12" Spacing="16">
<TextBlock Text="Fenster" FontSize="14" FontWeight="SemiBold" />
<ItemsControl Name="windowButtons">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="shell:WindowMenuEntry">
<Button Command="{Binding Command}"
MinWidth="190" MinHeight="56"
Margin="0,0,8,8"
HorizontalContentAlignment="Left">
<StackPanel Orientation="Horizontal" Spacing="10">
<Image Source="{Binding Icon}" Width="28" Height="28"
VerticalAlignment="Center"
IsVisible="{Binding Icon, Converter={x:Static ObjectConverters.IsNotNull}}" />
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontWeight="{Binding FontWeight}" />
<TextBlock Text="{Binding CheckMark}" FontSize="11" Foreground="{DynamicResource AppPositiveBrush}" />
</StackPanel>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Name="lblPortInfo"
Foreground="{DynamicResource AppWarningBrush}"
TextWrapping="Wrap"
Text="Portierung läuft: Dashboard, Terminal, Einstellungen und die Modul-Fenster werden noch auf Avalonia umgestellt. Die Trading-Dienste laufen davon unabhängig." />
</StackPanel>
</ScrollViewer>
</DockPanel>
</Window>
@@ -0,0 +1,144 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Core.Modularity;
using PolyTraderSharp;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// „Startleiste" der Anwendung Gegenstück zur bisherigen LauncherForm. Öffnet die Fenster,
/// spiegelt deren Offen-Status und zeigt die Kernkennzahlen in der Statusleiste.
/// Layout vollständig in LauncherWindow.axaml.
/// </summary>
public partial class LauncherWindow : Window
{
private readonly AvaloniaUiHost _uiHost;
private readonly IServiceProvider _services;
private readonly TradingState _state;
private readonly DispatcherTimer _statusTimer = new() { Interval = TimeSpan.FromSeconds(1) };
private readonly ObservableCollection<WindowMenuEntry> _windowButtons = new();
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
public LauncherWindow(AvaloniaUiHost uiHost, IServiceProvider services) : this()
{
_uiHost = uiHost;
_services = services;
_state = services.GetRequiredService<TradingState>();
// currentViewId == null => dies ist der Launcher (rechte Menueaktion "Beenden").
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(uiHost, null, this);
this.FindControl<ItemsControl>("windowButtons")!.ItemsSource = _windowButtons;
RebuildWindowButtons();
_uiHost.OpenStateChanged += OnOpenStateChanged;
this.FindControl<Button>("btnLiveTrading")!.Click += (_, _) =>
{
_state.LiveTradingMode = Next(_state.LiveTradingMode);
UpdateTradingToggles();
};
this.FindControl<Button>("btnDemoTrading")!.Click += (_, _) =>
{
_state.DemoTradingMode = Next(_state.DemoTradingMode);
UpdateTradingToggles();
};
_statusTimer.Tick += (_, _) => UpdateStatus();
_statusTimer.Start();
UpdateStatus();
// Schliessen des Launchers laeuft ueber dieselbe Sicherheitsabfrage wie "Beenden".
Closing += (_, e) =>
{
if (_uiHost.ShutdownConfirmed) return;
e.Cancel = true;
Dispatcher.UIThread.Post(_uiHost.RequestShutdown);
};
// Die Umschalterflaechen werden im Code gesetzt und folgen dem Thema nicht von selbst.
ThemeManager.ThemeChanged += UpdateTradingToggles;
Closed += (_, _) =>
{
_statusTimer.Stop();
_uiHost.OpenStateChanged -= OnOpenStateChanged;
ThemeManager.ThemeChanged -= UpdateTradingToggles;
};
}
/// <summary>Dreistufiger Wechsel wie bisher: Inactive -> SellOnly -> Active -> Inactive.</summary>
private static TradingMode Next(TradingMode mode) => mode switch
{
TradingMode.Inactive => TradingMode.SellOnly,
TradingMode.SellOnly => TradingMode.Active,
_ => TradingMode.Inactive
};
private void OnOpenStateChanged()
{
if (Dispatcher.UIThread.CheckAccess()) RebuildWindowButtons();
else Dispatcher.UIThread.Post(RebuildWindowButtons);
}
private void RebuildWindowButtons()
{
_windowButtons.Clear();
foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
{
string id = view.Id;
bool open = _uiHost.IsOpen(id);
_windowButtons.Add(new WindowMenuEntry
{
Title = view.Title,
Icon = ViewIcons.Resolve(view.IconKey),
IsCurrent = open,
IsChecked = open,
Command = new RelayCommand(() => _uiHost.OpenView(id))
});
}
}
private void UpdateTradingToggles()
{
Apply(this.FindControl<Button>("btnLiveTrading")!, "LiveTrading", _state.LiveTradingMode);
Apply(this.FindControl<Button>("btnDemoTrading")!, "DemoTrading", _state.DemoTradingMode);
// Aussage wie in der WinForms-Oberflaeche (gruen/orange/rot); die konkreten Toene
// kommen aus dem Thema, damit sie auf hellem wie dunklem Grund lesbar bleiben.
static void Apply(Button btn, string label, TradingMode mode)
{
(string text, string brushKey) = mode switch
{
TradingMode.Active => ($"{label} (AKTIV)", "AppToggleActiveBrush"),
TradingMode.SellOnly => ($"{label} (SELL-ONLY)", "AppToggleSellOnlyBrush"),
_ => ($"{label} (DEAKTIVIERT)", "AppToggleInactiveBrush")
};
btn.Content = text;
btn.Background = ThemeManager.Brush(brushKey);
}
}
private void UpdateStatus()
{
string trading = _state.GlobalTradingPaused
? "Pausiert"
: $"Live={_state.LiveTradingMode} / Demo={_state.DemoTradingMode}";
this.FindControl<TextBlock>("lblTrading")!.Text = $"Trading: {trading}";
int moduleCount = _services.GetServices<IPolyTraderModule>().Count();
this.FindControl<TextBlock>("lblModules")!.Text = $"Module: {moduleCount}";
this.FindControl<TextBlock>("lblRatelimit")!.Text =
$"API: {(_state.IsAlchemyHealthy ? "WSS aktiv" : "Polling")}";
UpdateTradingToggles();
}
}
}
@@ -0,0 +1,167 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:acc="using:PolyTrader.Modules.Accounting.Models"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
x:Class="PolyTrader.App.Avalonia.Views.Modules.AccountingWindow"
Title="Accounting"
Width="1571" Height="1067">
<!-- Drei Tabs (Übersicht/BWA, Ledger, Abruf/Status), Aufbau nach
docs/UI-SPEZIFIKATION-WinForms.md. KPI-Kacheln zweizeilig wie zuvor. -->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblStatus" Text="Bereit." />
</Border>
<TabControl>
<TabItem Header="Übersicht / BWA">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbAccount" MinWidth="220" />
<TextBlock Text="Von:" VerticalAlignment="Center" />
<CalendarDatePicker Name="dtFrom" MinWidth="140" />
<TextBlock Text="Bis:" VerticalAlignment="Center" />
<CalendarDatePicker Name="dtTo" MinWidth="140" />
<TextBlock Text="Währung:" VerticalAlignment="Center" />
<ComboBox Name="cbCurrency" MinWidth="100" />
<Button Name="btnCompute" Content="Berechnen" />
<Button Name="btnPdf" Content="PDF-Export" />
<Button Name="btnCsv" Content="CSV-Export" />
</StackPanel>
</Border>
<DockPanel>
<WrapPanel DockPanel.Dock="Top" Margin="12" Orientation="Horizontal">
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Netto-Handelsergebnis" />
<TextBlock Name="kpiNet" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Endsaldo" />
<TextBlock Name="kpiClosing" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Einzahlungen" />
<TextBlock Name="kpiDeposits" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Auszahlungen" />
<TextBlock Name="kpiWithdrawals" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Fees" />
<TextBlock Name="kpiFees" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Rewards" />
<TextBlock Name="kpiRewards" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Handelsvolumen" />
<TextBlock Name="kpiVolume" Classes="value" Text="—" />
</StackPanel>
</Border>
<Border Classes="kpi">
<StackPanel>
<TextBlock Classes="caption" Text="Trades" />
<TextBlock Name="kpiTrades" Classes="value" Text="—" />
</StackPanel>
</Border>
</WrapPanel>
<TextBlock DockPanel.Dock="Top" Name="lblCurrencyNote" Margin="12,0"
Foreground="{DynamicResource AppMutedTextBrush}" FontSize="11" TextWrapping="Wrap" />
<DataGrid Name="gridMonthly" Margin="12" x:DataType="vm:MonthlyRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Monat" Width="140" Binding="{Binding Month}" />
<DataGridTextColumn Header="Handelsergebnis" Width="160" Binding="{Binding Net}" />
<DataGridTextColumn Header="Einzahlungen" Width="140" Binding="{Binding Deposits}" />
<DataGridTextColumn Header="Auszahlungen" Width="140" Binding="{Binding Withdrawals}" />
<DataGridTextColumn Header="Fees" Width="120" Binding="{Binding Fees}" />
<DataGridTextColumn Header="Volumen" Width="140" Binding="{Binding Volume}" />
<DataGridTextColumn Header="Trades" Width="90" Binding="{Binding Trades}" />
<DataGridTextColumn Header="Endsaldo" Width="*" Binding="{Binding Closing}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</DockPanel>
</TabItem>
<TabItem Header="Ledger">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbLedgerAccount" MinWidth="220" />
<Button Name="btnLedgerRefresh" Content="Aktualisieren" />
</StackPanel>
</Border>
<DataGrid Name="gridLedger" x:DataType="acc:LedgerEntry">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeitpunkt" Width="150" Binding="{Binding Timestamp, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Typ" Width="120" Binding="{Binding EventType}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketSlug}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Size" Width="90" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Preis" Width="80" Binding="{Binding PriceUsdc, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Brutto" Width="100" Binding="{Binding GrossUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Fee" Width="90" Binding="{Binding FeeUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Netto" Width="100" Binding="{Binding NetUsdc, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Quelle" Width="110" Binding="{Binding Source}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Abruf / Status">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbStatusAccount" MinWidth="220" />
<Button Name="btnIngestNow" Content="Inkrement jetzt" />
<Button Name="btnBackfill" Content="Backfill (volle Historie)" />
<Button Name="btnStatusRefresh" Content="Aktualisieren" />
</StackPanel>
</Border>
<DataGrid Name="gridRuns" x:DataType="acc:IngestRun">
<DataGrid.Columns>
<DataGridTextColumn Header="Start" Width="160" Binding="{Binding StartedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridTextColumn Header="Ende" Width="160" Binding="{Binding FinishedAt, StringFormat=\{0:dd.MM.yyyy HH:mm:ss\}}" />
<DataGridCheckBoxColumn Header="Backfill" Width="80" Binding="{Binding Backfill}" />
<DataGridCheckBoxColumn Header="Erfolg" Width="70" Binding="{Binding Success}" />
<DataGridTextColumn Header="Neu" Width="70" Binding="{Binding NewEntries}" />
<DataGridTextColumn Header="Duplikate" Width="90" Binding="{Binding DuplicateEntries}" />
<DataGridTextColumn Header="Meldung" Width="*" Binding="{Binding Message}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,253 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Models;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using PolyTraderSharp;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// Accounting: Periodenabrechnung mit KPI-Kacheln und Monatsübersicht, Ledger-Ansicht und
/// Abruf-/Status-Tab. Layout vollständig in AccountingWindow.axaml.
/// </summary>
public partial class AccountingWindow : Window
{
private readonly ObservableCollection<MonthlyRow> _monthly = new();
private readonly ObservableCollection<LedgerEntry> _ledger = new();
private readonly ObservableCollection<IngestRun> _runs = new();
private readonly TradingState _state = null!;
private readonly AccountingReportService _reports = null!;
private readonly ILedgerRepository _ledgerRepo = null!;
private readonly IIngestRunRepository _runRepo = null!;
private readonly AccountingIngestService _ingest = null!;
private readonly TerminalLogger _logger = null!;
private PeriodStatement? _statement;
private CurrencyContext _currency = new("USDC", 1m, true, string.Empty);
public AccountingWindow() => AvaloniaXamlLoader.Load(this);
public AccountingWindow(IModuleUiHost host, TradingState state, AccountingReportService reports,
ILedgerRepository ledgerRepo, IIngestRunRepository runRepo,
AccountingIngestService ingest, TerminalLogger logger) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "accounting.main", this);
_state = state;
_reports = reports;
_ledgerRepo = ledgerRepo;
_runRepo = runRepo;
_ingest = ingest;
_logger = logger;
this.FindControl<DataGrid>("gridMonthly")!.ItemsSource = _monthly;
this.FindControl<DataGrid>("gridLedger")!.ItemsSource = _ledger;
this.FindControl<DataGrid>("gridRuns")!.ItemsSource = _runs;
var cbCurrency = this.FindControl<ComboBox>("cbCurrency")!;
cbCurrency.ItemsSource = new[] { "USDC", "USD", "EUR" };
cbCurrency.SelectedIndex = 0;
// Vorbelegung wie bisher: laufender Monat.
var now = DateTime.Now;
this.FindControl<CalendarDatePicker>("dtFrom")!.SelectedDate = new DateTime(now.Year, now.Month, 1);
this.FindControl<CalendarDatePicker>("dtTo")!.SelectedDate = now.Date;
FillAccountCombos();
this.FindControl<Button>("btnCompute")!.Click += (_, _) => Compute();
this.FindControl<Button>("btnPdf")!.Click += async (_, _) => await ExportAsync(pdf: true);
this.FindControl<Button>("btnCsv")!.Click += async (_, _) => await ExportAsync(pdf: false);
this.FindControl<Button>("btnLedgerRefresh")!.Click += (_, _) => LoadLedger();
this.FindControl<Button>("btnStatusRefresh")!.Click += (_, _) => LoadRuns();
this.FindControl<Button>("btnIngestNow")!.Click += async (_, _) => await RunIngestAsync(backfill: false);
this.FindControl<Button>("btnBackfill")!.Click += async (_, _) => await RunIngestAsync(backfill: true);
Compute();
LoadLedger();
LoadRuns();
}
private void Status(string text) => this.FindControl<TextBlock>("lblStatus")!.Text = text;
private void FillAccountCombos()
{
var items = new List<AccountChoice> { new(null, "Alle Konten") };
items.AddRange(_state.Accounts.Values.OrderBy(a => a.AccountId)
.Select(a => new AccountChoice(a.AccountId, $"#{a.AccountId} {a.Name}{(a.IsDemo ? " (Demo)" : "")}")));
foreach (var name in new[] { "cbAccount", "cbLedgerAccount", "cbStatusAccount" })
{
var cb = this.FindControl<ComboBox>(name)!;
cb.ItemsSource = items.ToList();
cb.SelectedIndex = 0;
}
}
private int? AccountIdOf(string comboName) =>
(this.FindControl<ComboBox>(comboName)!.SelectedItem as AccountChoice)?.Id;
// ===== Übersicht / BWA =====
private void Compute()
{
try
{
DateTime from = this.FindControl<CalendarDatePicker>("dtFrom")!.SelectedDate?.Date ?? DateTime.Today;
DateTime to = this.FindControl<CalendarDatePicker>("dtTo")!.SelectedDate?.Date ?? DateTime.Today;
int? accountId = AccountIdOf("cbAccount");
string code = this.FindControl<ComboBox>("cbCurrency")!.SelectedItem as string ?? "USDC";
_currency = _reports.ResolveCurrency(code, to);
_statement = _reports.BuildStatement(accountId, from, to);
string unit = _currency.Code;
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, _currency);
this.FindControl<TextBlock>("kpiNet")!.Text = $"{V(_statement.NetTradingResultUsdc):N2} {unit}";
this.FindControl<TextBlock>("kpiClosing")!.Text = $"{V(_statement.ClosingBalanceUsdc):N2} {unit}";
this.FindControl<TextBlock>("kpiDeposits")!.Text = $"{V(_statement.Deposits):N2} {unit}";
this.FindControl<TextBlock>("kpiWithdrawals")!.Text = $"{V(_statement.Withdrawals):N2} {unit}";
this.FindControl<TextBlock>("kpiFees")!.Text = $"{V(_statement.Fees):N2} {unit}";
this.FindControl<TextBlock>("kpiRewards")!.Text = $"{V(_statement.Rewards):N2} {unit}";
this.FindControl<TextBlock>("kpiVolume")!.Text = $"{V(_statement.TradeVolume):N2} {unit}";
this.FindControl<TextBlock>("kpiTrades")!.Text = _statement.TradeCount.ToString();
this.FindControl<TextBlock>("lblCurrencyNote")!.Text = _currency.Note;
_monthly.Clear();
foreach (var m in _reports.BuildMonthly(accountId, from, to))
_monthly.Add(new MonthlyRow
{
Month = m.From.ToString("MM/yyyy"),
Net = $"{V(m.NetTradingResultUsdc):N2}",
Deposits = $"{V(m.Deposits):N2}",
Withdrawals = $"{V(m.Withdrawals):N2}",
Fees = $"{V(m.Fees):N2}",
Volume = $"{V(m.TradeVolume):N2}",
Trades = m.TradeCount,
Closing = $"{V(m.ClosingBalanceUsdc):N2}"
});
Status($"Abrechnung berechnet ({from:dd.MM.yyyy}{to:dd.MM.yyyy}, {unit}).");
}
catch (Exception ex)
{
Status($"Berechnung fehlgeschlagen: {ex.Message}");
}
}
// ===== Export =====
private async Task ExportAsync(bool pdf)
{
if (_statement == null) { Status("Bitte zuerst berechnen."); return; }
try
{
var picker = await StorageProvider.SaveFilePickerAsync(new global::Avalonia.Platform.Storage.FilePickerSaveOptions
{
Title = pdf ? "PDF-Export" : "CSV-Export",
SuggestedFileName = $"abrechnung_{_statement.From:yyyyMMdd}_{_statement.To:yyyyMMdd}" + (pdf ? ".pdf" : ".csv"),
DefaultExtension = pdf ? "pdf" : "csv"
});
if (picker == null) return;
string path = picker.Path.LocalPath;
int? accountId = AccountIdOf("cbAccount");
var entries = _ledgerRepo.Query(accountId, _statement.From, _statement.To, 100000);
if (pdf)
{
byte[] bytes = PdfExporter.Render(_statement,
_reports.BuildMonthly(accountId, _statement.From, _statement.To),
entries, _currency.Code, _currency.Factor, _currency.Note);
await File.WriteAllBytesAsync(path, bytes);
}
else
{
await File.WriteAllTextAsync(path, CsvExporter.Statement(_statement) + "\n" + CsvExporter.Ledger(entries));
}
_logger.Info($"Accounting-Export geschrieben: {path}");
Status($"Export gespeichert: {path}");
}
catch (Exception ex)
{
Status($"Export fehlgeschlagen: {ex.Message}");
}
}
// ===== Ledger =====
private void LoadLedger()
{
try
{
_ledger.Clear();
foreach (var e in _ledgerRepo.Query(AccountIdOf("cbLedgerAccount"), null, null, 5000))
_ledger.Add(e);
Status($"{_ledger.Count} Ledger-Sätze geladen.");
}
catch (Exception ex)
{
Status($"Ledger konnte nicht geladen werden: {ex.Message}");
}
}
// ===== Abruf / Status =====
private void LoadRuns()
{
try
{
_runs.Clear();
foreach (var r in _runRepo.GetRecent(AccountIdOf("cbStatusAccount"), 200)) _runs.Add(r);
}
catch (Exception ex)
{
Status($"Abrufhistorie konnte nicht geladen werden: {ex.Message}");
}
}
private async Task RunIngestAsync(bool backfill)
{
var incremental = this.FindControl<Button>("btnIngestNow")!;
var full = this.FindControl<Button>("btnBackfill")!;
incremental.IsEnabled = full.IsEnabled = false;
try
{
Status(backfill ? "Backfill läuft …" : "Inkrementeller Abruf läuft …");
await _ingest.IngestAllAsync(backfill, CancellationToken.None);
LoadRuns();
LoadLedger();
Status($"Abruf abgeschlossen um {DateTime.Now:HH:mm:ss}.");
}
catch (Exception ex)
{
Status($"Abruf fehlgeschlagen: {ex.Message}");
}
finally
{
incremental.IsEnabled = full.IsEnabled = true;
}
}
private sealed record AccountChoice(int? Id, string Label)
{
public override string ToString() => Label;
}
}
}
@@ -0,0 +1,161 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:ct="using:PolyTraderSharp.Models"
x:Class="PolyTrader.App.Avalonia.Views.Modules.CopyTradingWindow"
Title="Copytrading"
Width="1714" Height="1167">
<!--
Ein Fenster fürs ganze Modul, vier Tabs Aufbau nach docs/UI-SPEZIFIKATION-WinForms.md.
Spaltenreihenfolge und -überschriften entsprechen der bisherigen Oberfläche; feste
Pixelbreiten sind in Sternbreiten übersetzt, damit das Fenster mitwächst.
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<TabControl>
<TabItem Header="Master-Trader">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Button Name="btnTraderRefresh" Content="Aktualisieren" />
<Button Name="btnTraderSave" Content="Speichern" />
<TextBlock Name="lblTraderHint" VerticalAlignment="Center"
Margin="12,0,0,0" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<Grid ColumnDefinitions="*,4,460">
<DataGrid Name="gridTraders" Grid.Column="0" x:DataType="ct:TrackedTrader">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="50" Binding="{Binding Id}" />
<DataGridTextColumn Header="Wallet" Width="170" Binding="{Binding WalletAddress}" />
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding DisplayName}" />
<DataGridTextColumn Header="Kategorie" Width="110" Binding="{Binding Category}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="50" Binding="{Binding IsActive}" />
<DataGridTextColumn Header="Trades (7T)" Width="90" Binding="{Binding TotalTrades}" />
<DataGridTextColumn Header="Winrate %" Width="90" Binding="{Binding Winrate30t, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="PnL (7T)" Width="100" Binding="{Binding TotalPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-PnL (30T)" Width="110" Binding="{Binding CopyPnl30d, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Profit-Faktor" Width="100" Binding="{Binding CopyProfitFactor, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Ø PnL/Trade" Width="100" Binding="{Binding CopyAvgPnlPerTrade, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Copy-Trades" Width="90" Binding="{Binding CopyTradeCount30d}" />
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<DockPanel Grid.Column="2">
<HeaderedContentControl DockPanel.Dock="Bottom" Header="Zugewiesene Accounts"
Margin="8" Height="220">
<ListBox Name="listAccounts">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:AccountAssignment">
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}"
Content="{Binding Label}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</HeaderedContentControl>
<controls:SettingsEditor Name="editorTrader" />
</DockPanel>
</Grid>
</DockPanel>
</TabItem>
<TabItem Header="Offene Trades">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Button Name="btnOpenRefresh" Content="Aktualisieren" />
</StackPanel>
</Border>
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblOpenSummary" Text="—" />
</Border>
<DataGrid Name="gridOpen" x:DataType="vm:CopyOpenTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Account" Width="120" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="120" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="55" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="65" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Aktuell" Width="65" Binding="{Binding Current, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="80" Binding="{Binding Amount, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Wert" Width="80" Binding="{Binding Value, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgewinn" Width="90" Binding="{Binding Unrealized, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Buchgew. %" Width="80" Binding="{Binding UnrealizedPct, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Status" Width="90" Binding="{Binding Status}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Geschlossene Trades">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnClosedRefresh" Content="Aktualisieren" />
<TextBlock Text="Markt:" VerticalAlignment="Center" />
<TextBox Name="tbClosedMarket" MinWidth="200" Watermark="Suche …" />
<TextBlock Text="Master:" VerticalAlignment="Center" />
<ComboBox Name="cbClosedMaster" MinWidth="160" />
<TextBlock Text="Ergebnis:" VerticalAlignment="Center" />
<ComboBox Name="cbClosedResult" MinWidth="120" />
<TextBlock Text="Von:" VerticalAlignment="Center" />
<CalendarDatePicker Name="dtClosedFrom" MinWidth="140" />
<TextBlock Text="Bis:" VerticalAlignment="Center" />
<CalendarDatePicker Name="dtClosedTo" MinWidth="140" />
<Button Name="btnClosedReset" Content="Zurücksetzen" />
</StackPanel>
</Border>
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblClosedSummary" Text="—" />
</Border>
<DataGrid Name="gridClosed" x:DataType="vm:CopyClosedTradeRow">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="60" Binding="{Binding TradeId}" />
<DataGridTextColumn Header="Account" Width="130" Binding="{Binding Account}" />
<DataGridTextColumn Header="Master-Trader" Width="130" Binding="{Binding Trader}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding Market}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding Entry, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding Exit, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="70" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="80" Binding="{Binding Pnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="70" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridTextColumn Header="Eröffnet" Width="130" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Geschlossen" Width="130" Binding="{Binding ClosedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Account-Einstellungen">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbSettingsAccount" MinWidth="240" />
<Button Name="btnSettingsSave" Content="Speichern" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<controls:SettingsEditor Name="editorAccount" />
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.Controls;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// Copytrading-Fenster mit vier Tabs: Master-Trader, Offene Trades, Geschlossene Trades,
/// Account-Einstellungen. Layout vollständig in CopyTradingWindow.axaml.
/// </summary>
public partial class CopyTradingWindow : Window
{
private readonly ObservableCollection<TrackedTrader> _traders = new();
private readonly ObservableCollection<AccountAssignment> _assignments = new();
private readonly ObservableCollection<CopyOpenTradeRow> _openTrades = new();
private readonly ObservableCollection<CopyClosedTradeRow> _closedTrades = new();
private readonly TradingState _state;
private readonly CopyTradingState _copyState;
private readonly ITrackedTraderRepository _traderRepo;
private readonly ICopyTradingAccountSettingsRepository _settingsRepo;
private readonly ICopyTradeLogRepository _tradeLog;
private List<CopyClosedTradeRow> _closedBase = new();
private bool _loading;
public CopyTradingWindow() => AvaloniaXamlLoader.Load(this);
public CopyTradingWindow(IModuleUiHost host, TradingState state, CopyTradingState copyState,
ITrackedTraderRepository traderRepo, ICopyTradingAccountSettingsRepository settingsRepo,
ICopyTradeLogRepository tradeLog) : this()
{
this.FindControl<WindowMenuBar>("menuBar")!.Attach(host, "copytrading.main", this);
_state = state;
_copyState = copyState;
_traderRepo = traderRepo;
_settingsRepo = settingsRepo;
_tradeLog = tradeLog;
// --- Master-Trader ---
var gridTraders = this.FindControl<DataGrid>("gridTraders")!;
gridTraders.ItemsSource = _traders;
gridTraders.SelectionChanged += (_, _) => ShowSelectedTrader();
this.FindControl<ListBox>("listAccounts")!.ItemsSource = _assignments;
this.FindControl<Button>("btnTraderRefresh")!.Click += (_, _) => LoadTraders();
this.FindControl<Button>("btnTraderSave")!.Click += (_, _) => SaveSelectedTrader();
// --- Offene Trades ---
var gridOpen = this.FindControl<DataGrid>("gridOpen")!;
gridOpen.ItemsSource = _openTrades;
// Zeilenfärbung nach Buchgewinn: die Kategorie kommt aus der getesteten TradeRowColoring,
// die Farbe aus TradeRowPalette. LoadingRow greift auch bei virtualisierten Zeilen.
gridOpen.LoadingRow += (_, e) =>
{
if (e.Row.DataContext is CopyOpenTradeRow row) e.Row.Background = row.RowBrush;
};
this.FindControl<Button>("btnOpenRefresh")!.Click += (_, _) => LoadOpenTrades();
// --- Geschlossene Trades ---
var gridClosed = this.FindControl<DataGrid>("gridClosed")!;
gridClosed.ItemsSource = _closedTrades;
gridClosed.LoadingRow += (_, e) =>
{
if (e.Row.DataContext is CopyClosedTradeRow row) e.Row.Background = row.RowBrush;
};
var cbResult = this.FindControl<ComboBox>("cbClosedResult")!;
cbResult.ItemsSource = new[] { "Alle", "Gewinner", "Verlierer" };
cbResult.SelectedIndex = 0;
this.FindControl<Button>("btnClosedRefresh")!.Click += (_, _) => LoadClosedTrades();
this.FindControl<Button>("btnClosedReset")!.Click += (_, _) => ResetClosedFilter();
this.FindControl<TextBox>("tbClosedMarket")!.TextChanged += (_, _) => { if (!_loading) ApplyClosedFilter(); };
foreach (var n in new[] { "cbClosedMaster", "cbClosedResult" })
this.FindControl<ComboBox>(n)!.SelectionChanged += (_, _) => { if (!_loading) ApplyClosedFilter(); };
foreach (var n in new[] { "dtClosedFrom", "dtClosedTo" })
this.FindControl<CalendarDatePicker>(n)!.SelectedDateChanged += (_, _) => { if (!_loading) ApplyClosedFilter(); };
// --- Account-Einstellungen ---
var cbAcc = this.FindControl<ComboBox>("cbSettingsAccount")!;
cbAcc.SelectionChanged += (_, _) => ShowAccountSettings();
this.FindControl<Button>("btnSettingsSave")!.Click += (_, _) => SaveAccountSettings();
LoadTraders();
LoadOpenTrades();
LoadClosedTrades();
LoadAccountList();
// Zeilenhintergruende werden in LoadingRow gesetzt und wechseln daher nicht von selbst
// mit dem Thema. Ein Neuzuweisen der Quelle laesst die Zeilen neu erzeugen.
void OnTheme()
{
gridOpen.ItemsSource = null; gridOpen.ItemsSource = _openTrades;
gridClosed.ItemsSource = null; gridClosed.ItemsSource = _closedTrades;
}
Shell.ThemeManager.ThemeChanged += OnTheme;
Closed += (_, _) => Shell.ThemeManager.ThemeChanged -= OnTheme;
}
// ===== Master-Trader =====
private void LoadTraders()
{
_traders.Clear();
foreach (var t in _copyState.Traders.Values.OrderBy(t => t.Id)) _traders.Add(t);
var grid = this.FindControl<DataGrid>("gridTraders")!;
if (_traders.Count > 0 && grid.SelectedItem == null) grid.SelectedIndex = 0;
}
private void ShowSelectedTrader()
{
var trader = this.FindControl<DataGrid>("gridTraders")!.SelectedItem as TrackedTrader;
this.FindControl<SettingsEditor>("editorTrader")!.Show(trader);
_assignments.Clear();
if (trader == null) return;
foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId))
_assignments.Add(new AccountAssignment
{
AccountId = acc.AccountId,
Label = $"#{acc.AccountId} {acc.Name}{(acc.IsDemo ? " (Demo)" : "")}",
IsAssigned = trader.AssignedAccountIds.Contains(acc.AccountId)
});
}
private void SaveSelectedTrader()
{
if (this.FindControl<DataGrid>("gridTraders")!.SelectedItem is not TrackedTrader trader) return;
trader.AssignedAccountIds = _assignments.Where(a => a.IsAssigned).Select(a => a.AccountId).ToHashSet();
_traderRepo.Update(trader);
_copyState.Traders[trader.Id] = trader; // Hot-Path-State synchron halten
this.FindControl<TextBlock>("lblTraderHint")!.Text =
$"Gespeichert: #{trader.Id} {trader.DisplayName} ({trader.AssignedAccountIds.Count} Account(s)) um {DateTime.Now:HH:mm:ss}.";
}
// ===== Offene Trades =====
private void LoadOpenTrades()
{
_openTrades.Clear();
foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId))
foreach (var p in acc.OpenPositions.Values.OrderByDescending(p => p.OpenedAt))
{
decimal unrealized = p.CurrentValueUsd - p.AmountUsd;
decimal pct = p.AmountUsd > 0m ? 100m * unrealized / p.AmountUsd : 0m;
_openTrades.Add(new CopyOpenTradeRow
{
Account = acc.Name + (acc.IsDemo ? " (Demo)" : ""),
Trader = string.IsNullOrEmpty(p.SourceTraderName) ? $"#{p.SourceTraderId}" : p.SourceTraderName,
Market = p.MarketQuestion,
Outcome = p.Outcome,
Side = p.Side,
Entry = p.EntryPrice,
Current = p.CurrentPrice,
Size = p.Size,
Amount = p.AmountUsd,
Value = p.CurrentValueUsd,
Unrealized = unrealized,
UnrealizedPct = pct,
Status = p.ExitPending ? "Exit läuft" : "Offen"
});
}
decimal totalAmount = _openTrades.Sum(r => r.Amount);
decimal totalUnreal = _openTrades.Sum(r => r.Unrealized);
this.FindControl<TextBlock>("lblOpenSummary")!.Text =
$"{_openTrades.Count} offene Position(en) · Einsatz {totalAmount:N2} USDC · Buchgewinn {totalUnreal:N2} USDC";
}
// ===== Geschlossene Trades =====
private void LoadClosedTrades()
{
try
{
var trades = _tradeLog.Find(_ => true);
_closedBase = trades
.OrderByDescending(t => t.ClosedAt)
.Select(t => new CopyClosedTradeRow
{
TradeId = t.TradeId,
Account = ResolveAccount(t.AccountId, t.IsDemo),
Trader = ResolveTrader(t.SourceTraderId),
Market = t.MarketQuestion,
Outcome = t.Outcome,
Side = t.Side,
Entry = t.EntryPrice,
Exit = t.ExitPrice,
Size = t.Size,
Pnl = t.RealizedPnl,
PnlPercent = t.PnlPercent,
OpenedAt = t.OpenedAt,
ClosedAt = t.ClosedAt
})
.ToList();
}
catch
{
_closedBase = new List<CopyClosedTradeRow>(); // DB nicht bereit -> leer statt Absturz
}
_loading = true;
var cbMaster = this.FindControl<ComboBox>("cbClosedMaster")!;
var masters = new List<string> { "Alle Master" };
masters.AddRange(_closedBase.Select(r => r.Trader).Distinct().OrderBy(x => x));
cbMaster.ItemsSource = masters;
cbMaster.SelectedIndex = 0;
_loading = false;
ApplyClosedFilter();
}
/// <summary>Kontoname aus dem State; fällt auf die ID zurück, wenn das Konto fehlt.</summary>
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
return _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name)
? acc.Name + suffix
: $"#{accountId}{suffix}";
}
private string ResolveTrader(int traderId) =>
_copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName)
? t.DisplayName
: $"#{traderId}";
private void ResetClosedFilter()
{
_loading = true;
this.FindControl<TextBox>("tbClosedMarket")!.Text = string.Empty;
this.FindControl<ComboBox>("cbClosedMaster")!.SelectedIndex = 0;
this.FindControl<ComboBox>("cbClosedResult")!.SelectedIndex = 0;
this.FindControl<CalendarDatePicker>("dtClosedFrom")!.SelectedDate = null;
this.FindControl<CalendarDatePicker>("dtClosedTo")!.SelectedDate = null;
_loading = false;
ApplyClosedFilter();
}
private void ApplyClosedFilter()
{
string market = this.FindControl<TextBox>("tbClosedMarket")!.Text?.Trim() ?? string.Empty;
string master = this.FindControl<ComboBox>("cbClosedMaster")!.SelectedItem as string ?? "Alle Master";
string result = this.FindControl<ComboBox>("cbClosedResult")!.SelectedItem as string ?? "Alle";
DateTime? from = this.FindControl<CalendarDatePicker>("dtClosedFrom")!.SelectedDate;
DateTime? to = this.FindControl<CalendarDatePicker>("dtClosedTo")!.SelectedDate;
IEnumerable<CopyClosedTradeRow> rows = _closedBase;
if (market.Length > 0)
rows = rows.Where(r => r.Market.Contains(market, StringComparison.OrdinalIgnoreCase));
if (master != "Alle Master") rows = rows.Where(r => r.Trader == master);
if (result == "Gewinner") rows = rows.Where(r => r.Pnl > 0m);
else if (result == "Verlierer") rows = rows.Where(r => r.Pnl < 0m);
if (from.HasValue) rows = rows.Where(r => r.ClosedAt >= from.Value.Date);
if (to.HasValue) rows = rows.Where(r => r.ClosedAt < to.Value.Date.AddDays(1));
var list = rows.ToList();
_closedTrades.Clear();
foreach (var r in list) _closedTrades.Add(r);
decimal pnl = list.Sum(r => r.Pnl);
int wins = list.Count(r => r.Pnl > 0m);
decimal winrate = list.Count > 0 ? 100m * wins / list.Count : 0m;
this.FindControl<TextBlock>("lblClosedSummary")!.Text =
$"{list.Count} Trade(s) · PnL {pnl:N2} USDC · Winrate {winrate:N1} %";
}
// ===== Account-Einstellungen =====
private void LoadAccountList()
{
var cb = this.FindControl<ComboBox>("cbSettingsAccount")!;
var items = _state.Accounts.Values.OrderBy(a => a.AccountId)
.Select(a => new AccountChoice(a.AccountId, $"#{a.AccountId} {a.Name}{(a.IsDemo ? " (Demo)" : "")}"))
.ToList();
cb.ItemsSource = items;
if (items.Count > 0) cb.SelectedIndex = 0;
}
private void ShowAccountSettings()
{
var editor = this.FindControl<SettingsEditor>("editorAccount")!;
if (this.FindControl<ComboBox>("cbSettingsAccount")!.SelectedItem is not AccountChoice choice)
{
editor.Show(null);
return;
}
// Fehlt für ein Konto noch ein Eintrag, wird er hier angelegt wie bei der Hydration.
if (!_copyState.AccountSettings.TryGetValue(choice.Id, out var settings))
{
settings = new CopyTradingAccountSettings { AccountId = choice.Id };
_copyState.AccountSettings[choice.Id] = settings;
}
editor.Show(settings);
}
private void SaveAccountSettings()
{
if (this.FindControl<ComboBox>("cbSettingsAccount")!.SelectedItem is not AccountChoice choice) return;
if (!_copyState.AccountSettings.TryGetValue(choice.Id, out var settings)) return;
_settingsRepo.Upsert(settings);
this.FindControl<TextBlock>("lblSettingsHint")!.Text =
$"Gespeichert für Account #{choice.Id} um {DateTime.Now:HH:mm:ss}.";
}
private sealed record AccountChoice(int Id, string Label)
{
public override string ToString() => Label;
}
}
}
@@ -0,0 +1,18 @@
# Modul-Fenster
Die Fenster der Module liegen bewusst **hier in der App** und nicht in den Modulprojekten.
**Warum:** Die App ist der Kompositionswurzel sie referenziert ohnehin alle vier Module. Damit
bleiben die Modulprojekte (Trading-Engine, Scanner, Supervisor, Accounting) **frei von Avalonia**.
Das ist für den kopflosen Linux-Betrieb (`--headless`) der entscheidende Punkt: der Daemon soll
keine GUI-Bibliothek mitschleppen. Zusätzlich können die Fenster so die gemeinsamen Bausteine der
Shell nutzen (`WindowMenuBar`, `SettingsEditor`, `DialogWindow`), ohne dass diese in eine eigene
Bibliothek ausgelagert werden müssen.
**Folge:** `IPolyTraderModule.RegisterUi` bleibt je Modul ein No-Op; registriert werden die Ansichten
in `Shell/ModuleViews.cs`. Die stabilen View-IDs (`copytrading.main` usw.) bleiben unverändert,
sodass Symbole, Reihenfolge und Launcher-Verhalten identisch sind.
**Alternative, falls Module später doch ihre UI selbst mitbringen sollen:** eine gemeinsame
Bibliothek `PolyTrader.Ui.Shared` mit den obigen Bausteinen anlegen und die Fenster in
`PolyTrader.Modules.X.Ui`-Projekte verschieben. Der Aufbau der Fenster bliebe dabei unverändert.
@@ -0,0 +1,97 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:rf="using:PolyTrader.Modules.ResolutionFarming.Models"
x:Class="PolyTrader.App.Avalonia.Views.Modules.ResolutionFarmingWindow"
Title="ResolutionFarming"
Width="1429" Height="1030">
<!-- Vier Tabs (Kandidaten / Positionen / Historie+Statistik / Settings), Aufbau nach
docs/UI-SPEZIFIKATION-WinForms.md: je Tab ein Grid mit „Aktualisieren", Konto-Auswahl oben. -->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbAccount" MinWidth="240" />
<Button Name="btnRefresh" Content="Aktualisieren" />
<TextBlock Name="lblStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<TabControl>
<TabItem Header="Kandidaten">
<DataGrid Name="gridCandidates" x:DataType="rf:RfCandidate">
<DataGrid.Columns>
<DataGridTextColumn Header="Gescannt" Width="140" Binding="{Binding ScannedAt, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Ask" Width="70" Binding="{Binding Ask, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Fee (bps)" Width="80" Binding="{Binding FeeBps}" />
<DataGridTextColumn Header="Netto-Edge %" Width="110" Binding="{Binding NetEdgePct, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Score" Width="80" Binding="{Binding Score, StringFormat=\{0:F2\}}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Positionen">
<DockPanel>
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblPositionsSummary" Text="—" />
</Border>
<DataGrid Name="gridPositions" x:DataType="rf:RfPosition">
<DataGrid.Columns>
<DataGridTextColumn Header="Eröffnet" Width="140" Binding="{Binding OpenedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="Einsatz" Width="90" Binding="{Binding AmountUsd, StringFormat=\{0:F2\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
<DataGridTextColumn Header="Auflösung" Width="140" Binding="{Binding EndDate, StringFormat=\{0:dd.MM.yyyy\}}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Historie / Statistik">
<DockPanel>
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblHistorySummary" Text="—" />
</Border>
<DataGrid Name="gridHistory" x:DataType="rf:RfClosedTrade">
<DataGrid.Columns>
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Outcome" Width="80" Binding="{Binding Outcome}" />
<DataGridTextColumn Header="Kategorie" Width="120" Binding="{Binding Category}" />
<DataGridTextColumn Header="Entry" Width="70" Binding="{Binding EntryPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Exit" Width="70" Binding="{Binding ExitPrice, StringFormat=\{0:F3\}}" />
<DataGridTextColumn Header="Size" Width="80" Binding="{Binding Size, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL" Width="90" Binding="{Binding RealizedPnl, StringFormat=\{0:F2\}}" />
<DataGridTextColumn Header="PnL %" Width="80" Binding="{Binding PnlPercent, StringFormat=\{0:F1\}}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Settings">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnSettingsSave" Content="Speichern" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<controls:SettingsEditor Name="editorSettings" />
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,127 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.Controls;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.ResolutionFarming.Models;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTraderSharp;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// ResolutionFarming: Kandidaten, offene Positionen, Historie und Einstellungen je Konto.
/// Layout vollständig in ResolutionFarmingWindow.axaml.
/// </summary>
public partial class ResolutionFarmingWindow : Window
{
private readonly ObservableCollection<RfCandidate> _candidates = new();
private readonly ObservableCollection<RfPosition> _positions = new();
private readonly ObservableCollection<RfClosedTrade> _history = new();
private readonly TradingState _state;
private readonly IRfCandidateRepository _candidateRepo;
private readonly IRfPositionRepository _positionRepo;
private readonly IRfClosedTradeRepository _closedRepo;
private readonly IRfSettingsRepository _settingsRepo;
private RfSettings? _currentSettings;
public ResolutionFarmingWindow() => AvaloniaXamlLoader.Load(this);
public ResolutionFarmingWindow(IModuleUiHost host, TradingState state,
IRfCandidateRepository candidateRepo, IRfPositionRepository positionRepo,
IRfClosedTradeRepository closedRepo, IRfSettingsRepository settingsRepo) : this()
{
this.FindControl<WindowMenuBar>("menuBar")!.Attach(host, "resolutionfarming.main", this);
_state = state;
_candidateRepo = candidateRepo;
_positionRepo = positionRepo;
_closedRepo = closedRepo;
_settingsRepo = settingsRepo;
this.FindControl<DataGrid>("gridCandidates")!.ItemsSource = _candidates;
this.FindControl<DataGrid>("gridPositions")!.ItemsSource = _positions;
this.FindControl<DataGrid>("gridHistory")!.ItemsSource = _history;
this.FindControl<ComboBox>("cbAccount")!.SelectionChanged += (_, _) => LoadAll();
this.FindControl<Button>("btnRefresh")!.Click += (_, _) => LoadAll();
this.FindControl<Button>("btnSettingsSave")!.Click += (_, _) => SaveSettings();
LoadAccounts();
}
private void LoadAccounts()
{
var cb = this.FindControl<ComboBox>("cbAccount")!;
var items = _state.Accounts.Values.OrderBy(a => a.AccountId)
.Select(a => new AccountChoice(a.AccountId, $"#{a.AccountId} {a.Name}{(a.IsDemo ? " (Demo)" : "")}"))
.ToList();
cb.ItemsSource = items;
if (items.Count > 0) cb.SelectedIndex = 0; else Status("Keine Konten vorhanden.");
}
private int? SelectedAccountId =>
(this.FindControl<ComboBox>("cbAccount")!.SelectedItem as AccountChoice)?.Id;
private void Status(string text) => this.FindControl<TextBlock>("lblStatus")!.Text = text;
private void LoadAll()
{
if (SelectedAccountId is not int accountId) return;
try
{
_candidates.Clear();
foreach (var c in _candidateRepo.GetRecent(accountId, 500)) _candidates.Add(c);
_positions.Clear();
foreach (var p in _positionRepo.GetOpen(accountId).OrderByDescending(p => p.OpenedAt)) _positions.Add(p);
_history.Clear();
foreach (var t in _closedRepo.Find(t => t.AccountId == accountId).OrderByDescending(t => t.Id))
_history.Add(t);
this.FindControl<TextBlock>("lblPositionsSummary")!.Text =
$"{_positions.Count} offene Position(en) · Einsatz {_positions.Sum(p => p.AmountUsd):N2} USDC";
decimal pnl = _history.Sum(t => t.RealizedPnl);
int wins = _history.Count(t => t.RealizedPnl > 0m);
decimal winrate = _history.Count > 0 ? 100m * wins / _history.Count : 0m;
this.FindControl<TextBlock>("lblHistorySummary")!.Text =
$"{_history.Count} abgeschlossen · PnL {pnl:N2} USDC · Winrate {winrate:N1} %";
_currentSettings = _settingsRepo.Get(accountId) ?? new RfSettings { AccountId = accountId };
this.FindControl<SettingsEditor>("editorSettings")!.Show(_currentSettings);
Status($"Geladen um {DateTime.Now:HH:mm:ss}.");
}
catch (Exception ex)
{
Status($"Laden fehlgeschlagen: {ex.Message}");
}
}
private void SaveSettings()
{
if (_currentSettings == null) return;
try
{
_settingsRepo.Upsert(_currentSettings);
this.FindControl<TextBlock>("lblSettingsHint")!.Text =
$"Gespeichert für Konto #{_currentSettings.AccountId} um {DateTime.Now:HH:mm:ss}.";
}
catch (Exception ex)
{
this.FindControl<TextBlock>("lblSettingsHint")!.Text = $"Speichern fehlgeschlagen: {ex.Message}";
}
}
private sealed record AccountChoice(int Id, string Label)
{
public override string ToString() => Label;
}
}
}
@@ -0,0 +1,129 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
xmlns:sup="using:PolyTrader.Modules.Supervisor.Persistence"
xmlns:cf="using:PolyTrader.Modules.Supervisor.Counterfactual"
xmlns:svc="using:PolyTrader.Modules.Supervisor.Services"
x:Class="PolyTrader.App.Avalonia.Views.Modules.SupervisorWindow"
Title="Supervisor"
Width="1786" Height="1163">
<!-- Vier Tabs (Analyse / Dossiers / Berichte / Counterfactual), Aufbau nach
docs/UI-SPEZIFIKATION-WinForms.md. Der Analyse-Tab ist chat-artig: Verlauf oben,
Eingabe unten, Profil- und Modellauswahl in der Werkzeugleiste. -->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<TabControl>
<TabItem Header="Analyse">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Profil:" VerticalAlignment="Center" />
<ComboBox Name="cbProfile" MinWidth="180" />
<TextBlock Text="Modell:" VerticalAlignment="Center" />
<TextBox Name="tbModel" MinWidth="240" Watermark="(Standardmodell)" />
<Button Name="btnClearChat" Content="Verlauf leeren" />
<TextBlock Name="lblChatStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<Border Classes="toolbar" DockPanel.Dock="Bottom">
<DockPanel>
<Button Name="btnSend" Content="Senden" DockPanel.Dock="Right" MinWidth="100" />
<TextBox Name="tbQuestion" Watermark="Frage an den Supervisor …"
AcceptsReturn="False" Margin="0,0,8,0" />
</DockPanel>
</Border>
<ScrollViewer Name="chatScroll">
<ItemsControl Name="chatLines" Margin="12">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ChatEntry">
<StackPanel Margin="0,0,0,12">
<TextBlock Text="{Binding Header}" FontWeight="SemiBold"
Foreground="{Binding HeaderBrush}" />
<SelectableTextBlock Text="{Binding Text}" TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</TabItem>
<TabItem Header="Dossiers">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnDossierRefresh" Content="Aktualisieren" />
<Button Name="btnDossierOpen" Content="Dossier öffnen" />
<TextBlock Text="SignalId:" VerticalAlignment="Center" />
<TextBox Name="tbSignalId" MinWidth="240" />
</StackPanel>
</Border>
<Grid ColumnDefinitions="*,4,*">
<DataGrid Name="gridSignals" Grid.Column="0" x:DataType="svc:SignalSummary">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeit" Width="130" Binding="{Binding FirstSeen, StringFormat=\{0:dd.MM. HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="110" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Side" Width="60" Binding="{Binding Side}" />
<DataGridTextColumn Header="Entscheidung" Width="120" Binding="{Binding LastDecision}" />
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<ScrollViewer Grid.Column="2">
<SelectableTextBlock Name="txtDossier" Margin="12" TextWrapping="Wrap"
FontFamily="Consolas,Menlo,DejaVu Sans Mono,monospace"
FontSize="12" />
</ScrollViewer>
</Grid>
</DockPanel>
</TabItem>
<TabItem Header="Berichte">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<Button Name="btnReportsRefresh" Content="Aktualisieren" />
</Border>
<DataGrid Name="gridReports" x:DataType="sup:SupervisorReport">
<DataGrid.Columns>
<DataGridTextColumn Header="Erstellt" Width="150" Binding="{Binding CreatedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Profil" Width="140" Binding="{Binding Profile}" />
<DataGridTextColumn Header="Modell" Width="200" Binding="{Binding Model}" />
<DataGridTextColumn Header="Frage" Width="300" Binding="{Binding Question}" />
<DataGridTextColumn Header="Antwort" Width="*" Binding="{Binding Answer}" />
<DataGridTextColumn Header="Tools" Width="70" Binding="{Binding ToolCallCount}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
<TabItem Header="Counterfactual">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<Button Name="btnCfRefresh" Content="Aktualisieren" />
</Border>
<DataGrid Name="gridCounterfactual" x:DataType="cf:CounterfactualRecord">
<DataGrid.Columns>
<DataGridTextColumn Header="Geprüft" Width="150" Binding="{Binding CheckedAt, StringFormat=\{0:dd.MM.yyyy HH:mm\}}" />
<DataGridTextColumn Header="Modul" Width="120" Binding="{Binding ModuleName}" />
<DataGridTextColumn Header="Markt" Width="*" Binding="{Binding MarketQuestion}" />
<DataGridTextColumn Header="Grund der Ablehnung" Width="260" Binding="{Binding Reason}" />
<DataGridTextColumn Header="Signalpreis" Width="100" Binding="{Binding SignalPrice, StringFormat=\{0:F3\}}" />
<DataGridCheckBoxColumn Header="Wäre Gewinner" Width="120" Binding="{Binding IsWinner}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,191 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Analytics;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Counterfactual;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTrader.Modules.Supervisor.Services;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views.Modules
{
/// <summary>
/// Supervisor: Analyse-Chat gegen den Agenten, Dossiers je Signal, gespeicherte Berichte und
/// Counterfactuals. Layout vollständig in SupervisorWindow.axaml.
/// </summary>
public partial class SupervisorWindow : Window
{
private readonly ObservableCollection<ChatEntry> _chat = new();
private readonly ObservableCollection<SignalSummary> _signals = new();
private readonly ObservableCollection<SupervisorReport> _reports = new();
private readonly ObservableCollection<CounterfactualRecord> _counterfactuals = new();
private readonly SupervisorAgent _agent = null!;
private readonly DossierService _dossiers = null!;
private readonly ISupervisorReportRepository _reportRepo = null!;
private readonly ISupervisorCounterfactualRepository _cfRepo = null!;
private readonly TerminalLogger _logger = null!;
public SupervisorWindow() => AvaloniaXamlLoader.Load(this);
public SupervisorWindow(IModuleUiHost host, SupervisorAgent agent, DossierService dossiers,
ISupervisorReportRepository reportRepo, ISupervisorCounterfactualRepository cfRepo,
TerminalLogger logger) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "supervisor.main", this);
_agent = agent;
_dossiers = dossiers;
_reportRepo = reportRepo;
_cfRepo = cfRepo;
_logger = logger;
this.FindControl<ItemsControl>("chatLines")!.ItemsSource = _chat;
this.FindControl<DataGrid>("gridSignals")!.ItemsSource = _signals;
this.FindControl<DataGrid>("gridReports")!.ItemsSource = _reports;
this.FindControl<DataGrid>("gridCounterfactual")!.ItemsSource = _counterfactuals;
var cbProfile = this.FindControl<ComboBox>("cbProfile")!;
cbProfile.ItemsSource = SupervisorProfiles.All.ToList();
cbProfile.SelectedIndex = 0;
this.FindControl<Button>("btnSend")!.Click += async (_, _) => await AskAsync();
this.FindControl<Button>("btnClearChat")!.Click += (_, _) => _chat.Clear();
this.FindControl<Button>("btnDossierRefresh")!.Click += (_, _) => LoadSignals();
this.FindControl<Button>("btnDossierOpen")!.Click += (_, _) => OpenDossier();
this.FindControl<Button>("btnReportsRefresh")!.Click += (_, _) => LoadReports();
this.FindControl<Button>("btnCfRefresh")!.Click += (_, _) => LoadCounterfactuals();
// Auswahl im Signal-Grid übernimmt die SignalId ins Eingabefeld.
this.FindControl<DataGrid>("gridSignals")!.SelectionChanged += (_, _) =>
{
if (this.FindControl<DataGrid>("gridSignals")!.SelectedItem is SignalSummary s)
this.FindControl<TextBox>("tbSignalId")!.Text = s.SignalId;
};
LoadSignals();
LoadReports();
LoadCounterfactuals();
}
// ===== Analyse =====
private async System.Threading.Tasks.Task AskAsync()
{
var tbQuestion = this.FindControl<TextBox>("tbQuestion")!;
string question = tbQuestion.Text?.Trim() ?? string.Empty;
if (question.Length == 0) return;
var btn = this.FindControl<Button>("btnSend")!;
var status = this.FindControl<TextBlock>("lblChatStatus")!;
AddChat("Du", question, ChatRole.User);
tbQuestion.Text = string.Empty;
btn.IsEnabled = false;
status.Text = "Der Supervisor denkt nach …";
try
{
var profile = this.FindControl<ComboBox>("cbProfile")!.SelectedItem as SupervisorProfile;
string? model = this.FindControl<TextBox>("tbModel")!.Text?.Trim();
if (string.IsNullOrWhiteSpace(model)) model = null;
// Zwischenmeldungen des Agenten (Tool-Aufrufe) laufen über die Statuszeile.
var progress = new Progress<string>(m => Dispatcher.UIThread.Post(() => status.Text = m));
var result = await _agent.AskAsync(question, model, progress, profile);
AddChat($"Supervisor ({profile?.Name ?? "Allgemein"})", result.Answer, ChatRole.Agent);
status.Text = $"{result.ToolInvocations.Count} Tool-Aufruf(e) · " +
$"{result.PromptTokens + result.CompletionTokens} Token.";
}
catch (Exception ex)
{
AddChat("Fehler", ex.Message, ChatRole.Error);
status.Text = "Anfrage fehlgeschlagen.";
_logger.Error($"Supervisor-Anfrage fehlgeschlagen: {ex.Message}");
}
finally
{
btn.IsEnabled = true;
LoadReports(); // die Anfrage wurde als Bericht abgelegt
}
}
private void AddChat(string who, string text, ChatRole role)
{
_chat.Add(new ChatEntry { Header = $"[{DateTime.Now:HH:mm:ss}] {who}", Text = text, Role = role });
this.FindControl<ScrollViewer>("chatScroll")!.ScrollToEnd();
}
// ===== Dossiers =====
private void LoadSignals()
{
try
{
_signals.Clear();
foreach (var s in _dossiers.RecentSignals(200)) _signals.Add(s);
}
catch (Exception ex)
{
_logger.Error($"Signale konnten nicht geladen werden: {ex.Message}");
}
}
private void OpenDossier()
{
string signalId = this.FindControl<TextBox>("tbSignalId")!.Text?.Trim() ?? string.Empty;
var target = this.FindControl<SelectableTextBlock>("txtDossier")!;
if (signalId.Length == 0)
{
target.Text = "Bitte eine SignalId auswählen oder eingeben.";
return;
}
try
{
var dossier = _dossiers.BuildForSignal(signalId);
target.Text = DossierBuilder.ToMarkdown(dossier);
}
catch (Exception ex)
{
target.Text = $"Dossier konnte nicht erstellt werden: {ex.Message}";
}
}
// ===== Berichte / Counterfactual =====
private void LoadReports()
{
try
{
_reports.Clear();
foreach (var r in _reportRepo.GetRecent(200)) _reports.Add(r);
}
catch (Exception ex)
{
_logger.Error($"Berichte konnten nicht geladen werden: {ex.Message}");
}
}
private void LoadCounterfactuals()
{
try
{
_counterfactuals.Clear();
foreach (var c in _cfRepo.GetRecent(500)) _counterfactuals.Add(c);
}
catch (Exception ex)
{
_logger.Error($"Counterfactuals konnten nicht geladen werden: {ex.Message}");
}
}
}
}
@@ -0,0 +1,75 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:models="using:PolyTraderSharp.Models"
x:Class="PolyTrader.App.Avalonia.Views.SettingsWindow"
Title="Server Settings"
Width="1185" Height="820">
<!--
Einstellungen. Ersetzt die beiden PropertyGrids der WinForms-Fassung durch den
kategorisierten SettingsEditor (Überschrift, Beschriftung links, Feld rechts,
Erklärung klein darunter).
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<Border Classes="statusbar" DockPanel.Dock="Bottom">
<TextBlock Name="lblStatus" Text="Bereit." />
</Border>
<TabControl>
<TabItem Header="Allgemeine Einstellungen">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Button Name="btnSave" Content="Speichern" />
<Button Name="btnReload" Content="Neu laden" />
<Separator Margin="6,0" />
<Button Name="btnGenMasterKey" Content="Master-Key erzeugen"
ToolTip.Tip="Erzeugt einen zufälligen AES-Master-Key (nur wenn noch keiner existiert)." />
<Button Name="btnOpenRouterKey" Content="OpenRouter-Key setzen …"
ToolTip.Tip="Speichert den OpenRouter-API-Key für den Supervisor (gitignorierte Datei openrouter.key)." />
<Button Name="btnWatchdogToken" Content="Watchdog-Token setzen …"
ToolTip.Tip="Speichert den Watchdog-Agent-Token (maskierte Eingabe, bei gesetztem Master-Key verschlüsselt)." />
<Button Name="btnTestWatchdog" Content="Test-Heartbeat senden"
ToolTip.Tip="Sendet sofort einen Heartbeat an den konfigurierten Watchdog-Server." />
</StackPanel>
</Border>
<controls:SettingsEditor Name="editorServer" />
</DockPanel>
</TabItem>
<TabItem Header="Polymarket Accounts">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Button Name="btnAccNew" Content="Neuer Account" />
<Button Name="btnAccDelete" Content="Löschen" />
<Button Name="btnAccSave" Content="Account speichern" />
</StackPanel>
</Border>
<Grid ColumnDefinitions="380,4,*">
<DataGrid Name="gridAccounts" Grid.Column="0" x:DataType="models:AccountState">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Width="50" Binding="{Binding AccountId}" />
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}" />
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
<DataGridCheckBoxColumn Header="Aktiv" Width="60" Binding="{Binding IsActive}" />
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<controls:SettingsEditor Name="editorAccount" Grid.Column="2" />
</Grid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,327 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PolyTrader.App.Avalonia.Controls;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Einstellungen: allgemeine Server-Settings und die Polymarket-Accounts. Beide Bereiche nutzen
/// den <see cref="SettingsEditor"/> anstelle der früheren PropertyGrids.
/// Layout vollständig in SettingsWindow.axaml.
/// </summary>
public partial class SettingsWindow : Window
{
private const string SettingsPath = "server_settings.xml";
private readonly ObservableCollection<AccountState> _accounts = new();
private ServerSettings _settings = new();
private IAccountRepository? _accountRepo;
private TradingState? _state;
private WatchdogHeartbeatService? _watchdog;
private MullvadVpnService? _vpn;
private TerminalLogger? _logger;
public SettingsWindow() => AvaloniaXamlLoader.Load(this);
public SettingsWindow(IModuleUiHost host, IAccountRepository accountRepo, TradingState state,
TerminalLogger logger, WatchdogHeartbeatService? watchdog = null,
MullvadVpnService? vpn = null) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.settings", this);
_accountRepo = accountRepo;
_state = state;
_logger = logger;
_watchdog = watchdog;
_vpn = vpn;
var grid = this.FindControl<DataGrid>("gridAccounts")!;
grid.ItemsSource = _accounts;
grid.SelectionChanged += (_, _) =>
this.FindControl<SettingsEditor>("editorAccount")!.Show(grid.SelectedItem);
this.FindControl<Button>("btnSave")!.Click += (_, _) => SaveServerSettings();
this.FindControl<Button>("btnReload")!.Click += (_, _) => ReloadServerSettings();
this.FindControl<Button>("btnGenMasterKey")!.Click += async (_, _) => await GenerateMasterKeyAsync();
this.FindControl<Button>("btnOpenRouterKey")!.Click += async (_, _) => await SetOpenRouterKeyAsync();
this.FindControl<Button>("btnWatchdogToken")!.Click += async (_, _) => await SetWatchdogTokenAsync();
this.FindControl<Button>("btnTestWatchdog")!.Click += async (_, _) => await TestWatchdogAsync();
this.FindControl<Button>("btnAccNew")!.Click += (_, _) => AddAccount();
this.FindControl<Button>("btnAccDelete")!.Click += async (_, _) => await DeleteAccountAsync();
this.FindControl<Button>("btnAccSave")!.Click += (_, _) => SaveSelectedAccount();
ReloadServerSettings();
LoadAccounts();
UpdateMasterKeyButtonState();
}
private void Status(string text) => this.FindControl<TextBlock>("lblStatus")!.Text = text;
// ===== Server-Settings =====
private void ReloadServerSettings()
{
_settings = ServerSettings.Load(SettingsPath);
this.FindControl<SettingsEditor>("editorServer")!.Show(_settings);
Status("Einstellungen geladen.");
}
private void SaveServerSettings()
{
try
{
_settings.Save(SettingsPath);
_vpn?.ReloadSettings();
_watchdog?.ReloadSettings();
_logger?.Info("Server-Einstellungen gespeichert und Dienste neu geladen.");
Status($"Gespeichert um {DateTime.Now:HH:mm:ss}.");
}
catch (Exception ex)
{
Status($"Speichern fehlgeschlagen: {ex.Message}");
}
}
// ===== Polymarket Accounts =====
private void LoadAccounts()
{
if (_state == null) return;
_accounts.Clear();
foreach (var a in _state.Accounts.Values.OrderBy(a => a.AccountId)) _accounts.Add(a);
var grid = this.FindControl<DataGrid>("gridAccounts")!;
if (_accounts.Count > 0) grid.SelectedIndex = 0;
}
private void AddAccount()
{
if (_state == null || _accountRepo == null) return;
int newId = _state.Accounts.Count > 0 ? _state.Accounts.Keys.Max() + 1 : 1;
var acc = new AccountState { AccountId = newId, Name = "Neuer Account" };
_state.Accounts[acc.AccountId] = acc;
_accountRepo.Upsert(acc);
_accounts.Add(acc);
this.FindControl<DataGrid>("gridAccounts")!.SelectedItem = acc;
Status($"Account #{newId} angelegt.");
}
private async System.Threading.Tasks.Task DeleteAccountAsync()
{
if (_state == null || _accountRepo == null) return;
if (this.FindControl<DataGrid>("gridAccounts")!.SelectedItem is not AccountState acc) return;
bool ok = await DialogWindow.Confirm(this, "Löschen bestätigen",
$"Account „{acc.Name}\" (ID {acc.AccountId}) wirklich löschen?", "Löschen");
if (!ok) return;
_state.Accounts.TryRemove(acc.AccountId, out _);
_accountRepo.Delete(acc.AccountId);
_accounts.Remove(acc);
this.FindControl<SettingsEditor>("editorAccount")!.Show(null);
Status($"Account #{acc.AccountId} gelöscht.");
}
private void SaveSelectedAccount()
{
if (this.FindControl<DataGrid>("gridAccounts")!.SelectedItem is not AccountState acc) return;
_accountRepo?.Upsert(acc);
_state?.Accounts.AddOrUpdate(acc.AccountId, acc, (_, _) => acc);
Status($"Account #{acc.AccountId} gespeichert um {DateTime.Now:HH:mm:ss}.");
}
// ===== Master-Key (at-rest-Verschlüsselung) =====
private static string MasterKeyFilePath => Path.Combine(AppContext.BaseDirectory, "master.key");
private static bool MasterKeyExists() =>
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"))
|| File.Exists(MasterKeyFilePath);
/// <summary>Schaltfläche nur aktiv, solange KEIN Master-Key existiert (Überschreiben = Lockout-Gefahr).</summary>
private void UpdateMasterKeyButtonState() =>
this.FindControl<Button>("btnGenMasterKey")!.IsEnabled = !MasterKeyExists();
private async System.Threading.Tasks.Task GenerateMasterKeyAsync()
{
// Sicherheitsnetz gegen Race/Doppelklick: einen bestehenden Key NIEMALS überschreiben.
if (MasterKeyExists())
{
await DialogWindow.Info(this, "Master-Key vorhanden",
"Es existiert bereits ein Master-Key Erzeugung abgebrochen. Ein Überschreiben würde den " +
"Zugriff auf bereits verschlüsselte Wallet-Keys unwiederbringlich zerstören.");
UpdateMasterKeyButtonState();
return;
}
try
{
byte[] keyBytes = RandomNumberGenerator.GetBytes(32); // 256-Bit-Schlüssel
File.WriteAllText(MasterKeyFilePath, Convert.ToBase64String(keyBytes));
RestrictToOwner(MasterKeyFilePath);
_logger?.Info("🔐 Master-Key erzeugt und in master.key gespeichert. At-rest-Verschlüsselung wird beim nächsten Start aktiv.");
await DialogWindow.Info(this, "Master-Key erzeugt",
"Ein zufälliger 32-Byte-Master-Key wurde erzeugt und in der Datei „master.key\" " +
"(App-Ordner, gitignored) gespeichert.\n\n" +
"WICHTIG:\n" +
"• Sichere diese Datei SOFORT separat (Passwort-Manager / Offline-Backup).\n" +
"• Master-Key-Verlust = KEIN Zugriff mehr auf die verschlüsselten Wallet-Keys!\n" +
"• Die Verschlüsselung der Account-Credentials wird beim nächsten Programmstart aktiv.");
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Fehler beim Erzeugen des Master-Keys: {ex.Message}");
}
UpdateMasterKeyButtonState();
}
/// <summary>
/// Beschränkt eine Schlüsseldatei auf den Besitzer. Auf Linux legt <see cref="File.WriteAllText"/>
/// mit der üblichen umask sonst <c>644</c> an die Datei wäre für alle lesbar. Unter Windows
/// greift die NTFS-Vererbung, dort ist nichts zu tun.
/// </summary>
private void RestrictToOwner(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
try
{
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
catch (Exception ex)
{
_logger?.Warning($"⚠️ Dateirechte für {Path.GetFileName(path)} konnten nicht gesetzt werden: {ex.Message}. " +
"Bitte manuell prüfen (chmod 600).");
}
}
// ===== OpenRouter-API-Key (Supervisor) =====
private async System.Threading.Tasks.Task SetOpenRouterKeyAsync()
{
string keyFile = Path.Combine(AppContext.BaseDirectory, "openrouter.key");
bool exists = File.Exists(keyFile) ||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_OPENROUTER_KEY"));
string? key = await DialogWindow.Prompt(this, "OpenRouter-API-Key",
"OpenRouter-API-Key für den Supervisor eingeben.\n" +
"Wird in der gitignorierten Datei „openrouter.key\" gespeichert (leer = entfernen).\n" +
(exists ? "Aktuell ist bereits ein Key hinterlegt." : "Aktuell ist KEIN Key gesetzt."));
if (key == null) return; // Abbruch
try
{
if (string.IsNullOrWhiteSpace(key))
{
if (File.Exists(keyFile)) File.Delete(keyFile);
_logger?.Info("OpenRouter-Key aus openrouter.key entfernt.");
Status("OpenRouter-Key entfernt.");
return;
}
File.WriteAllText(keyFile, key.Trim());
RestrictToOwner(keyFile);
_logger?.Info("OpenRouter-Key gespeichert (openrouter.key).");
Status("OpenRouter-Key gespeichert wirkt bei der nächsten Supervisor-Anfrage.");
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Fehler beim Speichern des OpenRouter-Keys: {ex.Message}");
}
}
// ===== Watchdog =====
private async System.Threading.Tasks.Task SetWatchdogTokenAsync()
{
bool exists = !string.IsNullOrWhiteSpace(_settings.WatchdogToken);
string? token = await DialogWindow.Prompt(this, "Watchdog Agent-Token",
"Agent-Token aus dem Watchdog-Admin eingeben.\n" +
"Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" +
(exists ? "Aktuell ist ein Token gesetzt." : "Aktuell ist KEIN Token gesetzt."));
if (token == null) return; // Abbruch
try
{
// Protect() gibt ohne Master-Key den Klartext unverändert zurück kein stiller
// Sicherheitsverlust, es wird unten darauf hingewiesen.
_settings.WatchdogToken = string.IsNullOrWhiteSpace(token)
? string.Empty
: SecretProtection.Protect(token.Trim());
_settings.Save(SettingsPath);
_watchdog?.ReloadSettings();
this.FindControl<SettingsEditor>("editorServer")!.Show(_settings);
if (_settings.WatchdogToken.Length == 0)
{
_logger?.Info("Watchdog-Agent-Token entfernt.");
Status("Watchdog-Token entfernt.");
return;
}
bool encrypted = SecretProtection.IsEncrypted(_settings.WatchdogToken);
_logger?.Info($"Watchdog-Agent-Token gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
Status(encrypted
? "Watchdog-Token gespeichert (verschlüsselt) und sofort aktiv."
: "Watchdog-Token gespeichert ACHTUNG: ohne Master-Key im Klartext. Mit „Master-Key erzeugen\" ändern.");
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Fehler beim Speichern des Watchdog-Tokens: {ex.Message}");
}
}
/// <summary>
/// Sendet einen Heartbeat mit den GESPEICHERTEN Einstellungen. Ungespeicherte Änderungen
/// wirken bewusst nicht sonst würde ein erfolgreicher Test etwas bestätigen, das im
/// laufenden Betrieb gar nicht gilt.
/// </summary>
private async System.Threading.Tasks.Task TestWatchdogAsync()
{
if (_watchdog == null) return;
var btn = this.FindControl<Button>("btnTestWatchdog")!;
btn.IsEnabled = false;
try
{
Status("Sende Test-Heartbeat …");
var result = await _watchdog.SendHeartbeatAsync("ok", "Test-Heartbeat aus dem PolyTrader-Settings-Fenster");
if (result.Success)
{
_logger?.Info($"✅ Watchdog-Test-Heartbeat erfolgreich gesendet ({result.Detail}).");
Status($"Test-Heartbeat erfolgreich ({result.Detail}). Der Monitor sollte jetzt auf „up\" stehen.");
}
else
{
_logger?.Warning($"⚠️ Watchdog-Test-Heartbeat fehlgeschlagen: {result.Detail}");
await DialogWindow.Info(this, "Watchdog",
$"Test-Heartbeat fehlgeschlagen:\n\n{result.Detail}\n\n" +
"Bitte URL, Agent-Token und Source prüfen und die Einstellungen vorher speichern.");
Status("Test-Heartbeat fehlgeschlagen.");
}
}
finally
{
btn.IsEnabled = true;
}
}
}
}
@@ -0,0 +1,29 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PolyTrader.App.Avalonia.Views.ShutdownConfirmWindow"
Title="PolyTrader beenden"
Width="480" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<!-- Entspricht dem bisherigen ShutdownConfirmDialog: Beenden ist 10 Sekunden gesperrt,
Countdown im Text, Abbrechen jederzeit möglich. -->
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="PolyTrader sicher beenden?"
FontSize="16" FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap"
Text="Offene Orders und laufende Jobs werden geordnet heruntergefahren. Das kann einen Moment dauern. Die Schaltfläche ist kurz gesperrt, damit das Beenden nicht versehentlich ausgelöst wird." />
<TextBlock Name="lblCountdown"
Foreground="{DynamicResource AppNegativeBrush}"
Text="Beenden möglich in 10 s …" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button Name="btnCancel" Content="Abbrechen" Padding="16,6" IsCancel="True" />
<Button Name="btnShutdown" Content="Beenden (10 s)" Padding="16,6" IsEnabled="False" />
</StackPanel>
</StackPanel>
</Window>
@@ -0,0 +1,51 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Sicherheitsabfrage vor dem Beenden. Die Beenden-Schaltfläche ist bewusst
/// <see cref="LockSeconds"/> Sekunden gesperrt (Verhalten aus der WinForms-Oberfläche
/// übernommen), Abbrechen ist jederzeit möglich. Liefert <c>true</c>, wenn bestätigt wurde.
/// </summary>
public partial class ShutdownConfirmWindow : Window
{
private const int LockSeconds = 10;
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromSeconds(1) };
private int _remaining = LockSeconds;
public ShutdownConfirmWindow()
{
AvaloniaXamlLoader.Load(this);
var countdown = this.FindControl<TextBlock>("lblCountdown")!;
var shutdown = this.FindControl<Button>("btnShutdown")!;
var cancel = this.FindControl<Button>("btnCancel")!;
shutdown.Click += (_, _) => Close(true);
cancel.Click += (_, _) => Close(false);
_timer.Tick += (_, _) =>
{
_remaining--;
if (_remaining > 0)
{
countdown.Text = $"Beenden möglich in {_remaining} s …";
shutdown.Content = $"Beenden ({_remaining} s)";
return;
}
_timer.Stop();
countdown.Text = "Beenden ist jetzt möglich.";
shutdown.Content = "Beenden";
shutdown.IsEnabled = true;
};
_timer.Start();
Closed += (_, _) => _timer.Stop();
}
}
}
@@ -0,0 +1,91 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
xmlns:vm="using:PolyTrader.App.Avalonia.ViewModels"
x:Class="PolyTrader.App.Avalonia.Views.TerminalWindow"
Title="Terminal / Logs"
Width="1429" Height="1083">
<!--
Terminal- und Log-Ansicht.
Live-Tab: farbige Log-Zeilen, abschaltbarer Autoscroll, Level-Filter.
Log-Viewer-Tab: die JSONL-Tagesdateien, filterbar nach Datum, Level, CID und Volltext.
Die Live-Ausgabe ist ein virtualisiertes ItemsControl über eine begrenzte Zeilenliste
statt einer RichTextBox. Damit entfällt das Auto-Clear der WinForms-Fassung, das bei
Erreichen der RAM-Grenze den gesamten Verlauf verwarf: es werden nur noch die ältesten
Zeilen verdrängt, der sichtbare Verlauf bleibt erhalten.
-->
<DockPanel>
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
<TabControl>
<TabItem Header="Live">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnAutoscroll" Content="Stop Autoscroll" />
<Button Name="btnClear" Content="Terminal leeren" />
<Button Name="btnCopyAll" Content="Alles kopieren" />
<TextBlock Text="Level:" VerticalAlignment="Center" />
<ComboBox Name="cbLogLevel" MinWidth="150" />
<TextBlock Name="lblLiveStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<Border Background="#1E1E1E">
<ScrollViewer Name="logScroll">
<ItemsControl Name="logLines" Margin="8">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:LogLine">
<SelectableTextBlock Text="{Binding Text}"
Foreground="{Binding Brush}"
FontFamily="Consolas,Menlo,DejaVu Sans Mono,monospace"
FontSize="12"
TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</DockPanel>
</TabItem>
<TabItem Header="Log Viewer">
<DockPanel>
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Datum:" VerticalAlignment="Center" />
<CalendarDatePicker Name="dtViewerDate" MinWidth="150" />
<TextBlock Text="Level:" VerticalAlignment="Center" />
<ComboBox Name="cbViewerLevel" MinWidth="130" />
<TextBlock Text="CID:" VerticalAlignment="Center" />
<TextBox Name="tbViewerCid" MinWidth="150" />
<TextBlock Text="Text:" VerticalAlignment="Center" />
<TextBox Name="tbViewerText" MinWidth="220" />
<Button Name="btnViewerLoad" Content="Laden" />
<TextBlock Name="lblViewerStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<DataGrid Name="gridLogs" x:DataType="vm:LogViewerRow">
<DataGrid.Columns>
<DataGridTextColumn Header="Zeit" Width="90" Binding="{Binding Time}" />
<DataGridTextColumn Header="Level" Width="110" Binding="{Binding Level}" />
<DataGridTextColumn Header="CID" Width="160" Binding="{Binding Cid}" />
<DataGridTextColumn Header="Nachricht" Width="*" Binding="{Binding Message}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,219 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Terminal-/Log-Ansicht. Live-Ausgabe des <see cref="TerminalLogger"/> plus Viewer für die
/// JSONL-Tagesdateien. Layout vollständig in TerminalWindow.axaml.
/// </summary>
public partial class TerminalWindow : Window
{
/// <summary>
/// Obergrenze der angezeigten Zeilen. Die WinForms-Fassung leerte bei Erreichen einer
/// Zeichengrenze das GESAMTE Terminal; hier werden stattdessen nur die ältesten Zeilen
/// verdrängt, der jüngste Verlauf bleibt also immer sichtbar.
/// </summary>
private const int MaxLines = 5000;
/// <summary>Höchstzahl je UI-Takt verarbeiteter Meldungen hält die Oberfläche flüssig.</summary>
private const int MaxPerTick = 500;
private static readonly IBrush BrushDefault = Brushes.WhiteSmoke;
private static readonly IBrush BrushError = Brushes.Tomato;
private static readonly IBrush BrushWarning = Brushes.Gold;
private static readonly IBrush BrushTrade = Brushes.LightGreen;
private static readonly IBrush BrushReasoning = Brushes.Orange;
private readonly ConcurrentQueue<LogMessageEventArgs> _queue = new();
private readonly ObservableCollection<LogLine> _lines = new();
private readonly ObservableCollection<LogViewerRow> _viewerRows = new();
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(250) };
private TerminalLogger? _logger;
private EventHandler<LogMessageEventArgs>? _logHandler;
private bool _autoScroll = true;
public TerminalWindow() => AvaloniaXamlLoader.Load(this);
public TerminalWindow(IModuleUiHost host, TerminalLogger logger) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.terminal", this);
_logger = logger;
this.FindControl<ItemsControl>("logLines")!.ItemsSource = _lines;
this.FindControl<DataGrid>("gridLogs")!.ItemsSource = _viewerRows;
var levels = new[] { "Alle" }.Concat(Enum.GetNames<LogLevel>()).ToArray();
var cbLevel = this.FindControl<ComboBox>("cbLogLevel")!;
var cbViewerLevel = this.FindControl<ComboBox>("cbViewerLevel")!;
cbLevel.ItemsSource = levels;
cbViewerLevel.ItemsSource = levels;
cbLevel.SelectedIndex = 0;
cbViewerLevel.SelectedIndex = 0;
this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate = DateTime.Now;
this.FindControl<Button>("btnAutoscroll")!.Click += (_, _) => ToggleAutoscroll();
this.FindControl<Button>("btnClear")!.Click += (_, _) => { _lines.Clear(); UpdateLiveStatus(); };
this.FindControl<Button>("btnCopyAll")!.Click += async (_, _) => await CopyAllAsync();
this.FindControl<Button>("btnViewerLoad")!.Click += (_, _) => LoadViewer();
// Doppelklick auf eine Zeile: nach deren CID filtern (komplette Signal-Kette ansehen).
this.FindControl<DataGrid>("gridLogs")!.DoubleTapped += (_, _) =>
{
if (this.FindControl<DataGrid>("gridLogs")!.SelectedItem is LogViewerRow row
&& !string.IsNullOrEmpty(row.Cid))
{
this.FindControl<TextBox>("tbViewerCid")!.Text = row.Cid;
this.FindControl<TextBox>("tbViewerText")!.Text = string.Empty;
LoadViewer();
}
};
// Jüngste Historie vorladen, damit das Fenster beim Öffnen nicht leer ist.
foreach (var e in _logger.GetHistory(TimeSpan.FromMinutes(10)))
_queue.Enqueue(e);
_logHandler = (_, e) => _queue.Enqueue(e);
_logger.OnLogMessage += _logHandler;
_timer.Tick += (_, _) => ProcessQueue();
_timer.Start();
Closed += (_, _) =>
{
_timer.Stop();
if (_logger != null && _logHandler != null) _logger.OnLogMessage -= _logHandler;
};
ProcessQueue();
}
// ===== Live-Ausgabe =====
private void ToggleAutoscroll()
{
_autoScroll = !_autoScroll;
var btn = this.FindControl<Button>("btnAutoscroll")!;
btn.Content = _autoScroll ? "Stop Autoscroll" : "Start Autoscroll";
btn.Background = new SolidColorBrush(_autoScroll ? Colors.LightGreen : Colors.IndianRed);
}
private void ProcessQueue()
{
if (_queue.IsEmpty) return;
string filter = this.FindControl<ComboBox>("cbLogLevel")!.SelectedItem as string ?? "Alle";
int processed = 0;
bool appended = false;
while (processed < MaxPerTick && _queue.TryDequeue(out var e))
{
processed++;
if (filter != "Alle" && e.Level.ToString() != filter) continue;
// Anzeige-Zeitzone aus den Server-Settings (siehe AppTimeZone) nicht mehr fest
// gegen eine Windows-Zeitzonen-ID gerechnet.
DateTime t = AppTimeZone.ToDisplay(e.Timestamp);
_lines.Add(new LogLine
{
Text = $"[{t:HH:mm:ss}] [{e.Level}] {e.Message}",
Brush = e.Level switch
{
LogLevel.Error => BrushError,
LogLevel.Warning => BrushWarning,
LogLevel.Trade => BrushTrade,
LogLevel.TradeReasoning => BrushReasoning,
_ => BrushDefault
}
});
appended = true;
}
// Ringpuffer: nur die aeltesten Zeilen verdraengen statt alles zu verwerfen.
while (_lines.Count > MaxLines) _lines.RemoveAt(0);
if (appended)
{
UpdateLiveStatus();
if (_autoScroll) this.FindControl<ScrollViewer>("logScroll")!.ScrollToEnd();
}
}
private void UpdateLiveStatus() =>
this.FindControl<TextBlock>("lblLiveStatus")!.Text =
$"{_lines.Count} Zeilen (max. {MaxLines}) · vollständige Logs im Ordner /Logs";
private async System.Threading.Tasks.Task CopyAllAsync()
{
try
{
var sb = new StringBuilder();
foreach (var l in _lines) sb.AppendLine(l.Text);
var clipboard = GetTopLevel(this)?.Clipboard;
if (clipboard != null) await clipboard.SetTextAsync(sb.ToString());
}
catch
{
// Zwischenablage kann kurzzeitig belegt sein bewusst ignorieren, wie bisher.
}
}
// ===== Log Viewer (JSONL-Tagesdateien) =====
private void LoadViewer()
{
var status = this.FindControl<TextBlock>("lblViewerStatus")!;
try
{
DateTime day = this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate ?? DateTime.Now;
string path = Path.Combine(AppContext.BaseDirectory, "Logs", $"{day:yyyy-MM-dd}.jsonl");
_viewerRows.Clear();
if (!File.Exists(path))
{
status.Text = "Keine JSONL-Datei für dieses Datum.";
return;
}
string levelFilter = this.FindControl<ComboBox>("cbViewerLevel")!.SelectedItem as string ?? "Alle";
string cidFilter = this.FindControl<TextBox>("tbViewerCid")!.Text?.Trim() ?? string.Empty;
string textFilter = this.FindControl<TextBox>("tbViewerText")!.Text?.Trim() ?? string.Empty;
int count = 0;
foreach (string line in File.ReadLines(path))
{
var p = LogJson.ParseLine(line);
if (p == null) continue;
if (levelFilter != "Alle" && p.Level != levelFilter) continue;
if (cidFilter.Length > 0 && !p.Cid.Contains(cidFilter, StringComparison.OrdinalIgnoreCase)) continue;
if (textFilter.Length > 0 && !p.Message.Contains(textFilter, StringComparison.OrdinalIgnoreCase)) continue;
_viewerRows.Add(new LogViewerRow { Time = p.Time, Level = p.Level, Cid = p.Cid, Message = p.Message });
if (++count >= 20000) break; // Schutz bei sehr grossen Tagen
}
status.Text = $"{_viewerRows.Count} Einträge.";
}
catch (Exception ex)
{
status.Text = $"Fehler: {ex.Message}";
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="PolyTrader.App.Avalonia"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
</assembly>
@@ -0,0 +1,13 @@
{
"Database": {
"_comment": "MySqlConnectionString liegt in der gitignorierten appsettings.Local.json.",
"MySqlConnectionString": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
@@ -5,6 +5,15 @@ using System.Xml.Serialization;
namespace PolyTraderSharp.Models namespace PolyTraderSharp.Models
{ {
/// <summary>Farbschema der Oberfläche.</summary>
public enum AppThemeVariant
{
/// <summary>Folgt der Einstellung des Betriebssystems.</summary>
System,
Hell,
Dunkel
}
public class ServerSettings public class ServerSettings
{ {
[Category("Module")] [Category("Module")]
@@ -16,6 +25,21 @@ namespace PolyTraderSharp.Models
[XmlArrayItem("Module")] [XmlArrayItem("Module")]
public List<string> DisabledModules { get; set; } = new(); public List<string> DisabledModules { get; set; } = new();
[Category("Anzeige")]
[DisplayName("Farbschema")]
[Description("Hell, Dunkel oder der Einstellung des Betriebssystems folgen. " +
"Lässt sich jederzeit über den Umschalter in der Fensterleiste wechseln; " +
"der zuletzt gewählte Stand wird hier gespeichert.")]
public AppThemeVariant Theme { get; set; } = AppThemeVariant.System;
[Category("Anzeige")]
[DisplayName("Zeitzone")]
[Description("Zeitzone für Anzeige und Logdatei-Tagesgrenzen, IANA-Schreibweise (z.B. \"Europe/Berlin\", " +
"\"America/New_York\"). Leer = Zeitzone des Systems. Wird üblicherweise EINMALIG bei der " +
"Installation gesetzt: eine spätere Änderung verschiebt Tagesgrenzen in Logs und Auswertungen. " +
"Greift erst nach einem Neustart.")]
public string ApplicationTimeZoneId { get; set; } = PolyTraderSharp.Services.AppTimeZone.DefaultId;
[Category("Mullvad VPN")] [Category("Mullvad VPN")]
[DisplayName("VPN Enabled")] [DisplayName("VPN Enabled")]
[Description("Enable or disable automatic VPN rotation.")] [Description("Enable or disable automatic VPN rotation.")]
@@ -0,0 +1,13 @@
namespace PolyTrader.Core.Modularity
{
/// <summary>
/// Aktivierungs-Zustand eines Moduls für die Anzeige: Name, ob es in dieser Sitzung läuft, und
/// ein optionaler Grund, warum es nicht aktivierbar ist (z.B. fehlender API-Key, siehe
/// <see cref="IPolyTraderModule.GetActivationBlocker"/>).
///
/// <para>Liegt bewusst im Core und nicht in einer Oberfläche: Die Information ist eine Aussage
/// über die Modularität, keine Darstellungsfrage und wird von WinForms- wie Avalonia-Shell
/// gleichermaßen gebraucht.</para>
/// </summary>
public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker);
}
+20 -12
View File
@@ -1,21 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows.Forms;
namespace PolyTrader.Core.Modularity namespace PolyTrader.Core.Modularity
{ {
/// <summary> /// <summary>
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist /// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die Shell zeigt je View
/// ein designbares <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird /// höchstens eine Instanz und holt ein bereits offenes Fenster wieder in den Vordergrund.
/// (mit DI-Abhängigkeiten). Die Shell zeigt je View höchstens eine Instanz und holt ein ///
/// bereits offenes Fenster wieder in den Vordergrund. /// <para><b>Bewusst toolkit-neutral:</b> <see cref="CreateView"/> liefert ein <see cref="object"/>,
/// keinen konkreten Fenstertyp. Dadurch bleibt der Core plattformunabhängig (<c>net8.0</c>) und
/// trägt keine WinForms-Abhängigkeit mehr. Die jeweilige Shell kennt ihr Toolkit und castet:
/// die WinForms-Shell auf <c>System.Windows.Forms.Form</c>, die künftige Avalonia-Shell auf
/// <c>Avalonia.Controls.Window</c>. Module liefern das Fenster, die Shell hängt es auf.</para>
/// </summary> /// </summary>
public sealed class ModuleView public sealed class ModuleView
{ {
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary> /// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
public string Id { get; init; } = Guid.NewGuid().ToString(); public string Id { get; init; } = Guid.NewGuid().ToString();
/// <summary>Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt).</summary> /// <summary>Titel (Fallback-Fenstertitel, falls das Fenster keinen eigenen setzt).</summary>
public string Title { get; init; } = "Fenster"; public string Title { get; init; } = "Fenster";
/// <summary>Optionale Gruppierung (z.B. "Core", "CopyTrading").</summary> /// <summary>Optionale Gruppierung (z.B. "Core", "CopyTrading").</summary>
@@ -25,21 +28,26 @@ namespace PolyTrader.Core.Modularity
public int Order { get; init; } = 0; public int Order { get; init; } = 0;
/// <summary> /// <summary>
/// Optionales Icon für Menü/Buttons. Wird von der Shell/den Menüs genutzt. Settable, damit die /// Logischer Schlüssel des Symbols für Menüs/Buttons (z.B. "dashboard", "money"). Die Shell
/// App den von Modulen registrierten Views zentral ein Icon aus ihren Ressourcen zuweisen kann /// löst ihn gegen ihre eigenen Ressourcen auf der Core kennt keine Bilddaten mehr (früher
/// <c>System.Drawing.Image</c>, das seit .NET 7 Windows-only ist und auf Linux wirft).
/// Settable, damit die App den von Modulen registrierten Views zentral ein Symbol zuweisen kann
/// (Module referenzieren die App-Ressourcen nicht). /// (Module referenzieren die App-Ressourcen nicht).
/// </summary> /// </summary>
public System.Drawing.Image? Icon { get; set; } public string? IconKey { get; set; }
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary> /// <summary>
public Func<Form> CreateForm { get; init; } = () => new Form(); /// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung). Rückgabetyp ist
/// <see cref="object"/> siehe Klassen-Doku zur Toolkit-Neutralität.
/// </summary>
public Func<object> CreateView { get; init; } = () => new object();
} }
/// <summary> /// <summary>
/// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten. /// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten.
/// Die Shell (Launcher) öffnet die Fenster auf Anforderung und verwaltet den Offen-Status. /// Die Shell (Launcher) öffnet die Fenster auf Anforderung und verwaltet den Offen-Status.
/// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core /// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core
/// kennen) das gemeinsame „Fenster"-Menü bauen (siehe <see cref="WindowMenu"/>). /// kennen) das gemeinsame „Fenster"-Menü bauen.
/// </summary> /// </summary>
public interface IModuleUiHost public interface IModuleUiHost
{ {
+13 -5
View File
@@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <!-- Linux-Portierung P1: Der Core ist plattformneutral. Der UI-Contract (ModuleView/IModuleUiHost)
ist toolkit-neutral (Func<object> statt Func<Form>, IconKey statt System.Drawing.Image),
damit hier weder WinForms noch System.Drawing.Common hängen letzteres ist seit .NET 7
Windows-only und wirft auf Linux. Die WinForms-Umsetzung liegt in PolyTrader.App (Ui/). -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Der Core stellt den UI-Contract (IModuleUiHost/ModuleView mit System.Windows.Forms.Control)
bereit, damit Module designbare UserControls beitragen können. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -19,6 +20,13 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Nethereum.Web3" Version="6.1.0" /> <PackageReference Include="Nethereum.Web3" Version="6.1.0" />
<!-- Sicherheit: Nethereum 6.1.0 laesst transitiv Newtonsoft.Json [11.0.2, 14.0.0) zu und
wuerde ohne Pinnung auf 11.0.2 aufloesen - das hat eine bekannte Luecke hoher Schwere
(GHSA-5crp-9r3c-p9vr). Die .NET-10-Paketpruefung meldet das jetzt auch fuer transitive
Abhaengigkeiten. 13.0.4 liegt im erlaubten Bereich und ist die Version, die auch die App
schon nutzt. Hier im Core gepinnt, damit JEDER Consumer (Module, kuenftiger Linux-Daemon)
sie bekommt und nicht nur die WinForms-App. -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup> </ItemGroup>
<!-- Erlaubt dem Testprojekt, interne Helfer (z.B. SecretProtection.Reset) zu testen. --> <!-- Erlaubt dem Testprojekt, interne Helfer (z.B. SecretProtection.Reset) zu testen. -->
@@ -0,0 +1,85 @@
using System;
namespace PolyTraderSharp.Services
{
/// <summary>
/// Anzeige-Zeitzone der Anwendung. Wird beim Start EINMAL aus den Server-Settings gesetzt
/// (<c>ApplicationTimeZoneId</c>) und danach überall zum Umrechnen von UTC-Zeitstempeln in die
/// dargestellte Ortszeit verwendet.
///
/// <para><b>Warum das nötig war:</b> Die Terminal-Ansicht rechnete bisher hart gegen
/// <c>"W. Europe Standard Time"</c> eine <i>Windows</i>-Zeitzonen-ID. Auf Linux funktioniert die
/// nur über die ICU-Zuordnung und fällt komplett aus, wenn ICU fehlt oder
/// <c>InvariantGlobalization</c> gesetzt ist. Dann hätte das Fenster beim Öffnen geworfen.</para>
///
/// <para><b>Auflösung:</b> Zuerst die konfigurierte ID direkt, dann über die Umrechnung zwischen
/// IANA- und Windows-Schreibweise (damit dieselbe Konfiguration auf beiden Plattformen trägt),
/// zuletzt die Zeitzone des Systems. Ein Fehlschlag ist damit nie fatal er wird nur gemeldet.</para>
///
/// <para>Die Zeitzone wird bewusst einmalig bei der Installation festgelegt und nicht im
/// laufenden Betrieb gewechselt: eine Änderung verschiebt Logdatei-Tagesgrenzen und
/// Auswertungszeiträume. Änderungen greifen deshalb erst nach einem Neustart.</para>
/// </summary>
public static class AppTimeZone
{
/// <summary>Empfehlung für neue Installationen IANA-Schreibweise, funktioniert auf beiden Plattformen.</summary>
public const string DefaultId = "Europe/Berlin";
/// <summary>Aktuell gültige Anzeige-Zeitzone. Vor <see cref="Configure"/> die des Systems.</summary>
public static TimeZoneInfo Current { get; private set; } = TimeZoneInfo.Local;
/// <summary>Die tatsächlich verwendete ID (kann von der gewünschten abweichen, wenn ausgewichen wurde).</summary>
public static string CurrentId => Current.Id;
/// <summary>
/// Setzt die Anzeige-Zeitzone. Leere Angabe = Systemzeitzone. Meldet über
/// <paramref name="warn"/>, wenn auf etwas anderes als das Gewünschte ausgewichen wurde.
/// </summary>
public static void Configure(string? timeZoneId, Action<string>? warn = null)
{
if (string.IsNullOrWhiteSpace(timeZoneId))
{
Current = TimeZoneInfo.Local;
return;
}
string id = timeZoneId.Trim();
if (TryFind(id, out var tz)) { Current = tz!; return; }
// Andere Schreibweise versuchen: eine unter Windows gepflegte Konfiguration soll auch
// auf Linux tragen und umgekehrt.
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(id, out string? windowsId)
&& TryFind(windowsId!, out tz))
{
Current = tz!;
warn?.Invoke($"Zeitzone „{id}\" wurde als {tz!.Id}\" aufgelöst.");
return;
}
if (TimeZoneInfo.TryConvertWindowsIdToIanaId(id, out string? ianaId)
&& TryFind(ianaId!, out tz))
{
Current = tz!;
warn?.Invoke($"Zeitzone „{id}\" wurde als {tz!.Id}\" aufgelöst.");
return;
}
Current = TimeZoneInfo.Local;
warn?.Invoke($"⚠️ Zeitzone „{id}\" ist auf diesem System unbekannt es wird die Systemzeitzone " +
$"„{TimeZoneInfo.Local.Id}\" verwendet. Empfohlen ist die IANA-Schreibweise, z.B. {DefaultId}\".");
}
/// <summary>Rechnet einen Zeitstempel in die Anzeige-Zeitzone um (UTC-Kennzeichnung wird beachtet).</summary>
public static DateTime ToDisplay(DateTime value) =>
value.Kind == DateTimeKind.Utc
? TimeZoneInfo.ConvertTimeFromUtc(value, Current)
: TimeZoneInfo.ConvertTime(value, Current);
private static bool TryFind(string id, out TimeZoneInfo? tz)
{
try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); return true; }
catch (TimeZoneNotFoundException) { tz = null; return false; }
catch (InvalidTimeZoneException) { tz = null; return false; }
}
}
}
@@ -0,0 +1,61 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PolyTrader.Core.Persistence;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services
{
/// <summary>
/// Hydriert den Core-<see cref="TradingState"/> EINMALIG beim Start: Accounts und bei
/// Demo-Konten deren offene Positionen. Muss als ERSTER IHostedService laufen, damit die
/// Trading-BackgroundServices nicht gegen einen leeren State anlaufen (Startup-Race).
///
/// <para>Zuvor erledigte das der modulübergreifende <c>StartupHydrationService</c> der WinForms-App.
/// Der ist mit der Linux-Portierung aufgeteilt worden: Der Core-Anteil steht jetzt hier, der
/// Copytrading-Anteil im Modul (<c>CopyTradingHydrationService</c>). Das behebt nebenbei einen
/// latenten Fehler bei deaktiviertem Copytrading-Modul wären die Accounts vorher gar nicht
/// mehr hydriert worden, obwohl sie zum Core gehören.</para>
/// </summary>
public sealed class CoreStateHydrationService : IHostedService
{
private readonly TradingState _state;
private readonly IAccountRepository _accountRepo;
private readonly IPositionRepository _positionRepo;
private readonly TerminalLogger _logger;
public CoreStateHydrationService(TradingState state, IAccountRepository accountRepo,
IPositionRepository positionRepo, TerminalLogger logger)
{
_state = state;
_accountRepo = accountRepo;
_positionRepo = positionRepo;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
foreach (var acc in _accountRepo.GetAll())
{
if (acc.IsDemo)
foreach (var pos in _positionRepo.GetDemo(acc.AccountId))
acc.OpenPositions.TryAdd(pos.TokenId, pos);
_state.Accounts[acc.AccountId] = acc;
}
_logger.Info($"Core-Hydration abgeschlossen: {_state.Accounts.Count} Accounts geladen.");
}
catch (System.Exception ex)
{
_logger.Error($"Core-Hydration fehlgeschlagen: {ex.Message}");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
+11 -2
View File
@@ -1,11 +1,20 @@
using System.ComponentModel; using System.Collections.ObjectModel;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services namespace PolyTraderSharp.Services
{ {
/// <summary>
/// Registrierung der Hintergrund-Jobs für die Jobs-Ansicht.
///
/// <para><see cref="ObservableCollection{T}"/> statt des früheren <c>BindingList</c>: Avalonia-
/// Steuerelemente werten <c>INotifyCollectionChanged</c> aus, das <c>BindingList</c> nicht
/// implementiert neu registrierte Jobs wären in der Oberfläche sonst unsichtbar geblieben.
/// Die Zeilen selbst melden Änderungen weiterhin über <c>INotifyPropertyChanged</c>
/// (<see cref="JobStatusRow"/>).</para>
/// </summary>
public class JobManager public class JobManager
{ {
public BindingList<JobStatusRow> Jobs { get; } = new BindingList<JobStatusRow>(); public ObservableCollection<JobStatusRow> Jobs { get; } = new();
public void RegisterJob(JobStatusRow job) public void RegisterJob(JobStatusRow job)
{ {
@@ -1,4 +1,4 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -49,21 +49,22 @@ namespace PolyTrader.Modules.Accounting
services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>()); services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>());
} }
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services) /// <summary>
/// Aktuell ohne Ansicht: Die WinForms-UI wurde mit der Linux-Portierung entfernt, die
/// Avalonia-Ansicht folgt (Stufe L3/L4). Die Fachlogik dieses Moduls laeuft davon
/// unabhaengig weiter die Shell zeigt schlicht kein Fenster fuer das Modul an.
///
/// <para><b>Beim Nachbau zu erhalten</b> (Spezifikation: docs/UI-SPEZIFIKATION-WinForms.md,
/// Originalcode: Git-Tag <c>winforms-final</c>):</para>
/// <list type="bullet">
/// <item>View-ID <c>accounting.main</c> (stabil Launcher-Button und Symbol haengen daran)</item>
/// <item>Titel <c>Accounting</c>, Gruppe <c>Accounting</c>, Order <c>400</c></item>
/// <item>Ein Fenster fuers ganze Modul: AccountingMainForm mit Tabs: Uebersicht+BWA / Ledger / Abruf+Status</item>
/// </list>
/// </summary>
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
{ {
host.RegisterView(new ModuleView // Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
{
Id = "accounting.main",
Title = "Accounting",
Group = "Accounting",
Order = 400,
CreateForm = () =>
{
var form = new Ui.AccountingMainForm();
form.Initialize(services);
return form;
}
});
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -28,9 +28,15 @@ namespace PolyTrader.Modules.Accounting.Logic
decimal V(decimal usdc) => Math.Round(usdc * currencyFactor, 2, MidpointRounding.AwayFromZero); decimal V(decimal usdc) => Math.Round(usdc * currencyFactor, 2, MidpointRounding.AwayFromZero);
string M(decimal usdc) => V(usdc).ToString("N2") + " " + currencyCode; string M(decimal usdc) => V(usdc).ToString("N2") + " " + currencyCode;
// Schriftauflösung sicherstellen: der PDFsharp-Core-Build findet ohne Resolver keine
// Systemschriften (siehe PdfFontResolver). Idempotent, kostet nach dem ersten Mal nichts.
PdfFontResolver.EnsureRegistered();
var doc = new Document(); var doc = new Document();
doc.Info.Title = "Buchhalterische Abrechnung"; doc.Info.Title = "Buchhalterische Abrechnung";
var style = doc.Styles["Normal"]; var style = doc.Styles["Normal"];
// Wunschschrift; der Resolver weicht auf eine verfügbare aus, wenn es sie nicht gibt
// (auf Linux z.B. DejaVu Sans).
style.Font.Name = "Segoe UI"; style.Font.Name = "Segoe UI";
style.Font.Size = 9; style.Font.Size = 9;
@@ -0,0 +1,151 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using PdfSharp.Fonts;
namespace PolyTrader.Modules.Accounting.Logic
{
/// <summary>
/// Schriftauflösung für den PDF-Export. Nötig, seit der plattformneutrale PDFsharp-Core-Build
/// verwendet wird: der frühere <c>-GDI</c>-Build zog Systemschriften über
/// <c>System.Drawing.Common</c>, das seit .NET 7 Windows-only ist. Der Core-Build bringt keine
/// eigene Schriftsuche mit und braucht deshalb diesen Resolver.
///
/// <para>Der Resolver durchsucht die üblichen Schriftverzeichnisse des Betriebssystems nach einer
/// Kandidatenliste (Windows: Segoe UI/Arial, Linux: DejaVu/Liberation/Noto). Damit sind keine
/// Schriftdateien im Repository nötig und die Lizenzfrage der Schriften bleibt beim System.</para>
///
/// <para><b>Voraussetzung auf Linux:</b> mindestens eine der Kandidatenschriften muss installiert
/// sein. Auf Desktop-Distributionen ist das der Normalfall; auf schlanken Servern/Containern
/// genügt <c>apt install fonts-dejavu-core</c> (bzw. <c>fonts-liberation</c>). Fehlt jede Schrift,
/// wirft <see cref="EnsureRegistered"/> beim ersten Export eine Meldung mit genau diesem Hinweis
/// besser als eine kryptische Meldung aus dem Renderer-Inneren.</para>
/// </summary>
public sealed class PdfFontResolver : IFontResolver
{
/// <summary>Bevorzugte Schriftfamilien in Reihenfolge erste gefundene gewinnt.</summary>
private static readonly string[] PreferredFamilies =
{
"Segoe UI", "DejaVu Sans", "Liberation Sans", "Noto Sans", "Arial", "FreeSans"
};
/// <summary>Dateinamens-Kandidaten je Familie und Schnitt (klein geschrieben, ohne Endung).</summary>
private static readonly Dictionary<string, (string Regular, string Bold)> FileNames = new(StringComparer.OrdinalIgnoreCase)
{
["Segoe UI"] = ("segoeui", "segoeuib"),
["DejaVu Sans"] = ("DejaVuSans", "DejaVuSans-Bold"),
["Liberation Sans"] = ("LiberationSans-Regular", "LiberationSans-Bold"),
["Noto Sans"] = ("NotoSans-Regular", "NotoSans-Bold"),
["Arial"] = ("arial", "arialbd"),
["FreeSans"] = ("FreeSans", "FreeSansBold"),
};
private static readonly ConcurrentDictionary<string, byte[]> Cache = new();
private static readonly object RegisterLock = new();
private static bool _registered;
/// <summary>
/// Registriert den Resolver einmalig global. Muss vor dem ersten Rendern laufen; mehrfache
/// Aufrufe sind unschädlich (PDFsharp erlaubt nur eine Zuweisung pro Prozess).
/// </summary>
public static void EnsureRegistered()
{
if (_registered) return;
lock (RegisterLock)
{
if (_registered) return;
if (FindAnyAvailableFamily() == null)
{
throw new InvalidOperationException(
"Keine geeignete Schriftart gefunden. Der PDF-Export braucht mindestens eine von: " +
string.Join(", ", PreferredFamilies) + ". " +
"Auf schlanken Linux-Systemen nachinstallieren, z.B. 'apt install fonts-dejavu-core'.");
}
GlobalFontSettings.FontResolver = new PdfFontResolver();
_registered = true;
}
}
/// <summary>Erste verfügbare Familie aus <see cref="PreferredFamilies"/>, sonst <c>null</c>.</summary>
internal static string? FindAnyAvailableFamily() =>
PreferredFamilies.FirstOrDefault(f => LocateFile(f, bold: false) != null);
public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
// Angeforderte Familie bevorzugen, sonst die erste verfügbare (z.B. "Segoe UI" auf Linux).
string? family = LocateFile(familyName, isBold) != null ? familyName : FindAnyAvailableFamily();
if (family == null) return null;
// Kursiv wird bewusst nicht aufgelöst: der Bericht nutzt es nicht, und ein fehlender
// Kursiv-Schnitt wäre sonst ein harter Fehler statt einer kleinen Abweichung.
return new FontResolverInfo($"{family}|{(isBold ? "b" : "r")}");
}
public byte[]? GetFont(string faceName)
{
return Cache.GetOrAdd(faceName, key =>
{
var parts = key.Split('|');
string family = parts[0];
bool bold = parts.Length > 1 && parts[1] == "b";
string? path = LocateFile(family, bold) ?? LocateFile(family, bold: false);
return path != null ? File.ReadAllBytes(path) : Array.Empty<byte>();
});
}
/// <summary>Sucht die Datei zu Familie + Schnitt in allen Schriftverzeichnissen des Systems.</summary>
private static string? LocateFile(string family, bool bold)
{
if (!FileNames.TryGetValue(family, out var names)) return null;
string stem = bold ? names.Bold : names.Regular;
foreach (string dir in FontDirectories())
{
if (!Directory.Exists(dir)) continue;
foreach (string ext in new[] { ".ttf", ".otf" })
{
try
{
// Rekursiv, weil Linux die Schriften in Unterordnern ablegt (truetype/dejavu/…).
var hit = Directory.EnumerateFiles(dir, stem + ext, SearchOption.AllDirectories)
.FirstOrDefault();
if (hit != null) return hit;
}
catch (UnauthorizedAccessException) { /* einzelne Ordner ohne Zugriff überspringen */ }
catch (IOException) { /* z.B. defekter Symlink */ }
}
}
return null;
}
/// <summary>Schriftverzeichnisse je Plattform.</summary>
private static IEnumerable<string> FontDirectories()
{
if (OperatingSystem.IsWindows())
{
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
yield break;
}
if (OperatingSystem.IsMacOS())
{
yield return "/System/Library/Fonts";
yield return "/Library/Fonts";
yield break;
}
// Linux/Unix
yield return "/usr/share/fonts";
yield return "/usr/local/share/fonts";
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(home))
{
yield return Path.Combine(home, ".fonts");
yield return Path.Combine(home, ".local", "share", "fonts");
}
}
}
}
@@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" /> <ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="PDFsharp-MigraDoc-GDI" Version="6.2.4" /> <!-- Core-Build statt -GDI: der GDI-Build braucht System.Drawing.Common und ist damit
Windows-only. Der Core-Build findet dafuer keine Systemschriften von selbst -
siehe Logic/PdfFontResolver.cs. -->
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -21,11 +24,12 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <!-- Linux-Portierung: plattformneutral. Die WinForms-UI dieses Moduls (Ingest, BWA, Export)
wurde entfernt; die Avalonia-Ansicht kommt spaeter direkt hierher zurueck Avalonia ist
im Gegensatz zu WinForms plattformuebergreifend und braucht kein eigenes UI-Projekt. -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Modul trägt eigene WinForms-UI bei (Ledger/Status; BWA/Steuer/Export folgen A-2..A-4). -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -1,609 +0,0 @@
namespace PolyTrader.Modules.Accounting.Ui
{
partial class AccountingMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AccountingMainForm));
tabControlAcc = new TabControl();
tabUebersicht = new TabPage();
dgvMonthly = new DataGridView();
flpKpis = new FlowLayoutPanel();
lblKpiNet = new Label();
lblKpiClosing = new Label();
lblKpiDeposits = new Label();
lblKpiWithdrawals = new Label();
lblKpiFees = new Label();
lblKpiRewards = new Label();
lblKpiVolume = new Label();
lblKpiTrades = new Label();
pnlOverviewTop = new Panel();
btnExportPdf = new Button();
btnExportCsv = new Button();
btnCalc = new Button();
cbCurrency = new ComboBox();
lblWaehrung = new Label();
dtTo = new DateTimePicker();
lblBis = new Label();
dtFrom = new DateTimePicker();
lblVon = new Label();
cbOvAccount = new ComboBox();
lblOvKonto = new Label();
tabLedger = new TabPage();
dgvLedger = new DataGridView();
toolStripLedger = new ToolStrip();
lblLedgerKonto = new ToolStripLabel();
cbLedgerAccount = new ToolStripComboBox();
sepLedger = new ToolStripSeparator();
btnLedgerRefresh = new ToolStripButton();
tabStatus = new TabPage();
dgvRuns = new DataGridView();
toolStripStatus = new ToolStrip();
lblStatusKonto = new ToolStripLabel();
cbStatusAccount = new ToolStripComboBox();
sepStatus = new ToolStripSeparator();
btnIncremental = new ToolStripButton();
btnBackfill = new ToolStripButton();
sepStatus2 = new ToolStripSeparator();
btnStatusRefresh = new ToolStripButton();
lblAccStatus = new Label();
tabControlAcc.SuspendLayout();
tabUebersicht.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvMonthly).BeginInit();
flpKpis.SuspendLayout();
pnlOverviewTop.SuspendLayout();
tabLedger.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvLedger).BeginInit();
toolStripLedger.SuspendLayout();
tabStatus.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvRuns).BeginInit();
toolStripStatus.SuspendLayout();
SuspendLayout();
//
// tabControlAcc
//
tabControlAcc.Controls.Add(tabUebersicht);
tabControlAcc.Controls.Add(tabLedger);
tabControlAcc.Controls.Add(tabStatus);
tabControlAcc.Dock = DockStyle.Fill;
tabControlAcc.Location = new Point(0, 0);
tabControlAcc.Margin = new Padding(4, 5, 4, 5);
tabControlAcc.Name = "tabControlAcc";
tabControlAcc.SelectedIndex = 0;
tabControlAcc.Size = new Size(1571, 1030);
tabControlAcc.TabIndex = 0;
//
// tabUebersicht
//
tabUebersicht.Controls.Add(dgvMonthly);
tabUebersicht.Controls.Add(flpKpis);
tabUebersicht.Controls.Add(pnlOverviewTop);
tabUebersicht.Location = new Point(4, 34);
tabUebersicht.Margin = new Padding(4, 5, 4, 5);
tabUebersicht.Name = "tabUebersicht";
tabUebersicht.Padding = new Padding(4, 5, 4, 5);
tabUebersicht.Size = new Size(1563, 992);
tabUebersicht.TabIndex = 0;
tabUebersicht.Text = "Übersicht / BWA";
tabUebersicht.UseVisualStyleBackColor = true;
//
// dgvMonthly
//
dgvMonthly.AllowUserToAddRows = false;
dgvMonthly.AllowUserToDeleteRows = false;
dgvMonthly.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvMonthly.Dock = DockStyle.Fill;
dgvMonthly.Location = new Point(4, 222);
dgvMonthly.Margin = new Padding(4, 5, 4, 5);
dgvMonthly.Name = "dgvMonthly";
dgvMonthly.ReadOnly = true;
dgvMonthly.RowHeadersVisible = false;
dgvMonthly.RowHeadersWidth = 62;
dgvMonthly.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvMonthly.Size = new Size(1555, 765);
dgvMonthly.TabIndex = 2;
//
// flpKpis
//
flpKpis.Controls.Add(lblKpiNet);
flpKpis.Controls.Add(lblKpiClosing);
flpKpis.Controls.Add(lblKpiDeposits);
flpKpis.Controls.Add(lblKpiWithdrawals);
flpKpis.Controls.Add(lblKpiFees);
flpKpis.Controls.Add(lblKpiRewards);
flpKpis.Controls.Add(lblKpiVolume);
flpKpis.Controls.Add(lblKpiTrades);
flpKpis.Dock = DockStyle.Top;
flpKpis.Location = new Point(4, 65);
flpKpis.Margin = new Padding(4, 5, 4, 5);
flpKpis.Name = "flpKpis";
flpKpis.Padding = new Padding(6, 7, 6, 7);
flpKpis.Size = new Size(1555, 157);
flpKpis.TabIndex = 1;
//
// lblKpiNet
//
lblKpiNet.BorderStyle = BorderStyle.FixedSingle;
lblKpiNet.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiNet.Location = new Point(12, 14);
lblKpiNet.Margin = new Padding(6, 7, 6, 7);
lblKpiNet.MinimumSize = new Size(285, 122);
lblKpiNet.Name = "lblKpiNet";
lblKpiNet.Padding = new Padding(11, 13, 11, 13);
lblKpiNet.Size = new Size(285, 122);
lblKpiNet.TabIndex = 0;
lblKpiNet.Text = "Netto-Handelsergebnis\n—";
lblKpiNet.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiClosing
//
lblKpiClosing.BorderStyle = BorderStyle.FixedSingle;
lblKpiClosing.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiClosing.Location = new Point(309, 14);
lblKpiClosing.Margin = new Padding(6, 7, 6, 7);
lblKpiClosing.MinimumSize = new Size(256, 122);
lblKpiClosing.Name = "lblKpiClosing";
lblKpiClosing.Padding = new Padding(11, 13, 11, 13);
lblKpiClosing.Size = new Size(256, 122);
lblKpiClosing.TabIndex = 1;
lblKpiClosing.Text = "Endsaldo\n—";
lblKpiClosing.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiDeposits
//
lblKpiDeposits.BorderStyle = BorderStyle.FixedSingle;
lblKpiDeposits.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiDeposits.Location = new Point(577, 14);
lblKpiDeposits.Margin = new Padding(6, 7, 6, 7);
lblKpiDeposits.MinimumSize = new Size(213, 122);
lblKpiDeposits.Name = "lblKpiDeposits";
lblKpiDeposits.Padding = new Padding(11, 13, 11, 13);
lblKpiDeposits.Size = new Size(213, 122);
lblKpiDeposits.TabIndex = 2;
lblKpiDeposits.Text = "Einzahlungen\n—";
lblKpiDeposits.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiWithdrawals
//
lblKpiWithdrawals.BorderStyle = BorderStyle.FixedSingle;
lblKpiWithdrawals.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiWithdrawals.Location = new Point(802, 14);
lblKpiWithdrawals.Margin = new Padding(6, 7, 6, 7);
lblKpiWithdrawals.MinimumSize = new Size(213, 122);
lblKpiWithdrawals.Name = "lblKpiWithdrawals";
lblKpiWithdrawals.Padding = new Padding(11, 13, 11, 13);
lblKpiWithdrawals.Size = new Size(213, 122);
lblKpiWithdrawals.TabIndex = 3;
lblKpiWithdrawals.Text = "Auszahlungen\n—";
lblKpiWithdrawals.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiFees
//
lblKpiFees.BorderStyle = BorderStyle.FixedSingle;
lblKpiFees.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiFees.Location = new Point(1027, 14);
lblKpiFees.Margin = new Padding(6, 7, 6, 7);
lblKpiFees.MinimumSize = new Size(199, 122);
lblKpiFees.Name = "lblKpiFees";
lblKpiFees.Padding = new Padding(11, 13, 11, 13);
lblKpiFees.Size = new Size(199, 122);
lblKpiFees.TabIndex = 4;
lblKpiFees.Text = "Fees\n—";
lblKpiFees.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiRewards
//
lblKpiRewards.BorderStyle = BorderStyle.FixedSingle;
lblKpiRewards.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiRewards.Location = new Point(1238, 14);
lblKpiRewards.Margin = new Padding(6, 7, 6, 7);
lblKpiRewards.MinimumSize = new Size(199, 122);
lblKpiRewards.Name = "lblKpiRewards";
lblKpiRewards.Padding = new Padding(11, 13, 11, 13);
lblKpiRewards.Size = new Size(199, 122);
lblKpiRewards.TabIndex = 5;
lblKpiRewards.Text = "Rewards\n—";
lblKpiRewards.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiVolume
//
lblKpiVolume.BorderStyle = BorderStyle.FixedSingle;
lblKpiVolume.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiVolume.Location = new Point(12, 150);
lblKpiVolume.Margin = new Padding(6, 7, 6, 7);
lblKpiVolume.MinimumSize = new Size(228, 122);
lblKpiVolume.Name = "lblKpiVolume";
lblKpiVolume.Padding = new Padding(11, 13, 11, 13);
lblKpiVolume.Size = new Size(228, 122);
lblKpiVolume.TabIndex = 6;
lblKpiVolume.Text = "Handelsvolumen\n—";
lblKpiVolume.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiTrades
//
lblKpiTrades.BorderStyle = BorderStyle.FixedSingle;
lblKpiTrades.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiTrades.Location = new Point(252, 150);
lblKpiTrades.Margin = new Padding(6, 7, 6, 7);
lblKpiTrades.MinimumSize = new Size(171, 122);
lblKpiTrades.Name = "lblKpiTrades";
lblKpiTrades.Padding = new Padding(11, 13, 11, 13);
lblKpiTrades.Size = new Size(171, 122);
lblKpiTrades.TabIndex = 7;
lblKpiTrades.Text = "Trades\n—";
lblKpiTrades.TextAlign = ContentAlignment.MiddleLeft;
//
// pnlOverviewTop
//
pnlOverviewTop.Controls.Add(btnExportPdf);
pnlOverviewTop.Controls.Add(btnExportCsv);
pnlOverviewTop.Controls.Add(btnCalc);
pnlOverviewTop.Controls.Add(cbCurrency);
pnlOverviewTop.Controls.Add(lblWaehrung);
pnlOverviewTop.Controls.Add(dtTo);
pnlOverviewTop.Controls.Add(lblBis);
pnlOverviewTop.Controls.Add(dtFrom);
pnlOverviewTop.Controls.Add(lblVon);
pnlOverviewTop.Controls.Add(cbOvAccount);
pnlOverviewTop.Controls.Add(lblOvKonto);
pnlOverviewTop.Dock = DockStyle.Top;
pnlOverviewTop.Location = new Point(4, 5);
pnlOverviewTop.Margin = new Padding(4, 5, 4, 5);
pnlOverviewTop.Name = "pnlOverviewTop";
pnlOverviewTop.Size = new Size(1555, 60);
pnlOverviewTop.TabIndex = 0;
//
// btnExportPdf
//
btnExportPdf.Location = new Point(1334, 8);
btnExportPdf.Margin = new Padding(4, 5, 4, 5);
btnExportPdf.Name = "btnExportPdf";
btnExportPdf.Size = new Size(129, 43);
btnExportPdf.TabIndex = 10;
btnExportPdf.Text = "PDF-Export";
btnExportPdf.UseVisualStyleBackColor = true;
//
// btnExportCsv
//
btnExportCsv.Location = new Point(1197, 8);
btnExportCsv.Margin = new Padding(4, 5, 4, 5);
btnExportCsv.Name = "btnExportCsv";
btnExportCsv.Size = new Size(129, 43);
btnExportCsv.TabIndex = 9;
btnExportCsv.Text = "CSV-Export";
btnExportCsv.UseVisualStyleBackColor = true;
//
// btnCalc
//
btnCalc.Location = new Point(1046, 8);
btnCalc.Margin = new Padding(4, 5, 4, 5);
btnCalc.Name = "btnCalc";
btnCalc.Size = new Size(143, 43);
btnCalc.TabIndex = 8;
btnCalc.Text = "Berechnen";
btnCalc.UseVisualStyleBackColor = true;
//
// cbCurrency
//
cbCurrency.DropDownStyle = ComboBoxStyle.DropDownList;
cbCurrency.Items.AddRange(new object[] { "USDC", "USD", "EUR" });
cbCurrency.Location = new Point(921, 10);
cbCurrency.Margin = new Padding(4, 5, 4, 5);
cbCurrency.Name = "cbCurrency";
cbCurrency.Size = new Size(113, 33);
cbCurrency.TabIndex = 7;
//
// lblWaehrung
//
lblWaehrung.AutoSize = true;
lblWaehrung.Location = new Point(821, 15);
lblWaehrung.Margin = new Padding(4, 0, 4, 0);
lblWaehrung.Name = "lblWaehrung";
lblWaehrung.Size = new Size(88, 25);
lblWaehrung.TabIndex = 6;
lblWaehrung.Text = "Währung:";
//
// dtTo
//
dtTo.Format = DateTimePickerFormat.Short;
dtTo.Location = new Point(636, 10);
dtTo.Margin = new Padding(4, 5, 4, 5);
dtTo.Name = "dtTo";
dtTo.Size = new Size(155, 31);
dtTo.TabIndex = 5;
//
// lblBis
//
lblBis.AutoSize = true;
lblBis.Location = new Point(593, 15);
lblBis.Margin = new Padding(4, 0, 4, 0);
lblBis.Name = "lblBis";
lblBis.Size = new Size(38, 25);
lblBis.TabIndex = 4;
lblBis.Text = "Bis:";
//
// dtFrom
//
dtFrom.Format = DateTimePickerFormat.Short;
dtFrom.Location = new Point(429, 10);
dtFrom.Margin = new Padding(4, 5, 4, 5);
dtFrom.Name = "dtFrom";
dtFrom.Size = new Size(155, 31);
dtFrom.TabIndex = 3;
//
// lblVon
//
lblVon.AutoSize = true;
lblVon.Location = new Point(381, 15);
lblVon.Margin = new Padding(4, 0, 4, 0);
lblVon.Name = "lblVon";
lblVon.Size = new Size(47, 25);
lblVon.TabIndex = 2;
lblVon.Text = "Von:";
//
// cbOvAccount
//
cbOvAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbOvAccount.Location = new Point(79, 10);
cbOvAccount.Margin = new Padding(4, 5, 4, 5);
cbOvAccount.Name = "cbOvAccount";
cbOvAccount.Size = new Size(284, 33);
cbOvAccount.TabIndex = 1;
//
// lblOvKonto
//
lblOvKonto.AutoSize = true;
lblOvKonto.Location = new Point(9, 15);
lblOvKonto.Margin = new Padding(4, 0, 4, 0);
lblOvKonto.Name = "lblOvKonto";
lblOvKonto.Size = new Size(64, 25);
lblOvKonto.TabIndex = 0;
lblOvKonto.Text = "Konto:";
//
// tabLedger
//
tabLedger.Controls.Add(dgvLedger);
tabLedger.Controls.Add(toolStripLedger);
tabLedger.Location = new Point(4, 34);
tabLedger.Margin = new Padding(4, 5, 4, 5);
tabLedger.Name = "tabLedger";
tabLedger.Padding = new Padding(4, 5, 4, 5);
tabLedger.Size = new Size(1563, 992);
tabLedger.TabIndex = 1;
tabLedger.Text = "Ledger";
tabLedger.UseVisualStyleBackColor = true;
//
// dgvLedger
//
dgvLedger.AllowUserToAddRows = false;
dgvLedger.AllowUserToDeleteRows = false;
dgvLedger.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvLedger.Dock = DockStyle.Fill;
dgvLedger.Location = new Point(4, 39);
dgvLedger.Margin = new Padding(4, 5, 4, 5);
dgvLedger.Name = "dgvLedger";
dgvLedger.ReadOnly = true;
dgvLedger.RowHeadersVisible = false;
dgvLedger.RowHeadersWidth = 62;
dgvLedger.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvLedger.Size = new Size(1555, 948);
dgvLedger.TabIndex = 1;
//
// toolStripLedger
//
toolStripLedger.ImageScalingSize = new Size(24, 24);
toolStripLedger.Items.AddRange(new ToolStripItem[] { lblLedgerKonto, cbLedgerAccount, sepLedger, btnLedgerRefresh });
toolStripLedger.Location = new Point(4, 5);
toolStripLedger.Name = "toolStripLedger";
toolStripLedger.Padding = new Padding(0, 0, 3, 0);
toolStripLedger.Size = new Size(1555, 34);
toolStripLedger.TabIndex = 0;
//
// lblLedgerKonto
//
lblLedgerKonto.Name = "lblLedgerKonto";
lblLedgerKonto.Size = new Size(64, 29);
lblLedgerKonto.Text = "Konto:";
//
// cbLedgerAccount
//
cbLedgerAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbLedgerAccount.Name = "cbLedgerAccount";
cbLedgerAccount.Size = new Size(341, 34);
//
// sepLedger
//
sepLedger.Name = "sepLedger";
sepLedger.Size = new Size(6, 34);
//
// btnLedgerRefresh
//
btnLedgerRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnLedgerRefresh.Name = "btnLedgerRefresh";
btnLedgerRefresh.Size = new Size(116, 29);
btnLedgerRefresh.Text = "Aktualisieren";
//
// tabStatus
//
tabStatus.Controls.Add(dgvRuns);
tabStatus.Controls.Add(toolStripStatus);
tabStatus.Location = new Point(4, 34);
tabStatus.Margin = new Padding(4, 5, 4, 5);
tabStatus.Name = "tabStatus";
tabStatus.Padding = new Padding(4, 5, 4, 5);
tabStatus.Size = new Size(1563, 992);
tabStatus.TabIndex = 2;
tabStatus.Text = "Abruf / Status";
tabStatus.UseVisualStyleBackColor = true;
//
// dgvRuns
//
dgvRuns.AllowUserToAddRows = false;
dgvRuns.AllowUserToDeleteRows = false;
dgvRuns.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvRuns.Dock = DockStyle.Fill;
dgvRuns.Location = new Point(4, 39);
dgvRuns.Margin = new Padding(4, 5, 4, 5);
dgvRuns.Name = "dgvRuns";
dgvRuns.ReadOnly = true;
dgvRuns.RowHeadersVisible = false;
dgvRuns.RowHeadersWidth = 62;
dgvRuns.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvRuns.Size = new Size(1555, 948);
dgvRuns.TabIndex = 1;
//
// toolStripStatus
//
toolStripStatus.ImageScalingSize = new Size(24, 24);
toolStripStatus.Items.AddRange(new ToolStripItem[] { lblStatusKonto, cbStatusAccount, sepStatus, btnIncremental, btnBackfill, sepStatus2, btnStatusRefresh });
toolStripStatus.Location = new Point(4, 5);
toolStripStatus.Name = "toolStripStatus";
toolStripStatus.Padding = new Padding(0, 0, 3, 0);
toolStripStatus.Size = new Size(1555, 34);
toolStripStatus.TabIndex = 0;
//
// lblStatusKonto
//
lblStatusKonto.Name = "lblStatusKonto";
lblStatusKonto.Size = new Size(64, 29);
lblStatusKonto.Text = "Konto:";
//
// cbStatusAccount
//
cbStatusAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbStatusAccount.Name = "cbStatusAccount";
cbStatusAccount.Size = new Size(341, 34);
//
// sepStatus
//
sepStatus.Name = "sepStatus";
sepStatus.Size = new Size(6, 34);
//
// btnIncremental
//
btnIncremental.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnIncremental.Name = "btnIncremental";
btnIncremental.Size = new Size(134, 29);
btnIncremental.Text = "Inkrement jetzt";
btnIncremental.ToolTipText = "Nur neue Ereignisse seit dem letzten Stand abrufen (mit Lookback-Überlappung).";
//
// btnBackfill
//
btnBackfill.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnBackfill.Name = "btnBackfill";
btnBackfill.Size = new Size(188, 29);
btnBackfill.Text = "Backfill (volle Historie)";
btnBackfill.ToolTipText = "Komplette Historie neu abrufen (idempotent bucht nichts doppelt).";
//
// sepStatus2
//
sepStatus2.Name = "sepStatus2";
sepStatus2.Size = new Size(6, 34);
//
// btnStatusRefresh
//
btnStatusRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnStatusRefresh.Name = "btnStatusRefresh";
btnStatusRefresh.Size = new Size(116, 29);
btnStatusRefresh.Text = "Aktualisieren";
//
// lblAccStatus
//
lblAccStatus.Dock = DockStyle.Bottom;
lblAccStatus.Location = new Point(0, 1030);
lblAccStatus.Margin = new Padding(4, 0, 4, 0);
lblAccStatus.Name = "lblAccStatus";
lblAccStatus.Padding = new Padding(9, 3, 9, 3);
lblAccStatus.Size = new Size(1571, 37);
lblAccStatus.TabIndex = 1;
//
// AccountingMainForm
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1571, 1067);
Controls.Add(tabControlAcc);
Controls.Add(lblAccStatus);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 5, 4, 5);
Name = "AccountingMainForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "Accounting";
tabControlAcc.ResumeLayout(false);
tabUebersicht.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvMonthly).EndInit();
flpKpis.ResumeLayout(false);
pnlOverviewTop.ResumeLayout(false);
pnlOverviewTop.PerformLayout();
tabLedger.ResumeLayout(false);
tabLedger.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgvLedger).EndInit();
toolStripLedger.ResumeLayout(false);
toolStripLedger.PerformLayout();
tabStatus.ResumeLayout(false);
tabStatus.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgvRuns).EndInit();
toolStripStatus.ResumeLayout(false);
toolStripStatus.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControlAcc;
private System.Windows.Forms.TabPage tabUebersicht;
private System.Windows.Forms.Panel pnlOverviewTop;
private System.Windows.Forms.Label lblOvKonto;
private System.Windows.Forms.ComboBox cbOvAccount;
private System.Windows.Forms.Label lblVon;
private System.Windows.Forms.DateTimePicker dtFrom;
private System.Windows.Forms.Label lblBis;
private System.Windows.Forms.DateTimePicker dtTo;
private System.Windows.Forms.Label lblWaehrung;
private System.Windows.Forms.ComboBox cbCurrency;
private System.Windows.Forms.Button btnCalc;
private System.Windows.Forms.Button btnExportCsv;
private System.Windows.Forms.Button btnExportPdf;
private System.Windows.Forms.FlowLayoutPanel flpKpis;
private System.Windows.Forms.Label lblKpiNet;
private System.Windows.Forms.Label lblKpiClosing;
private System.Windows.Forms.Label lblKpiDeposits;
private System.Windows.Forms.Label lblKpiWithdrawals;
private System.Windows.Forms.Label lblKpiFees;
private System.Windows.Forms.Label lblKpiRewards;
private System.Windows.Forms.Label lblKpiVolume;
private System.Windows.Forms.Label lblKpiTrades;
private System.Windows.Forms.DataGridView dgvMonthly;
private System.Windows.Forms.TabPage tabLedger;
private System.Windows.Forms.ToolStrip toolStripLedger;
private System.Windows.Forms.ToolStripLabel lblLedgerKonto;
private System.Windows.Forms.ToolStripComboBox cbLedgerAccount;
private System.Windows.Forms.ToolStripSeparator sepLedger;
private System.Windows.Forms.ToolStripButton btnLedgerRefresh;
private System.Windows.Forms.DataGridView dgvLedger;
private System.Windows.Forms.TabPage tabStatus;
private System.Windows.Forms.ToolStrip toolStripStatus;
private System.Windows.Forms.ToolStripLabel lblStatusKonto;
private System.Windows.Forms.ToolStripComboBox cbStatusAccount;
private System.Windows.Forms.ToolStripSeparator sepStatus;
private System.Windows.Forms.ToolStripButton btnIncremental;
private System.Windows.Forms.ToolStripButton btnBackfill;
private System.Windows.Forms.ToolStripSeparator sepStatus2;
private System.Windows.Forms.ToolStripButton btnStatusRefresh;
private System.Windows.Forms.DataGridView dgvRuns;
private System.Windows.Forms.Label lblAccStatus;
}
}
@@ -1,322 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using PolyTraderSharp;
namespace PolyTrader.Modules.Accounting.Ui
{
/// <summary>
/// Hauptfenster des Accounting-Moduls: Tab „Übersicht / BWA" (neutrale Periodenabrechnung + KPIs +
/// Monatsvergleich, Währung USDC/USD/EUR, CSV-Export), „Ledger" (Buchungssätze) und „Abruf / Status"
/// (Ingest-Läufe, manueller Backfill/Inkrement). Layout im Designer; Aggregation pur im
/// <see cref="AccountingEngine"/>. Die US-Steuerschicht (A-3) ist bewusst NICHT hier sie hängt an
/// den CPA-Antworten.
/// </summary>
public partial class AccountingMainForm : Form
{
private ILedgerRepository? _ledger;
private IIngestRunRepository? _runs;
private AccountingIngestService? _ingest;
private AccountingReportService? _report;
private TradingState? _state;
public AccountingMainForm()
{
InitializeComponent();
// Übersicht / BWA
btnCalc.Click += (_, _) => Recalculate();
btnExportCsv.Click += (_, _) => ExportCsv();
btnExportPdf.Click += (_, _) => ExportPdf();
cbOvAccount.SelectedIndexChanged += (_, _) => Recalculate();
cbCurrency.SelectedIndexChanged += (_, _) => Recalculate();
// Ledger + Status
btnLedgerRefresh.Click += (_, _) => LoadLedger();
cbLedgerAccount.SelectedIndexChanged += (_, _) => LoadLedger();
btnStatusRefresh.Click += (_, _) => LoadRuns();
btnIncremental.Click += async (_, _) => await RunIngestAsync(backfill: false);
btnBackfill.Click += async (_, _) => await RunIngestAsync(backfill: true);
}
public void Initialize(IServiceProvider services)
{
_ledger = services.GetRequiredService<ILedgerRepository>();
_runs = services.GetRequiredService<IIngestRunRepository>();
_ingest = services.GetRequiredService<AccountingIngestService>();
_report = services.GetRequiredService<AccountingReportService>();
_state = services.GetRequiredService<TradingState>();
// Standard-Zeitraum: laufender Monat.
var now = DateTime.Now;
dtFrom.Value = new DateTime(now.Year, now.Month, 1);
dtTo.Value = now;
if (cbCurrency.Items.Count > 0) cbCurrency.SelectedIndex = 0; // USDC
PopulateAccounts();
Recalculate();
LoadLedger();
LoadRuns();
}
// ---------------- Konten-Filter ----------------
private void PopulateAccounts()
{
var items = new List<AccountItem> { new(null, "Alle Live-Konten") };
if (_state != null)
items.AddRange(_state.Accounts.Values
.Where(a => !a.IsDemo && !string.IsNullOrWhiteSpace(a.WalletAddress))
.OrderBy(a => a.AccountId)
.Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{a.AccountId})")));
cbOvAccount.DisplayMember = nameof(AccountItem.Label);
cbOvAccount.DataSource = new List<AccountItem>(items);
foreach (var combo in new[] { cbLedgerAccount, cbStatusAccount })
{
combo.ComboBox.DisplayMember = nameof(AccountItem.Label);
combo.ComboBox.DataSource = new List<AccountItem>(items);
}
}
private static int? IdOf(object? item) => item is AccountItem it ? it.Id : null;
// ---------------- Übersicht / BWA ----------------
private void Recalculate()
{
if (_report == null) return;
try
{
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1); // inklusive gewählter Bis-Tag
int? accId = IdOf(cbOvAccount.SelectedItem);
var statement = _report.BuildStatement(accId, from, to);
var monthly = _report.BuildMonthly(accId, from, to);
var cur = _report.ResolveCurrency(cbCurrency.SelectedItem?.ToString() ?? "USDC", to);
UpdateKpis(statement, cur);
dgvMonthly.DataSource = monthly.Select(m => new MonthlyRow(m, cur)).ToList();
lblAccStatus.Text = cur.Available
? $"Währung {cur.Code}: {cur.Note}"
: $"⚠️ {cur.Note} (Anzeige in USDC).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"Abrechnung nicht möglich (acc_-Migration angewendet?): {ex.Message}";
}
}
private void UpdateKpis(PeriodStatement s, CurrencyContext cur)
{
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "");
string unit = view.Code;
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, view);
lblKpiNet.Text = $"Netto-Handelsergebnis\n{V(s.NetTradingResultUsdc):N2} {unit}";
lblKpiNet.ForeColor = s.NetTradingResultUsdc >= 0 ? System.Drawing.Color.ForestGreen : System.Drawing.Color.Firebrick;
lblKpiClosing.Text = $"Endsaldo\n{V(s.ClosingBalanceUsdc):N2} {unit}";
lblKpiDeposits.Text = $"Einzahlungen\n{V(s.Deposits):N2} {unit}";
lblKpiWithdrawals.Text = $"Auszahlungen\n{V(s.Withdrawals):N2} {unit}";
lblKpiFees.Text = $"Fees\n{V(s.Fees):N2} {unit}";
lblKpiRewards.Text = $"Rewards\n{V(s.Rewards):N2} {unit}";
lblKpiVolume.Text = $"Handelsvolumen\n{V(s.TradeVolume):N2} {unit}";
lblKpiTrades.Text = $"Trades\n{s.TradeCount}";
}
private void ExportCsv()
{
if (_ledger == null || _report == null) return;
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1);
int? accId = IdOf(cbOvAccount.SelectedItem);
using var dlg = new SaveFileDialog
{
Filter = "CSV-Datei (*.csv)|*.csv",
FileName = $"accounting_{(accId?.ToString() ?? "alle")}_{from:yyyyMMdd}-{dtTo.Value:yyyyMMdd}.csv"
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
var statement = _report.BuildStatement(accId, from, to);
var entries = _ledger.Query(accId, from, to, 100000);
string csv = CsvExporter.Statement(statement) + Environment.NewLine + CsvExporter.Ledger(entries);
System.IO.File.WriteAllText(dlg.FileName, csv, new System.Text.UTF8Encoding(true));
lblAccStatus.Text = $"CSV exportiert: {dlg.FileName} ({entries.Count} Buchungen).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"CSV-Export fehlgeschlagen: {ex.Message}";
}
}
private void ExportPdf()
{
if (_ledger == null || _report == null) return;
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1);
int? accId = IdOf(cbOvAccount.SelectedItem);
using var dlg = new SaveFileDialog
{
Filter = "PDF-Datei (*.pdf)|*.pdf",
FileName = $"abrechnung_{(accId?.ToString() ?? "alle")}_{from:yyyyMMdd}-{dtTo.Value:yyyyMMdd}.pdf"
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
var statement = _report.BuildStatement(accId, from, to);
var monthly = _report.BuildMonthly(accId, from, to);
var entries = _ledger.Query(accId, from, to, 100000);
var cur = _report.ResolveCurrency(cbCurrency.SelectedItem?.ToString() ?? "USDC", to);
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "Native Buchungswährung.");
byte[] pdf = PdfExporter.Render(statement, monthly, entries, view.Code, view.Factor, view.Note);
System.IO.File.WriteAllBytes(dlg.FileName, pdf);
lblAccStatus.Text = $"PDF exportiert: {dlg.FileName} ({entries.Count} Buchungen, {view.Code}).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"PDF-Export fehlgeschlagen: {ex.Message}";
}
}
// ---------------- Ledger ----------------
private void LoadLedger()
{
if (_ledger == null) return;
try
{
var rows = _ledger.Query(IdOf(cbLedgerAccount.SelectedItem), null, null, 1000);
dgvLedger.DataSource = rows.Select(e => new LedgerRow(e)).ToList();
lblAccStatus.Text = rows.Count == 0
? "Noch keine Buchungen. (Ingest-Quellen sind offline bis zur Live-Anbindung im Zielland.)"
: $"{rows.Count} Buchungssätze angezeigt.";
}
catch (Exception ex)
{
lblAccStatus.Text = $"Ledger nicht lesbar (acc_-Migration angewendet?): {ex.Message}";
}
}
// ---------------- Abruf / Status ----------------
private void LoadRuns()
{
if (_runs == null) return;
try
{
dgvRuns.DataSource = _runs.GetRecent(IdOf(cbStatusAccount.SelectedItem), 100);
}
catch (Exception ex)
{
lblAccStatus.Text = $"Ingest-Läufe nicht lesbar: {ex.Message}";
}
}
private async System.Threading.Tasks.Task RunIngestAsync(bool backfill)
{
if (_ingest == null) return;
btnBackfill.Enabled = false;
btnIncremental.Enabled = false;
lblAccStatus.Text = backfill ? "Backfill läuft …" : "Inkrementeller Abruf läuft …";
try
{
await System.Threading.Tasks.Task.Run(() => _ingest.IngestAllAsync(backfill, CancellationToken.None));
lblAccStatus.Text = "Abruf abgeschlossen.";
LoadRuns();
LoadLedger();
Recalculate();
}
catch (Exception ex)
{
lblAccStatus.Text = $"Abruf-Fehler: {ex.Message}";
}
finally
{
btnBackfill.Enabled = true;
btnIncremental.Enabled = true;
}
}
private sealed record AccountItem(int? Id, string Label);
/// <summary>Anzeige-Zeile des Monatsvergleichs (Beträge in der gewählten Währung).</summary>
private sealed class MonthlyRow
{
public MonthlyRow(PeriodStatement m, CurrencyContext cur)
{
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "");
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, view);
Monat = m.From.ToString("yyyy-MM");
Anfangssaldo = V(m.OpeningBalanceUsdc);
Einzahlungen = V(m.Deposits);
Auszahlungen = V(m.Withdrawals);
Handelsvolumen = V(m.TradeVolume);
Rewards = V(m.Rewards);
Fees = V(m.Fees);
Handelsergebnis = V(m.NetTradingResultUsdc);
Endsaldo = V(m.ClosingBalanceUsdc);
Trades = m.TradeCount;
}
public string Monat { get; }
public decimal Anfangssaldo { get; }
public decimal Einzahlungen { get; }
public decimal Auszahlungen { get; }
public decimal Handelsvolumen { get; }
public decimal Rewards { get; }
public decimal Fees { get; }
public decimal Handelsergebnis { get; }
public decimal Endsaldo { get; }
public int Trades { get; }
}
/// <summary>Anzeige-Zeile fürs Ledger-Grid.</summary>
private sealed class LedgerRow
{
public LedgerRow(Models.LedgerEntry e)
{
Zeit = e.Timestamp;
Konto = e.AccountId;
Typ = e.EventType.ToString();
Markt = e.MarketSlug;
Outcome = e.Outcome;
Side = e.Side;
Size = e.Size;
Preis = e.PriceUsdc;
Brutto = e.GrossUsdc;
Fee = e.FeeUsdc;
Netto = e.NetUsdc;
TxHash = e.TxHash;
Quelle = e.Source;
}
public DateTime Zeit { get; }
public int Konto { get; }
public string Typ { get; }
public string Markt { get; }
public string Outcome { get; }
public string Side { get; }
public decimal Size { get; }
public decimal Preis { get; }
public decimal Brutto { get; }
public decimal Fee { get; }
public decimal Netto { get; }
public string TxHash { get; }
public string Quelle { get; }
}
}
}
@@ -1,202 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="toolStripLedger.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="toolStripStatus.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>206, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
AABJRU5ErkJggg==
</value>
</data>
</root>
@@ -1,4 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Channels; using System.Threading.Channels;
@@ -10,7 +10,6 @@ using PolyTrader.Core.Modularity;
using PolyTrader.Core.Streaming; using PolyTrader.Core.Streaming;
using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTrader.Modules.CopyTrading.Persistence.Ef; using PolyTrader.Modules.CopyTrading.Persistence.Ef;
using PolyTrader.Modules.CopyTrading.Ui;
using PolyTraderSharp; using PolyTraderSharp;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
using PolyTraderSharp.Services; using PolyTraderSharp.Services;
@@ -58,6 +57,8 @@ namespace PolyTrader.Modules.CopyTrading
// K2 Neustart-Reconciliation: MUSS als erster Modul-HostedService laufen (nach der // K2 Neustart-Reconciliation: MUSS als erster Modul-HostedService laufen (nach der
// State-Hydration, vor TraderMonitor/Engine), damit verwaiste CLOB-Orders vom vorherigen // State-Hydration, vor TraderMonitor/Engine), damit verwaiste CLOB-Orders vom vorherigen
// Lauf storniert sind, bevor das erste Signal verarbeitet wird. // Lauf storniert sind, bevor das erste Signal verarbeitet wird.
// Muss vor den Trading-Diensten des Moduls laufen (Settings + Trader in den State).
services.AddHostedService<CopyTradingHydrationService>();
services.AddHostedService<StartupOrderReconciliationService>(); services.AddHostedService<StartupOrderReconciliationService>();
// Modul-Services (Signalquelle, Ausführung, Analytics) // Modul-Services (Signalquelle, Ausführung, Analytics)
@@ -81,24 +82,22 @@ namespace PolyTrader.Modules.CopyTrading
services.AddHostedService(sp => sp.GetRequiredService<SellLadderService>()); services.AddHostedService(sp => sp.GetRequiredService<SellLadderService>());
} }
/// <summary>
/// Aktuell ohne Ansicht: Die WinForms-UI wurde mit der Linux-Portierung entfernt, die
/// Avalonia-Ansicht folgt (Stufe L3/L4). Die Fachlogik dieses Moduls laeuft davon
/// unabhaengig weiter die Shell zeigt schlicht kein Fenster fuer das Modul an.
///
/// <para><b>Beim Nachbau zu erhalten</b> (Spezifikation: docs/UI-SPEZIFIKATION-WinForms.md,
/// Originalcode: Git-Tag <c>winforms-final</c>):</para>
/// <list type="bullet">
/// <item>View-ID <c>copytrading.main</c> (stabil Launcher-Button und Symbol haengen daran)</item>
/// <item>Titel <c>Copytrading</c>, Gruppe <c>CopyTrading</c>, Order <c>100</c></item>
/// <item>Ein Fenster fuers ganze Modul: CopyTradingMainForm mit Tabs: Master-Trader / Offene Trades / Geschlossene Trades / Account-Einstellungen</item>
/// </list>
/// </summary>
public void RegisterUi(IModuleUiHost host, IServiceProvider services) public void RegisterUi(IModuleUiHost host, IServiceProvider services)
{ {
// EIN Fenster fürs ganze Modul: die einzelnen Ansichten sind Tabs im // Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
// CopyTradingMainForm (Master-Trader / geschlossene Trades / Account-Einstellungen).
// So bleibt der Launcher schlank ein Button je Modul statt je View.
host.RegisterView(new ModuleView
{
Id = "copytrading.main",
Title = "Copytrading",
Group = "CopyTrading",
Order = 100,
CreateForm = () =>
{
var form = new CopyTradingMainForm();
form.Initialize(services);
return form;
}
});
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
@@ -1,18 +1,34 @@
using System.Drawing;
namespace PolyTrader.Modules.CopyTrading.Logic namespace PolyTrader.Modules.CopyTrading.Logic
{ {
/// <summary>Farbkategorie einer Trade-Zeile. Die konkrete Farbe legt die jeweilige UI fest.</summary>
public enum TradeRowTint
{
/// <summary>&lt; 0 % Verlust.</summary>
Loss,
/// <summary>010 % kleiner Gewinn.</summary>
SmallWin,
/// <summary>&gt; 10 % deutlicher Gewinn.</summary>
BigWin
}
/// <summary> /// <summary>
/// Reine Zeilenfärbung nach realisiertem/unrealisiertem PnL-Prozent (getestet, damit die /// Reine Einstufung von Trade-Zeilen nach realisiertem/unrealisiertem PnL-Prozent (getestet,
/// Schwellen belastbar sind): Verlust rot, 010 % hellgrün, > 10 % kräftiges Grün. /// damit die Schwellen belastbar sind): Verlust, 010 %, &gt; 10 %.
///
/// <para>Liefert bewusst eine <see cref="TradeRowTint"/>-Kategorie statt einer Farbe: Die
/// Modul-Logik ist damit frei von <c>System.Drawing</c> (seit .NET 7 Windows-only) und die
/// Zuordnung Kategorie → Farbe liegt dort, wo sie hingehört in der jeweiligen Oberfläche.</para>
/// </summary> /// </summary>
public static class TradeRowColoring public static class TradeRowColoring
{ {
public static readonly Color Loss = Color.FromArgb(245, 200, 200); // < 0 % rot /// <summary>Schwelle, ab der ein Gewinn als deutlich gilt (in Prozent, exklusiv).</summary>
public static readonly Color SmallWin = Color.FromArgb(212, 240, 212); // 010 % hellgrün public const decimal BigWinThresholdPercent = 10m;
public static readonly Color BigWin = Color.FromArgb(140, 214, 140); // > 10 % grün
public static Color ForPnlPercent(decimal pnlPercent) => public static TradeRowTint ForPnlPercent(decimal pnlPercent) =>
pnlPercent < 0m ? Loss : (pnlPercent > 10m ? BigWin : SmallWin); pnlPercent < 0m
? TradeRowTint.Loss
: (pnlPercent > BigWinThresholdPercent ? TradeRowTint.BigWin : TradeRowTint.SmallWin);
} }
} }
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" /> <ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
@@ -20,11 +20,12 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <!-- Linux-Portierung: plattformneutral. Die WinForms-UI dieses Moduls (Trading-Engine, SELL-Leiter, Master-Trader-Analytics)
wurde entfernt; die Avalonia-Ansicht kommt spaeter direkt hierher zurueck Avalonia ist
im Gegensatz zu WinForms plattformuebergreifend und braucht kein eigenes UI-Projekt. -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Modul trägt später eigene WinForms-UI-Tabs bei. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -3,7 +3,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using PolyTrader.Core.Persistence;
using PolyTrader.Modules.CopyTrading.Logic; using PolyTrader.Modules.CopyTrading.Logic;
using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
@@ -11,35 +10,27 @@ using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services namespace PolyTraderSharp.Services
{ {
/// <summary> /// <summary>
/// Hydriert den TradingState (Accounts, Demo-Positionen, Trader) EINMALIG beim App-Start. /// Hydriert den Copytrading-Zustand beim Start: Account-Settings (legt fehlende Default-Einträge
/// Registriert als erster IHostedService, damit die Hydration abgeschlossen ist, bevor die /// an) und die verfolgten Master-Trader. Läuft nach <see cref="CoreStateHydrationService"/>,
/// Trading-BackgroundServices (TraderMonitor, CopyTradingEngine, ...) ihre ExecuteAsync /// weil er die dort geladenen Accounts braucht die Reihenfolge ergibt sich aus der
/// starten. Behebt den Startup-Race, bei dem Services gegen einen leeren State anliefen /// Registrierungsreihenfolge (Core zuerst, dann Module).
/// (früher lag diese Logik in frm_main.LoadDatabaseAndState und lief erst NACH AppHost.Start()). ///
/// <para>Zuvor Teil des modulübergreifenden <c>StartupHydrationService</c> der WinForms-App.</para>
/// </summary> /// </summary>
public class StartupHydrationService : IHostedService public sealed class CopyTradingHydrationService : IHostedService
{ {
private readonly TradingState _state; private readonly TradingState _state;
private readonly CopyTradingState _copyState; private readonly CopyTradingState _copyState;
private readonly IAccountRepository _accountRepo;
private readonly IPositionRepository _positionRepo;
private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo; private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo;
private readonly ITrackedTraderRepository _traderRepo; private readonly ITrackedTraderRepository _traderRepo;
private readonly TerminalLogger _logger; private readonly TerminalLogger _logger;
public StartupHydrationService( public CopyTradingHydrationService(TradingState state, CopyTradingState copyState,
TradingState state,
CopyTradingState copyState,
IAccountRepository accountRepo,
IPositionRepository positionRepo,
ICopyTradingAccountSettingsRepository accountSettingsRepo, ICopyTradingAccountSettingsRepository accountSettingsRepo,
ITrackedTraderRepository traderRepo, ITrackedTraderRepository traderRepo, TerminalLogger logger)
TerminalLogger logger)
{ {
_state = state; _state = state;
_copyState = copyState; _copyState = copyState;
_accountRepo = accountRepo;
_positionRepo = positionRepo;
_accountSettingsRepo = accountSettingsRepo; _accountSettingsRepo = accountSettingsRepo;
_traderRepo = traderRepo; _traderRepo = traderRepo;
_logger = logger; _logger = logger;
@@ -49,31 +40,16 @@ namespace PolyTraderSharp.Services
{ {
try try
{ {
var accounts = _accountRepo.GetAll(); HydrateAccountSettings(_state.Accounts.Values.ToList());
foreach (var acc in accounts)
{
if (acc.IsDemo)
{
foreach (var pos in _positionRepo.GetDemo(acc.AccountId))
{
acc.OpenPositions.TryAdd(pos.TokenId, pos);
}
}
_state.Accounts[acc.AccountId] = acc;
}
HydrateAccountSettings(accounts);
foreach (var trd in _traderRepo.GetAll()) foreach (var trd in _traderRepo.GetAll())
{
_copyState.Traders[trd.Id] = trd; _copyState.Traders[trd.Id] = trd;
}
_logger.Info($"Startup-Hydration abgeschlossen: {_state.Accounts.Count} Accounts, {_copyState.Traders.Count} Trader geladen."); _logger.Info($"Copytrading-Hydration abgeschlossen: {_copyState.Traders.Count} Trader geladen.");
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
_logger.Error($"Startup-Hydration fehlgeschlagen: {ex.Message}"); _logger.Error($"Copytrading-Hydration fehlgeschlagen: {ex.Message}");
} }
return Task.CompletedTask; return Task.CompletedTask;
@@ -1,126 +0,0 @@
namespace PolyTrader.Modules.CopyTrading.Ui
{
partial class AccountSettingsView
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.toolbar = new System.Windows.Forms.ToolStrip();
this.tsAccountLabel = new System.Windows.Forms.ToolStripLabel();
this.tsAccounts = new System.Windows.Forms.ToolStripComboBox();
this.tsSep = new System.Windows.Forms.ToolStripSeparator();
this.tsSave = new System.Windows.Forms.ToolStripButton();
this.pgSettings = new System.Windows.Forms.PropertyGrid();
this.statusPanel = new System.Windows.Forms.Panel();
this.lblHint = new System.Windows.Forms.Label();
this.toolbar.SuspendLayout();
this.statusPanel.SuspendLayout();
this.SuspendLayout();
//
// toolbar
//
this.toolbar.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.tsAccountLabel,
this.tsAccounts,
this.tsSep,
this.tsSave});
this.toolbar.Location = new System.Drawing.Point(0, 0);
this.toolbar.Name = "toolbar";
this.toolbar.Size = new System.Drawing.Size(560, 25);
this.toolbar.TabIndex = 0;
//
// tsAccountLabel
//
this.tsAccountLabel.Name = "tsAccountLabel";
this.tsAccountLabel.Size = new System.Drawing.Size(56, 22);
this.tsAccountLabel.Text = "Account:";
//
// tsAccounts
//
this.tsAccounts.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.tsAccounts.Name = "tsAccounts";
this.tsAccounts.Size = new System.Drawing.Size(320, 25);
//
// tsSep
//
this.tsSep.Name = "tsSep";
this.tsSep.Size = new System.Drawing.Size(6, 25);
//
// tsSave
//
this.tsSave.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsSave.Name = "tsSave";
this.tsSave.Size = new System.Drawing.Size(69, 22);
this.tsSave.Text = "Speichern";
//
// pgSettings
//
this.pgSettings.Dock = System.Windows.Forms.DockStyle.Fill;
this.pgSettings.Location = new System.Drawing.Point(0, 25);
this.pgSettings.Name = "pgSettings";
this.pgSettings.PropertySort = System.Windows.Forms.PropertySort.Categorized;
this.pgSettings.Size = new System.Drawing.Size(560, 549);
this.pgSettings.TabIndex = 1;
this.pgSettings.ToolbarVisible = false;
//
// statusPanel
//
this.statusPanel.Controls.Add(this.lblHint);
this.statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.statusPanel.Location = new System.Drawing.Point(0, 574);
this.statusPanel.Name = "statusPanel";
this.statusPanel.Size = new System.Drawing.Size(560, 26);
this.statusPanel.TabIndex = 2;
//
// lblHint
//
this.lblHint.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblHint.ForeColor = System.Drawing.Color.DimGray;
this.lblHint.Location = new System.Drawing.Point(0, 0);
this.lblHint.Name = "lblHint";
this.lblHint.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
this.lblHint.Size = new System.Drawing.Size(560, 26);
this.lblHint.TabIndex = 0;
this.lblHint.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// AccountSettingsView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.pgSettings);
this.Controls.Add(this.statusPanel);
this.Controls.Add(this.toolbar);
this.Name = "AccountSettingsView";
this.Size = new System.Drawing.Size(560, 600);
this.toolbar.ResumeLayout(false);
this.toolbar.PerformLayout();
this.statusPanel.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolbar;
private System.Windows.Forms.ToolStripLabel tsAccountLabel;
private System.Windows.Forms.ToolStripComboBox tsAccounts;
private System.Windows.Forms.ToolStripSeparator tsSep;
private System.Windows.Forms.ToolStripButton tsSave;
private System.Windows.Forms.PropertyGrid pgSettings;
private System.Windows.Forms.Panel statusPanel;
private System.Windows.Forms.Label lblHint;
}
}
@@ -1,77 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Bearbeitet die copytrading-spezifischen Detail-Einstellungen je Account
/// (Investment-/Zeit-Limits, <see cref="CopyTradingAccountSettings"/>). Persistiert über
/// das Repo und aktualisiert den Hot-Path-State (<see cref="CopyTradingState.AccountSettings"/>).
/// Layout im Designer (AccountSettingsView.Designer.cs), Daten/Logik hier.
/// </summary>
public partial class AccountSettingsView : UserControl
{
private ICopyTradingAccountSettingsRepository? _repo;
private TradingState? _state;
private CopyTradingState? _copyState;
private CopyTradingAccountSettings? _current;
// Parameterloser Konstruktor für den WinForms-Designer.
public AccountSettingsView()
{
InitializeComponent();
tsAccounts.SelectedIndexChanged += (_, _) => LoadSelected();
tsSave.Click += (_, _) => Save();
}
/// <summary>Injiziert die Abhängigkeiten (nach der DI-Auflösung) und füllt die Auswahl.</summary>
public void Initialize(ICopyTradingAccountSettingsRepository repo, TradingState state, CopyTradingState copyState)
{
_repo = repo;
_state = state;
_copyState = copyState;
PopulateAccounts();
}
private void PopulateAccounts()
{
if (_state == null) return;
var items = _state.Accounts.Values
.OrderBy(a => a.AccountId)
.Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{a.AccountId}){(a.IsDemo ? " · Demo" : "")}"))
.ToList();
tsAccounts.ComboBox.DisplayMember = nameof(AccountItem.Label);
tsAccounts.ComboBox.ValueMember = nameof(AccountItem.Id);
tsAccounts.ComboBox.DataSource = items;
if (items.Count > 0) LoadSelected();
else lblHint.Text = "Keine Accounts vorhanden.";
}
private void LoadSelected()
{
if (_repo == null || tsAccounts.SelectedItem is not AccountItem item) return;
_current = _repo.Get(item.Id) ?? new CopyTradingAccountSettings { AccountId = item.Id };
pgSettings.SelectedObject = _current;
lblHint.Text = $"Einstellungen für Account #{item.Id}.";
}
private void Save()
{
if (_current == null || _repo == null || _copyState == null) return;
_repo.Upsert(_current);
_copyState.AccountSettings[_current.AccountId] = _current;
lblHint.Text = $"Gespeichert für Account #{_current.AccountId} um {DateTime.Now:HH:mm:ss}.";
}
private sealed record AccountItem(int Id, string Label);
}
}
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -1,392 +0,0 @@
namespace PolyTrader.Modules.CopyTrading.Ui
{
partial class ClosedTradesView
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.toolbar = new System.Windows.Forms.ToolStrip();
this.tsRefresh = new System.Windows.Forms.ToolStripButton();
this.dgvTrades = new System.Windows.Forms.DataGridView();
this.colTradeId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAccount = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colTrader = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colMarket = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colOutcome = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSide = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colEntry = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colExit = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSize = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnlPct = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colOpenedAt = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colClosedAt = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.statusPanel = new System.Windows.Forms.Panel();
this.lblSummary = new System.Windows.Forms.Label();
this.pnlFilters = new System.Windows.Forms.Panel();
this.lblMarkt = new System.Windows.Forms.Label();
this.tbMarket = new System.Windows.Forms.TextBox();
this.lblMaster = new System.Windows.Forms.Label();
this.cbMaster = new System.Windows.Forms.ComboBox();
this.lblErgebnis = new System.Windows.Forms.Label();
this.cbResult = new System.Windows.Forms.ComboBox();
this.lblVon = new System.Windows.Forms.Label();
this.dtFrom = new System.Windows.Forms.DateTimePicker();
this.lblBis = new System.Windows.Forms.Label();
this.dtTo = new System.Windows.Forms.DateTimePicker();
this.btnReset = new System.Windows.Forms.Button();
this.toolbar.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).BeginInit();
this.statusPanel.SuspendLayout();
this.pnlFilters.SuspendLayout();
this.SuspendLayout();
//
// toolbar
//
this.toolbar.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.tsRefresh});
this.toolbar.Location = new System.Drawing.Point(0, 0);
this.toolbar.Name = "toolbar";
this.toolbar.Size = new System.Drawing.Size(1100, 25);
this.toolbar.TabIndex = 0;
//
// tsRefresh
//
this.tsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsRefresh.Name = "tsRefresh";
this.tsRefresh.Size = new System.Drawing.Size(90, 22);
this.tsRefresh.Text = "Aktualisieren";
//
// dgvTrades
//
this.dgvTrades.AllowUserToAddRows = false;
this.dgvTrades.AllowUserToDeleteRows = false;
this.dgvTrades.AutoGenerateColumns = false;
this.dgvTrades.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvTrades.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colTradeId,
this.colAccount,
this.colTrader,
this.colMarket,
this.colOutcome,
this.colSide,
this.colEntry,
this.colExit,
this.colSize,
this.colPnl,
this.colPnlPct,
this.colOpenedAt,
this.colClosedAt});
this.dgvTrades.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvTrades.Location = new System.Drawing.Point(0, 25);
this.dgvTrades.Name = "dgvTrades";
this.dgvTrades.ReadOnly = true;
this.dgvTrades.RowHeadersVisible = false;
this.dgvTrades.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvTrades.Size = new System.Drawing.Size(1100, 549);
this.dgvTrades.TabIndex = 1;
//
// colTradeId
//
this.colTradeId.DataPropertyName = "TradeId";
this.colTradeId.HeaderText = "#";
this.colTradeId.Name = "colTradeId";
this.colTradeId.ReadOnly = true;
this.colTradeId.Width = 60;
//
// colAccount
//
this.colAccount.DataPropertyName = "AccountName";
this.colAccount.HeaderText = "Account";
this.colAccount.Name = "colAccount";
this.colAccount.ReadOnly = true;
this.colAccount.Width = 130;
//
// colTrader
//
this.colTrader.DataPropertyName = "SourceTraderName";
this.colTrader.HeaderText = "Master-Trader";
this.colTrader.Name = "colTrader";
this.colTrader.ReadOnly = true;
this.colTrader.Width = 130;
//
// colMarket
//
this.colMarket.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.colMarket.DataPropertyName = "MarketQuestion";
this.colMarket.HeaderText = "Markt";
this.colMarket.Name = "colMarket";
this.colMarket.ReadOnly = true;
//
// colOutcome
//
this.colOutcome.DataPropertyName = "Outcome";
this.colOutcome.HeaderText = "Outcome";
this.colOutcome.Name = "colOutcome";
this.colOutcome.ReadOnly = true;
this.colOutcome.Width = 80;
//
// colSide
//
this.colSide.DataPropertyName = "Side";
this.colSide.HeaderText = "Side";
this.colSide.Name = "colSide";
this.colSide.ReadOnly = true;
this.colSide.Width = 60;
//
// colEntry
//
this.colEntry.DataPropertyName = "EntryPrice";
this.colEntry.HeaderText = "Entry";
this.colEntry.Name = "colEntry";
this.colEntry.ReadOnly = true;
this.colEntry.Width = 70;
//
// colExit
//
this.colExit.DataPropertyName = "ExitPrice";
this.colExit.HeaderText = "Exit";
this.colExit.Name = "colExit";
this.colExit.ReadOnly = true;
this.colExit.Width = 70;
//
// colSize
//
this.colSize.DataPropertyName = "Size";
this.colSize.HeaderText = "Size";
this.colSize.Name = "colSize";
this.colSize.ReadOnly = true;
this.colSize.Width = 70;
//
// colPnl
//
this.colPnl.DataPropertyName = "RealizedPnl";
this.colPnl.HeaderText = "PnL";
this.colPnl.Name = "colPnl";
this.colPnl.ReadOnly = true;
this.colPnl.Width = 80;
//
// colPnlPct
//
this.colPnlPct.DataPropertyName = "PnlPercent";
this.colPnlPct.HeaderText = "PnL %";
this.colPnlPct.Name = "colPnlPct";
this.colPnlPct.ReadOnly = true;
this.colPnlPct.Width = 70;
//
// colOpenedAt
//
this.colOpenedAt.DataPropertyName = "OpenedAt";
this.colOpenedAt.HeaderText = "Eröffnet";
this.colOpenedAt.Name = "colOpenedAt";
this.colOpenedAt.ReadOnly = true;
this.colOpenedAt.Width = 130;
//
// colClosedAt
//
this.colClosedAt.DataPropertyName = "ClosedAt";
this.colClosedAt.HeaderText = "Geschlossen";
this.colClosedAt.Name = "colClosedAt";
this.colClosedAt.ReadOnly = true;
this.colClosedAt.Width = 130;
//
// statusPanel
//
this.statusPanel.Controls.Add(this.lblSummary);
this.statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.statusPanel.Location = new System.Drawing.Point(0, 574);
this.statusPanel.Name = "statusPanel";
this.statusPanel.Size = new System.Drawing.Size(1100, 26);
this.statusPanel.TabIndex = 2;
//
// lblSummary
//
this.lblSummary.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblSummary.Location = new System.Drawing.Point(0, 0);
this.lblSummary.Name = "lblSummary";
this.lblSummary.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
this.lblSummary.Size = new System.Drawing.Size(1100, 26);
this.lblSummary.TabIndex = 0;
this.lblSummary.Text = "—";
this.lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// pnlFilters
//
this.pnlFilters.Controls.Add(this.lblMarkt);
this.pnlFilters.Controls.Add(this.tbMarket);
this.pnlFilters.Controls.Add(this.lblMaster);
this.pnlFilters.Controls.Add(this.cbMaster);
this.pnlFilters.Controls.Add(this.lblErgebnis);
this.pnlFilters.Controls.Add(this.cbResult);
this.pnlFilters.Controls.Add(this.lblVon);
this.pnlFilters.Controls.Add(this.dtFrom);
this.pnlFilters.Controls.Add(this.lblBis);
this.pnlFilters.Controls.Add(this.dtTo);
this.pnlFilters.Controls.Add(this.btnReset);
this.pnlFilters.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlFilters.Location = new System.Drawing.Point(0, 25);
this.pnlFilters.Name = "pnlFilters";
this.pnlFilters.Size = new System.Drawing.Size(1100, 34);
this.pnlFilters.TabIndex = 3;
//
// lblMarkt
//
this.lblMarkt.AutoSize = true;
this.lblMarkt.Location = new System.Drawing.Point(6, 9);
this.lblMarkt.Name = "lblMarkt";
this.lblMarkt.Size = new System.Drawing.Size(41, 15);
this.lblMarkt.Text = "Markt:";
//
// tbMarket
//
this.tbMarket.Location = new System.Drawing.Point(51, 6);
this.tbMarket.Name = "tbMarket";
this.tbMarket.PlaceholderText = "Suchtext …";
this.tbMarket.Size = new System.Drawing.Size(200, 23);
this.tbMarket.TabIndex = 0;
//
// lblMaster
//
this.lblMaster.AutoSize = true;
this.lblMaster.Location = new System.Drawing.Point(263, 9);
this.lblMaster.Name = "lblMaster";
this.lblMaster.Size = new System.Drawing.Size(48, 15);
this.lblMaster.Text = "Master:";
//
// cbMaster
//
this.cbMaster.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbMaster.Location = new System.Drawing.Point(315, 6);
this.cbMaster.Name = "cbMaster";
this.cbMaster.Size = new System.Drawing.Size(170, 23);
this.cbMaster.TabIndex = 1;
//
// lblErgebnis
//
this.lblErgebnis.AutoSize = true;
this.lblErgebnis.Location = new System.Drawing.Point(497, 9);
this.lblErgebnis.Name = "lblErgebnis";
this.lblErgebnis.Size = new System.Drawing.Size(58, 15);
this.lblErgebnis.Text = "Ergebnis:";
//
// cbResult
//
this.cbResult.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbResult.Items.AddRange(new object[] { "Alle", "Gewinner", "Verlierer" });
this.cbResult.Location = new System.Drawing.Point(559, 6);
this.cbResult.Name = "cbResult";
this.cbResult.Size = new System.Drawing.Size(110, 23);
this.cbResult.TabIndex = 2;
//
// lblVon
//
this.lblVon.AutoSize = true;
this.lblVon.Location = new System.Drawing.Point(681, 9);
this.lblVon.Name = "lblVon";
this.lblVon.Size = new System.Drawing.Size(31, 15);
this.lblVon.Text = "Von:";
//
// dtFrom
//
this.dtFrom.Checked = false;
this.dtFrom.Format = System.Windows.Forms.DateTimePickerFormat.Short;
this.dtFrom.Location = new System.Drawing.Point(714, 6);
this.dtFrom.Name = "dtFrom";
this.dtFrom.ShowCheckBox = true;
this.dtFrom.Size = new System.Drawing.Size(120, 23);
this.dtFrom.TabIndex = 3;
//
// lblBis
//
this.lblBis.AutoSize = true;
this.lblBis.Location = new System.Drawing.Point(840, 9);
this.lblBis.Name = "lblBis";
this.lblBis.Size = new System.Drawing.Size(26, 15);
this.lblBis.Text = "Bis:";
//
// dtTo
//
this.dtTo.Checked = false;
this.dtTo.Format = System.Windows.Forms.DateTimePickerFormat.Short;
this.dtTo.Location = new System.Drawing.Point(868, 6);
this.dtTo.Name = "dtTo";
this.dtTo.ShowCheckBox = true;
this.dtTo.Size = new System.Drawing.Size(120, 23);
this.dtTo.TabIndex = 4;
//
// btnReset
//
this.btnReset.Location = new System.Drawing.Point(994, 5);
this.btnReset.Name = "btnReset";
this.btnReset.Size = new System.Drawing.Size(90, 25);
this.btnReset.TabIndex = 5;
this.btnReset.Text = "Zurücksetzen";
this.btnReset.UseVisualStyleBackColor = true;
//
// ClosedTradesView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.dgvTrades);
this.Controls.Add(this.statusPanel);
this.Controls.Add(this.pnlFilters);
this.Controls.Add(this.toolbar);
this.Name = "ClosedTradesView";
this.Size = new System.Drawing.Size(1100, 600);
this.toolbar.ResumeLayout(false);
this.toolbar.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).EndInit();
this.statusPanel.ResumeLayout(false);
this.pnlFilters.ResumeLayout(false);
this.pnlFilters.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolbar;
private System.Windows.Forms.ToolStripButton tsRefresh;
private System.Windows.Forms.DataGridView dgvTrades;
private System.Windows.Forms.DataGridViewTextBoxColumn colTradeId;
private System.Windows.Forms.DataGridViewTextBoxColumn colAccount;
private System.Windows.Forms.DataGridViewTextBoxColumn colTrader;
private System.Windows.Forms.DataGridViewTextBoxColumn colMarket;
private System.Windows.Forms.DataGridViewTextBoxColumn colOutcome;
private System.Windows.Forms.DataGridViewTextBoxColumn colSide;
private System.Windows.Forms.DataGridViewTextBoxColumn colEntry;
private System.Windows.Forms.DataGridViewTextBoxColumn colExit;
private System.Windows.Forms.DataGridViewTextBoxColumn colSize;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct;
private System.Windows.Forms.DataGridViewTextBoxColumn colOpenedAt;
private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt;
private System.Windows.Forms.Panel statusPanel;
private System.Windows.Forms.Label lblSummary;
private System.Windows.Forms.Panel pnlFilters;
private System.Windows.Forms.Label lblMarkt;
private System.Windows.Forms.TextBox tbMarket;
private System.Windows.Forms.Label lblMaster;
private System.Windows.Forms.ComboBox cbMaster;
private System.Windows.Forms.Label lblErgebnis;
private System.Windows.Forms.ComboBox cbResult;
private System.Windows.Forms.Label lblVon;
private System.Windows.Forms.DateTimePicker dtFrom;
private System.Windows.Forms.Label lblBis;
private System.Windows.Forms.DateTimePicker dtTo;
private System.Windows.Forms.Button btnReset;
}
}
@@ -1,177 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Zeigt die geschlossenen Copytrades des Moduls (mod_copytrading_closed_trades) mit
/// aufgelösten Account-/Master-Trader-Namen und einer Kurzauswertung.
/// Layout im Designer (ClosedTradesView.Designer.cs), Daten/Logik hier.
/// </summary>
public partial class ClosedTradesView : UserControl
{
private ICopyTradeLogRepository? _tradeLog;
private TradingState? _state;
private CopyTradingState? _copyState;
private List<ClosedTradeRow> _allRows = new();
private bool _loading;
// Parameterloser Konstruktor für den WinForms-Designer.
public ClosedTradesView()
{
InitializeComponent();
colEntry.DefaultCellStyle.Format = "F3";
colExit.DefaultCellStyle.Format = "F3";
colSize.DefaultCellStyle.Format = "F2";
colPnl.DefaultCellStyle.Format = "F2";
colPnlPct.DefaultCellStyle.Format = "F1";
colOpenedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
colClosedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
tsRefresh.Click += (_, _) => LoadData();
// Zeilenfärbung nach PnL-% (grün/hellgrün/rot) nach jedem (Neu-)Binden.
dgvTrades.DataBindingComplete += (_, _) => ColorRows();
// Filter: Markt (Text), Master (Combo), Ergebnis (Win/Loss), Datum (Von/Bis, optional).
tbMarket.TextChanged += (_, _) => ApplyFilter();
cbMaster.SelectedIndexChanged += (_, _) => ApplyFilter();
cbResult.SelectedIndexChanged += (_, _) => ApplyFilter();
dtFrom.ValueChanged += (_, _) => ApplyFilter();
dtTo.ValueChanged += (_, _) => ApplyFilter();
btnReset.Click += (_, _) => ResetFilters();
if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0;
}
/// <summary>Injiziert die Abhängigkeiten (nach der DI-Auflösung) und lädt die Daten.</summary>
public void Initialize(ICopyTradeLogRepository tradeLog, TradingState state, CopyTradingState copyState)
{
_tradeLog = tradeLog;
_state = state;
_copyState = copyState;
LoadData();
}
private void LoadData()
{
if (_tradeLog == null) return;
_allRows = _tradeLog.Find(_ => true)
.OrderByDescending(t => t.ClosedAt)
.Select(t => new ClosedTradeRow
{
TradeId = t.TradeId,
AccountId = t.AccountId,
SourceTraderId = t.SourceTraderId,
IsDemo = t.IsDemo,
TokenId = t.TokenId,
MarketSlug = t.MarketSlug,
MarketQuestion = t.MarketQuestion,
Outcome = t.Outcome,
Side = t.Side,
EntryPrice = t.EntryPrice,
ExitPrice = t.ExitPrice,
Size = t.Size,
RealizedPnl = t.RealizedPnl,
PnlPercent = t.PnlPercent,
TotalFees = t.TotalFees,
OpenedAt = t.OpenedAt,
ClosedAt = t.ClosedAt,
ExitReason = t.ExitReason,
AccountName = ResolveAccount(t.AccountId, t.IsDemo),
SourceTraderName = ResolveTrader(t.SourceTraderId)
})
.ToList();
PopulateMasters();
ApplyFilter();
}
private void PopulateMasters()
{
_loading = true;
string prev = cbMaster.SelectedItem as string ?? "Alle";
cbMaster.Items.Clear();
cbMaster.Items.Add("Alle");
foreach (var m in _allRows.Select(r => r.SourceTraderName)
.Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s))
cbMaster.Items.Add(m);
int idx = cbMaster.Items.IndexOf(prev);
cbMaster.SelectedIndex = idx >= 0 ? idx : 0;
_loading = false;
}
private void ApplyFilter()
{
if (_loading) return;
IEnumerable<ClosedTradeRow> q = _allRows;
string market = tbMarket.Text.Trim();
if (market.Length > 0)
q = q.Where(r => (r.MarketQuestion?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false)
|| (r.MarketSlug?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false));
if (cbMaster.SelectedItem is string master && master != "Alle")
q = q.Where(r => r.SourceTraderName == master);
string result = cbResult.SelectedItem as string ?? "Alle";
if (result == "Gewinner") q = q.Where(r => r.RealizedPnl > 0m);
else if (result == "Verlierer") q = q.Where(r => r.RealizedPnl < 0m);
if (dtFrom.Checked) { DateTime f = dtFrom.Value.Date; q = q.Where(r => r.ClosedAt >= f); }
if (dtTo.Checked) { DateTime t = dtTo.Value.Date.AddDays(1).AddTicks(-1); q = q.Where(r => r.ClosedAt <= t); }
var rows = q.ToList();
dgvTrades.DataSource = new BindingList<ClosedTradeRow>(rows);
decimal totalPnl = rows.Sum(x => x.RealizedPnl);
int wins = rows.Count(x => x.RealizedPnl > 0);
double winrate = rows.Count > 0 ? (double)wins / rows.Count * 100 : 0;
lblSummary.Text = $"{rows.Count} / {_allRows.Count} Trades | PnL: {totalPnl:F2} USDC | Winrate: {winrate:F1}%";
}
private void ResetFilters()
{
_loading = true;
tbMarket.Text = string.Empty;
if (cbMaster.Items.Count > 0) cbMaster.SelectedIndex = 0;
if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0;
dtFrom.Checked = false;
dtTo.Checked = false;
_loading = false;
ApplyFilter();
}
private void ColorRows()
{
foreach (DataGridViewRow row in dgvTrades.Rows)
if (row.DataBoundItem is ClosedTradeRow r)
row.DefaultCellStyle.BackColor = TradeRowColoring.ForPnlPercent(r.PnlPercent);
}
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
private string ResolveTrader(int traderId)
{
if (_copyState != null && _copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName))
return t.DisplayName;
return traderId > 0 ? $"#{traderId}" : "Unbekannt";
}
}
}
@@ -1,167 +0,0 @@
namespace PolyTrader.Modules.CopyTrading.Ui
{
partial class CopyTradingMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(CopyTradingMainForm));
tabControl = new TabControl();
tabMasters = new TabPage();
masterTradersView = new MasterTradersView();
tabOpen = new TabPage();
openTradesView = new OpenTradesView();
tabClosed = new TabPage();
closedTradesView = new ClosedTradesView();
tabSettings = new TabPage();
accountSettingsView = new AccountSettingsView();
tabControl.SuspendLayout();
tabMasters.SuspendLayout();
tabOpen.SuspendLayout();
tabClosed.SuspendLayout();
tabSettings.SuspendLayout();
SuspendLayout();
//
// tabControl
//
tabControl.Controls.Add(tabMasters);
tabControl.Controls.Add(tabOpen);
tabControl.Controls.Add(tabClosed);
tabControl.Controls.Add(tabSettings);
tabControl.Dock = DockStyle.Fill;
tabControl.Location = new Point(0, 0);
tabControl.Margin = new Padding(4, 5, 4, 5);
tabControl.Name = "tabControl";
tabControl.SelectedIndex = 0;
tabControl.Size = new Size(1714, 1167);
tabControl.TabIndex = 0;
//
// tabMasters
//
tabMasters.Controls.Add(masterTradersView);
tabMasters.Location = new Point(4, 34);
tabMasters.Margin = new Padding(4, 5, 4, 5);
tabMasters.Name = "tabMasters";
tabMasters.Padding = new Padding(4, 5, 4, 5);
tabMasters.Size = new Size(1706, 1129);
tabMasters.TabIndex = 0;
tabMasters.Text = "Master-Trader";
tabMasters.UseVisualStyleBackColor = true;
//
// masterTradersView
//
masterTradersView.Dock = DockStyle.Fill;
masterTradersView.Location = new Point(4, 5);
masterTradersView.Margin = new Padding(6, 8, 6, 8);
masterTradersView.Name = "masterTradersView";
masterTradersView.Size = new Size(1698, 1119);
masterTradersView.TabIndex = 0;
//
// tabOpen
//
tabOpen.Controls.Add(openTradesView);
tabOpen.Location = new Point(4, 34);
tabOpen.Margin = new Padding(4, 5, 4, 5);
tabOpen.Name = "tabOpen";
tabOpen.Padding = new Padding(4, 5, 4, 5);
tabOpen.Size = new Size(1706, 1129);
tabOpen.TabIndex = 3;
tabOpen.Text = "Offene Trades";
tabOpen.UseVisualStyleBackColor = true;
//
// openTradesView
//
openTradesView.Dock = DockStyle.Fill;
openTradesView.Location = new Point(4, 5);
openTradesView.Margin = new Padding(6, 8, 6, 8);
openTradesView.Name = "openTradesView";
openTradesView.Size = new Size(1698, 1119);
openTradesView.TabIndex = 0;
//
// tabClosed
//
tabClosed.Controls.Add(closedTradesView);
tabClosed.Location = new Point(4, 34);
tabClosed.Margin = new Padding(4, 5, 4, 5);
tabClosed.Name = "tabClosed";
tabClosed.Padding = new Padding(4, 5, 4, 5);
tabClosed.Size = new Size(1706, 1129);
tabClosed.TabIndex = 1;
tabClosed.Text = "Geschlossene Trades";
tabClosed.UseVisualStyleBackColor = true;
//
// closedTradesView
//
closedTradesView.Dock = DockStyle.Fill;
closedTradesView.Location = new Point(4, 5);
closedTradesView.Margin = new Padding(6, 8, 6, 8);
closedTradesView.Name = "closedTradesView";
closedTradesView.Size = new Size(1698, 1119);
closedTradesView.TabIndex = 0;
//
// tabSettings
//
tabSettings.Controls.Add(accountSettingsView);
tabSettings.Location = new Point(4, 34);
tabSettings.Margin = new Padding(4, 5, 4, 5);
tabSettings.Name = "tabSettings";
tabSettings.Padding = new Padding(4, 5, 4, 5);
tabSettings.Size = new Size(1706, 1129);
tabSettings.TabIndex = 2;
tabSettings.Text = "Account-Einstellungen";
tabSettings.UseVisualStyleBackColor = true;
//
// accountSettingsView
//
accountSettingsView.Dock = DockStyle.Fill;
accountSettingsView.Location = new Point(4, 5);
accountSettingsView.Margin = new Padding(6, 8, 6, 8);
accountSettingsView.Name = "accountSettingsView";
accountSettingsView.Size = new Size(1698, 1119);
accountSettingsView.TabIndex = 0;
//
// CopyTradingMainForm
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1714, 1167);
Controls.Add(tabControl);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 5, 4, 5);
MinimumSize = new Size(1162, 763);
Name = "CopyTradingMainForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "Copytrading";
tabControl.ResumeLayout(false);
tabMasters.ResumeLayout(false);
tabOpen.ResumeLayout(false);
tabClosed.ResumeLayout(false);
tabSettings.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage tabMasters;
private System.Windows.Forms.TabPage tabOpen;
private System.Windows.Forms.TabPage tabClosed;
private System.Windows.Forms.TabPage tabSettings;
private MasterTradersView masterTradersView;
private OpenTradesView openTradesView;
private ClosedTradesView closedTradesView;
private AccountSettingsView accountSettingsView;
}
}
@@ -1,41 +0,0 @@
using System;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Das Hauptfenster des Copytrading-Moduls. Bündelt alle Modul-Ansichten in einem
/// TabControl (Master-Trader, geschlossene Trades, Account-Einstellungen), sodass der
/// Launcher nur EINEN "Copytrading"-Button braucht und nicht je View ein eigenes Fenster
/// öffnet. Layout im Designer (CopyTradingMainForm.Designer.cs).
/// </summary>
public partial class CopyTradingMainForm : Form
{
// Parameterloser Konstruktor für den WinForms-Designer.
public CopyTradingMainForm()
{
InitializeComponent();
}
/// <summary>Versorgt die einzelnen Tab-Ansichten mit ihren Abhängigkeiten (aus dem DI-Container).</summary>
public void Initialize(IServiceProvider services)
{
var state = services.GetRequiredService<TradingState>();
var copyState = services.GetRequiredService<CopyTradingState>();
masterTradersView.Initialize(
services.GetRequiredService<ITrackedTraderRepository>(), state, copyState);
openTradesView.Initialize(state, copyState);
closedTradesView.Initialize(
services.GetRequiredService<ICopyTradeLogRepository>(), state, copyState);
accountSettingsView.Initialize(
services.GetRequiredService<ICopyTradingAccountSettingsRepository>(), state, copyState);
}
}
}
@@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
YII=
</value>
</data>
</root>
@@ -1,348 +0,0 @@
namespace PolyTrader.Modules.CopyTrading.Ui
{
partial class MasterTradersView
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.toolbar = new System.Windows.Forms.ToolStrip();
this.tsNew = new System.Windows.Forms.ToolStripButton();
this.tsSave = new System.Windows.Forms.ToolStripButton();
this.tsDelete = new System.Windows.Forms.ToolStripButton();
this.tsSep = new System.Windows.Forms.ToolStripSeparator();
this.tsRefresh = new System.Windows.Forms.ToolStripButton();
this.grid = new System.Windows.Forms.DataGridView();
this.colId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colWallet = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colName = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCategory = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colActive = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.colTrades = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colWinrate = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCopyPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCopyPf = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCopyAvg = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCopyCount = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.splitter = new System.Windows.Forms.Splitter();
this.rightPanel = new System.Windows.Forms.Panel();
this.pgDetail = new System.Windows.Forms.PropertyGrid();
this.grpAccounts = new System.Windows.Forms.GroupBox();
this.clbAccounts = new System.Windows.Forms.CheckedListBox();
this.statusPanel = new System.Windows.Forms.Panel();
this.lblHint = new System.Windows.Forms.Label();
this.toolbar.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.grid)).BeginInit();
this.rightPanel.SuspendLayout();
this.grpAccounts.SuspendLayout();
this.statusPanel.SuspendLayout();
this.SuspendLayout();
//
// toolbar
//
this.toolbar.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.tsNew,
this.tsSave,
this.tsDelete,
this.tsSep,
this.tsRefresh});
this.toolbar.Location = new System.Drawing.Point(0, 0);
this.toolbar.Name = "toolbar";
this.toolbar.Size = new System.Drawing.Size(1180, 25);
this.toolbar.TabIndex = 0;
//
// tsNew
//
this.tsNew.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsNew.Name = "tsNew";
this.tsNew.Size = new System.Drawing.Size(35, 22);
this.tsNew.Text = "Neu";
//
// tsSave
//
this.tsSave.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsSave.Name = "tsSave";
this.tsSave.Size = new System.Drawing.Size(69, 22);
this.tsSave.Text = "Speichern";
//
// tsDelete
//
this.tsDelete.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsDelete.Name = "tsDelete";
this.tsDelete.Size = new System.Drawing.Size(57, 22);
this.tsDelete.Text = "Löschen";
//
// tsSep
//
this.tsSep.Name = "tsSep";
this.tsSep.Size = new System.Drawing.Size(6, 25);
//
// tsRefresh
//
this.tsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsRefresh.Name = "tsRefresh";
this.tsRefresh.Size = new System.Drawing.Size(90, 22);
this.tsRefresh.Text = "Aktualisieren";
//
// grid
//
this.grid.AllowUserToAddRows = false;
this.grid.AllowUserToDeleteRows = false;
this.grid.AutoGenerateColumns = false;
this.grid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.grid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.grid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colId,
this.colWallet,
this.colName,
this.colCategory,
this.colActive,
this.colTrades,
this.colWinrate,
this.colPnl,
this.colCopyPnl,
this.colCopyPf,
this.colCopyAvg,
this.colCopyCount});
this.grid.Dock = System.Windows.Forms.DockStyle.Fill;
this.grid.Location = new System.Drawing.Point(0, 25);
this.grid.MultiSelect = false;
this.grid.Name = "grid";
this.grid.ReadOnly = true;
this.grid.RowHeadersVisible = false;
this.grid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.grid.Size = new System.Drawing.Size(715, 589);
this.grid.TabIndex = 4;
//
// colId
//
this.colId.DataPropertyName = "Id";
this.colId.HeaderText = "#";
this.colId.Name = "colId";
this.colId.ReadOnly = true;
this.colId.Width = 50;
//
// colWallet
//
this.colWallet.DataPropertyName = "WalletAddress";
this.colWallet.HeaderText = "Wallet";
this.colWallet.Name = "colWallet";
this.colWallet.ReadOnly = true;
this.colWallet.Width = 170;
//
// colName
//
this.colName.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.colName.DataPropertyName = "DisplayName";
this.colName.HeaderText = "Name";
this.colName.Name = "colName";
this.colName.ReadOnly = true;
//
// colCategory
//
this.colCategory.DataPropertyName = "Category";
this.colCategory.HeaderText = "Kategorie";
this.colCategory.Name = "colCategory";
this.colCategory.ReadOnly = true;
this.colCategory.Width = 110;
//
// colActive
//
this.colActive.DataPropertyName = "IsActive";
this.colActive.HeaderText = "Aktiv";
this.colActive.Name = "colActive";
this.colActive.ReadOnly = true;
this.colActive.Width = 50;
//
// colTrades
//
this.colTrades.DataPropertyName = "TotalTrades";
this.colTrades.HeaderText = "Trades (7T)";
this.colTrades.Name = "colTrades";
this.colTrades.ReadOnly = true;
this.colTrades.Width = 90;
//
// colWinrate
//
this.colWinrate.DataPropertyName = "Winrate30t";
this.colWinrate.HeaderText = "Winrate %";
this.colWinrate.Name = "colWinrate";
this.colWinrate.ReadOnly = true;
this.colWinrate.Width = 90;
//
// colPnl
//
this.colPnl.DataPropertyName = "TotalPnl";
this.colPnl.HeaderText = "PnL (7T)";
this.colPnl.Name = "colPnl";
this.colPnl.ReadOnly = true;
this.colPnl.Width = 100;
//
// colCopyPnl
//
this.colCopyPnl.DataPropertyName = "CopyPnl30d";
this.colCopyPnl.HeaderText = "Copy-PnL (30T)";
this.colCopyPnl.Name = "colCopyPnl";
this.colCopyPnl.ReadOnly = true;
this.colCopyPnl.Width = 110;
//
// colCopyPf
//
this.colCopyPf.DataPropertyName = "CopyProfitFactor";
this.colCopyPf.HeaderText = "Profit-Faktor";
this.colCopyPf.Name = "colCopyPf";
this.colCopyPf.ReadOnly = true;
this.colCopyPf.Width = 100;
//
// colCopyAvg
//
this.colCopyAvg.DataPropertyName = "CopyAvgPnlPerTrade";
this.colCopyAvg.HeaderText = "Ø PnL/Trade";
this.colCopyAvg.Name = "colCopyAvg";
this.colCopyAvg.ReadOnly = true;
this.colCopyAvg.Width = 100;
//
// colCopyCount
//
this.colCopyCount.DataPropertyName = "CopyTradeCount30d";
this.colCopyCount.HeaderText = "Copy-Trades";
this.colCopyCount.Name = "colCopyCount";
this.colCopyCount.ReadOnly = true;
this.colCopyCount.Width = 90;
//
// splitter
//
this.splitter.Dock = System.Windows.Forms.DockStyle.Right;
this.splitter.Location = new System.Drawing.Point(715, 25);
this.splitter.Name = "splitter";
this.splitter.Size = new System.Drawing.Size(5, 589);
this.splitter.TabIndex = 3;
this.splitter.TabStop = false;
//
// rightPanel
//
this.rightPanel.Controls.Add(this.pgDetail);
this.rightPanel.Controls.Add(this.grpAccounts);
this.rightPanel.Dock = System.Windows.Forms.DockStyle.Right;
this.rightPanel.Location = new System.Drawing.Point(720, 25);
this.rightPanel.Name = "rightPanel";
this.rightPanel.Padding = new System.Windows.Forms.Padding(6, 0, 0, 0);
this.rightPanel.Size = new System.Drawing.Size(460, 589);
this.rightPanel.TabIndex = 2;
//
// pgDetail
//
this.pgDetail.Dock = System.Windows.Forms.DockStyle.Fill;
this.pgDetail.Location = new System.Drawing.Point(6, 0);
this.pgDetail.Name = "pgDetail";
this.pgDetail.PropertySort = System.Windows.Forms.PropertySort.Categorized;
this.pgDetail.Size = new System.Drawing.Size(454, 399);
this.pgDetail.TabIndex = 0;
this.pgDetail.ToolbarVisible = false;
//
// grpAccounts
//
this.grpAccounts.Controls.Add(this.clbAccounts);
this.grpAccounts.Dock = System.Windows.Forms.DockStyle.Bottom;
this.grpAccounts.Location = new System.Drawing.Point(6, 399);
this.grpAccounts.Name = "grpAccounts";
this.grpAccounts.Padding = new System.Windows.Forms.Padding(8);
this.grpAccounts.Size = new System.Drawing.Size(454, 190);
this.grpAccounts.TabIndex = 1;
this.grpAccounts.TabStop = false;
this.grpAccounts.Text = "Zugewiesene Accounts (kopieren diesen Trader)";
//
// clbAccounts
//
this.clbAccounts.CheckOnClick = true;
this.clbAccounts.Dock = System.Windows.Forms.DockStyle.Fill;
this.clbAccounts.IntegralHeight = false;
this.clbAccounts.Location = new System.Drawing.Point(8, 24);
this.clbAccounts.Name = "clbAccounts";
this.clbAccounts.Size = new System.Drawing.Size(438, 158);
this.clbAccounts.TabIndex = 0;
//
// statusPanel
//
this.statusPanel.Controls.Add(this.lblHint);
this.statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.statusPanel.Location = new System.Drawing.Point(0, 614);
this.statusPanel.Name = "statusPanel";
this.statusPanel.Size = new System.Drawing.Size(1180, 26);
this.statusPanel.TabIndex = 1;
//
// lblHint
//
this.lblHint.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblHint.ForeColor = System.Drawing.Color.DimGray;
this.lblHint.Location = new System.Drawing.Point(0, 0);
this.lblHint.Name = "lblHint";
this.lblHint.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
this.lblHint.Size = new System.Drawing.Size(1180, 26);
this.lblHint.TabIndex = 0;
this.lblHint.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// MasterTradersView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.grid);
this.Controls.Add(this.splitter);
this.Controls.Add(this.rightPanel);
this.Controls.Add(this.statusPanel);
this.Controls.Add(this.toolbar);
this.Name = "MasterTradersView";
this.Size = new System.Drawing.Size(1180, 640);
this.toolbar.ResumeLayout(false);
this.toolbar.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.grid)).EndInit();
this.rightPanel.ResumeLayout(false);
this.grpAccounts.ResumeLayout(false);
this.statusPanel.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolbar;
private System.Windows.Forms.ToolStripButton tsNew;
private System.Windows.Forms.ToolStripButton tsSave;
private System.Windows.Forms.ToolStripButton tsDelete;
private System.Windows.Forms.ToolStripSeparator tsSep;
private System.Windows.Forms.ToolStripButton tsRefresh;
private System.Windows.Forms.DataGridView grid;
private System.Windows.Forms.DataGridViewTextBoxColumn colId;
private System.Windows.Forms.DataGridViewTextBoxColumn colWallet;
private System.Windows.Forms.DataGridViewTextBoxColumn colName;
private System.Windows.Forms.DataGridViewTextBoxColumn colCategory;
private System.Windows.Forms.DataGridViewCheckBoxColumn colActive;
private System.Windows.Forms.DataGridViewTextBoxColumn colTrades;
private System.Windows.Forms.DataGridViewTextBoxColumn colWinrate;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyPf;
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyAvg;
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyCount;
private System.Windows.Forms.Splitter splitter;
private System.Windows.Forms.Panel rightPanel;
private System.Windows.Forms.PropertyGrid pgDetail;
private System.Windows.Forms.GroupBox grpAccounts;
private System.Windows.Forms.CheckedListBox clbAccounts;
private System.Windows.Forms.Panel statusPanel;
private System.Windows.Forms.Label lblHint;
}
}
@@ -1,152 +0,0 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Verwaltung der Master-Trader (mod_copytrading_traders): Liste, Detail-Editor
/// (PropertyGrid) und Account-Zuweisung (welche Accounts diesen Trader kopieren).
/// Persistiert über das Repo und hält den Hot-Path-State
/// (<see cref="CopyTradingState.Traders"/>) synchron.
/// Layout im Designer (MasterTradersView.Designer.cs), Daten/Logik hier.
/// </summary>
public partial class MasterTradersView : UserControl
{
private ITrackedTraderRepository? _repo;
private TradingState? _state;
private CopyTradingState? _copyState;
private BindingList<TrackedTrader> _binding = new();
private TrackedTrader? _current;
// Parameterloser Konstruktor für den WinForms-Designer.
public MasterTradersView()
{
InitializeComponent();
colWinrate.DefaultCellStyle.Format = "F1";
colPnl.DefaultCellStyle.Format = "F2";
colCopyPnl.DefaultCellStyle.Format = "F2";
colCopyPf.DefaultCellStyle.Format = "F2";
colCopyAvg.DefaultCellStyle.Format = "F2";
// Spaltenbreiten proportional (grid-weites Fill): keine überlaufenden Fixbreiten mehr,
// die Spalten teilen sich immer exakt die verfügbare Breite (Fix des „verbuggten" Grids).
colId.FillWeight = 40;
colWallet.FillWeight = 150;
colName.FillWeight = 200;
colCategory.FillWeight = 90;
colActive.FillWeight = 50;
colTrades.FillWeight = 85;
colWinrate.FillWeight = 90;
colPnl.FillWeight = 90;
colCopyPnl.FillWeight = 110;
colCopyPf.FillWeight = 95;
colCopyAvg.FillWeight = 95;
colCopyCount.FillWeight = 95;
tsNew.Click += (_, _) => AddNew();
tsSave.Click += (_, _) => SaveCurrent();
tsDelete.Click += (_, _) => DeleteCurrent();
tsRefresh.Click += (_, _) => LoadData();
grid.SelectionChanged += (_, _) => OnSelectionChanged();
}
/// <summary>Injiziert die Abhängigkeiten (nach der DI-Auflösung) und lädt die Trader.</summary>
public void Initialize(ITrackedTraderRepository repo, TradingState state, CopyTradingState copyState)
{
_repo = repo;
_state = state;
_copyState = copyState;
LoadData();
}
private void LoadData()
{
if (_repo == null) return;
var traders = _repo.GetAll().OrderBy(t => t.Id).ToList();
_binding = new BindingList<TrackedTrader>(traders);
grid.DataSource = _binding;
if (traders.Count > 0)
grid.CurrentCell = grid.Rows[0].Cells[0];
else
ClearDetail();
lblHint.Text = $"{traders.Count} Master-Trader geladen.";
}
private void OnSelectionChanged()
{
if (grid.CurrentRow?.DataBoundItem is TrackedTrader t)
BindDetail(t);
}
private void BindDetail(TrackedTrader trader)
{
_current = trader;
pgDetail.SelectedObject = trader;
clbAccounts.Items.Clear();
if (_state == null) return;
foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId))
{
string label = string.IsNullOrEmpty(acc.Name) ? $"#{acc.AccountId}" : $"{acc.Name} (#{acc.AccountId}){(acc.IsDemo ? " · Demo" : "")}";
int idx = clbAccounts.Items.Add(new AccountItem(acc.AccountId, label));
clbAccounts.SetItemChecked(idx, trader.AssignedAccountIds.Contains(acc.AccountId));
}
}
private void ClearDetail()
{
_current = null;
pgDetail.SelectedObject = null;
clbAccounts.Items.Clear();
}
private void AddNew()
{
int nextId = _binding.Count > 0 ? _binding.Max(t => t.Id) + 1 : 1;
var trader = new TrackedTrader { Id = nextId, DisplayName = $"Neuer Trader {nextId}" };
_binding.Add(trader);
grid.CurrentCell = grid.Rows[_binding.Count - 1].Cells[0];
lblHint.Text = $"Neuer Master-Trader #{nextId} Felder ausfüllen und Speichern.";
}
private void SaveCurrent()
{
if (_current == null || _repo == null || _copyState == null) { lblHint.Text = "Kein Trader ausgewählt."; return; }
_current.AssignedAccountIds = clbAccounts.CheckedItems.Cast<AccountItem>().Select(a => a.Id).ToHashSet();
_repo.Upsert(_current);
_copyState.Traders[_current.Id] = _current;
grid.Refresh();
lblHint.Text = $"Gespeichert: #{_current.Id} {_current.DisplayName} ({_current.AssignedAccountIds.Count} Account(s)) um {DateTime.Now:HH:mm:ss}.";
}
private void DeleteCurrent()
{
if (_current == null || _repo == null || _copyState == null) { lblHint.Text = "Kein Trader ausgewählt."; return; }
var id = _current.Id;
if (MessageBox.Show($"Master-Trader #{id} ({_current.DisplayName}) wirklich löschen?",
"Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
return;
_repo.Delete(id);
_copyState.Traders.TryRemove(id, out _);
_binding.Remove(_current);
ClearDetail();
lblHint.Text = $"Master-Trader #{id} gelöscht.";
}
private sealed record AccountItem(int Id, string Label)
{
public override string ToString() => Label;
}
}
}
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -1,241 +0,0 @@
namespace PolyTrader.Modules.CopyTrading.Ui
{
partial class OpenTradesView
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.toolbar = new System.Windows.Forms.ToolStrip();
this.tsRefresh = new System.Windows.Forms.ToolStripButton();
this.dgvOpen = new System.Windows.Forms.DataGridView();
this.colAccount = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colTrader = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colMarket = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colOutcome = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSide = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colEntry = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colCurrent = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSize = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAmount = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colValue = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colUnrealized = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colUnrealizedPct = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colStatus = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.statusPanel = new System.Windows.Forms.Panel();
this.lblSummary = new System.Windows.Forms.Label();
this.toolbar.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvOpen)).BeginInit();
this.statusPanel.SuspendLayout();
this.SuspendLayout();
//
// toolbar
//
this.toolbar.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.tsRefresh });
this.toolbar.Location = new System.Drawing.Point(0, 0);
this.toolbar.Name = "toolbar";
this.toolbar.Size = new System.Drawing.Size(1100, 25);
this.toolbar.TabIndex = 0;
//
// tsRefresh
//
this.tsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.tsRefresh.Name = "tsRefresh";
this.tsRefresh.Size = new System.Drawing.Size(90, 22);
this.tsRefresh.Text = "Aktualisieren";
//
// dgvOpen
//
this.dgvOpen.AllowUserToAddRows = false;
this.dgvOpen.AllowUserToDeleteRows = false;
this.dgvOpen.AutoGenerateColumns = false;
this.dgvOpen.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dgvOpen.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvOpen.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colAccount, this.colTrader, this.colMarket, this.colOutcome, this.colSide,
this.colEntry, this.colCurrent, this.colSize, this.colAmount, this.colValue,
this.colUnrealized, this.colUnrealizedPct, this.colStatus});
this.dgvOpen.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvOpen.Location = new System.Drawing.Point(0, 25);
this.dgvOpen.Name = "dgvOpen";
this.dgvOpen.ReadOnly = true;
this.dgvOpen.RowHeadersVisible = false;
this.dgvOpen.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvOpen.Size = new System.Drawing.Size(1100, 549);
this.dgvOpen.TabIndex = 1;
//
// colAccount
//
this.colAccount.DataPropertyName = "AccountName";
this.colAccount.HeaderText = "Account";
this.colAccount.Name = "colAccount";
this.colAccount.ReadOnly = true;
this.colAccount.FillWeight = 120;
//
// colTrader
//
this.colTrader.DataPropertyName = "SourceTraderName";
this.colTrader.HeaderText = "Master-Trader";
this.colTrader.Name = "colTrader";
this.colTrader.ReadOnly = true;
this.colTrader.FillWeight = 120;
//
// colMarket
//
this.colMarket.DataPropertyName = "MarketQuestion";
this.colMarket.HeaderText = "Markt";
this.colMarket.Name = "colMarket";
this.colMarket.ReadOnly = true;
this.colMarket.FillWeight = 240;
//
// colOutcome
//
this.colOutcome.DataPropertyName = "Outcome";
this.colOutcome.HeaderText = "Outcome";
this.colOutcome.Name = "colOutcome";
this.colOutcome.ReadOnly = true;
this.colOutcome.FillWeight = 80;
//
// colSide
//
this.colSide.DataPropertyName = "Side";
this.colSide.HeaderText = "Side";
this.colSide.Name = "colSide";
this.colSide.ReadOnly = true;
this.colSide.FillWeight = 55;
//
// colEntry
//
this.colEntry.DataPropertyName = "EntryPrice";
this.colEntry.HeaderText = "Entry";
this.colEntry.Name = "colEntry";
this.colEntry.ReadOnly = true;
this.colEntry.FillWeight = 65;
//
// colCurrent
//
this.colCurrent.DataPropertyName = "CurrentPrice";
this.colCurrent.HeaderText = "Aktuell";
this.colCurrent.Name = "colCurrent";
this.colCurrent.ReadOnly = true;
this.colCurrent.FillWeight = 65;
//
// colSize
//
this.colSize.DataPropertyName = "Size";
this.colSize.HeaderText = "Size";
this.colSize.Name = "colSize";
this.colSize.ReadOnly = true;
this.colSize.FillWeight = 70;
//
// colAmount
//
this.colAmount.DataPropertyName = "AmountUsd";
this.colAmount.HeaderText = "Einsatz";
this.colAmount.Name = "colAmount";
this.colAmount.ReadOnly = true;
this.colAmount.FillWeight = 80;
//
// colValue
//
this.colValue.DataPropertyName = "CurrentValueUsd";
this.colValue.HeaderText = "Wert";
this.colValue.Name = "colValue";
this.colValue.ReadOnly = true;
this.colValue.FillWeight = 80;
//
// colUnrealized
//
this.colUnrealized.DataPropertyName = "UnrealizedPnl";
this.colUnrealized.HeaderText = "Buchgewinn";
this.colUnrealized.Name = "colUnrealized";
this.colUnrealized.ReadOnly = true;
this.colUnrealized.FillWeight = 90;
//
// colUnrealizedPct
//
this.colUnrealizedPct.DataPropertyName = "UnrealizedPct";
this.colUnrealizedPct.HeaderText = "Buchgew. %";
this.colUnrealizedPct.Name = "colUnrealizedPct";
this.colUnrealizedPct.ReadOnly = true;
this.colUnrealizedPct.FillWeight = 80;
//
// colStatus
//
this.colStatus.DataPropertyName = "Status";
this.colStatus.HeaderText = "Status";
this.colStatus.Name = "colStatus";
this.colStatus.ReadOnly = true;
this.colStatus.FillWeight = 90;
//
// statusPanel
//
this.statusPanel.Controls.Add(this.lblSummary);
this.statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.statusPanel.Location = new System.Drawing.Point(0, 574);
this.statusPanel.Name = "statusPanel";
this.statusPanel.Size = new System.Drawing.Size(1100, 26);
this.statusPanel.TabIndex = 2;
//
// lblSummary
//
this.lblSummary.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblSummary.Location = new System.Drawing.Point(0, 0);
this.lblSummary.Name = "lblSummary";
this.lblSummary.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
this.lblSummary.Size = new System.Drawing.Size(1100, 26);
this.lblSummary.TabIndex = 0;
this.lblSummary.Text = "—";
this.lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// OpenTradesView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.dgvOpen);
this.Controls.Add(this.statusPanel);
this.Controls.Add(this.toolbar);
this.Name = "OpenTradesView";
this.Size = new System.Drawing.Size(1100, 600);
this.toolbar.ResumeLayout(false);
this.toolbar.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvOpen)).EndInit();
this.statusPanel.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolbar;
private System.Windows.Forms.ToolStripButton tsRefresh;
private System.Windows.Forms.DataGridView dgvOpen;
private System.Windows.Forms.DataGridViewTextBoxColumn colAccount;
private System.Windows.Forms.DataGridViewTextBoxColumn colTrader;
private System.Windows.Forms.DataGridViewTextBoxColumn colMarket;
private System.Windows.Forms.DataGridViewTextBoxColumn colOutcome;
private System.Windows.Forms.DataGridViewTextBoxColumn colSide;
private System.Windows.Forms.DataGridViewTextBoxColumn colEntry;
private System.Windows.Forms.DataGridViewTextBoxColumn colCurrent;
private System.Windows.Forms.DataGridViewTextBoxColumn colSize;
private System.Windows.Forms.DataGridViewTextBoxColumn colAmount;
private System.Windows.Forms.DataGridViewTextBoxColumn colValue;
private System.Windows.Forms.DataGridViewTextBoxColumn colUnrealized;
private System.Windows.Forms.DataGridViewTextBoxColumn colUnrealizedPct;
private System.Windows.Forms.DataGridViewTextBoxColumn colStatus;
private System.Windows.Forms.Panel statusPanel;
private System.Windows.Forms.Label lblSummary;
}
}
@@ -1,123 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTraderSharp;
using PolyTraderSharp.Models;
namespace PolyTrader.Modules.CopyTrading.Ui
{
/// <summary>
/// Listet ALLE aktuell offenen Positionen (Portfolios der Accounts) mit Buchgewinn/-verlust.
/// Quelle ist der Laufzeit-State (<see cref="TradingState.Accounts"/>), nicht die DB so ist die
/// Anzeige live. Zeilenfärbung nach Buchgewinn-% (<see cref="TradeRowColoring"/>).
/// Layout im Designer (OpenTradesView.Designer.cs).
/// </summary>
public partial class OpenTradesView : UserControl
{
private TradingState? _state;
private CopyTradingState? _copyState;
public OpenTradesView()
{
InitializeComponent();
colEntry.DefaultCellStyle.Format = "F3";
colCurrent.DefaultCellStyle.Format = "F3";
colSize.DefaultCellStyle.Format = "F2";
colAmount.DefaultCellStyle.Format = "F2";
colValue.DefaultCellStyle.Format = "F2";
colUnrealized.DefaultCellStyle.Format = "F2";
colUnrealizedPct.DefaultCellStyle.Format = "F1";
tsRefresh.Click += (_, _) => LoadData();
dgvOpen.DataBindingComplete += (_, _) => ColorRows();
}
public void Initialize(TradingState state, CopyTradingState copyState)
{
_state = state;
_copyState = copyState;
LoadData();
}
private void LoadData()
{
if (_state == null) return;
var rows = _state.Accounts.Values
.SelectMany(a => a.OpenPositions.Values.Select(p => (acc: a, pos: p)))
.OrderByDescending(x => x.pos.CurrentValueUsd)
.Select(x =>
{
decimal unrealized = x.pos.CurrentValueUsd - x.pos.AmountUsd;
decimal pct = x.pos.AmountUsd > 0m ? unrealized / x.pos.AmountUsd * 100m : 0m;
return new OpenTradeRow
{
AccountName = ResolveAccount(x.acc.AccountId, x.pos.IsDemo),
SourceTraderName = ResolveTrader(x.pos.SourceTraderId, x.pos.SourceTraderName),
MarketQuestion = x.pos.MarketQuestion,
Outcome = x.pos.Outcome,
Side = x.pos.Side,
EntryPrice = x.pos.EntryPrice,
CurrentPrice = x.pos.CurrentPrice,
Size = x.pos.Size,
AmountUsd = x.pos.AmountUsd,
CurrentValueUsd = x.pos.CurrentValueUsd,
UnrealizedPnl = unrealized,
UnrealizedPct = pct,
Status = x.pos.ExitPending ? "Exit läuft" : "offen"
};
})
.ToList();
dgvOpen.DataSource = new BindingList<OpenTradeRow>(rows);
decimal totalValue = rows.Sum(r => r.CurrentValueUsd);
decimal totalUnrealized = rows.Sum(r => r.UnrealizedPnl);
lblSummary.Text = $"{rows.Count} offene Positionen | Wert: {totalValue:F2} USDC | Buchgewinn: {totalUnrealized:F2} USDC";
}
private void ColorRows()
{
foreach (DataGridViewRow row in dgvOpen.Rows)
if (row.DataBoundItem is OpenTradeRow r)
row.DefaultCellStyle.BackColor = TradeRowColoring.ForPnlPercent(r.UnrealizedPct);
}
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
private string ResolveTrader(int traderId, string fallback)
{
if (_copyState != null && _copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName))
return t.DisplayName;
return !string.IsNullOrEmpty(fallback) ? fallback : (traderId > 0 ? $"#{traderId}" : "System");
}
/// <summary>Anzeige-Zeile für offene Positionen.</summary>
private sealed class OpenTradeRow
{
public string AccountName { get; set; } = string.Empty;
public string SourceTraderName { get; set; } = string.Empty;
public string MarketQuestion { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Side { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal CurrentPrice { get; set; }
public decimal Size { get; set; }
public decimal AmountUsd { get; set; }
public decimal CurrentValueUsd { get; set; }
public decimal UnrealizedPnl { get; set; }
public decimal UnrealizedPct { get; set; }
public string Status { get; set; } = string.Empty;
}
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" /> <ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
@@ -20,11 +20,12 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <!-- Linux-Portierung: plattformneutral. Die WinForms-UI dieses Moduls (Scanner und Ausfuehrung)
wurde entfernt; die Avalonia-Ansicht kommt spaeter direkt hierher zurueck Avalonia ist
im Gegensatz zu WinForms plattformuebergreifend und braucht kein eigenes UI-Projekt. -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Modul trägt eigene WinForms-UI-Tabs bei. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -1,4 +1,4 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -54,22 +54,22 @@ namespace PolyTrader.Modules.ResolutionFarming
// Live-Marktquelle, Live-Execution, On-Chain-Auto-Redeem und Kalibrierung folgen (Zielland). // Live-Marktquelle, Live-Execution, On-Chain-Auto-Redeem und Kalibrierung folgen (Zielland).
} }
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services) /// <summary>
/// Aktuell ohne Ansicht: Die WinForms-UI wurde mit der Linux-Portierung entfernt, die
/// Avalonia-Ansicht folgt (Stufe L3/L4). Die Fachlogik dieses Moduls laeuft davon
/// unabhaengig weiter die Shell zeigt schlicht kein Fenster fuer das Modul an.
///
/// <para><b>Beim Nachbau zu erhalten</b> (Spezifikation: docs/UI-SPEZIFIKATION-WinForms.md,
/// Originalcode: Git-Tag <c>winforms-final</c>):</para>
/// <list type="bullet">
/// <item>View-ID <c>resolutionfarming.main</c> (stabil Launcher-Button und Symbol haengen daran)</item>
/// <item>Titel <c>ResolutionFarming</c>, Gruppe <c>ResolutionFarming</c>, Order <c>200</c></item>
/// <item>Ein Fenster fuers ganze Modul: ResolutionFarmingMainForm mit Tabs: Kandidaten / Positionen / Historie+Statistik / Settings</item>
/// </list>
/// </summary>
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
{ {
// EIN Fenster fürs ganze Modul (Tabs: Kandidaten/Positionen/Historie/Settings). // Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
host.RegisterView(new ModuleView
{
Id = "resolutionfarming.main",
Title = "ResolutionFarming",
Group = "ResolutionFarming",
Order = 200,
CreateForm = () =>
{
var form = new Ui.ResolutionFarmingMainForm();
form.Initialize(services);
return form;
}
});
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
@@ -1,379 +0,0 @@
namespace PolyTrader.Modules.ResolutionFarming.Ui
{
partial class ResolutionFarmingMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ResolutionFarmingMainForm));
tabControlRf = new TabControl();
tabKandidaten = new TabPage();
dgvCandidates = new DataGridView();
pnlCandTop = new Panel();
btnCandRefresh = new Button();
cbCandAccount = new ComboBox();
lblCandKonto = new Label();
tabPositionen = new TabPage();
dgvPositions = new DataGridView();
pnlPosTop = new Panel();
btnPosRefresh = new Button();
tabHistorie = new TabPage();
dgvHistory = new DataGridView();
lblHistSummary = new Label();
pnlHistTop = new Panel();
btnHistRefresh = new Button();
tabSettings = new TabPage();
pgSettings = new PropertyGrid();
pnlSetTop = new Panel();
btnSettingsSave = new Button();
cbSettingsAccount = new ComboBox();
lblSetKonto = new Label();
lblRfStatus = new Label();
tabControlRf.SuspendLayout();
tabKandidaten.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvCandidates).BeginInit();
pnlCandTop.SuspendLayout();
tabPositionen.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvPositions).BeginInit();
pnlPosTop.SuspendLayout();
tabHistorie.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvHistory).BeginInit();
pnlHistTop.SuspendLayout();
tabSettings.SuspendLayout();
pnlSetTop.SuspendLayout();
SuspendLayout();
//
// tabControlRf
//
tabControlRf.Controls.Add(tabKandidaten);
tabControlRf.Controls.Add(tabPositionen);
tabControlRf.Controls.Add(tabHistorie);
tabControlRf.Controls.Add(tabSettings);
tabControlRf.Dock = DockStyle.Fill;
tabControlRf.Location = new Point(0, 0);
tabControlRf.Margin = new Padding(4, 5, 4, 5);
tabControlRf.Name = "tabControlRf";
tabControlRf.SelectedIndex = 0;
tabControlRf.Size = new Size(1429, 993);
tabControlRf.TabIndex = 0;
//
// tabKandidaten
//
tabKandidaten.Controls.Add(dgvCandidates);
tabKandidaten.Controls.Add(pnlCandTop);
tabKandidaten.Location = new Point(4, 34);
tabKandidaten.Margin = new Padding(4, 5, 4, 5);
tabKandidaten.Name = "tabKandidaten";
tabKandidaten.Padding = new Padding(4, 5, 4, 5);
tabKandidaten.Size = new Size(1421, 955);
tabKandidaten.TabIndex = 0;
tabKandidaten.Text = "Kandidaten";
tabKandidaten.UseVisualStyleBackColor = true;
//
// dgvCandidates
//
dgvCandidates.AllowUserToAddRows = false;
dgvCandidates.AllowUserToDeleteRows = false;
dgvCandidates.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvCandidates.Dock = DockStyle.Fill;
dgvCandidates.Location = new Point(4, 62);
dgvCandidates.Margin = new Padding(4, 5, 4, 5);
dgvCandidates.Name = "dgvCandidates";
dgvCandidates.ReadOnly = true;
dgvCandidates.RowHeadersVisible = false;
dgvCandidates.RowHeadersWidth = 62;
dgvCandidates.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvCandidates.Size = new Size(1413, 888);
dgvCandidates.TabIndex = 1;
//
// pnlCandTop
//
pnlCandTop.Controls.Add(btnCandRefresh);
pnlCandTop.Controls.Add(cbCandAccount);
pnlCandTop.Controls.Add(lblCandKonto);
pnlCandTop.Dock = DockStyle.Top;
pnlCandTop.Location = new Point(4, 5);
pnlCandTop.Margin = new Padding(4, 5, 4, 5);
pnlCandTop.Name = "pnlCandTop";
pnlCandTop.Size = new Size(1413, 57);
pnlCandTop.TabIndex = 0;
//
// btnCandRefresh
//
btnCandRefresh.Location = new Point(464, 7);
btnCandRefresh.Margin = new Padding(4, 5, 4, 5);
btnCandRefresh.Name = "btnCandRefresh";
btnCandRefresh.Size = new Size(157, 43);
btnCandRefresh.TabIndex = 2;
btnCandRefresh.Text = "Aktualisieren";
btnCandRefresh.UseVisualStyleBackColor = true;
//
// cbCandAccount
//
cbCandAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbCandAccount.Location = new Point(79, 8);
cbCandAccount.Margin = new Padding(4, 5, 4, 5);
cbCandAccount.Name = "cbCandAccount";
cbCandAccount.Size = new Size(370, 33);
cbCandAccount.TabIndex = 1;
//
// lblCandKonto
//
lblCandKonto.AutoSize = true;
lblCandKonto.Location = new Point(9, 15);
lblCandKonto.Margin = new Padding(4, 0, 4, 0);
lblCandKonto.Name = "lblCandKonto";
lblCandKonto.Size = new Size(64, 25);
lblCandKonto.TabIndex = 0;
lblCandKonto.Text = "Konto:";
//
// tabPositionen
//
tabPositionen.Controls.Add(dgvPositions);
tabPositionen.Controls.Add(pnlPosTop);
tabPositionen.Location = new Point(4, 34);
tabPositionen.Margin = new Padding(4, 5, 4, 5);
tabPositionen.Name = "tabPositionen";
tabPositionen.Padding = new Padding(4, 5, 4, 5);
tabPositionen.Size = new Size(1421, 955);
tabPositionen.TabIndex = 1;
tabPositionen.Text = "Positionen";
tabPositionen.UseVisualStyleBackColor = true;
//
// dgvPositions
//
dgvPositions.AllowUserToAddRows = false;
dgvPositions.AllowUserToDeleteRows = false;
dgvPositions.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvPositions.Dock = DockStyle.Fill;
dgvPositions.Location = new Point(4, 62);
dgvPositions.Margin = new Padding(4, 5, 4, 5);
dgvPositions.Name = "dgvPositions";
dgvPositions.ReadOnly = true;
dgvPositions.RowHeadersVisible = false;
dgvPositions.RowHeadersWidth = 62;
dgvPositions.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvPositions.Size = new Size(1413, 888);
dgvPositions.TabIndex = 1;
//
// pnlPosTop
//
pnlPosTop.Controls.Add(btnPosRefresh);
pnlPosTop.Dock = DockStyle.Top;
pnlPosTop.Location = new Point(4, 5);
pnlPosTop.Margin = new Padding(4, 5, 4, 5);
pnlPosTop.Name = "pnlPosTop";
pnlPosTop.Size = new Size(1413, 57);
pnlPosTop.TabIndex = 0;
//
// btnPosRefresh
//
btnPosRefresh.Location = new Point(9, 7);
btnPosRefresh.Margin = new Padding(4, 5, 4, 5);
btnPosRefresh.Name = "btnPosRefresh";
btnPosRefresh.Size = new Size(157, 43);
btnPosRefresh.TabIndex = 0;
btnPosRefresh.Text = "Aktualisieren";
btnPosRefresh.UseVisualStyleBackColor = true;
//
// tabHistorie
//
tabHistorie.Controls.Add(dgvHistory);
tabHistorie.Controls.Add(lblHistSummary);
tabHistorie.Controls.Add(pnlHistTop);
tabHistorie.Location = new Point(4, 34);
tabHistorie.Margin = new Padding(4, 5, 4, 5);
tabHistorie.Name = "tabHistorie";
tabHistorie.Padding = new Padding(4, 5, 4, 5);
tabHistorie.Size = new Size(1421, 955);
tabHistorie.TabIndex = 2;
tabHistorie.Text = "Historie / Statistik";
tabHistorie.UseVisualStyleBackColor = true;
//
// dgvHistory
//
dgvHistory.AllowUserToAddRows = false;
dgvHistory.AllowUserToDeleteRows = false;
dgvHistory.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvHistory.Dock = DockStyle.Fill;
dgvHistory.Location = new Point(4, 142);
dgvHistory.Margin = new Padding(4, 5, 4, 5);
dgvHistory.Name = "dgvHistory";
dgvHistory.ReadOnly = true;
dgvHistory.RowHeadersVisible = false;
dgvHistory.RowHeadersWidth = 62;
dgvHistory.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvHistory.Size = new Size(1413, 808);
dgvHistory.TabIndex = 2;
//
// lblHistSummary
//
lblHistSummary.Dock = DockStyle.Top;
lblHistSummary.Location = new Point(4, 62);
lblHistSummary.Margin = new Padding(4, 0, 4, 0);
lblHistSummary.Name = "lblHistSummary";
lblHistSummary.Padding = new Padding(9, 10, 9, 10);
lblHistSummary.Size = new Size(1413, 80);
lblHistSummary.TabIndex = 1;
//
// pnlHistTop
//
pnlHistTop.Controls.Add(btnHistRefresh);
pnlHistTop.Dock = DockStyle.Top;
pnlHistTop.Location = new Point(4, 5);
pnlHistTop.Margin = new Padding(4, 5, 4, 5);
pnlHistTop.Name = "pnlHistTop";
pnlHistTop.Size = new Size(1413, 57);
pnlHistTop.TabIndex = 0;
//
// btnHistRefresh
//
btnHistRefresh.Location = new Point(9, 7);
btnHistRefresh.Margin = new Padding(4, 5, 4, 5);
btnHistRefresh.Name = "btnHistRefresh";
btnHistRefresh.Size = new Size(157, 43);
btnHistRefresh.TabIndex = 0;
btnHistRefresh.Text = "Aktualisieren";
btnHistRefresh.UseVisualStyleBackColor = true;
//
// tabSettings
//
tabSettings.Controls.Add(pgSettings);
tabSettings.Controls.Add(pnlSetTop);
tabSettings.Location = new Point(4, 34);
tabSettings.Margin = new Padding(4, 5, 4, 5);
tabSettings.Name = "tabSettings";
tabSettings.Padding = new Padding(4, 5, 4, 5);
tabSettings.Size = new Size(1421, 955);
tabSettings.TabIndex = 3;
tabSettings.Text = "Settings";
tabSettings.UseVisualStyleBackColor = true;
//
// pgSettings
//
pgSettings.Dock = DockStyle.Fill;
pgSettings.Location = new Point(4, 62);
pgSettings.Margin = new Padding(4, 5, 4, 5);
pgSettings.Name = "pgSettings";
pgSettings.Size = new Size(1413, 888);
pgSettings.TabIndex = 1;
//
// pnlSetTop
//
pnlSetTop.Controls.Add(btnSettingsSave);
pnlSetTop.Controls.Add(cbSettingsAccount);
pnlSetTop.Controls.Add(lblSetKonto);
pnlSetTop.Dock = DockStyle.Top;
pnlSetTop.Location = new Point(4, 5);
pnlSetTop.Margin = new Padding(4, 5, 4, 5);
pnlSetTop.Name = "pnlSetTop";
pnlSetTop.Size = new Size(1413, 57);
pnlSetTop.TabIndex = 0;
//
// btnSettingsSave
//
btnSettingsSave.Location = new Point(464, 7);
btnSettingsSave.Margin = new Padding(4, 5, 4, 5);
btnSettingsSave.Name = "btnSettingsSave";
btnSettingsSave.Size = new Size(157, 43);
btnSettingsSave.TabIndex = 2;
btnSettingsSave.Text = "Speichern";
btnSettingsSave.UseVisualStyleBackColor = true;
//
// cbSettingsAccount
//
cbSettingsAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbSettingsAccount.Location = new Point(79, 8);
cbSettingsAccount.Margin = new Padding(4, 5, 4, 5);
cbSettingsAccount.Name = "cbSettingsAccount";
cbSettingsAccount.Size = new Size(370, 33);
cbSettingsAccount.TabIndex = 1;
//
// lblSetKonto
//
lblSetKonto.AutoSize = true;
lblSetKonto.Location = new Point(9, 15);
lblSetKonto.Margin = new Padding(4, 0, 4, 0);
lblSetKonto.Name = "lblSetKonto";
lblSetKonto.Size = new Size(64, 25);
lblSetKonto.TabIndex = 0;
lblSetKonto.Text = "Konto:";
//
// lblRfStatus
//
lblRfStatus.Dock = DockStyle.Bottom;
lblRfStatus.Location = new Point(0, 993);
lblRfStatus.Margin = new Padding(4, 0, 4, 0);
lblRfStatus.Name = "lblRfStatus";
lblRfStatus.Padding = new Padding(9, 3, 9, 3);
lblRfStatus.Size = new Size(1429, 37);
lblRfStatus.TabIndex = 1;
//
// ResolutionFarmingMainForm
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1429, 1030);
Controls.Add(tabControlRf);
Controls.Add(lblRfStatus);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 5, 4, 5);
Name = "ResolutionFarmingMainForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "ResolutionFarming";
tabControlRf.ResumeLayout(false);
tabKandidaten.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvCandidates).EndInit();
pnlCandTop.ResumeLayout(false);
pnlCandTop.PerformLayout();
tabPositionen.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvPositions).EndInit();
pnlPosTop.ResumeLayout(false);
tabHistorie.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvHistory).EndInit();
pnlHistTop.ResumeLayout(false);
tabSettings.ResumeLayout(false);
pnlSetTop.ResumeLayout(false);
pnlSetTop.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControlRf;
private System.Windows.Forms.TabPage tabKandidaten;
private System.Windows.Forms.Panel pnlCandTop;
private System.Windows.Forms.Label lblCandKonto;
private System.Windows.Forms.ComboBox cbCandAccount;
private System.Windows.Forms.Button btnCandRefresh;
private System.Windows.Forms.DataGridView dgvCandidates;
private System.Windows.Forms.TabPage tabPositionen;
private System.Windows.Forms.Panel pnlPosTop;
private System.Windows.Forms.Button btnPosRefresh;
private System.Windows.Forms.DataGridView dgvPositions;
private System.Windows.Forms.TabPage tabHistorie;
private System.Windows.Forms.Panel pnlHistTop;
private System.Windows.Forms.Button btnHistRefresh;
private System.Windows.Forms.Label lblHistSummary;
private System.Windows.Forms.DataGridView dgvHistory;
private System.Windows.Forms.TabPage tabSettings;
private System.Windows.Forms.Panel pnlSetTop;
private System.Windows.Forms.Label lblSetKonto;
private System.Windows.Forms.ComboBox cbSettingsAccount;
private System.Windows.Forms.Button btnSettingsSave;
private System.Windows.Forms.PropertyGrid pgSettings;
private System.Windows.Forms.Label lblRfStatus;
}
}
@@ -1,147 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Modules.ResolutionFarming.Models;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTraderSharp;
namespace PolyTrader.Modules.ResolutionFarming.Ui
{
/// <summary>
/// Hauptfenster des ResolutionFarming-Moduls: Tabs Kandidaten, Positionen, Historie/Statistik,
/// Settings. Layout im Designer (ResolutionFarmingMainForm.Designer.cs), Verhalten/Daten hier.
/// DB-Zugriffe defensiv (Guarded), damit die UI auch ohne angewendete rf_-Migration bedienbar bleibt.
/// </summary>
public partial class ResolutionFarmingMainForm : Form
{
private IRfCandidateRepository? _candidates;
private IRfPositionRepository? _positions;
private IRfClosedTradeRepository? _closed;
private IRfSettingsRepository? _settingsRepo;
private TradingState? _state;
private RfSettings? _currentSettings;
public ResolutionFarmingMainForm()
{
InitializeComponent();
btnCandRefresh.Click += (_, _) => RefreshCandidates();
cbCandAccount.SelectedIndexChanged += (_, _) => RefreshCandidates();
btnPosRefresh.Click += (_, _) => RefreshPositions();
btnHistRefresh.Click += (_, _) => RefreshHistory();
btnSettingsSave.Click += (_, _) => SaveSettings();
cbSettingsAccount.SelectedIndexChanged += (_, _) => LoadSettings();
}
/// <summary>Injiziert Repos/State (nach DI-Auflösung) und lädt die Ansichten.</summary>
public void Initialize(IServiceProvider services)
{
_candidates = services.GetRequiredService<IRfCandidateRepository>();
_positions = services.GetRequiredService<IRfPositionRepository>();
_closed = services.GetRequiredService<IRfClosedTradeRepository>();
_settingsRepo = services.GetRequiredService<IRfSettingsRepository>();
_state = services.GetRequiredService<TradingState>();
PopulateAccounts();
RefreshCandidates();
RefreshPositions();
RefreshHistory();
}
// ---------------- Daten ----------------
private void PopulateAccounts()
{
if (_state == null) return;
var items = _state.Accounts.Values
.OrderBy(a => a.AccountId)
.Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{a.AccountId}){(a.IsDemo ? " · Demo" : "")}"))
.ToList();
foreach (var combo in new[] { cbCandAccount, cbSettingsAccount })
{
combo.DisplayMember = nameof(AccountItem.Label);
combo.ValueMember = nameof(AccountItem.Id);
combo.DataSource = items.ToList();
}
if (items.Count > 0) LoadSettings();
else SetStatus("Keine Accounts vorhanden.");
}
private int? SelectedAccountId(ComboBox combo) =>
combo.SelectedItem is AccountItem it ? it.Id : (int?)null;
private void RefreshCandidates()
{
if (_candidates == null || SelectedAccountId(cbCandAccount) is not int accId) return;
Guarded("Kandidaten", () => dgvCandidates.DataSource = _candidates.GetRecent(accId, 200));
}
private void RefreshPositions()
{
if (_positions == null) return;
Guarded("Positionen", () => dgvPositions.DataSource = _positions.GetAllOpen());
}
private void RefreshHistory()
{
if (_closed == null) return;
Guarded("Historie", () =>
{
var trades = _closed.Find(_ => true);
dgvHistory.DataSource = trades;
lblHistSummary.Text = BuildSummary(trades);
});
}
private static string BuildSummary(System.Collections.Generic.List<RfClosedTrade> trades)
{
if (trades.Count == 0) return "Noch keine abgeschlossenen Trades.";
int wins = trades.Count(t => t.RealizedPnl > 0m);
decimal pnl = trades.Sum(t => t.RealizedPnl);
decimal fees = trades.Sum(t => t.TotalFees);
double winrate = 100.0 * wins / trades.Count;
// Winrate je Preisband (Kalibrierung: realisierte Winrate sollte > Band-Mitte liegen).
var bands = trades
.GroupBy(t => $"{Math.Floor(t.EntryPrice * 20m) / 20m:F2}") // 5-¢-Bänder
.OrderBy(g => g.Key)
.Select(g => $"{g.Key}: {100.0 * g.Count(x => x.RealizedPnl > 0m) / g.Count():F0}% ({g.Count()})");
return $"Trades: {trades.Count} | Winrate: {winrate:F1}% | Netto-PnL: {pnl:F2} USDC | Fees: {fees:F2}\n" +
$"Winrate je Preisband: {string.Join(" | ", bands)}";
}
private void LoadSettings()
{
if (_settingsRepo == null || SelectedAccountId(cbSettingsAccount) is not int accId) return;
Guarded("Settings", () =>
{
_currentSettings = _settingsRepo.Get(accId) ?? new RfSettings { AccountId = accId };
pgSettings.SelectedObject = _currentSettings;
SetStatus($"Einstellungen für Konto #{accId}.");
});
}
private void SaveSettings()
{
if (_settingsRepo == null || _currentSettings == null) return;
Guarded("Speichern", () =>
{
_settingsRepo.Upsert(_currentSettings);
SetStatus($"Gespeichert für Konto #{_currentSettings.AccountId} um {DateTime.Now:HH:mm:ss}.");
});
}
private void Guarded(string what, Action action)
{
try { action(); }
catch (Exception ex) { SetStatus($"{what}: DB nicht bereit ({ex.Message}). Migration angewendet?"); }
}
private void SetStatus(string text) => lblRfStatus.Text = text;
private sealed record AccountItem(int Id, string Label);
}
}
@@ -1,208 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
RK5CYII=
</value>
</data>
</root>
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" /> <ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
@@ -25,11 +25,12 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <!-- Linux-Portierung: plattformneutral. Die WinForms-UI dieses Moduls (KI-Analyse, Dossiers, MCP-Light)
wurde entfernt; die Avalonia-Ansicht kommt spaeter direkt hierher zurueck Avalonia ist
im Gegensatz zu WinForms plattformuebergreifend und braucht kein eigenes UI-Projekt. -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Modul trägt eigene WinForms-UI bei (Dossier-Browser, später Analyse-Chat). -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -54,21 +54,22 @@ namespace PolyTrader.Modules.Supervisor
services.AddHostedService<Mcp.McpLightServer>(); services.AddHostedService<Mcp.McpLightServer>();
} }
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services) /// <summary>
/// Aktuell ohne Ansicht: Die WinForms-UI wurde mit der Linux-Portierung entfernt, die
/// Avalonia-Ansicht folgt (Stufe L3/L4). Die Fachlogik dieses Moduls laeuft davon
/// unabhaengig weiter die Shell zeigt schlicht kein Fenster fuer das Modul an.
///
/// <para><b>Beim Nachbau zu erhalten</b> (Spezifikation: docs/UI-SPEZIFIKATION-WinForms.md,
/// Originalcode: Git-Tag <c>winforms-final</c>):</para>
/// <list type="bullet">
/// <item>View-ID <c>supervisor.main</c> (stabil Launcher-Button und Symbol haengen daran)</item>
/// <item>Titel <c>Supervisor</c>, Gruppe <c>Supervisor</c>, Order <c>300</c></item>
/// <item>Ein Fenster fuers ganze Modul: SupervisorMainForm mit Tabs: Analyse (Chat) / Dossiers / Berichte / Counterfactual</item>
/// </list>
/// </summary>
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
{ {
host.RegisterView(new ModuleView // Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
{
Id = "supervisor.main",
Title = "Supervisor",
Group = "Supervisor",
Order = 300,
CreateForm = () =>
{
var form = new Ui.SupervisorMainForm();
form.Initialize(services);
return form;
}
});
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
@@ -1,524 +0,0 @@
namespace PolyTrader.Modules.Supervisor.Ui
{
partial class SupervisorMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SupervisorMainForm));
tabControlSup = new TabControl();
tabAnalyse = new TabPage();
rtbChat = new RichTextBox();
pnlChatInput = new Panel();
tbChatInput = new TextBox();
btnSend = new Button();
toolStripChat = new ToolStrip();
lblProfil = new ToolStripLabel();
cbProfile = new ToolStripComboBox();
lblModel = new ToolStripLabel();
tbModel = new ToolStripTextBox();
sepChat = new ToolStripSeparator();
btnClearChat = new ToolStripButton();
tabDossiers = new TabPage();
splitDossiers = new SplitContainer();
dgvSignals = new DataGridView();
tbDossier = new TextBox();
toolStripDossier = new ToolStrip();
btnDossierRefresh = new ToolStripButton();
sepDossier = new ToolStripSeparator();
lblSignal = new ToolStripLabel();
tbSignalId = new ToolStripTextBox();
btnDossierOpen = new ToolStripButton();
tabBerichte = new TabPage();
splitReports = new SplitContainer();
dgvReports = new DataGridView();
tbReport = new TextBox();
toolStripReports = new ToolStrip();
btnReportsRefresh = new ToolStripButton();
tabCounterfactual = new TabPage();
dgvCounterfactuals = new DataGridView();
toolStripCf = new ToolStrip();
btnCfRefresh = new ToolStripButton();
lblStatus = new Label();
tabControlSup.SuspendLayout();
tabAnalyse.SuspendLayout();
pnlChatInput.SuspendLayout();
toolStripChat.SuspendLayout();
tabDossiers.SuspendLayout();
((System.ComponentModel.ISupportInitialize)splitDossiers).BeginInit();
splitDossiers.Panel1.SuspendLayout();
splitDossiers.Panel2.SuspendLayout();
splitDossiers.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvSignals).BeginInit();
toolStripDossier.SuspendLayout();
tabBerichte.SuspendLayout();
((System.ComponentModel.ISupportInitialize)splitReports).BeginInit();
splitReports.Panel1.SuspendLayout();
splitReports.Panel2.SuspendLayout();
splitReports.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvReports).BeginInit();
toolStripReports.SuspendLayout();
tabCounterfactual.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvCounterfactuals).BeginInit();
toolStripCf.SuspendLayout();
SuspendLayout();
//
// tabControlSup
//
tabControlSup.Controls.Add(tabAnalyse);
tabControlSup.Controls.Add(tabDossiers);
tabControlSup.Controls.Add(tabBerichte);
tabControlSup.Controls.Add(tabCounterfactual);
tabControlSup.Dock = DockStyle.Fill;
tabControlSup.Location = new Point(0, 0);
tabControlSup.Margin = new Padding(4, 5, 4, 5);
tabControlSup.Name = "tabControlSup";
tabControlSup.SelectedIndex = 0;
tabControlSup.Size = new Size(1786, 1126);
tabControlSup.TabIndex = 0;
//
// tabAnalyse
//
tabAnalyse.Controls.Add(rtbChat);
tabAnalyse.Controls.Add(pnlChatInput);
tabAnalyse.Controls.Add(toolStripChat);
tabAnalyse.Location = new Point(4, 34);
tabAnalyse.Margin = new Padding(4, 5, 4, 5);
tabAnalyse.Name = "tabAnalyse";
tabAnalyse.Padding = new Padding(4, 5, 4, 5);
tabAnalyse.Size = new Size(1778, 1088);
tabAnalyse.TabIndex = 0;
tabAnalyse.Text = "Analyse";
tabAnalyse.UseVisualStyleBackColor = true;
//
// rtbChat
//
rtbChat.BackColor = Color.White;
rtbChat.Dock = DockStyle.Fill;
rtbChat.Font = new Font("Segoe UI", 9.5F);
rtbChat.Location = new Point(4, 39);
rtbChat.Margin = new Padding(4, 5, 4, 5);
rtbChat.Name = "rtbChat";
rtbChat.ReadOnly = true;
rtbChat.Size = new Size(1770, 944);
rtbChat.TabIndex = 1;
rtbChat.Text = "";
//
// pnlChatInput
//
pnlChatInput.Controls.Add(tbChatInput);
pnlChatInput.Controls.Add(btnSend);
pnlChatInput.Dock = DockStyle.Bottom;
pnlChatInput.Location = new Point(4, 983);
pnlChatInput.Margin = new Padding(4, 5, 4, 5);
pnlChatInput.Name = "pnlChatInput";
pnlChatInput.Padding = new Padding(6, 7, 6, 7);
pnlChatInput.Size = new Size(1770, 100);
pnlChatInput.TabIndex = 2;
//
// tbChatInput
//
tbChatInput.Dock = DockStyle.Fill;
tbChatInput.Location = new Point(6, 7);
tbChatInput.Margin = new Padding(4, 5, 4, 5);
tbChatInput.Multiline = true;
tbChatInput.Name = "tbChatInput";
tbChatInput.PlaceholderText = "Analyse-Frage stellen … (Strg+Enter zum Senden)";
tbChatInput.Size = new Size(1601, 86);
tbChatInput.TabIndex = 0;
//
// btnSend
//
btnSend.Dock = DockStyle.Right;
btnSend.Location = new Point(1607, 7);
btnSend.Margin = new Padding(4, 5, 4, 5);
btnSend.Name = "btnSend";
btnSend.Size = new Size(157, 86);
btnSend.TabIndex = 1;
btnSend.Text = "Senden";
btnSend.UseVisualStyleBackColor = true;
//
// toolStripChat
//
toolStripChat.ImageScalingSize = new Size(24, 24);
toolStripChat.Items.AddRange(new ToolStripItem[] { lblProfil, cbProfile, lblModel, tbModel, sepChat, btnClearChat });
toolStripChat.Location = new Point(4, 5);
toolStripChat.Name = "toolStripChat";
toolStripChat.Padding = new Padding(0, 0, 3, 0);
toolStripChat.Size = new Size(1770, 34);
toolStripChat.TabIndex = 0;
//
// lblProfil
//
lblProfil.Name = "lblProfil";
lblProfil.Size = new Size(57, 29);
lblProfil.Text = "Profil:";
//
// cbProfile
//
cbProfile.DropDownStyle = ComboBoxStyle.DropDownList;
cbProfile.Name = "cbProfile";
cbProfile.Size = new Size(227, 34);
//
// lblModel
//
lblModel.Name = "lblModel";
lblModel.Size = new Size(71, 29);
lblModel.Text = "Modell:";
//
// tbModel
//
tbModel.AutoSize = false;
tbModel.Name = "tbModel";
tbModel.Size = new Size(313, 31);
tbModel.Text = "openrouter/auto";
//
// sepChat
//
sepChat.Name = "sepChat";
sepChat.Size = new Size(6, 34);
//
// btnClearChat
//
btnClearChat.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnClearChat.Name = "btnClearChat";
btnClearChat.Size = new Size(122, 29);
btnClearChat.Text = "Verlauf leeren";
//
// tabDossiers
//
tabDossiers.Controls.Add(splitDossiers);
tabDossiers.Controls.Add(toolStripDossier);
tabDossiers.Location = new Point(4, 34);
tabDossiers.Margin = new Padding(4, 5, 4, 5);
tabDossiers.Name = "tabDossiers";
tabDossiers.Padding = new Padding(4, 5, 4, 5);
tabDossiers.Size = new Size(1778, 1088);
tabDossiers.TabIndex = 1;
tabDossiers.Text = "Dossiers";
tabDossiers.UseVisualStyleBackColor = true;
//
// splitDossiers
//
splitDossiers.Dock = DockStyle.Fill;
splitDossiers.Location = new Point(4, 39);
splitDossiers.Margin = new Padding(4, 5, 4, 5);
splitDossiers.Name = "splitDossiers";
//
// splitDossiers.Panel1
//
splitDossiers.Panel1.Controls.Add(dgvSignals);
//
// splitDossiers.Panel2
//
splitDossiers.Panel2.Controls.Add(tbDossier);
splitDossiers.Size = new Size(1770, 1044);
splitDossiers.SplitterDistance = 601;
splitDossiers.SplitterWidth = 6;
splitDossiers.TabIndex = 1;
//
// dgvSignals
//
dgvSignals.AllowUserToAddRows = false;
dgvSignals.AllowUserToDeleteRows = false;
dgvSignals.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvSignals.Dock = DockStyle.Fill;
dgvSignals.Location = new Point(0, 0);
dgvSignals.Margin = new Padding(4, 5, 4, 5);
dgvSignals.MultiSelect = false;
dgvSignals.Name = "dgvSignals";
dgvSignals.ReadOnly = true;
dgvSignals.RowHeadersVisible = false;
dgvSignals.RowHeadersWidth = 62;
dgvSignals.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvSignals.Size = new Size(601, 1044);
dgvSignals.TabIndex = 0;
//
// tbDossier
//
tbDossier.Dock = DockStyle.Fill;
tbDossier.Font = new Font("Consolas", 9.5F);
tbDossier.Location = new Point(0, 0);
tbDossier.Margin = new Padding(4, 5, 4, 5);
tbDossier.Multiline = true;
tbDossier.Name = "tbDossier";
tbDossier.ReadOnly = true;
tbDossier.ScrollBars = ScrollBars.Both;
tbDossier.Size = new Size(1163, 1044);
tbDossier.TabIndex = 0;
tbDossier.WordWrap = false;
//
// toolStripDossier
//
toolStripDossier.ImageScalingSize = new Size(24, 24);
toolStripDossier.Items.AddRange(new ToolStripItem[] { btnDossierRefresh, sepDossier, lblSignal, tbSignalId, btnDossierOpen });
toolStripDossier.Location = new Point(4, 5);
toolStripDossier.Name = "toolStripDossier";
toolStripDossier.Padding = new Padding(0, 0, 3, 0);
toolStripDossier.Size = new Size(1770, 34);
toolStripDossier.TabIndex = 0;
//
// btnDossierRefresh
//
btnDossierRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnDossierRefresh.Name = "btnDossierRefresh";
btnDossierRefresh.Size = new Size(116, 29);
btnDossierRefresh.Text = "Aktualisieren";
//
// sepDossier
//
sepDossier.Name = "sepDossier";
sepDossier.Size = new Size(6, 34);
//
// lblSignal
//
lblSignal.Name = "lblSignal";
lblSignal.Size = new Size(80, 29);
lblSignal.Text = "SignalId:";
//
// tbSignalId
//
tbSignalId.AutoSize = false;
tbSignalId.Name = "tbSignalId";
tbSignalId.Size = new Size(313, 31);
//
// btnDossierOpen
//
btnDossierOpen.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnDossierOpen.Name = "btnDossierOpen";
btnDossierOpen.Size = new Size(132, 29);
btnDossierOpen.Text = "Dossier öffnen";
//
// tabBerichte
//
tabBerichte.Controls.Add(splitReports);
tabBerichte.Controls.Add(toolStripReports);
tabBerichte.Location = new Point(4, 34);
tabBerichte.Margin = new Padding(4, 5, 4, 5);
tabBerichte.Name = "tabBerichte";
tabBerichte.Padding = new Padding(4, 5, 4, 5);
tabBerichte.Size = new Size(1778, 1089);
tabBerichte.TabIndex = 2;
tabBerichte.Text = "Berichte";
tabBerichte.UseVisualStyleBackColor = true;
//
// splitReports
//
splitReports.Dock = DockStyle.Fill;
splitReports.Location = new Point(4, 39);
splitReports.Margin = new Padding(4, 5, 4, 5);
splitReports.Name = "splitReports";
//
// splitReports.Panel1
//
splitReports.Panel1.Controls.Add(dgvReports);
//
// splitReports.Panel2
//
splitReports.Panel2.Controls.Add(tbReport);
splitReports.Size = new Size(1770, 1045);
splitReports.SplitterDistance = 744;
splitReports.SplitterWidth = 6;
splitReports.TabIndex = 1;
//
// dgvReports
//
dgvReports.AllowUserToAddRows = false;
dgvReports.AllowUserToDeleteRows = false;
dgvReports.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvReports.Dock = DockStyle.Fill;
dgvReports.Location = new Point(0, 0);
dgvReports.Margin = new Padding(4, 5, 4, 5);
dgvReports.MultiSelect = false;
dgvReports.Name = "dgvReports";
dgvReports.ReadOnly = true;
dgvReports.RowHeadersVisible = false;
dgvReports.RowHeadersWidth = 62;
dgvReports.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvReports.Size = new Size(744, 1045);
dgvReports.TabIndex = 0;
//
// tbReport
//
tbReport.Dock = DockStyle.Fill;
tbReport.Font = new Font("Segoe UI", 9.5F);
tbReport.Location = new Point(0, 0);
tbReport.Margin = new Padding(4, 5, 4, 5);
tbReport.Multiline = true;
tbReport.Name = "tbReport";
tbReport.ReadOnly = true;
tbReport.ScrollBars = ScrollBars.Both;
tbReport.Size = new Size(1020, 1045);
tbReport.TabIndex = 0;
//
// toolStripReports
//
toolStripReports.ImageScalingSize = new Size(24, 24);
toolStripReports.Items.AddRange(new ToolStripItem[] { btnReportsRefresh });
toolStripReports.Location = new Point(4, 5);
toolStripReports.Name = "toolStripReports";
toolStripReports.Padding = new Padding(0, 0, 3, 0);
toolStripReports.Size = new Size(1770, 34);
toolStripReports.TabIndex = 0;
//
// btnReportsRefresh
//
btnReportsRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnReportsRefresh.Name = "btnReportsRefresh";
btnReportsRefresh.Size = new Size(116, 29);
btnReportsRefresh.Text = "Aktualisieren";
//
// tabCounterfactual
//
tabCounterfactual.Controls.Add(dgvCounterfactuals);
tabCounterfactual.Controls.Add(toolStripCf);
tabCounterfactual.Location = new Point(4, 34);
tabCounterfactual.Margin = new Padding(4, 5, 4, 5);
tabCounterfactual.Name = "tabCounterfactual";
tabCounterfactual.Padding = new Padding(4, 5, 4, 5);
tabCounterfactual.Size = new Size(1778, 1089);
tabCounterfactual.TabIndex = 3;
tabCounterfactual.Text = "Counterfactual";
tabCounterfactual.UseVisualStyleBackColor = true;
//
// dgvCounterfactuals
//
dgvCounterfactuals.AllowUserToAddRows = false;
dgvCounterfactuals.AllowUserToDeleteRows = false;
dgvCounterfactuals.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvCounterfactuals.Dock = DockStyle.Fill;
dgvCounterfactuals.Location = new Point(4, 39);
dgvCounterfactuals.Margin = new Padding(4, 5, 4, 5);
dgvCounterfactuals.MultiSelect = false;
dgvCounterfactuals.Name = "dgvCounterfactuals";
dgvCounterfactuals.ReadOnly = true;
dgvCounterfactuals.RowHeadersVisible = false;
dgvCounterfactuals.RowHeadersWidth = 62;
dgvCounterfactuals.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvCounterfactuals.Size = new Size(1770, 1045);
dgvCounterfactuals.TabIndex = 1;
//
// toolStripCf
//
toolStripCf.ImageScalingSize = new Size(24, 24);
toolStripCf.Items.AddRange(new ToolStripItem[] { btnCfRefresh });
toolStripCf.Location = new Point(4, 5);
toolStripCf.Name = "toolStripCf";
toolStripCf.Padding = new Padding(0, 0, 3, 0);
toolStripCf.Size = new Size(1770, 34);
toolStripCf.TabIndex = 0;
//
// btnCfRefresh
//
btnCfRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnCfRefresh.Name = "btnCfRefresh";
btnCfRefresh.Size = new Size(116, 29);
btnCfRefresh.Text = "Aktualisieren";
//
// lblStatus
//
lblStatus.Dock = DockStyle.Bottom;
lblStatus.Location = new Point(0, 1126);
lblStatus.Margin = new Padding(4, 0, 4, 0);
lblStatus.Name = "lblStatus";
lblStatus.Padding = new Padding(9, 3, 9, 3);
lblStatus.Size = new Size(1786, 37);
lblStatus.TabIndex = 1;
//
// SupervisorMainForm
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1786, 1163);
Controls.Add(tabControlSup);
Controls.Add(lblStatus);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 5, 4, 5);
Name = "SupervisorMainForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "Supervisor";
tabControlSup.ResumeLayout(false);
tabAnalyse.ResumeLayout(false);
tabAnalyse.PerformLayout();
pnlChatInput.ResumeLayout(false);
pnlChatInput.PerformLayout();
toolStripChat.ResumeLayout(false);
toolStripChat.PerformLayout();
tabDossiers.ResumeLayout(false);
tabDossiers.PerformLayout();
splitDossiers.Panel1.ResumeLayout(false);
splitDossiers.Panel2.ResumeLayout(false);
splitDossiers.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)splitDossiers).EndInit();
splitDossiers.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvSignals).EndInit();
toolStripDossier.ResumeLayout(false);
toolStripDossier.PerformLayout();
tabBerichte.ResumeLayout(false);
tabBerichte.PerformLayout();
splitReports.Panel1.ResumeLayout(false);
splitReports.Panel2.ResumeLayout(false);
splitReports.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)splitReports).EndInit();
splitReports.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvReports).EndInit();
toolStripReports.ResumeLayout(false);
toolStripReports.PerformLayout();
tabCounterfactual.ResumeLayout(false);
tabCounterfactual.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgvCounterfactuals).EndInit();
toolStripCf.ResumeLayout(false);
toolStripCf.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControlSup;
private System.Windows.Forms.TabPage tabAnalyse;
private System.Windows.Forms.ToolStrip toolStripChat;
private System.Windows.Forms.ToolStripLabel lblProfil;
private System.Windows.Forms.ToolStripComboBox cbProfile;
private System.Windows.Forms.ToolStripLabel lblModel;
private System.Windows.Forms.ToolStripTextBox tbModel;
private System.Windows.Forms.ToolStripSeparator sepChat;
private System.Windows.Forms.ToolStripButton btnClearChat;
private System.Windows.Forms.RichTextBox rtbChat;
private System.Windows.Forms.Panel pnlChatInput;
private System.Windows.Forms.TextBox tbChatInput;
private System.Windows.Forms.Button btnSend;
private System.Windows.Forms.TabPage tabDossiers;
private System.Windows.Forms.ToolStrip toolStripDossier;
private System.Windows.Forms.ToolStripButton btnDossierRefresh;
private System.Windows.Forms.ToolStripSeparator sepDossier;
private System.Windows.Forms.ToolStripLabel lblSignal;
private System.Windows.Forms.ToolStripTextBox tbSignalId;
private System.Windows.Forms.ToolStripButton btnDossierOpen;
private System.Windows.Forms.SplitContainer splitDossiers;
private System.Windows.Forms.DataGridView dgvSignals;
private System.Windows.Forms.TextBox tbDossier;
private System.Windows.Forms.TabPage tabBerichte;
private System.Windows.Forms.ToolStrip toolStripReports;
private System.Windows.Forms.ToolStripButton btnReportsRefresh;
private System.Windows.Forms.SplitContainer splitReports;
private System.Windows.Forms.DataGridView dgvReports;
private System.Windows.Forms.TextBox tbReport;
private System.Windows.Forms.TabPage tabCounterfactual;
private System.Windows.Forms.ToolStrip toolStripCf;
private System.Windows.Forms.ToolStripButton btnCfRefresh;
private System.Windows.Forms.DataGridView dgvCounterfactuals;
private System.Windows.Forms.Label lblStatus;
}
}
@@ -1,205 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Analytics;
using PolyTrader.Modules.Supervisor.Agent;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTrader.Modules.Supervisor.Services;
namespace PolyTrader.Modules.Supervisor.Ui
{
/// <summary>
/// Hauptfenster des Supervisor-Moduls: Tab „Analyse" (Chat mit Profil-/Modellwahl, Tool-Aufrufe
/// transparent), Tab „Dossiers" (Signal-Browser), Tab „Berichte" (gespeicherte Analysen).
/// Layout im Designer (SupervisorMainForm.Designer.cs), Verhalten/Daten hier.
/// </summary>
public partial class SupervisorMainForm : Form
{
private DossierService? _dossiers;
private SupervisorAgent? _agent;
private ISupervisorReportRepository? _reports;
private ISupervisorCounterfactualRepository? _counterfactuals;
public SupervisorMainForm()
{
InitializeComponent();
foreach (var profile in SupervisorProfiles.All)
cbProfile.Items.Add(profile);
cbProfile.SelectedIndex = 0;
btnSend.Click += async (_, _) => await SendQuestionAsync();
tbChatInput.KeyDown += async (_, e) =>
{
if (e.Control && e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; await SendQuestionAsync(); }
};
btnClearChat.Click += (_, _) => rtbChat.Clear();
btnDossierRefresh.Click += (_, _) => LoadSignals();
btnDossierOpen.Click += (_, _) => OpenDossier(tbSignalId.Text.Trim());
dgvSignals.SelectionChanged += (_, _) => OpenSelectedSignal();
btnReportsRefresh.Click += (_, _) => LoadReports();
dgvReports.SelectionChanged += (_, _) => ShowSelectedReport();
btnCfRefresh.Click += (_, _) => LoadCounterfactuals();
}
public void Initialize(IServiceProvider services)
{
_dossiers = services.GetRequiredService<DossierService>();
_agent = services.GetRequiredService<SupervisorAgent>();
_reports = services.GetRequiredService<ISupervisorReportRepository>();
_counterfactuals = services.GetRequiredService<ISupervisorCounterfactualRepository>();
LoadSignals();
LoadReports();
LoadCounterfactuals();
AppendChat("System", "Supervisor bereit (read-only). API-Key: POLYTRADER_OPENROUTER_KEY oder Datei openrouter.key. " +
"MCP-Light für externe Clients: POLYTRADER_MCP_PORT setzen.", System.Drawing.Color.Gray);
}
// ===== Analyse-Chat =====
private async System.Threading.Tasks.Task SendQuestionAsync()
{
if (_agent == null) return;
string question = tbChatInput.Text.Trim();
if (question.Length == 0) return;
var profile = cbProfile.SelectedItem as SupervisorProfile ?? SupervisorProfiles.Allgemein;
tbChatInput.Text = "";
btnSend.Enabled = false;
AppendChat($"Du ({profile.Name})", question, System.Drawing.Color.DarkBlue);
lblStatus.Text = "Analyse läuft …";
var progress = new Progress<string>(msg => AppendChat("Tool", msg, System.Drawing.Color.DarkGoldenrod));
try
{
string model = tbModel.Text;
var result = await System.Threading.Tasks.Task.Run(() =>
_agent.AskAsync(question, model, progress, profile));
AppendChat("Supervisor", result.Answer, System.Drawing.Color.Black);
lblStatus.Text = $"Fertig. {result.ToolInvocations.Count} Tool-Aufruf(e), ~{result.PromptTokens + result.CompletionTokens} Tokens.";
SaveReport(profile, model, question, result);
}
catch (Exception ex)
{
AppendChat("Fehler", ex.Message, System.Drawing.Color.Firebrick);
lblStatus.Text = "Fehler bei der Analyse.";
}
finally
{
btnSend.Enabled = true;
}
}
private void SaveReport(SupervisorProfile profile, string model, string question, AgentResult result)
{
try
{
var calls = new List<object>();
foreach (var (tool, args, _) in result.ToolInvocations)
calls.Add(new { tool, args });
_reports?.Insert(new SupervisorReport
{
Profile = profile.Name,
Model = model,
Question = question,
Answer = result.Answer,
ToolCallsJson = System.Text.Json.JsonSerializer.Serialize(calls),
ToolCallCount = result.ToolInvocations.Count,
PromptTokens = result.PromptTokens,
CompletionTokens = result.CompletionTokens
});
LoadReports();
}
catch { /* Bericht-Ablage ist Beiwerk Analyse-Ergebnis steht im Chat */ }
}
private void AppendChat(string who, string text, System.Drawing.Color color)
{
if (InvokeRequired) { BeginInvoke(() => AppendChat(who, text, color)); return; }
rtbChat.SelectionStart = rtbChat.TextLength;
rtbChat.SelectionColor = color;
rtbChat.AppendText($"[{DateTime.Now:HH:mm:ss}] {who}: {text}{Environment.NewLine}{Environment.NewLine}");
rtbChat.ScrollToCaret();
}
// ===== Dossier-Browser =====
private void LoadSignals()
{
if (_dossiers == null) return;
try
{
List<SignalSummary> signals = _dossiers.RecentSignals(200);
dgvSignals.DataSource = signals;
lblStatus.Text = signals.Count == 0
? "Noch keine Journal-Einträge (Entscheidungen entstehen, sobald Signale verarbeitet werden)."
: $"{signals.Count} Signale.";
}
catch (Exception ex)
{
lblStatus.Text = $"Journal nicht lesbar: {ex.Message}";
}
}
private void OpenSelectedSignal()
{
if (dgvSignals.CurrentRow?.DataBoundItem is SignalSummary s)
OpenDossier(s.SignalId);
}
private void OpenDossier(string signalId)
{
if (_dossiers == null || string.IsNullOrWhiteSpace(signalId)) return;
try
{
var dossier = _dossiers.BuildForSignal(signalId);
tbDossier.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine);
tbSignalId.Text = signalId;
}
catch (Exception ex)
{
tbDossier.Text = $"Dossier konnte nicht geladen werden: {ex.Message}";
}
}
// ===== Berichte =====
private void LoadReports()
{
if (_reports == null) return;
try
{
dgvReports.DataSource = _reports.GetRecent(100);
}
catch (Exception ex)
{
lblStatus.Text = $"Berichte nicht lesbar (sup_-Migration angewendet?): {ex.Message}";
}
}
// ===== Counterfactuals =====
private void LoadCounterfactuals()
{
if (_counterfactuals == null) return;
try
{
dgvCounterfactuals.DataSource = _counterfactuals.GetRecent(200);
}
catch (Exception ex)
{
lblStatus.Text = $"Counterfactuals nicht lesbar (Migration angewendet?): {ex.Message}";
}
}
private void ShowSelectedReport()
{
if (dgvReports.CurrentRow?.DataBoundItem is SupervisorReport r)
tbReport.Text =
$"[{r.CreatedAt:dd.MM.yyyy HH:mm}] Profil {r.Profile} · Modell {r.Model} · {r.ToolCallCount} Tool-Aufrufe{Environment.NewLine}{Environment.NewLine}" +
$"FRAGE:{Environment.NewLine}{r.Question}{Environment.NewLine}{Environment.NewLine}" +
$"ANTWORT:{Environment.NewLine}{r.Answer.Replace("\n", Environment.NewLine)}";
}
}
}
@@ -1,210 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="toolStripChat.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="toolStripDossier.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>188, 17</value>
</metadata>
<metadata name="toolStripReports.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>382, 17</value>
</metadata>
<metadata name="toolStripCf.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>578, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAMAEBAAAAAAIABMAwAANgAAABgYAAAAACAAMwYAAIIDAAAgIAAAAAAgAFEHAAC1CQAAiVBORw0K
GgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADE0lEQVR4nG1TTWhUVxT+7l8mb/6nGc3YOBkJCTFG
Y22GxBiipUGpi6jgShG6KJJdU3Xvwm1Azaagdlm6E/pDaaGh0E3FUgRbnRijkjTGxJDJ/P+8effdW+6b
+Aeey4F3zz3f98757rmku7t7plarPVhZWbkFAPF4/HhfX9+5SCR8knMRMDEpnUqhUPxhbm7u242NjV9M
rKOj47xlWXu53+/n6XT65uzs7Eoqlfy0t3fPxfi2bcTnawVn3ORCujJg2/WzyWTnmfn5zNWlpeXfx8bG
bmYyma85Y6wRi0YxOjr6cyLxIShjaG21XCEEI4R4BJRREELcSDTG0unhS8lk6lIsFoPBciYEzWY3oVzt
FEslFgkFab1aZVWt8dq0NgSMaI1isaxAqJvbzAnGGOVQCkJwrK4ViRCCIhJFrV5HrVoFowzaW0AgFEbD
tuE0bJrP5lUy1dGsTkODc45SIY8DQ0OYODaOwtMMPp+cRL5YQC6XwxdfTsFZf47R/XswODKCXC7bJNfN
9uDzcTiNCtoifiTiIXwyfhjdO7ejXsmjYZfQ+YEfH+3rRXp4EGGLQzpVtLRQMOKC7B08cr1zZ9fU/fsP
pBCcB4JhWIEAsusvvd6NGTHbtrejUi6jWinDcRw5MNDPnyw+muETvausfxAY71oGF8IcQrmu900p9QiU
UpDOY++GhBCQjoP2pB8P/95k/FD7s/nP9pfBD64xuEaVLX+fqS1nYLJG8evy+jzZBaRuzyTu9fRUouVS
mZh6bVtDm8S3jHhaEdOWDoaCemEhkD89tfYxXQSW/vk3e4M7Fm3lRDaqCusvNJYX33UTkzUFSxBpcg3G
YLmZkShxpuNthUNHR0JH4C+7oZQLRt9txFVQBAwWDYrf/iz88dU3zrQ3X9pUR0xh2HH7Ar8x3M8nYmGJ
mm0EeWOWjyFX5Lj7UP50+pqcJASryhB4k/qGJHD5BM4O7aanEm0Y8XH4zLktYa9lceevR+r7Kz/iO0JQ
2QLr5mt5pROBVs2rD3VG0NPC0GI2DReN/wpYAFCiBDDg5n+B/wES01Pdhr/66QAAAABJRU5ErkJggolQ
TkcNChoKAAAADUlIRFIAAAAYAAAAGAgGAAAA4Hc9+AAABfpJREFUeJylVWtsVMcV/mbu3L13n96HwXmY
x5oQJ+A62BSXBiVrUUIgDZWaihRUVVSIOj/yIz+itP9i+W+iqGqlVCJVE6WiSgQJUl4FQgKxIosUikWI
X5BiGwXc+LFee3e93rv3zkx07tqWTdpfPdKR7p055zvnfHPODGtsbNxfqVQOjoyMPLV161azoaFBnThx
QqIqOxrua9ibSqQO27Z9NwC9sM7K5fJ/srns68P/Hj4FoIcW9+/fbwwPD/PLly+76XT6ZCAQeItFo9G9
TU1N/8jn86/19/c/wxiDEKK1vr7+hebm5gOrV6+GZduwrCCWi+PMwymXMTExgatXr75969atl13X7aW9
zZs3H43FYh19fX1PiEKhYKXTaZVKpTpyudz8+Pj4+ba2bX/dtq0tpTWTUkoYwjQY4+AL4AqAIQKwLCbX
r29AOp0+cOnSxccuXfrXkdra2szOnTs7stmsunDhgiWobMYY10o5ra2tz0npPdeypRW3x771ApYlVq2q
pYrA+SL8QhCl4HmeMTk5hYrjeJlMeyoajb0rhOCccQcMFmFTABiGgdGRUUOYAd30g2Y2eO26SiaTorY2
iUql4lOxSP6iMFLOQTYTE5NiYOi62tLSwr++/rUeHR01auI1vp0fgMQ0TUzncqx/YBBBy+I10SiK+QK0
UuBCQEkJrath6Jy4YQBaY35Ogmzzs3k+OHgNxUKBJWpiS7ZieUrkqDwP3A5i5MYNCNNEMBzBXGEW0Wis
CurTI5HPzYBxhnAsThWAcwbP9ap9xqm+qvjEamgwMHhSwnUrCIZD+PkvD6ClbTuEO49Hdu+G6ynMzc35
6lQ8tO/bh/a9PwNzStjx8I8QT6ZQqZThehUfa0UAimoIKhnIZqewc/fj2PXow+g58SbGh77EoSNHsPa+
jZiemsJ0dgrrNmzErw8fxlefvIeB7jN4cs9jyOza5e8rqVc0hE8R8WUFLJ/vQr6AH25pRjwaQePGDahb
sw5xYWB9/V04O5PznRrW3oM4B+rrapHc+wTWrFmHCjNRKBZh22G/65YCEKu2JZDPF+F5ZdTEIvjjKy9j
y9YWtO77BbRU+POrf8K5c+chRLX0M6c+glspoeHHGTpnvHHsb7jSe8X39dwSPM+BLWwQtqA7YdaxMD1b
xNiURCQcxnun/oljx8+ChszPQggkEgkEQyH/f+zKTXx8/g80B1hsc2qCeLwGxWIRkSRDLBEAeYv7U2Ab
EjncFZxFRBVg2S4Mg4PzxNJRaQDS86D0TPXgGIch4iv2afCknPFnZu26GURsDsIWz++Ae/DRi4g2ljjK
E9UJ+n9EE+d5XrgWwvqbcMW7Q7iR7i8MbK9ZtcmZn5JaamMxCPG7qP9NmD/Oy9peA8xg0gomjS/6JwcI
W3x8nQ0dup3tsdTqTdIIKKnKBjmSWhZgCoAbALsjiGaAkgDNllOuJk5qGJayFDMmb2d7CFsQUO8oPnjw
q7GD9zfXBSulm9rgYI4LjAwD09NA2VnIdnkATd0HJBNAfT0gOCAVtB2q4/1Xx4qEST5Cvag568LpB+6d
feeeu4O/sSJxzy3NCJrGaFiDOpOyvPNoKFuqLkjPhGKQnkYgFJfFSUdc7p9955WzOK07dZW9zgzEq71I
v/5bfvKR7bVNHitJ5RQN06QJX3G1rBClqbuAigsYVkQKHTI+/2Kq7/Bf1FPPtmKkqxue76o12NNPwxzs
xgMvHeJ/b3so2qRMT8tySSupqRifoaX3cvFbQzODaWGHGHcFu/hloe93b6pfPZjB0PHjcBmD9i8N+ji+
CV7fBAZ/f0wdOtMz+/6337g6GLB4KGQygzHJGDy+oPRNa7QXClicbMmHfAmDsAhzMZkl6ewE7+qCCaDu
mXY8+dNW1rEqgfo1dTyViDD/qSShrHJFjW/GVXYyh1sf9erXjn6GDwGMd3bC7epaMv3+WBFd7QxGN0D3
QnRPE9p+0oRM2ERK0r1OrcjA5lxkP+1D9+k+XARQyAClzzSo0v8xNXcIkX60w6/GBhAGQG/gcqU1m2zu
bOHl8h262pYvryzyJgAAAABJRU5ErkJggolQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAA
BxhJREFUeJy1l2lsXFcVx39vG49nvMV2NmcZO3Ed2U1LKKJt7JBIjRNFiaryBb4ggbAgjYrUIhBQCnxi
UeknUKWqqvgCUlUJWgpEaULSFDW08JVmcSIn8cx4SWzHmfHYM2/mzdvQuZ5xvYYUKVe6mnnvnnP+/3uW
e+7TgEhXV5czNDRUA5RZY2zdurXBNM1vhWH4jTAMH5V3YRiqNU3Tqr8XNU37ved5vxsbG5tdy9ZiTA1Y
19nZmRFjN2/eXE5C2759+w5N0/5imubunp4etmxpEzJYVmSJRdctMzY2xvj4LQYHB/E873IYhl8eGRkZ
Fq6LwXfu3OkI6Rs3bjQLgQ0dHR2Thw8f5syZM6TT6SoJo729/TXDMI7v37+frq4uZmZyZGdy5PMFPM9f
QsA0Derq4qxraqSpqZGhoSEuXLiA7/tvpFKp5wBRiCQSCefIkSOcPXuWZDK50RTlIAjYtWsXruty/vx5
J51OxxOJxLVEYvs2ITY1dZdPLl6mtraWlpYW2traiNbUSAwqftIoOQ75fJ6pO9OkR0bZuGEDAwPfFKDj
YRgeTafTuxKJROHgwYMKSzariFdjKSQy09P09/dz+vTpQl9fH93dPVy7doNiqURHezuxeEzJ+r5PwbZX
BDYWixGPx7ELNslUikwmS3//Ya5eHdzqum5WbGczWfzAX8gfcyHYmsbwcJK6hkYGBgbQdYP/fHIR0zTp
7OwAdEql0oLiWkPsGKahdJLJtLLxcE83O3Z0REZGxkilUvT29X4aOhYNXdO4fes2kUgNmZkZwsBn86ZN
lB1XeeizDF3XWd/ayujoKBcvXaF1fTMTE1OKYLVqVhCQWIpiLpfDK5dpamxScXbuY+ereUJmfV092Zks
c7N5dF3jU+jVCDCv6DiOlBE1NTXYEmsBX2AtRJbuQr0Nw9XOBWVDcsYuFObfL9Mzl4BXDAm4hCOfn8Mt
l9F0HcM01brvy5qOaVnKWzIkPJ5bxvf8BVmhEvg+pmFgGAZlsbOKp/TlL0TR88qKdffn9vDWu+/w/A9e
4onH9uDdTvLG23/i0Se/RCGfV0kpU/7Lu9fefYdnv/9jvvj5PQQTKd56+4/KhpDzPFfZvGcIzEhEsZYd
2a6NlKKcd7/69leIxupwnBJR4PHeXv5x6s/UxuJKr2gXePzJXuTpleNfJRavwy2VlPG9fX38/dTfiFg1
BFqIbhhreyAeiyn3C9OibdN/YN+8i+WduLjsUAsc2vsYtl1UuSLTLhY51PcFRQB/XtZzHaV76MA+ZUts
Svhqo9GVHggryWMXHSzLrDzr/Pvjf7G3r5ezV1NLlD7850e4nqfcL8N1PT688BEH9u9bISs2xJZyvyS4
660k0NDQoB5836WSV+psf/bECXU8zxNSKaqMRCxLZbesUVl5/jvPUV5F1rIsotEoQTDfOzzfXUnALjoE
oUbZDQgJlGIYaphWFMOU3rR0yLofagTevOfCUMewokTXkg00gkqZum6gsGQuELg7FzBXssjOgmFWa3y1
ollr3J+sOMf3YM6xmC3N578m7fjSz5ncfewoOO8t7dwPYghizVEun3qPR36GasfhxBzXW0cmH6qvayFw
7z5QfN1qYW5yEsEUbCHg3bzDyW1TE9+L1W/Ef8AEjMhGclMTCKZgS86XXj3Hm3fHx9G1JgggXD4lLNVZ
8eJqU42KnOgstyO2BUOwBFOwhUD5yhiTczbDmfEJMNsIfRamUgrB0MDUwfofU2REVnTUZhbbMtsQDMES
TMGWEMhS4dVzvPBS7fDJ7iceAe4QVurVcSGVhuEkZDIwm1/R0BaG7LqhDpqboaMDursqOxcPGRZoLYxd
vyS7f0EwBbtqSo78zW+e4LcPPxR/ZnvXJvzCzfnGK7vR52e1m65VdMr7FffL/UWm6uQS+/hORoYmuHK9
8Nevva4I3BYPVG3Jbx3Qcf5FPkjsaG5Ztz6Kb99aqMrFu74XgcXeqMoasTayd0qkhzN3D77MU0ASyIvY
YlvSplqAHe//iJOJjsbWda1RfGea0F96Bb/focldoKaV7HSJdDI33f9rngbkO0FKzV9tMxbQKp54/0VO
tjZbzZsT9eDZhG7pvs8oVRVWFMwYt9NzTGfcTP/LClx2Pi39a7Esa5BI/OEEP+3cwLFNbVHqGg2VmNKa
JbjLyShDuo4uNyfDIp/zmbhV4sYUp77+Or8A0svBF/TWILEO2PRUD7t/eIxfNsRob2zUicU0amt15F6x
6LsEiVKxGGDbIblcwKxN6pVT/OSDQS4DE0B2Ofi9CFRzIlbJi/Xbmtn43SM83bOFQ/VR5ENhxZgrkRwc
59xvznByNKPq/E4l3vIV4/8/bUyreEOIyIVHLg6NoC5Gy++TUvFFIAfIl7HUuQDLrtdMn8/Sc8Ujcl7I
nUoOsOW66j4rx2vl4/a+Sue/FWVwQfGabmUAAAAASUVORK5CYII=
</value>
</data>
</root>
@@ -0,0 +1,70 @@
using System;
using PolyTraderSharp.Services;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für die Anzeige-Zeitzone. Die frühere Fassung rechnete hart gegen die
/// WINDOWS-ID "W. Europe Standard Time" auf Linux ohne vollständiges ICU wirft das.
/// Diese Tests halten fest, dass beide Schreibweisen tragen und ein unbekannter Wert
/// niemals fatal ist, sondern auf die Systemzeitzone zurückfällt.
/// </summary>
public class AppTimeZoneTests
{
[Theory]
[InlineData("Europe/Berlin")] // IANA Empfehlung, funktioniert auf beiden Plattformen
[InlineData("W. Europe Standard Time")] // Windows Altbestand muss weiter tragen
public void Known_ids_resolve_to_the_same_zone(string id)
{
AppTimeZone.Configure(id);
// Der Bezeichner unterscheidet sich je Plattform; entscheidend ist der Versatz.
var berlinWinter = new DateTime(2026, 1, 15, 12, 0, 0, DateTimeKind.Utc);
Assert.Equal(TimeSpan.FromHours(1), AppTimeZone.Current.GetUtcOffset(berlinWinter));
var berlinSommer = new DateTime(2026, 7, 15, 12, 0, 0, DateTimeKind.Utc);
Assert.Equal(TimeSpan.FromHours(2), AppTimeZone.Current.GetUtcOffset(berlinSommer));
}
[Fact]
public void Unknown_id_falls_back_to_system_zone_and_warns()
{
string? warning = null;
AppTimeZone.Configure("Gibt/EsNicht", w => warning = w);
Assert.Equal(TimeZoneInfo.Local.Id, AppTimeZone.Current.Id);
Assert.NotNull(warning);
Assert.Contains("Gibt/EsNicht", warning);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Empty_id_means_system_zone(string? id)
{
AppTimeZone.Configure(id);
Assert.Equal(TimeZoneInfo.Local.Id, AppTimeZone.Current.Id);
}
[Fact]
public void ToDisplay_converts_utc_into_the_configured_zone()
{
AppTimeZone.Configure("Europe/Berlin");
// 12:00 UTC im Januar = 13:00 Berliner Zeit (MEZ).
var utc = new DateTime(2026, 1, 15, 12, 0, 0, DateTimeKind.Utc);
Assert.Equal(13, AppTimeZone.ToDisplay(utc).Hour);
}
[Fact]
public void ToDisplay_leaves_already_local_timestamps_alone()
{
AppTimeZone.Configure(null); // Systemzeitzone
var local = new DateTime(2026, 1, 15, 12, 0, 0, DateTimeKind.Local);
Assert.Equal(12, AppTimeZone.ToDisplay(local).Hour);
}
}
}
@@ -1,12 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<!-- Core/Module sind net8.0-windows (WinForms); damit deren Assemblies sauber laden. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

Some files were not shown because too many files have changed in this diff Show More