Files
PolyTraderSharp/Ui/LauncherForm.cs
T
RichardandClaude Opus 4.8 3c8ea3534e UI Slice 4: Launcher-Live-Widgets + VS-Re-Serialisierungs-Regressionen behoben
Slice 4 - Launcher-Live-Ueberblick (LauncherWidgetsPanel, isoliertes UserControl, rechts angedockt,
30s-Refresh; minimaler Eingriff ins von Richard bearbeitete Launcher-Designer):
- Modul-PnL/Winrate-Kacheln (je Modul + Gesamt: Heute/7T/30T, gruen/rot) via TradeAnalytics.
- Supervisor-KI-Kurzfassung (letzter sup_report).
- Warnungen & Fehler (heutige JSONL-Logs, Error/Warning).
- Auffaellige Trades (24h, nach |PnL| sortiert).

Regressionen aus VS-Re-Serialisierung behoben (VS liess hand-erstellte DataGridView-Spalten fallen
-> col* null -> NRE beim Oeffnen):
- DashboardView (dgvTrades): Spalten-Instanziierung + AutoGenerateColumns=false + Columns.AddRange
  + Spalten-Konfig wiederhergestellt.
- JobsView (dgvJobs): dito (nur Button-Spalte hatte ueberlebt).
- Smoke-UI dauerhaft um JobsView/TerminalView/SettingsView erweitert -> faengt diese Regressionsklasse
  kuenftig ab.

Enthaelt ausserdem Richards zwischenzeitliche UI-Arbeit (Launcher-Icons cross_reference/emotion_batman/
file_start_workflow, Designer-Re-Serialisierungen, .ico-Sammlung, Modul-Form-.resx). Persoenliche
Notizdatei bewusst NICHT committet.

Build 0 Fehler, 396 Tests gruen, --smoke-ui alle 9 Views/Forms gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:31:59 +02:00

252 lines
10 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
}
// Gemeinsames „Fenster"-Menü (Launcher ist das aktuelle Fenster → currentViewId null).
PolyTrader.Core.Modularity.WindowMenu.Wire(miFenster, _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();
}
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; }
}
}
}