Modul-Fenster nach Avalonia - UI-Portierung inhaltlich abgeschlossen
Alle acht Fenster laufen jetzt unter Avalonia: Launcher, Dashboard, Settings, Terminal, Server Jobs, Copytrading, ResolutionFarming, Supervisor, Accounting (+ Shutdown- und Allzweck-Dialog). - Copytrading: vier Tabs (Master-Trader mit Account-Zuweisung, Offene Trades, Geschlossene Trades mit Filterleiste, Account-Einstellungen). Zeilenfaerbung nach PnL laeuft ueber DataGrid.LoadingRow - greift damit auch bei virtualisierten Zeilen, anders als das fruehere Faerben in DataBindingComplete. - ResolutionFarming: Kandidaten, Positionen, Historie, Settings - je Konto. - Supervisor: Analyse-Chat gegen den Agenten (Tool-Fortschritt in der Statuszeile), Dossiers mit Markdown-Ansicht, Berichte, Counterfactuals. - Accounting: KPI-Kacheln, Monats-BWA, Ledger, Abruf/Status, PDF- und CSV-Export ueber den plattformneutralen Datei-Dialog. Der SettingsEditor traegt wie erwartet die drei restlichen PropertyGrid-Stellen (Master-Trader, Account-Einstellungen, ResolutionFarming) mit je einem Aufruf. ENTSCHEIDUNG - Modul-Fenster liegen in der App, nicht in den Modulen (begruendet in Views/Modules/README.md): Die App ist der Kompositionswurzel und referenziert ohnehin alle Module. So bleiben die Modulprojekte FREI VON AVALONIA, was fuer den kopflosen Linux-Betrieb den Ausschlag gibt - der Daemon soll keine GUI-Bibliothek mitschleppen. Verifiziert: kein Modul zieht Avalonia. Registrierung in Shell/ModuleViews.cs, und zwar nur fuer tatsaechlich geladene Module - ein per DisabledModules abgeschaltetes Modul bekommt gar kein Fenster. Nebenbei: eigene View-Model-Typen CopyOpenTradeRow/CopyClosedTradeRow statt des Modul-Modells ClosedTradeRow - sie tragen den aufgeloesten Master-Trader-Namen und die Zeilenfarbe, die es dort nicht gibt (und vermeiden die Namensmehrdeutigkeit). Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (alle 8 Fenster + Launcher + Dialog + Editor-Pruefung), App laeuft real mit allen Trading-Diensten, Linux-Publish laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ namespace PolyTrader.App.Avalonia
|
|||||||
CoreViews.Register(uiHost, Services);
|
CoreViews.Register(uiHost, Services);
|
||||||
foreach (var module in Services.GetServices<Core.Modularity.IPolyTraderModule>())
|
foreach (var module in Services.GetServices<Core.Modularity.IPolyTraderModule>())
|
||||||
module.RegisterUi(uiHost, Services);
|
module.RegisterUi(uiHost, Services);
|
||||||
|
ModuleViews.Register(uiHost, Services);
|
||||||
ViewIcons.AssignDefaults(uiHost);
|
ViewIcons.AssignDefaults(uiHost);
|
||||||
|
|
||||||
var launcher = new LauncherWindow(uiHost, Services);
|
var launcher = new LauncherWindow(uiHost, Services);
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ namespace PolyTrader.App.Avalonia
|
|||||||
CoreViews.Register(uiHost, host.Services);
|
CoreViews.Register(uiHost, host.Services);
|
||||||
foreach (var module in host.Services.GetServices<IPolyTraderModule>())
|
foreach (var module in host.Services.GetServices<IPolyTraderModule>())
|
||||||
module.RegisterUi(uiHost, host.Services);
|
module.RegisterUi(uiHost, host.Services);
|
||||||
|
ModuleViews.Register(uiHost, host.Services);
|
||||||
ViewIcons.AssignDefaults(uiHost);
|
ViewIcons.AssignDefaults(uiHost);
|
||||||
|
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
|
|||||||
@@ -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,78 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using PolyTrader.Modules.CopyTrading.Logic;
|
||||||
|
|
||||||
|
namespace PolyTrader.App.Avalonia.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Avalonia-Farbpalette zu den <see cref="TradeRowTint"/>-Kategorien. Die Schwellenlogik liegt
|
||||||
|
/// (getestet) in <see cref="TradeRowColoring"/>; hier steht nur, wie die Kategorien aussehen.
|
||||||
|
/// Werte unverändert aus der WinForms-Oberfläche übernommen.
|
||||||
|
/// </summary>
|
||||||
|
public static class TradeRowPalette
|
||||||
|
{
|
||||||
|
public static readonly IBrush Loss = new SolidColorBrush(Color.FromRgb(245, 200, 200)); // rot
|
||||||
|
public static readonly IBrush SmallWin = new SolidColorBrush(Color.FromRgb(212, 240, 212)); // hellgrün
|
||||||
|
public static readonly IBrush BigWin = new SolidColorBrush(Color.FromRgb(140, 214, 140)); // grün
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace PolyTrader.App.Avalonia.ViewModels
|
namespace PolyTrader.App.Avalonia.ViewModels
|
||||||
{
|
{
|
||||||
@@ -33,4 +33,20 @@ namespace PolyTrader.App.Avalonia.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanToggle { get; init; } = true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace PolyTrader.App.Avalonia.ViewModels
|
namespace PolyTrader.App.Avalonia.ViewModels
|
||||||
{
|
{
|
||||||
@@ -20,4 +20,12 @@ namespace PolyTrader.App.Avalonia.ViewModels
|
|||||||
public string Cid { get; init; } = string.Empty;
|
public string Cid { get; init; } = string.Empty;
|
||||||
public string Message { get; init; } = string.Empty;
|
public string Message { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ein Eintrag im Supervisor-Analyseverlauf (Frage oder Antwort).</summary>
|
||||||
|
public sealed class ChatEntry
|
||||||
|
{
|
||||||
|
public string Header { get; init; } = string.Empty;
|
||||||
|
public string Text { get; init; } = string.Empty;
|
||||||
|
public IBrush HeaderBrush { get; init; } = Brushes.SteelBlue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="#777777" 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="#777777" />
|
||||||
|
</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="#DDDDDD" />
|
||||||
|
|
||||||
|
<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="#777777" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<controls:SettingsEditor Name="editorAccount" />
|
||||||
|
</DockPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
</TabControl>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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="#777777" />
|
||||||
|
</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="#777777" />
|
||||||
|
</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="#777777" />
|
||||||
|
</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="#DDDDDD" />
|
||||||
|
|
||||||
|
<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,192 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Media;
|
||||||
|
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, Brushes.SteelBlue);
|
||||||
|
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, Brushes.SeaGreen);
|
||||||
|
status.Text = $"{result.ToolInvocations.Count} Tool-Aufruf(e) · " +
|
||||||
|
$"{result.PromptTokens + result.CompletionTokens} Token.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddChat("Fehler", ex.Message, Brushes.Firebrick);
|
||||||
|
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, IBrush brush)
|
||||||
|
{
|
||||||
|
_chat.Add(new ChatEntry { Header = $"[{DateTime.Now:HH:mm:ss}] {who}", Text = text, HeaderBrush = brush });
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user