Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
487ea4466d | ||
|
|
669676ac60 | ||
|
|
929fa20ee0 | ||
|
|
4ba8149e64 | ||
|
|
0b8728b25f | ||
|
|
cd59e5c0a5 | ||
|
|
a2b1c18ee3 | ||
|
|
f8aa90882b | ||
|
|
53546ceeeb | ||
|
|
0a26b8563d | ||
|
|
5320d25d6d | ||
|
|
ad1593bfc1 |
@@ -1,5 +1,4 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace PolyTraderSharp.Models
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>favicon.ico</ApplicationIcon>
|
||||
|
||||
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.Supervis
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.Accounting", "src\PolyTrader.Modules.Accounting\PolyTrader.Modules.Accounting.csproj", "{A74EB53F-08E7-43A5-A664-A49E9AB50AEE}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -126,6 +140,7 @@ Global
|
||||
{98C70A7B-DC3D-48E3-BE5D-03F867E9BFA8} = {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}
|
||||
{C4D9F31B-9FC5-4BC6-98FF-55AE7A6BF23A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {60AA6BCF-B17E-4D52-A290-14154A3E97CF}
|
||||
|
||||
+21
-27
@@ -77,6 +77,11 @@ internal static class Program
|
||||
// Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht.
|
||||
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).
|
||||
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
|
||||
// Trading-Services gegen einen leeren State anlaufen.
|
||||
services.AddHostedService<StartupHydrationService>();
|
||||
services.AddHostedService<CoreStateHydrationService>();
|
||||
|
||||
// Module registrieren ihre eigenen Services/Channels/State selbst.
|
||||
foreach (var module in modules)
|
||||
@@ -215,7 +220,7 @@ internal static class Program
|
||||
Title = "Terminal",
|
||||
Group = "Core",
|
||||
Order = 10,
|
||||
CreateForm = () =>
|
||||
CreateView = () =>
|
||||
{
|
||||
var view = new PolyTraderSharp.Ui.Views.TerminalView();
|
||||
view.Initialize(viewServices.GetRequiredService<TerminalLogger>());
|
||||
@@ -228,7 +233,7 @@ internal static class Program
|
||||
Title = "Server Settings",
|
||||
Group = "Core",
|
||||
Order = 20,
|
||||
CreateForm = () =>
|
||||
CreateView = () =>
|
||||
{
|
||||
var view = new PolyTraderSharp.Ui.Views.SettingsView();
|
||||
view.Initialize(
|
||||
@@ -246,7 +251,7 @@ internal static class Program
|
||||
Title = "Server Jobs",
|
||||
Group = "Core",
|
||||
Order = 30,
|
||||
CreateForm = () =>
|
||||
CreateView = () =>
|
||||
{
|
||||
var view = new PolyTraderSharp.Ui.Views.JobsView();
|
||||
view.Initialize(viewServices.GetRequiredService<JobManager>());
|
||||
@@ -259,13 +264,13 @@ internal static class Program
|
||||
Title = "Dashboard",
|
||||
Group = "Core",
|
||||
Order = 5,
|
||||
CreateForm = () =>
|
||||
CreateView = () =>
|
||||
{
|
||||
var view = new PolyTraderSharp.Ui.Views.DashboardView();
|
||||
var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
|
||||
// Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
|
||||
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)))
|
||||
.ToList();
|
||||
view.Initialize(
|
||||
@@ -292,26 +297,12 @@ internal static class Program
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Weist den registrierten Views (Core + Module) ihr Menü-Icon aus den App-Ressourcen zu –
|
||||
/// gemappt über die stabile View-ID. Bereits gesetzte Icons bleiben erhalten.
|
||||
/// Weist den registrierten Views (Core + Module) ihren Standard-Symbolschlüssel zu.
|
||||
/// 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>
|
||||
private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost 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;
|
||||
}
|
||||
=> PolyTraderSharp.Ui.ViewIcons.AssignDefaults(uiHost);
|
||||
|
||||
private static void RunConfigMigrationFromJson(string folder)
|
||||
{
|
||||
@@ -474,7 +465,8 @@ internal static class Program
|
||||
services.AddCorePersistence(databaseOptions);
|
||||
services.AddSingleton<TerminalLogger>();
|
||||
services.AddSingleton<TradingState>();
|
||||
services.AddSingleton<StartupHydrationService>();
|
||||
services.AddSingleton<CoreStateHydrationService>();
|
||||
services.AddSingleton<CopyTradingHydrationService>();
|
||||
|
||||
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.
|
||||
try
|
||||
{
|
||||
host.Services.GetRequiredService<StartupHydrationService>()
|
||||
host.Services.GetRequiredService<CoreStateHydrationService>()
|
||||
.StartAsync(default).GetAwaiter().GetResult();
|
||||
host.Services.GetRequiredService<CopyTradingHydrationService>()
|
||||
.StartAsync(default).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -510,7 +504,7 @@ internal static class Program
|
||||
{
|
||||
try
|
||||
{
|
||||
using var form = view.CreateForm();
|
||||
using var form = (System.Windows.Forms.Form)view.CreateView();
|
||||
Console.WriteLine($"[OK] {view.Id} ({view.Title})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -62,7 +62,7 @@ namespace PolyTraderSharp.Ui
|
||||
|
||||
// Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
|
||||
// aktuelle Fenster → currentViewId null).
|
||||
PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null);
|
||||
WindowMenu.Wire(menuStrip, _uiHost, null);
|
||||
|
||||
btn_liveTrading.Click += (_, _) => CycleLiveTrading();
|
||||
btn_demoTrading.Click += (_, _) => CycleDemoTrading();
|
||||
|
||||
+6
-3
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
@@ -43,7 +43,7 @@ namespace PolyTraderSharp.Ui
|
||||
private void AttachWindowMenu(Form form, string currentViewId)
|
||||
{
|
||||
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.MainMenuStrip = menu;
|
||||
}
|
||||
@@ -103,7 +103,10 @@ namespace PolyTraderSharp.Ui
|
||||
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)
|
||||
form.Text = view.Title;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using PolyTrader.Core.Analytics;
|
||||
using PolyTrader.Core.Modularity;
|
||||
using PolyTrader.Core.Persistence;
|
||||
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>
|
||||
public class DashboardTradeRow
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
@@ -23,7 +23,7 @@ namespace PolyTraderSharp.Ui.Views
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using PolyTrader.Core.Modularity;
|
||||
|
||||
namespace PolyTrader.Core.Modularity
|
||||
namespace PolyTraderSharp.Ui
|
||||
{
|
||||
/// <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
|
||||
/// 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).
|
||||
@@ -57,11 +62,12 @@ namespace PolyTrader.Core.Modularity
|
||||
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||
{
|
||||
bool isCurrent = view.Id == currentViewId;
|
||||
var image = ViewIcons.Resolve(view.IconKey);
|
||||
var item = new ToolStripMenuItem(view.Title)
|
||||
{
|
||||
Image = view.Icon,
|
||||
Image = image,
|
||||
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||
DisplayStyle = view.Icon != null
|
||||
DisplayStyle = image != null
|
||||
? ToolStripItemDisplayStyle.ImageAndText
|
||||
: ToolStripItemDisplayStyle.Text,
|
||||
Checked = isCurrent || host.IsOpen(view.Id)
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
@@ -11,7 +11,7 @@
|
||||
|---|---|---|---|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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) |
|
||||
@@ -30,7 +30,7 @@
|
||||
| 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 der teuerste Einzelpunkt? | Der **`PropertyGrid`-Ersatz** durch handgebaute Steuerelemente (6 Instanzen in 4 Fenstern). |
|
||||
| Gesamtaufwand | **41–61 Personentage** (davon 2 PT bereits erledigt) |
|
||||
| Gesamtaufwand | **~33–51 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). |
|
||||
|
||||
---
|
||||
@@ -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 |
|
||||
| ~~**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 | 11–15 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) | 6–9 PT | Zeitzone bei Installation festlegbar; Deploymentcenter angebunden; Linux-Kleinkram erledigt |
|
||||
| **L2** | Headless-Einstiegspunkt + systemd + Feldtest im Zielland | 3–5 PT | **Trading läuft headless auf Linux.** ⭐ Meilenstein |
|
||||
| **L3** | P5 + P6 + P7 (Charts, Shell, Core-Views) | 18–24 PT | Avalonia-Grundgerüst steht; Dashboard, Settings, Terminal nutzbar |
|
||||
| **L4** | P8 + P9 (Modul-Fenster, Tests, CI) | 17–24 PT | WinForms funktional abgelöst |
|
||||
|
||||
@@ -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 | 0–10 % — 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.
|
||||
@@ -0,0 +1,6 @@
|
||||
bin/
|
||||
obj/
|
||||
appsettings.Local.json
|
||||
server_settings.xml
|
||||
master.key
|
||||
openrouter.key
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>Farbschema der Oberfläche.</summary>
|
||||
public enum AppThemeVariant
|
||||
{
|
||||
/// <summary>Folgt der Einstellung des Betriebssystems.</summary>
|
||||
System,
|
||||
Hell,
|
||||
Dunkel
|
||||
}
|
||||
|
||||
public class ServerSettings
|
||||
{
|
||||
[Category("Module")]
|
||||
@@ -16,6 +25,21 @@ namespace PolyTraderSharp.Models
|
||||
[XmlArrayItem("Module")]
|
||||
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")]
|
||||
[DisplayName("VPN Enabled")]
|
||||
[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);
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace PolyTrader.Core.Modularity
|
||||
{
|
||||
/// <summary>
|
||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist
|
||||
/// ein designbares <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird
|
||||
/// (mit DI-Abhängigkeiten). Die Shell zeigt je View höchstens eine Instanz und holt ein
|
||||
/// bereits offenes Fenster wieder in den Vordergrund.
|
||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. 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>
|
||||
public sealed class ModuleView
|
||||
{
|
||||
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
||||
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";
|
||||
|
||||
/// <summary>Optionale Gruppierung (z.B. "Core", "CopyTrading").</summary>
|
||||
@@ -25,21 +28,26 @@ namespace PolyTrader.Core.Modularity
|
||||
public int Order { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Optionales Icon für Menü/Buttons. Wird von der Shell/den Menüs genutzt. Settable, damit die
|
||||
/// App den von Modulen registrierten Views zentral ein Icon aus ihren Ressourcen zuweisen kann
|
||||
/// Logischer Schlüssel des Symbols für Menüs/Buttons (z.B. "dashboard", "money"). Die Shell
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public System.Drawing.Image? Icon { get; set; }
|
||||
public string? IconKey { get; set; }
|
||||
|
||||
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
|
||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// 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.
|
||||
/// Ü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>
|
||||
public interface IModuleUiHost
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,6 +20,13 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.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>
|
||||
|
||||
<!-- 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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
using System.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
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 BindingList<JobStatusRow> Jobs { get; } = new BindingList<JobStatusRow>();
|
||||
public ObservableCollection<JobStatusRow> Jobs { get; } = new();
|
||||
|
||||
public void RegisterJob(JobStatusRow job)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -49,21 +49,22 @@ namespace PolyTrader.Modules.Accounting
|
||||
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
|
||||
{
|
||||
Id = "accounting.main",
|
||||
Title = "Accounting",
|
||||
Group = "Accounting",
|
||||
Order = 400,
|
||||
CreateForm = () =>
|
||||
{
|
||||
var form = new Ui.AccountingMainForm();
|
||||
form.Initialize(services);
|
||||
return form;
|
||||
}
|
||||
});
|
||||
// Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -28,9 +28,15 @@ namespace PolyTrader.Modules.Accounting.Logic
|
||||
decimal V(decimal usdc) => Math.Round(usdc * currencyFactor, 2, MidpointRounding.AwayFromZero);
|
||||
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();
|
||||
doc.Info.Title = "Buchhalterische Abrechnung";
|
||||
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.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>
|
||||
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
|
||||
</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="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -21,11 +24,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI bei (Ledger/Status; BWA/Steuer/Export folgen A-2..A-4). -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
</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>
|
||||
AAABAAMAEBAAAAAAIABCAwAANgAAABgYAAAAACAAGAYAAHgDAAAgIAAAAAAgAOIGAACQCQAAiVBORw0K
|
||||
GgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADCUlEQVR4nH1TTWxUVRg993v3dWZoZ9rXgSnDlGKk
|
||||
EojGJmoFEmohMW40pi66QLdg2LCUmNQgRJu4cKPBxArRhSEQG4EQCC7ALjQWoTZhaKa1CYEZtNMydf5e
|
||||
Zzr33fvuNa+EhBbxW37n+zsn52P4nxj7eM/rLS2RfdTECcrX1WJ9bN/wr1cfr2Frm4wxjDGGP74a+LSr
|
||||
O/Vh84YNpLwy/LqANmF9L3Pns5cPX/zIGAPGmKFVzQhyZN4C4vHO1AeheJQENUmhoGrCl3bMpnjX5iMB
|
||||
HtQFM+jJww1cgPuiYeyIY4QrIOsM2rfBKGJUo+4H+Mo6ALSWD2NAASje+C19tpbNso42y04m23hnp2Mv
|
||||
z/3NJsZvnQaw+JDBf2jweOzviQz0927dTQRSSulfbmTHz0wtX1i1dHBw0HIch0ZGRtTKegDH+vutt9+J
|
||||
HtCcr88XKgAsIrJ0sqMVlmwsDh++eHIU8Fc0wxoKBoQ3W593vvi6t7h1Zw/gKcD4gKgDXGNm8k8cPXI5
|
||||
MTq/VAho8KGhoTeklLtnZ/MnShe+r7wIbYmm2+3uYqo0NxuLWiwUzGS+7xkwxZRQbiIZsieOv2tj9BvN
|
||||
jn8yfHLXzt4Dqc3dJZG9nE1gOoZwe8JiusWyAKYVvGoFUngmHIuyB3f/qva8f64DQCO4mqfTU0txp02+
|
||||
9EqfI/yUkwq5kD6D1gYaNjzhQ2kOhgo0k+C2tn461ntwy7Zka722cIVzIlqquXax7Hq6mKMamzSSYrRc
|
||||
rZCo1VhoXTPCsTgo2szcfxZ0YscLke27Xv2S2hnGf7i0iQvZ0OVSWSnlc0hBzWEXHudASEILqRvVgq7k
|
||||
c+A241u2P0dK3kHm5vz81HTuysRM7TtOoJblhuDZuzMymy6fv7WQWeh6ZtPeRDvfllgf4a0bI2T8MPIF
|
||||
icnfp9M3M4XzR0/d/7EkMQ1Acc/zxnLZe+zQwfe+zeXmMgDKwP3UazvCzw70JftSiVC3RTDXpyrXPj+X
|
||||
/xnAfCCeRQzKN6uNGLgi+Man+fMRHhjo0Sf/C3araRpvSoFKAAAAAElFTkSuQmCCiVBORw0KGgoAAAAN
|
||||
SUhEUgAAABgAAAAYCAYAAADgdz34AAAF30lEQVR4nJVWXWwU1xX+7p07s7M/tvcXezc2dk2AADFVtCC1
|
||||
UaIQlFRCLaposF/LE3noQ6smEe1DY1lKpEhVXhopVao89IG84FaVAg0NKMJOS5q2UH4FBidgsl5s1/Yu
|
||||
652dv/tXzcZQkganPdI8jGbO9835znfPGYL/I3793fhDpZ7CgK2pYReKEL6DO8vzjecOL14CoEYBOgao
|
||||
+3PIWoBag1AKrTXY5OiTP+ke6D0QT1jbYkkbUrjQAYcnLNdZrr9X/WT6te/9auqs1poQQvRdDLYWAVml
|
||||
//OrT7+y/fHHDlmpODxtSDOb087sRRAkkbMyiYeHNuw3k/Gdv/+R+QMQck7rUUrIWLsSuhZ+VMRWIJtZ
|
||||
V3ghXiypWn1BKg0jvFNn0gNz6z7zW3d0w2+5275d7o8ZaoQAGr85ZtwFWYugHQ4QI4QKTTQJuTLi+X4Q
|
||||
z0EynUempwdByyNeq2WCmSrg6nNFNqX+N4miSCSAwGkwi8Z1V65XL12/QjUPIbkPGQYwbVtnex+R/vyn
|
||||
Jgz2Xz39WoIpN+ecv1I9nC+cPtCz7VEkBtKSMhNShO0mydCnYeWq/feJc3Mf3XSP6kjaiV0KmLyn85pN
|
||||
3r8fxqk/dQz+4vull4Y2rtu74eFiT9w2oLSE1gqVSk3fulX724dTd9584/2Zca11cL+L1iS4750owdpQ
|
||||
ypR3PGR/g2oVI8TQoAYcidq7Zyr/BDD7oOSvjZM/3b5JLFRL1+uCzK4IqiEIBCAgkLGY7M8zdHV2iuJj
|
||||
uy9868fvrEQytd202gMyPDzcdtP4+Lj8IjnRz0InM+v7JreO7O15ouWBRp/UvigISHQYAcOEDFv46Pjk
|
||||
iwBeP/tWmeH5s/wugb4LfObMGfNs5LKjL+hFLNLBUly/+ssrGdM0sma+KFiGwLSTgJbgYQuC+xBBEyA8
|
||||
NN3QCiQtRTjl+21aLpe7bNse4pyLHTt2fPwVClUPNbnz6emPs6Hrai3DqLLVAslqMQYSFpgg1PtyMtu3
|
||||
b98TW7ZuPTY3N4cXXzr0Ggy7mpn++Yzp3lzHWCLfbAXrzHitq3uojFQiR1i+tz2koAJAhJCBAxhak5Ul
|
||||
XDh/PaFHQeHFjSNHoEdGIFmt1jAqldti8+ZN4jvP7P7ZtZsL0PbesJSzrFSmCNaRQWO5jubcNGoz5xC2
|
||||
TkIFCtwLIAUFZTEoEZBOm4HaaYeMEQWcCO5VUK1Wiet5LJGIg/Mg5GFo9PRttDoTTRVyR4rlecRgmLH1
|
||||
/QAbglQGWssVuLUqwkYVmeIAOksboBpzuHj+cOF3B/TGUkGXljiapS0HLzHDMGCaJnzfR7PpWH6o4Dcu
|
||||
66C1REIVMyO/EULA2G1oLUENG10dWWQKWwDraVTOH8PCh781Ysk8Nj25c8TuePa5Uu/6Qu32NP4xOf44
|
||||
C4IQIQ/RajYRcg4pJLQSUIoTJQHDoPBdD/M3ZhBPpUANCsYoTIvATHZDug3kN+8hqVwJlIRZkwZR43U6
|
||||
JckKTxWYlJGWAo1mE77vIeChNnSLEO0oirjWQlEDkmTynRCBD+4LBKEEFxIinEW+mENH7JYSy9PEopxU
|
||||
ZpdRrcx9dqPqfvD26eAy41zpMAyV4zSVUhq2HSPTF5cWN6bnCh3pDGAQMJMinbMglamlipzKIQMOZmVo
|
||||
LG5C1Kp0YdHD1Ce1U1dnmqdef3fpg8W6fy2a9kwIwRizqOO41qXLV4Xny/rR95ovZ71r/uBA/pvbB5O7
|
||||
urPWo4kkY+m0SVIpg9AEhYoTLC+5uHXTa16+0fzjXy7U33/75OJfAcygH+TIW8N8ZGRckj179uzu6+t7
|
||||
w3Ec//iJEy8HbmPedfnM57sGXQC6u7uM/A+fKZZ3PtKxK9fJBs0YIZxDn5taOf7KO7N/qPuiAmD+qacg
|
||||
JyZGFSVj6t5RBpAC0AsgOuYd0WRZXfbtpR/dA4gB6ATQA2A9gAEAfQCy0bPRUdBo2T9ompKDBw+yYrGo
|
||||
x8bGxIMmaptsfJii8K//AE1MKjL2xd+UL8e/AfnU0K0OeBWSAAAAAElFTkSuQmCCiVBORw0KGgoAAAAN
|
||||
SUhEUgAAACAAAAAgCAYAAABzenr0AAAGqUlEQVR4nM2Xa2wcVxXHf3d2dmdn1/bajl+NkzpxkxACDTSq
|
||||
aOOWqFWJ2qqiEiqPqB9AEFEQIPULhQ8UEEKo+UCQeAhESz5EAkRLCxU0CAgQpaRuUJu0TQqJE1Mnbvxc
|
||||
Z3e9M7OvmTsX3dlx6hAHMLEEV3vnsfeee/73f84954xQSvG/bOK/lfvtl4Zut5Pic0lDvE8IOo2EiWln
|
||||
aLgOSlHww/APVV99755vDB8B1EoCSBz66m3f71mz+qHO6/tJt7ag0mkMy8SdHgGZoK19DV6xRGF8gtkL
|
||||
k4/f+bUXPgPIpRYzlqv84KNDe9Zu3vjQuve8m1Q2Sc0rksx1U527gAhACIlbnIvG9Bw9V8to2ZUAkM5a
|
||||
xucHbxnCLUwhgxqoEIGBoQQiYeFdbICQyKAezdFztYyWXWpBc5kAbG1gYZoEQQ1hJGjfNERlagSDBEbS
|
||||
JNdnEQaSwK+jkNFcLRPJgnetDBjRVYWks+3UKyFKJTCVwLRskikbK92CIEFpao50SwcqDBZczVgJBhCG
|
||||
QeHcG3QObEbJM0weO4wSCfyqBzLAr7qk7AxdAzfQuXozUyPHEMbVfd1cpv5wdKayXwwPf2yjCGnr7aez
|
||||
/waEMLT3Efo+RioFoaRWKTF5+igXjp3m5LjznaudArFMAFlg4CcP37xnsCv9/rbreuhY20cyk4qoVkoS
|
||||
hpK66zE/WaCSdxmbq/76wW+//EXgPFC5VgAJIAf0f2rn+jvuurH7g30566akQetbKwoCqZyp+forfzyZ
|
||||
f/qHB8cOARPAvGbwWgEsgNBMdAIdQAuQXLSWdnkfcIEiUIi9f0VMsFguGZ/t1BIernfaAGoxmBULxeaf
|
||||
v77jZwnCB+Kz/e+bEEiMZ9775ed3AcEVC7K8Zmvl2z/xSQj8hQBzmbJLe4qetQaTo/sefyAORM61ArAE
|
||||
gursBIZpYaZsEpatgwOhX8FvVAjiu2x4BI0qUtaicS27EgCMQFF85dlfdYThgkMvsPDWzptPovnT4Ayz
|
||||
+K8iYWr37t11/RIEAfv377diB1qqqQSiY/unP6sngwahwv/EBPq0LOk0Qh+lI8PDBT0cBD5epR6ZVvqV
|
||||
3/WOfqEojNQGQ6h2IdjQXCPEbu0m27mG9u715Po3NU3Q8PD9pU0wfvQ82x99vheYXYqB5NM//wVSSu67
|
||||
71523D4UOdfLJ07fPdB2P5m+d6FUSOt172jqF+BM/RV3+m9MnXudkaNPgdSR0ELJFGGQjFhRmp3LTbBk
|
||||
Mxe/eF6FRqOBChUy8Oka3Ir0a8h6idrEBKqaBzODZXeTHtxK75adGKkW3OlTODOn8WbO0HAm6d+yg46B
|
||||
dzbNY5q8+MTVTWDqi9697p7n4se2DST4My8gVRzkIpMK8MsEjXmYP4uvbWVmSNo9rBp4Oz1vu4OgUefN
|
||||
l37K9N+Pk2nvikySSNf402NbvptNG4Nmgps1L4FKPHPLw6/uMhecTycRz/UI/IBQUyhDwrBBuMRZN7SD
|
||||
RU6mUNIhcMpQPkNDhSQ7bmTd0Ec5+/u9uPk3Sbb0sXrbdlKZro9Ybb3Y7WsxrQynn/tKFBvMJgMBMgzx
|
||||
PI9ASsKYEWQ1ynCX6TcMTv1F53iDllwbuqzPtC3cW6i5Ryhf/A2IJBvufgQhy6iGi6wXUP4Y/tTrFPLn
|
||||
kX508KxFDChcz41OgpQhUkmUrKF0WF9MQihYs3GAMPCpzDuRXDk/G025ODXVtGvCZGDLOqqj+yKWNFgl
|
||||
FeWiQylfpOw0pl8b836kBy/5gA4sjuPiaxOEEhkqSjN5Uq05LMuMo64iVGBndEyxyOasCEBQqxPUAurV
|
||||
Zp2Yac+CakRuU68HzM85OEWX87P1wwePl375gwOTLwEzQPUSA1qBG31UqIgBrezQi5PPbhqo3GWZojWT
|
||||
s7Ftk3TWJGkmonlR036YEiRTJnZO14PaTiFucR6nVKPs1J1T49UDH9878gRQins5TtH1CIDetW7lssOr
|
||||
r52IGJnOlw4/8q2xbwJ7t21o6fvwjp5bt67P3NnbkboplTKEnU2Qtk0yWeNSTvIDhVeWzBcbFBz/7MFj
|
||||
pR/veWr80KK6wP3nFG3qy6pOXeQ0I+GDuz50r6YmFsjrv4+PuueOj7ongSd1MXL/rasGP3Bb187re6xt
|
||||
7RlzUzrTDLu1quLMRPXAY0+O7zvxhjcRK56Pd1tbqigRcVWzPr67sVJNUT2u4WRcBemAYMVdp9bWuFuL
|
||||
vnr8WOHCbqtxXrmiFFsMIBWXWFqBXkCjvVoyWpCJkljc9fNCYaMV6Z3qfkXx8X/Z/gHxT0sQw4Da8gAA
|
||||
AABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Channels;
|
||||
@@ -10,7 +10,6 @@ using PolyTrader.Core.Modularity;
|
||||
using PolyTrader.Core.Streaming;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||
using PolyTrader.Modules.CopyTrading.Ui;
|
||||
using PolyTraderSharp;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
@@ -58,6 +57,8 @@ namespace PolyTrader.Modules.CopyTrading
|
||||
// K2 – Neustart-Reconciliation: MUSS als erster Modul-HostedService laufen (nach der
|
||||
// State-Hydration, vor TraderMonitor/Engine), damit verwaiste CLOB-Orders vom vorherigen
|
||||
// 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>();
|
||||
|
||||
// Modul-Services (Signalquelle, Ausführung, Analytics)
|
||||
@@ -81,24 +82,22 @@ namespace PolyTrader.Modules.CopyTrading
|
||||
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)
|
||||
{
|
||||
// EIN Fenster fürs ganze Modul: die einzelnen Ansichten sind Tabs im
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
// Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
{
|
||||
/// <summary>Farbkategorie einer Trade-Zeile. Die konkrete Farbe legt die jeweilige UI fest.</summary>
|
||||
public enum TradeRowTint
|
||||
{
|
||||
/// <summary>< 0 % – Verlust.</summary>
|
||||
Loss,
|
||||
|
||||
/// <summary>0–10 % – kleiner Gewinn.</summary>
|
||||
SmallWin,
|
||||
|
||||
/// <summary>> 10 % – deutlicher Gewinn.</summary>
|
||||
BigWin
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reine Zeilenfärbung nach realisiertem/unrealisiertem PnL-Prozent (getestet, damit die
|
||||
/// Schwellen belastbar sind): Verlust rot, 0–10 % hellgrün, > 10 % kräftiges Grün.
|
||||
/// Reine Einstufung von Trade-Zeilen nach realisiertem/unrealisiertem PnL-Prozent (getestet,
|
||||
/// damit die Schwellen belastbar sind): Verlust, 0–10 %, > 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>
|
||||
public static class TradeRowColoring
|
||||
{
|
||||
public static readonly Color Loss = Color.FromArgb(245, 200, 200); // < 0 % – rot
|
||||
public static readonly Color SmallWin = Color.FromArgb(212, 240, 212); // 0–10 % – hellgrün
|
||||
public static readonly Color BigWin = Color.FromArgb(140, 214, 140); // > 10 % – grün
|
||||
/// <summary>Schwelle, ab der ein Gewinn als deutlich gilt (in Prozent, exklusiv).</summary>
|
||||
public const decimal BigWinThresholdPercent = 10m;
|
||||
|
||||
public static Color ForPnlPercent(decimal pnlPercent) =>
|
||||
pnlPercent < 0m ? Loss : (pnlPercent > 10m ? BigWin : SmallWin);
|
||||
public static TradeRowTint ForPnlPercent(decimal pnlPercent) =>
|
||||
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>
|
||||
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
|
||||
@@ -20,11 +20,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt später eigene WinForms-UI-Tabs bei. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+12
-36
@@ -3,7 +3,6 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTrader.Modules.CopyTrading.Logic;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||
using PolyTraderSharp.Models;
|
||||
@@ -11,35 +10,27 @@ using PolyTraderSharp.Models;
|
||||
namespace PolyTraderSharp.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Hydriert den TradingState (Accounts, Demo-Positionen, Trader) EINMALIG beim App-Start.
|
||||
/// Registriert als erster IHostedService, damit die Hydration abgeschlossen ist, bevor die
|
||||
/// Trading-BackgroundServices (TraderMonitor, CopyTradingEngine, ...) ihre ExecuteAsync
|
||||
/// starten. Behebt den Startup-Race, bei dem Services gegen einen leeren State anliefen
|
||||
/// (früher lag diese Logik in frm_main.LoadDatabaseAndState und lief erst NACH AppHost.Start()).
|
||||
/// Hydriert den Copytrading-Zustand beim Start: Account-Settings (legt fehlende Default-Einträge
|
||||
/// an) und die verfolgten Master-Trader. Läuft nach <see cref="CoreStateHydrationService"/>,
|
||||
/// weil er die dort geladenen Accounts braucht – die Reihenfolge ergibt sich aus der
|
||||
/// Registrierungsreihenfolge (Core zuerst, dann Module).
|
||||
///
|
||||
/// <para>Zuvor Teil des modulübergreifenden <c>StartupHydrationService</c> der WinForms-App.</para>
|
||||
/// </summary>
|
||||
public class StartupHydrationService : IHostedService
|
||||
public sealed class CopyTradingHydrationService : IHostedService
|
||||
{
|
||||
private readonly TradingState _state;
|
||||
private readonly CopyTradingState _copyState;
|
||||
private readonly IAccountRepository _accountRepo;
|
||||
private readonly IPositionRepository _positionRepo;
|
||||
private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo;
|
||||
private readonly ITrackedTraderRepository _traderRepo;
|
||||
private readonly TerminalLogger _logger;
|
||||
|
||||
public StartupHydrationService(
|
||||
TradingState state,
|
||||
CopyTradingState copyState,
|
||||
IAccountRepository accountRepo,
|
||||
IPositionRepository positionRepo,
|
||||
public CopyTradingHydrationService(TradingState state, CopyTradingState copyState,
|
||||
ICopyTradingAccountSettingsRepository accountSettingsRepo,
|
||||
ITrackedTraderRepository traderRepo,
|
||||
TerminalLogger logger)
|
||||
ITrackedTraderRepository traderRepo, TerminalLogger logger)
|
||||
{
|
||||
_state = state;
|
||||
_copyState = copyState;
|
||||
_accountRepo = accountRepo;
|
||||
_positionRepo = positionRepo;
|
||||
_accountSettingsRepo = accountSettingsRepo;
|
||||
_traderRepo = traderRepo;
|
||||
_logger = logger;
|
||||
@@ -49,31 +40,16 @@ namespace PolyTraderSharp.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var accounts = _accountRepo.GetAll();
|
||||
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);
|
||||
HydrateAccountSettings(_state.Accounts.Values.ToList());
|
||||
|
||||
foreach (var trd in _traderRepo.GetAll())
|
||||
{
|
||||
_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)
|
||||
{
|
||||
_logger.Error($"Startup-Hydration fehlgeschlagen: {ex.Message}");
|
||||
_logger.Error($"Copytrading-Hydration fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
|
||||
@@ -20,11 +20,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI-Tabs bei. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -54,22 +54,22 @@ namespace PolyTrader.Modules.ResolutionFarming
|
||||
// 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).
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "resolutionfarming.main",
|
||||
Title = "ResolutionFarming",
|
||||
Group = "ResolutionFarming",
|
||||
Order = 200,
|
||||
CreateForm = () =>
|
||||
{
|
||||
var form = new Ui.ResolutionFarmingMainForm();
|
||||
form.Initialize(services);
|
||||
return form;
|
||||
}
|
||||
});
|
||||
// Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
-379
@@ -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>
|
||||
AAABAAMAEBAAAAAAIACjAwAANgAAABgYAAAAACAADAcAANkDAAAgIAAAAAAgAFgIAADlCgAAiVBORw0K
|
||||
GgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADaklEQVR4nGVTbUyTVxQ+96WUlQmEVtpBhdIXGKXa
|
||||
0eQNVhOxwJYQptbPNku1QnQs/hHxi2mmvuL+mWxhAbMtC9E4l8U2aqyJLsOauCAiWFu1UIpftIiOSkdb
|
||||
sG+Kr73LJZbU7f455577nCf3nOccgJTDsiyFMUbJe1PTjmmz2bI/BbLw9r8Ay7JUe3t7gvgMwyxXKJQN
|
||||
cnnBkfJylcDjefjbvVu9hxQq1UubzUYw+D0C9l1yWZlau3Ztw0n5Evln4lwxGvZ6IRQK8VXLqwQOh2Pg
|
||||
gvX8CqvJRJlSSJDRaEyz2WxvV66srmtoqL8klcmy3fdc8GzsmStXLNZ8IBIJJsYDvQPP/27Reh88ugkw
|
||||
m1oCZbVaEzRNS2vr9L8uysrKtl+2j13v6zO5BvobeZ4Pvwz4O3t6/qy5WpQhPXd4l693zWoDJr0CoBZY
|
||||
mpubj3Wd+hFvMX0xBflFDCEmcW2xvJLYno31a4IHdvK4zYKftmznAOBD0mwMgAgwUy4vNAYnJ3EgNP2d
|
||||
e9lHOf6ju1/c+Lx6vXts4n7P5vr1leoSe97rUNrk6JPE79f/ajE2fvl9k6XJjAAwBZmZ2fG5ePE/0Sie
|
||||
u+90izXLLhUtEsrUn6jPOyybOzSqMlseF6GmojE463veeEWrT1g2Gb5KE6Yvne+BIi8PZmdfY1F6OuJL
|
||||
VBMXb97eF/QMgwy/EdZpK/bI+JjgVSgMp9yPW9u8gXPVUoluLDCe4Dju7TwBx3GY47g5sUSCmFJFYavT
|
||||
2901OLw3OB0BiITeENvV/7Dl+NDTH0gCzhDSkWgUxePxeRmpYDA4HQ5HBiSLJThHIm0mwW9H/B2/3HLt
|
||||
842/iP/U52o94Qt0CigEDKNbJc8vqJ6ZmQG/P3BnQQWGqTK3fX0Ynz5zFhsMhoPJeBbAxymS0yzb7iGY
|
||||
DRs2DQKAiCghIMPkdA5ezMnJtucXFBjMW7edVCpp3d3+293O0cBQea5Io9ZUfrpKv3p/sZJecsVu50dG
|
||||
hr8BACInIqOMEEIYY5yv19f+rK+pWafTrYBoJAyxWIwHhNKkUikKRyLwx7Vrk3cH7xzy+Xxn3q3BwuYl
|
||||
SbJounRrRUXFlpLSkqp0oTADAUpMvQpOeDwex9CQp5vjOPL9+eT/rmeShPgSAFACACmRnDAAjAJAgtRN
|
||||
cMmkfwF7L4JfRi8mgQAAAABJRU5ErkJggolQTkcNChoKAAAADUlIRFIAAAAYAAAAGAgGAAAA4Hc9+AAA
|
||||
BtNJREFUeJydVn1QU9kVP/e9fEBICCQKm6wdljVAIBLCwgopYBLJFlTWtetEULojq2zrtrWd3W2n/9hN
|
||||
wrRT6yxqZ2e27o7KuDu1rTidFhdhuyqELxH5UEHDRyK4GhI+DMtHEpK8927nRdyi7Y6dnpk3797z7vud
|
||||
e37vd895AM+wgwcP8nU6HaekZMvh8vIKrNHkbH38zGw2E/B/GsIYo9UOlSqz5tSpU/jQofepjRvzjwMA
|
||||
938CetphMpnI+vp6emUaW1BQtFscH2fENK2VyeTywqJCCIfCqKmpaWBqyr2nq6trBKEIDH5mADZlq9XK
|
||||
JImT4pIyk36SlpH+C4UiJU4mk4FUKoGWllZwOp1QUFAQlskSuRcuNIJ70m2y2a6cZ2m02WzU0wE4q3du
|
||||
tVrprKyXNWlpKR/pDUVaqVQK7kkP9Pf3g9frjawTCoW4p6eHK5FKYWFh8crc0vx9QAj0AGD7tgzYnVss
|
||||
FpydnZ2eodrQWlb26trZ2dlQX18fb2Zmhlny+dpIBPwXXlTkIsDc+YX50OjwSO3g4M2PAeDBOwC8Y4AC
|
||||
5wCTuwAe0xuxiApYWhBCZEpK6kfbtm1d63Q6Qzabjef2eG7fH5/Y39Z6ZRefzx8RREdzp6c8vQM9vVUP
|
||||
B2/WAsC9nu2FJ98+UOE9AljNgptXMJ/gnb0bjSU/r609hn9/5ANqV8UeXLJlayOXG6MCgBh2gV5fXFn8
|
||||
SslpANgAANHsO5eM2jP46CEc/OBX+N7PquYupsuLsdlM4NVBVuTIq65+a7Cu7gzz5r5qZqep/G58QkIm
|
||||
AJC1LNgjlbCgfJNOJ2QnrSX5p/HHv8PzFYbQwmu5fvyXP+ILKWuPsorCOTnfSJhACOFNmwxlaWnKlDGH
|
||||
A2OE4CvPpHVuenqwb4f+5LYDu73HMVYDIgItJhO33mZb6jDmntTtfP3NQE/HMiBMiKSJ0eeOHP3HHs/y
|
||||
b7fv2PnX8vXKfY/ZiaQSEyPIFotj+X6/n/D5ffbrd11d3WWGupcM+qq03Jeidrxb3XYKM3mG+vqlLw35
|
||||
nxXs3r0/0NtJ4XCYGysSk2dvjDSX94+9s+21nce+ZyzexYvifZ/FValUKBKAy+dHLSwsAofDARpxbse5
|
||||
7obi5QlVEBsLi/WfUkmKZLH23bcufvldVbvxjYofBPu6GQ7GjEAoJs8PDF+s7HeYcjUapXZj7hsqVTrD
|
||||
0NSaFenjSAAOQcCS3w/8KD7E8jj4a4Cpww3NJmfD3/2i7Jc5gWsddPq65yTG935ZSPV3UyytXIGQ96dr
|
||||
t5pN10cOEAgtKZTKdX6/H3de7SYAAfPEQWM/tM/nA5FICMJYEZ/113nmG5i2vh/+miQ/WZ+WGh12jFB4
|
||||
ZAhxhGLEwQz5aUvnP/cOOKtLFYrZvMpK4u7EVwn+gB8FgyFA+N8qigyoUCgQDAaBQ3JAEhevBgBJi07H
|
||||
nHno+9tPv7j6o0H7KMNNTOTw5OvCBEmQ52zdn+8dcFZdLFXMNo2NhaxWKxbFCAok8VIIhcNAUfQSi2ux
|
||||
WB4FmJ6dvj7n9fr40dGMRCpNLtTpSgw2G2XWJeHmxeX631yy7R0fHvMBRUV9drmtsbxv9O0/lJZ6r+VV
|
||||
hlm60tPTNaJYsVEoEoLf54P5eW/LSgDMYelBCF1+Tva8fXZqKlelUmFNpubw2PDwJavt3vhoaSk/tbm5
|
||||
Prb1apT2xlDRfvuD9xUKxYw3EKBrampYrvlqtebDrCw1d2x0DC8uLPrb29vPR+oQQpg9B+zYP+lyffLA
|
||||
NQner+donW6TpLCw6GyCOOHF1ObmIIFQ6KR7/s/77Q9+XJaTM+twOIJWm43CGAv0+s0fGouLtWGKDk3N
|
||||
zKBJt+uMz+cbX8H9plyzVMVv3lx8OkO1YbtWqw3FCAS8xsbPH3Z2Xn1v/I7jsh8CLoLVw6P1kkRpolr/
|
||||
iuH4li1bs6TSNeH2jk7u7aFBR+/1azs8Hs+dxxk86hQYI4vFgk6cOJGRlaU5m5qWlpmXn0/L5XLS6XDA
|
||||
rZuDY/dd93tJRAQwZkipdI1SqVTmZarVsBxapjs7usgRu31udHR4n91ubzCbzZEC+kTDWWk2hFgsztSo
|
||||
sy3Pf2fdq5lZGqRSZeAYQQzicklgGAYIggCaZiAYCjEOp5O40T8A9ybGB4aGbtW43e4mVpSwqmQ/3TJZ
|
||||
qthCJctQZry+NjGxSiaXr09KShLESSRA0zSQJAkBnx8mJsbpSZdrwuv1NvT399YBgEOn04Wf7mr/0ZNX
|
||||
fCQACABAmpycbBAJRRtpBsewhRJjQASBwhQVvGO3j3zBNhwA8GGMI5L9L3jfZhgpFAr2VLNlmi3R4lUX
|
||||
O49mnz/r1+Vf7rQL5bqTXasAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6
|
||||
evQAAAgfSURBVHicnVd5bFTHGf+9Y/ftYZv4wGtDggGfOGC7FRjXwBrb+GIxFaUqTQKV0qipItE/qrRq
|
||||
i9KKtIW2ULVJSAtKK5KqiqJEaZNSDEEma2zsxWBEwBw+sFnWeH3u+mC99773qu/xHBZnKYaRRjPvzcz3
|
||||
+818x3zDyLIMhmHwmEVrsdQHZVke9/l8pWfOWPsByI8rhLBZPFkxiqIIi6VuYVxc/M1Nm6pfBZ5MFvuE
|
||||
BDSSJMGUasJLL72I1NTUg1VVNVYA/OMKYuapAqaionI5z2t+JsvyGlmWi2RJQvyCBSguXoOSkhK0NLeg
|
||||
ra0NoiiutVpPd8xHJQr2IwgwlRWb1nI8/058Qvyq7Oxs5ObmYunSDDAMi08++RROpxNpaWmorq7C1PQU
|
||||
Go434O703Q9OnmzYBUB8FAH+/4xzNbWbDyfEx/9g/YZ1KCgohGf6Lm7Z7Thx4jNEIhFMT09DEAS43W58
|
||||
/PG/sHLVSrAsj5Ak6wEIAHyPPFo59gloNlvqB5Yvz0yrr7fA7Xaho+MihodHIEYiiEgSIIpIeOqpe0IY
|
||||
Bj6fT6m3HI6f3+i8/CmAO48i8LAT0GzZsvXOs/n5pqqaarSebUNP301IoRBC4TCBXHI6B49lZ+fs5Xge
|
||||
oiRhxuNBIOC3H//vsRcBDAEYBxDEPAo/55uzWOqPZGdnmaprqvGfY8eV3YcCAfj9/itN1tN7/H7/GG06
|
||||
L2/FXjoNn9eL4aGhI21tZ98F4AQwAUBqLl0RARiU2W6QKkLzIcBUVdeUJiUlfb+6pgbHGhowNTWJYCgE
|
||||
x+Dg7y622/4NYBTAXQAJLMfB6/VOn28/98qdOwOdAEbUMaZpbU6wdM+vFKHW3+wNVpzveSgJJsoGhG3b
|
||||
tl+qq6vLH3e50N/fD6/PB7vDse+CrfV9FWAGQBhAIoClFBFVUi5V33xTcU7Q/Ov9mPrLflIyEnfvQevr
|
||||
v4T5XPdXSETbALNu3YbcxMTE/IULU3Cho4P8Gf5A8JIKPgjAS0erzqe+Xe37VFIaAi/b93tMHz4IRjCA
|
||||
lSUwPA9OVkKCMdYpsLOqWGgyvbp69Wpc+uIy6HjDERGNp07sVnceainJE898Y4Ws7poETamV+hrrmuzg
|
||||
xn0H4fn7G2B0evA6PbTGOJx97RdY195TQhjf2fm9z3Z894Xd0RGYVVtB0GqrkpOSMOZygcKsx+dt9Pt8
|
||||
tPNg8+rMwIb9B1C27w8gIJXEbNHSv/IDf4L3vbfACDrwgg6CTsDFPifM7d1m2sS2Hc//LdEYVwMGL6gx
|
||||
4gECWp7n0wOhAHiWVQjcnZk5DcBvW5vjNR98E/6jb8F/9BDKD7yBz++T0FKf/vnffRuMVgCv1SnBqb3L
|
||||
DrPtOoEPbH9u56Gilfnf3LXzefAcVxJNgJ9teZ7H2KgLHMdRZCH36lYuHWVUC1ZngAwGwX8eRsUfD6Hx
|
||||
x68ofk794D/eBiPowfIcNByPli+uobxNASc78el4rr6yfCNuOwbAssqeNV9xQ4ZhIMqiMkEQtNBrBYrj
|
||||
4vrzvSVNP/lR+8aSryMsSZAjIsLvH8GmP/9VWUd9OnaW56HhOTRduIyK++Bj680bC0ypqWiztSMcCd/b
|
||||
IAWIuQRkUcTYuAvxC+KhD4YhSxFJdTtn+fkes5XjWspLiyFyEUiRCMIfHlWVpwNFRKpWWwcqbffBAUQ0
|
||||
PJciaLXwzHjg8/khiQ/eT+xsRyKf5Dj4vH4Y44wwGuMyVfciQfYK2w2z1XYBnP6ehZNKqFKf/tHYHHDy
|
||||
Dk7Q6bN4DQ+/P0B6Vnw/JoFwODwWCoUUFWQ8swTkFSRAFaSQqCQSZ9rAGPTgBEGp1Kd/NDYHnApHcnKy
|
||||
cxS5JJ9wYhGIeGZmWsNEgOOgjzPAYDBsXZKxdIE6fp/EuRvmpsYmQKdXqrWxCfQvBjhovcFg3Ko3Gu7F
|
||||
llAIhEN4c20gdLO3552EhIRvJSYmwu2aQHFxMbwzMx8NOG5vUlUxSwIV57rMLSzbQv3Kc10xwcnSiwqL
|
||||
PqKMye1yQ8NxmJiYAOFEz2PVNtjb29Pl9Xr7KckYcDhQWFgAk8lkrq3dvDNq3pcnYW67XkL1IeAsraP1
|
||||
JIfkkVySTzjRVzU7qwIAHvut/tdHRkfBcBy6unvw7e3bSRVHa2vrdsUg0avWGOB1u2gdrSc5JI/kknzC
|
||||
iVYBq7Zkmr6rVztb3ePjxxx2O+hG9Hi9ePmHLyMtLf292trNnxcVfS0+isSkWr8Ep3GaR/NpHa0nOSSP
|
||||
5JJ89fKSY13HDAACWFZXZ7GmmkxJmVlZyH92BZ5Z/DSam5uVzDcUDH0oQz4uy3I3y7KMJEkywzB5DJgt
|
||||
WkG7w1xmRllZGe44B3Hjehf6+/owNjo6cfJkQ4WqLs8sgVhZMbldMoDlNTV1DckpKUm5eXlIT0/HqpUr
|
||||
wXEsrly5gs7OTgT8ATBgIEOGTq9DQUEBCgsLIYoSrl67huHhYfR0d8Ptck2cOnXSAuAWAHd0pvywtJzi
|
||||
dAqAjA3rza8lJSdbMpYtI4PE04sXkzqwaFEa5hZKWIdHRjDodGJ0ZEQ59gm3u+Fsa8tvATjUpIW8aV7v
|
||||
Ao2a9aRnZWWXZmQs/anRaFy2JCMDRqMRicnJD0Q0Wj85MYHpyUkMDQ2RtdsdjtsH+/pu2oibaisPgD+K
|
||||
wKw6jKpKFqanL8rJzMx8Tq/TF7MclzKXgCRJLr/fd6G/v/+D4eGhXjUzdqvZk/jELyPcu/cNqoHSQ4Ba
|
||||
us+jFxEb8m0yMMqSqCVrJw956BNtPgSiiVDUJGAiFOs9Qb5NgESE+vN6G/4P5QHt7NyXt0wAAAAASUVO
|
||||
RK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
|
||||
@@ -25,11 +25,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI bei (Dossier-Browser, später Analyse-Chat). -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -54,21 +54,22 @@ namespace PolyTrader.Modules.Supervisor
|
||||
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
|
||||
{
|
||||
Id = "supervisor.main",
|
||||
Title = "Supervisor",
|
||||
Group = "Supervisor",
|
||||
Order = 300,
|
||||
CreateForm = () =>
|
||||
{
|
||||
var form = new Ui.SupervisorMainForm();
|
||||
form.Initialize(services);
|
||||
return form;
|
||||
}
|
||||
});
|
||||
// Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
|
||||
}
|
||||
|
||||
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>
|
||||
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
|
||||
</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>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- Core/Module sind net8.0-windows (WinForms); damit deren Assemblies sauber laden. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user