- 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>
269 lines
11 KiB
C#
269 lines
11 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
|
||
}
|
||
|
||
// 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; }
|
||
}
|
||
}
|
||
}
|