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>
252 lines
10 KiB
C#
252 lines
10 KiB
C#
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; }
|
||
}
|
||
}
|
||
}
|