Files
PolyTraderSharp/Ui/LauncherForm.cs
T
RichardandClaude Opus 4.8 039bc240f8 UI Slice 5: Menueleiste (Fenster nebeneinander), sicheres Beenden, Modul-Aktivierung
- Fenster-Menue: WindowMenu fuellt die oberste MenuStrip mit Top-Level-Eintraegen
  nebeneinander (mit Icon) statt Untermenue "Fenster"; ModuleView.Icon zentral in der
  App zugewiesen (AssignMenuIcons). Kein miFenster mehr im Launcher-Designer.
- Sicheres Beenden: ShutdownConfirmDialog (10s-Timer sperrt "Jetzt beenden", Abbrechen
  jederzeit) via IModuleUiHost.RequestShutdown(). Nur der Launcher (Hauptprozess) bietet
  "Beenden"; andere Fenster nur "Fenster schliessen" (kein App-Shutdown, Module laufen
  weiter). Launcher-Schliessen-X routet ueber dieselbe Abfrage.
- Modul-Aktivierung (restart-basiert): ServerSettings.DisabledModules, in Program.Main
  vor der DI-Registrierung gefiltert -> deaktivierte Module werden nicht geladen.
  Dashboard-Tab "Module" zeigt Status + schaltet um (Hinweis: greift nach Neustart).
  Optionaler IPolyTraderModule.GetActivationBlocker fuer "nicht aktivierbar"-Info.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:46:09 +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).
PolyTrader.Core.Modularity.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; }
}
}
}