Files
PolyTraderSharp/src/PolyTrader.App.Avalonia/Views/Modules/CopyTradingWindow.axaml.cs
T
RichardandClaude Opus 5 dd8da3fd3f Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell
Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt,
welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen.

Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json
ueberschneiden und darum gemeinsam abgelegt werden:

Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig):
- Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert
- DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli
- WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt
  (version, os, checks, metrics, status stopped)
- Security: MasterKeyResolver, SecretRedactor, FilePermissions
- Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin)
- deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben
  ueber .gitignore aussen vor
- setup.json fuer die Erstinstallation
- UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md
  verweist auf den neuen Plan

Einfenster-Shell (UI-Redesign):
- ShellWindow + ShellNavModel als Seitenleisten-Shell
- WindowMenuBar und WindowMenuModel entfallen
- Modul- und Kernfenster auf die Shell-Einbettung angepasst

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:42:22 +02:00

362 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 : UserControl
{
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()
{
_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>("btnTraderNew")!.Click += (_, _) => AddNewTrader();
this.FindControl<Button>("btnTraderSave")!.Click += (_, _) => SaveSelectedTrader();
this.FindControl<Button>("btnTraderDelete")!.Click += (_, _) => DeleteSelectedTrader();
// --- 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;
}
// Kein Abmelden: Seiten leben in der Einfenster-Shell bis zum Programmende
// (siehe AvaloniaUiHost), ein Gegenstueck zu Window.Closed gibt es nicht.
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}.";
}
private void AddNewTrader()
{
int nextId = _traders.Count > 0 ? _traders.Max(t => t.Id) + 1 : 1;
var trader = new TrackedTrader { Id = nextId, DisplayName = $"Neuer Trader {nextId}" };
_traders.Add(trader);
var grid = this.FindControl<DataGrid>("gridTraders")!;
grid.SelectedItem = trader;
this.FindControl<TextBlock>("lblTraderHint")!.Text = $"Neuer Master-Trader #{nextId} – Felder ausfüllen und Speichern.";
}
private async void DeleteSelectedTrader()
{
var grid = this.FindControl<DataGrid>("gridTraders")!;
if (grid.SelectedItem is not TrackedTrader trader)
{
this.FindControl<TextBlock>("lblTraderHint")!.Text = "Kein Trader ausgewählt.";
return;
}
int id = trader.Id;
bool confirm = await DialogWindow.Confirm(this, "Löschen bestätigen",
$"Master-Trader #{id} ({trader.DisplayName}) wirklich löschen?");
if (!confirm) return;
_traderRepo.Delete(id);
_copyState.Traders.TryRemove(id, out _);
_traders.Remove(trader);
ShowSelectedTrader();
this.FindControl<TextBlock>("lblTraderHint")!.Text = $"Master-Trader #{id} gelöscht.";
}
// ===== 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;
}
}
}