UI: Launcher-Account-Uebersicht + pure TradeAnalytics-Fundament

- 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>
This commit is contained in:
Richard
2026-07-15 09:59:10 +02:00
co-authored by Claude Opus 4.8
parent 0aedddabe8
commit 7d63791e38
4 changed files with 365 additions and 1 deletions
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using PolyTraderSharp.Models;
namespace PolyTrader.Core.Analytics
{
/// <summary>
/// Reine, seiteneffektfreie Auswertungslogik über den generischen Core-Trade-Log
/// (<see cref="TradeRecord"/>). Speist Dashboard-Kennzahlen/Charts und die Launcher-Account-Übersicht.
/// Die Filterung (Account/Modul/Demo/Zeitraum) trifft der Aufrufer; hier wird nur aggregiert.
/// Vollständig unit-getestet, weil „profitabel ja/nein" davon abhängt.
/// </summary>
public static class TradeAnalytics
{
/// <summary>Kernkennzahlen eines Trade-Sets (ohne Fees die liegen nur in den Modul-Logs).</summary>
public readonly record struct Kpis(
int TradeCount, decimal NetPnl, decimal WinRatePct, decimal AvgPnlPerTrade, decimal ProfitFactor);
/// <summary>Profit-Faktor bei verlustfreiem Set (∞) als großer, endlicher Anzeigewert.</summary>
public const decimal NoLossProfitFactor = 999m;
public static Kpis ComputeKpis(IEnumerable<TradeRecord> trades)
{
var list = trades as IReadOnlyList<TradeRecord> ?? trades.ToList();
int n = list.Count;
if (n == 0) return new Kpis(0, 0m, 0m, 0m, 0m);
decimal net = list.Sum(t => t.RealizedPnl);
int wins = list.Count(t => t.RealizedPnl > 0m);
decimal grossWin = list.Where(t => t.RealizedPnl > 0m).Sum(t => t.RealizedPnl);
decimal grossLoss = list.Where(t => t.RealizedPnl < 0m).Sum(t => -t.RealizedPnl);
decimal pf = grossLoss > 0m ? grossWin / grossLoss : (grossWin > 0m ? NoLossProfitFactor : 0m);
return new Kpis(n, net, 100m * wins / n, net / n, pf);
}
/// <summary>Equity-Kurve: nach Abschlusszeit sortiert, kumulierter realisierter PnL.</summary>
public static List<(DateTime At, decimal Cumulative)> EquityCurve(IEnumerable<TradeRecord> trades)
{
var result = new List<(DateTime, decimal)>();
decimal cum = 0m;
foreach (var t in trades.OrderBy(t => t.ClosedAt))
{
cum += t.RealizedPnl;
result.Add((t.ClosedAt, cum));
}
return result;
}
/// <summary>PnL + Anzahl je Gruppierungsschlüssel (z. B. Modul oder Account), absteigend nach PnL.</summary>
public static List<(string Key, decimal Pnl, int Count)> PnlByKey(
IEnumerable<TradeRecord> trades, Func<TradeRecord, string> keySelector)
{
return trades
.GroupBy(keySelector)
.Select(g => (Key: g.Key, Pnl: g.Sum(t => t.RealizedPnl), Count: g.Count()))
.OrderByDescending(x => x.Pnl)
.ToList();
}
/// <summary>PnL je Kalendertag (nach <see cref="TradeRecord.ClosedAt"/>), chronologisch.</summary>
public static List<(DateTime Day, decimal Pnl)> PnlByDay(IEnumerable<TradeRecord> trades)
{
return trades
.GroupBy(t => t.ClosedAt.Date)
.Select(g => (Day: g.Key, Pnl: g.Sum(t => t.RealizedPnl)))
.OrderBy(x => x.Day)
.ToList();
}
/// <summary>Kurz-Zusammenfassung für die Launcher-Übersicht (z. B. „letzte 3 Tage" je Account).</summary>
public static (decimal Pnl, decimal WinRatePct, int Count) WindowSummary(IEnumerable<TradeRecord> trades)
{
var list = trades as IReadOnlyList<TradeRecord> ?? trades.ToList();
int n = list.Count;
if (n == 0) return (0m, 0m, 0);
int wins = list.Count(t => t.RealizedPnl > 0m);
return (list.Sum(t => t.RealizedPnl), 100m * wins / n, n);
}
}
}