Files
PolyTraderSharp/Ui/LauncherForm.cs
T
RichardandClaude Opus 5 ad1593bfc1 P1a: PolyTrader.Core auf net8.0 - UI-Contract toolkit-neutral
Der Core traegt keine WinForms-/System.Drawing-Abhaengigkeit mehr und baut als
plattformneutrales net8.0 (verifiziert: publish -r linux-x64 erfolgreich).

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

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

442 Tests gruen.

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

269 lines
11 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.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Analytics;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// „Startleiste" der PolyTrader.App. Fenster werden über die im Designer platzierten
/// Buttons in <c>toolstrip_windows</c> geöffnet bzw. in den Vordergrund geholt.
/// <c>toolstrip_quickbar</c> ist für Schnellaktionen (z.B. Trading an/aus) reserviert,
/// <c>statusStrip_info</c> zeigt Kernkennzahlen. Der Launcher kennt selbst kein Modul —
/// Buttons werden per stabiler View-ID an registrierte Views gebunden.
/// </summary>
public partial class LauncherForm : Form
{
private readonly ShellUiHost _uiHost;
private readonly IServiceProvider _services;
private readonly TradingState _state;
private readonly System.Windows.Forms.Timer _statusTimer = new() { Interval = 1000 };
private readonly Dictionary<string, ToolStripButton> _viewButtons;
private int _statusTicks;
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
{
_uiHost = uiHost;
_services = services;
_state = services.GetRequiredService<TradingState>();
InitializeComponent();
_uiHost.SetMainWindow(this);
// Fenster-Buttons: ALLE statisch im Designer, an stabile View-IDs gebunden. Ist eine View
// nicht registriert (Modul fehlt), wird der Button deaktiviert nichts wird zur Laufzeit angehängt.
_viewButtons = new Dictionary<string, ToolStripButton>
{
["core.dashboard"] = btn_dashboard,
["core.settings"] = btn_settings,
["core.terminal"] = btn_terminal,
["core.jobs"] = btn_jobs,
["copytrading.main"] = btn_copytrading,
["resolutionfarming.main"] = btn_resolutionfarming,
["supervisor.main"] = btn_supervisor,
["accounting.main"] = btn_accounting,
};
var registered = _uiHost.Views.Select(v => v.Id).ToHashSet();
foreach (var (id, btn) in _viewButtons)
{
var viewId = id;
if (registered.Contains(viewId))
btn.Click += (_, _) => _uiHost.OpenView(viewId);
else
btn.Enabled = false; // Modul/View nicht verfügbar
}
// Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
// aktuelle Fenster → currentViewId null).
WindowMenu.Wire(menuStrip, _uiHost, null);
btn_liveTrading.Click += (_, _) => CycleLiveTrading();
btn_demoTrading.Click += (_, _) => CycleDemoTrading();
// Offen-Status der Fenster spiegeln (Button „checked", wenn Fenster offen).
_uiHost.OpenStateChanged += UpdateWindowButtonStates;
// Account-Übersicht (dgv_accountlist): Zahlenformate + Polymarket-Button.
colAccBalance.DefaultCellStyle.Format = "N2";
colAccPnl3d.DefaultCellStyle.Format = "N2";
colAccWin3d.DefaultCellStyle.Format = "N1";
colAccOverall.DefaultCellStyle.Format = "N2";
dgv_accountlist.CellContentClick += AccountList_CellContentClick;
// Live-Überblick-Widgets (Modul-PnL/Winrate, Warnungen/Fehler, auffällige Trades, Supervisor-KI).
launcherWidgets.Initialize(_services);
_statusTimer.Tick += (_, _) => UpdateStatus();
_statusTimer.Start();
UpdateStatus();
UpdateTradingToggles();
UpdateWindowButtonStates();
}
/// <summary>
/// Fängt das Schließen des Launchers ab: Statt direkt zu beenden, läuft auch das Schließen-X
/// über die Sicherheitsabfrage (siehe <see cref="ShellUiHost.RequestShutdown"/>). Erst wenn dort
/// bestätigt wurde (<see cref="ShellUiHost.ShutdownConfirmed"/>), darf das Fenster schließen.
/// </summary>
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!_uiHost.ShutdownConfirmed)
{
e.Cancel = true;
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
return;
}
base.OnFormClosing(e);
}
private void UpdateWindowButtonStates()
{
if (IsDisposed) return;
foreach (var (id, btn) in _viewButtons)
btn.Checked = _uiHost.IsOpen(id);
}
private void CycleLiveTrading()
{
_state.LiveTradingMode = _state.LiveTradingMode switch
{
TradingMode.Inactive => TradingMode.SellOnly,
TradingMode.SellOnly => TradingMode.Active,
_ => TradingMode.Inactive
};
UpdateTradingToggles();
}
private void CycleDemoTrading()
{
_state.DemoTradingMode = _state.DemoTradingMode switch
{
TradingMode.Inactive => TradingMode.SellOnly,
TradingMode.SellOnly => TradingMode.Active,
_ => TradingMode.Inactive
};
UpdateTradingToggles();
}
private void UpdateTradingToggles()
{
ApplyToggle(btn_liveTrading, "LiveTrading", _state.LiveTradingMode);
ApplyToggle(btn_demoTrading, "DemoTrading", _state.DemoTradingMode);
static void ApplyToggle(ToolStripButton btn, string label, TradingMode mode)
{
switch (mode)
{
case TradingMode.Active:
btn.Text = $"{label} (AKTIV)";
btn.BackColor = System.Drawing.Color.LightGreen;
break;
case TradingMode.SellOnly:
btn.Text = $"{label} (SELL-ONLY)";
btn.BackColor = System.Drawing.Color.Orange;
break;
default:
btn.Text = $"{label} (DEAKTIVIERT)";
btn.BackColor = System.Drawing.Color.IndianRed;
break;
}
}
}
private void UpdateStatus()
{
string trading = _state.GlobalTradingPaused
? "Pausiert"
: $"Live={_state.LiveTradingMode} / Demo={_state.DemoTradingMode}";
lbl_trading.Text = $"Trading: {trading}";
int moduleCount = _services.GetServices<IPolyTraderModule>().Count();
lbl_modules.Text = $"Module: {moduleCount}";
lbl_ratelimit.Text = $"API: {(_state.IsAlchemyHealthy ? "WSS aktiv" : "Polling")}";
UpdateTradingToggles();
UpdateWindowButtonStates();
// Account-Übersicht + Widgets alle 30 s aktualisieren (DB-Abfragen nicht jede Sekunde).
if (_statusTicks++ % 30 == 0)
{
LoadAccountOverview();
launcherWidgets.RefreshData();
}
}
// ===== Account-Übersicht (dgv_accountlist) =====
private void LoadAccountOverview()
{
if (IsDisposed) return;
var tradeLog = _services.GetService<ITradeLogRepository>();
if (tradeLog == null) return;
DateTime since3d = DateTime.UtcNow.AddDays(-3);
var rows = new List<AccountOverviewRow>();
foreach (var acc in _state.Accounts.Values.OrderBy(a => a.AccountId))
{
List<TradeRecord> trades;
try { trades = tradeLog.Find(t => t.AccountId == acc.AccountId); }
catch { trades = new List<TradeRecord>(); } // DB nicht bereit -> leer statt Absturz
var (pnl3d, win3d, _) = TradeAnalytics.WindowSummary(trades.Where(t => t.ClosedAt >= since3d));
string modules = trades
.Select(t => t.ModuleName)
.Where(m => !string.IsNullOrEmpty(m))
.Distinct().OrderBy(m => m)
.DefaultIfEmpty("—")
.Aggregate((a, b) => a + ", " + b);
rows.Add(new AccountOverviewRow
{
AccountId = acc.AccountId,
Name = (string.IsNullOrEmpty(acc.Name) ? $"#{acc.AccountId}" : acc.Name) + (acc.IsDemo ? " (Demo)" : ""),
Modules = modules,
WalletAddress = acc.WalletAddress,
Balance = acc.TotalBalance,
Pnl3d = pnl3d,
WinRate3d = win3d,
OverallPnl = trades.Sum(t => t.RealizedPnl)
});
}
dgv_accountlist.DataSource = rows;
}
private void AccountList_CellContentClick(object? sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dgv_accountlist.Columns[e.ColumnIndex].Name != "colAccPoly") return;
if (dgv_accountlist.Rows[e.RowIndex].DataBoundItem is AccountOverviewRow row)
OpenPolymarketProfile(row.WalletAddress);
}
private void OpenPolymarketProfile(string walletAddress)
{
if (string.IsNullOrWhiteSpace(walletAddress))
{
MessageBox.Show("Für diesen Account ist keine Wallet-Adresse hinterlegt.", "Polymarket",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
Process.Start(new ProcessStartInfo
{
FileName = $"https://polymarket.com/profile/{walletAddress}",
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show($"Konnte Polymarket nicht öffnen: {ex.Message}", "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>Anzeige-Zeile der Account-Übersicht (Bindung an dgv_accountlist über DataPropertyName).</summary>
private sealed class AccountOverviewRow
{
public int AccountId { get; set; }
public string Name { get; set; } = string.Empty;
public string Modules { get; set; } = string.Empty;
public string WalletAddress { get; set; } = string.Empty;
public decimal Balance { get; set; }
public decimal Pnl3d { get; set; }
public decimal WinRate3d { get; set; }
public decimal OverallPnl { get; set; }
}
}
}