- TradeAnalytics (Core, pur/testbar): KPIs (Netto-PnL/Winrate/Ø/Profit-Faktor), Equity-Kurve, PnL je Modul/Account/Tag, Window-Summary. Speist Dashboard + Launcher. 7 Tests. - Launcher dgv_accountlist: Spalten via Designer (Account, Module, Polymarket-Button, Wallet-USDC, 3T-PnL, 3T-Winrate, Overall P/L). Daten je Account aus dem Core-Trade-Log via TradeAnalytics; Auto-Refresh alle 30 s; Polymarket-Button oeffnet das Wallet-Profil. DB-Abfragen fehlertolerant. Build 0 Fehler, 331 Tests gruen, --smoke-ui ok (Launcher laedt Uebersicht). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
255 lines
10 KiB
C#
255 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();
|
||
|
||
// Fenster-Buttons an stabile View-IDs binden (Views werden per DI registriert).
|
||
_viewButtons = new Dictionary<string, ToolStripButton>
|
||
{
|
||
["core.dashboard"] = btn_dashboard,
|
||
["core.settings"] = btn_settings,
|
||
["core.terminal"] = btn_terminal,
|
||
["core.jobs"] = btn_jobs,
|
||
};
|
||
foreach (var (id, btn) in _viewButtons)
|
||
{
|
||
var viewId = id;
|
||
btn.Click += (_, _) => _uiHost.OpenView(viewId);
|
||
}
|
||
|
||
// Modul-Views dynamisch anhängen (der Launcher kennt keine Modulnamen; er bindet
|
||
// nur an registrierte View-IDs). Core-Views sind bereits fest verdrahtet.
|
||
var moduleViews = _uiHost.Views
|
||
.Where(v => !_viewButtons.ContainsKey(v.Id))
|
||
.OrderBy(v => v.Order)
|
||
.ToList();
|
||
if (moduleViews.Count > 0)
|
||
toolstrip_windows.Items.Add(new ToolStripSeparator());
|
||
foreach (var view in moduleViews)
|
||
{
|
||
var btn = new ToolStripButton(view.Title)
|
||
{
|
||
Name = "btn_" + view.Id,
|
||
Overflow = ToolStripItemOverflow.AsNeeded,
|
||
TextImageRelation = TextImageRelation.ImageAboveText,
|
||
AutoSize = true
|
||
};
|
||
var viewId = view.Id;
|
||
btn.Click += (_, _) => _uiHost.OpenView(viewId);
|
||
toolstrip_windows.Items.Add(btn);
|
||
_viewButtons[view.Id] = btn;
|
||
}
|
||
|
||
btn_liveTrading.Click += (_, _) => CycleLiveTrading();
|
||
btn_demoTrading.Click += (_, _) => CycleDemoTrading();
|
||
miBeenden.Click += (_, _) => Close();
|
||
|
||
// 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;
|
||
|
||
_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 alle 30 s aktualisieren (DB-Abfrage je Account – nicht jede Sekunde).
|
||
if (_statusTicks++ % 30 == 0) LoadAccountOverview();
|
||
}
|
||
|
||
// ===== 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; }
|
||
}
|
||
}
|
||
}
|