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:
co-authored by
Claude Opus 4.8
parent
0aedddabe8
commit
7d63791e38
Generated
+76
-1
@@ -33,6 +33,13 @@ namespace PolyTraderSharp.Ui
|
|||||||
lbl_modules = new ToolStripStatusLabel();
|
lbl_modules = new ToolStripStatusLabel();
|
||||||
lbl_ratelimit = new ToolStripStatusLabel();
|
lbl_ratelimit = new ToolStripStatusLabel();
|
||||||
dgv_accountlist = new DataGridView();
|
dgv_accountlist = new DataGridView();
|
||||||
|
colAccName = new DataGridViewTextBoxColumn();
|
||||||
|
colAccModules = new DataGridViewTextBoxColumn();
|
||||||
|
colAccPoly = new DataGridViewButtonColumn();
|
||||||
|
colAccBalance = new DataGridViewTextBoxColumn();
|
||||||
|
colAccPnl3d = new DataGridViewTextBoxColumn();
|
||||||
|
colAccWin3d = new DataGridViewTextBoxColumn();
|
||||||
|
colAccOverall = new DataGridViewTextBoxColumn();
|
||||||
btn_accounting = new ToolStripButton();
|
btn_accounting = new ToolStripButton();
|
||||||
toolStripSeparator1 = new ToolStripSeparator();
|
toolStripSeparator1 = new ToolStripSeparator();
|
||||||
menuStrip.SuspendLayout();
|
menuStrip.SuspendLayout();
|
||||||
@@ -172,15 +179,76 @@ namespace PolyTraderSharp.Ui
|
|||||||
//
|
//
|
||||||
dgv_accountlist.AllowUserToAddRows = false;
|
dgv_accountlist.AllowUserToAddRows = false;
|
||||||
dgv_accountlist.AllowUserToDeleteRows = false;
|
dgv_accountlist.AllowUserToDeleteRows = false;
|
||||||
|
dgv_accountlist.AutoGenerateColumns = false;
|
||||||
dgv_accountlist.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
dgv_accountlist.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
|
dgv_accountlist.Columns.AddRange(new DataGridViewColumn[] {
|
||||||
|
colAccName, colAccModules, colAccPoly, colAccBalance, colAccPnl3d, colAccWin3d, colAccOverall });
|
||||||
dgv_accountlist.Dock = DockStyle.Fill;
|
dgv_accountlist.Dock = DockStyle.Fill;
|
||||||
dgv_accountlist.Location = new Point(0, 139);
|
dgv_accountlist.Location = new Point(0, 139);
|
||||||
dgv_accountlist.Name = "dgv_accountlist";
|
dgv_accountlist.Name = "dgv_accountlist";
|
||||||
dgv_accountlist.ReadOnly = true;
|
dgv_accountlist.ReadOnly = true;
|
||||||
|
dgv_accountlist.RowHeadersVisible = false;
|
||||||
dgv_accountlist.RowHeadersWidth = 62;
|
dgv_accountlist.RowHeadersWidth = 62;
|
||||||
|
dgv_accountlist.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||||
dgv_accountlist.Size = new Size(2599, 498);
|
dgv_accountlist.Size = new Size(2599, 498);
|
||||||
dgv_accountlist.TabIndex = 6;
|
dgv_accountlist.TabIndex = 6;
|
||||||
//
|
//
|
||||||
|
// colAccName
|
||||||
|
//
|
||||||
|
colAccName.DataPropertyName = "Name";
|
||||||
|
colAccName.HeaderText = "Account";
|
||||||
|
colAccName.Name = "colAccName";
|
||||||
|
colAccName.ReadOnly = true;
|
||||||
|
colAccName.Width = 200;
|
||||||
|
//
|
||||||
|
// colAccModules
|
||||||
|
//
|
||||||
|
colAccModules.DataPropertyName = "Modules";
|
||||||
|
colAccModules.HeaderText = "Module";
|
||||||
|
colAccModules.Name = "colAccModules";
|
||||||
|
colAccModules.ReadOnly = true;
|
||||||
|
colAccModules.Width = 220;
|
||||||
|
//
|
||||||
|
// colAccPoly
|
||||||
|
//
|
||||||
|
colAccPoly.HeaderText = "Polymarket";
|
||||||
|
colAccPoly.Name = "colAccPoly";
|
||||||
|
colAccPoly.Text = "Öffnen";
|
||||||
|
colAccPoly.UseColumnTextForButtonValue = true;
|
||||||
|
colAccPoly.Width = 100;
|
||||||
|
//
|
||||||
|
// colAccBalance
|
||||||
|
//
|
||||||
|
colAccBalance.DataPropertyName = "Balance";
|
||||||
|
colAccBalance.HeaderText = "Wallet (USDC)";
|
||||||
|
colAccBalance.Name = "colAccBalance";
|
||||||
|
colAccBalance.ReadOnly = true;
|
||||||
|
colAccBalance.Width = 130;
|
||||||
|
//
|
||||||
|
// colAccPnl3d
|
||||||
|
//
|
||||||
|
colAccPnl3d.DataPropertyName = "Pnl3d";
|
||||||
|
colAccPnl3d.HeaderText = "3T PnL";
|
||||||
|
colAccPnl3d.Name = "colAccPnl3d";
|
||||||
|
colAccPnl3d.ReadOnly = true;
|
||||||
|
colAccPnl3d.Width = 110;
|
||||||
|
//
|
||||||
|
// colAccWin3d
|
||||||
|
//
|
||||||
|
colAccWin3d.DataPropertyName = "WinRate3d";
|
||||||
|
colAccWin3d.HeaderText = "3T Winrate %";
|
||||||
|
colAccWin3d.Name = "colAccWin3d";
|
||||||
|
colAccWin3d.ReadOnly = true;
|
||||||
|
colAccWin3d.Width = 110;
|
||||||
|
//
|
||||||
|
// colAccOverall
|
||||||
|
//
|
||||||
|
colAccOverall.DataPropertyName = "OverallPnl";
|
||||||
|
colAccOverall.HeaderText = "Overall P/L";
|
||||||
|
colAccOverall.Name = "colAccOverall";
|
||||||
|
colAccOverall.ReadOnly = true;
|
||||||
|
colAccOverall.Width = 130;
|
||||||
|
//
|
||||||
// btn_accounting
|
// btn_accounting
|
||||||
//
|
//
|
||||||
btn_accounting.Image = Properties.Resources.coins_in_hand;
|
btn_accounting.Image = Properties.Resources.coins_in_hand;
|
||||||
@@ -245,5 +313,12 @@ namespace PolyTraderSharp.Ui
|
|||||||
private ToolStripButton btn_accounting;
|
private ToolStripButton btn_accounting;
|
||||||
private ToolStripSeparator toolStripSeparator1;
|
private ToolStripSeparator toolStripSeparator1;
|
||||||
private DataGridView dgv_accountlist;
|
private DataGridView dgv_accountlist;
|
||||||
|
private DataGridViewTextBoxColumn colAccName;
|
||||||
|
private DataGridViewTextBoxColumn colAccModules;
|
||||||
|
private DataGridViewButtonColumn colAccPoly;
|
||||||
|
private DataGridViewTextBoxColumn colAccBalance;
|
||||||
|
private DataGridViewTextBoxColumn colAccPnl3d;
|
||||||
|
private DataGridViewTextBoxColumn colAccWin3d;
|
||||||
|
private DataGridViewTextBoxColumn colAccOverall;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTrader.Core.Analytics;
|
||||||
using PolyTrader.Core.Modularity;
|
using PolyTrader.Core.Modularity;
|
||||||
|
using PolyTrader.Core.Persistence;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
namespace PolyTraderSharp.Ui
|
namespace PolyTraderSharp.Ui
|
||||||
{
|
{
|
||||||
@@ -21,6 +25,7 @@ namespace PolyTraderSharp.Ui
|
|||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
private readonly System.Windows.Forms.Timer _statusTimer = new() { Interval = 1000 };
|
private readonly System.Windows.Forms.Timer _statusTimer = new() { Interval = 1000 };
|
||||||
private readonly Dictionary<string, ToolStripButton> _viewButtons;
|
private readonly Dictionary<string, ToolStripButton> _viewButtons;
|
||||||
|
private int _statusTicks;
|
||||||
|
|
||||||
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
|
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
|
||||||
{
|
{
|
||||||
@@ -74,6 +79,13 @@ namespace PolyTraderSharp.Ui
|
|||||||
// Offen-Status der Fenster spiegeln (Button „checked", wenn Fenster offen).
|
// Offen-Status der Fenster spiegeln (Button „checked", wenn Fenster offen).
|
||||||
_uiHost.OpenStateChanged += UpdateWindowButtonStates;
|
_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.Tick += (_, _) => UpdateStatus();
|
||||||
_statusTimer.Start();
|
_statusTimer.Start();
|
||||||
UpdateStatus();
|
UpdateStatus();
|
||||||
@@ -149,6 +161,94 @@ namespace PolyTraderSharp.Ui
|
|||||||
|
|
||||||
UpdateTradingToggles();
|
UpdateTradingToggles();
|
||||||
UpdateWindowButtonStates();
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using PolyTrader.Core.Analytics;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>Sicherheitsnetz für die Dashboard-/Launcher-Auswertungen (KPIs, Equity-Kurve, Gruppierungen).</summary>
|
||||||
|
public class TradeAnalyticsTests
|
||||||
|
{
|
||||||
|
private static TradeRecord Rec(decimal pnl, DateTime closedAt, string module = "M", int account = 1) =>
|
||||||
|
new() { RealizedPnl = pnl, ClosedAt = closedAt, ModuleName = module, AccountId = account };
|
||||||
|
|
||||||
|
private static readonly DateTime T0 = new(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeKpis_basic_set()
|
||||||
|
{
|
||||||
|
var k = TradeAnalytics.ComputeKpis(new[]
|
||||||
|
{
|
||||||
|
Rec(10m, T0), Rec(-4m, T0), Rec(6m, T0), Rec(-2m, T0)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(4, k.TradeCount);
|
||||||
|
Assert.Equal(10m, k.NetPnl);
|
||||||
|
Assert.Equal(50m, k.WinRatePct);
|
||||||
|
Assert.Equal(2.5m, k.AvgPnlPerTrade);
|
||||||
|
Assert.Equal(2.6667m, Math.Round(k.ProfitFactor, 4)); // 16 / 6
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeKpis_no_losses_uses_sentinel_profitfactor()
|
||||||
|
{
|
||||||
|
var k = TradeAnalytics.ComputeKpis(new[] { Rec(5m, T0), Rec(3m, T0) });
|
||||||
|
Assert.Equal(100m, k.WinRatePct);
|
||||||
|
Assert.Equal(TradeAnalytics.NoLossProfitFactor, k.ProfitFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeKpis_empty_is_zero()
|
||||||
|
{
|
||||||
|
var k = TradeAnalytics.ComputeKpis(Array.Empty<TradeRecord>());
|
||||||
|
Assert.Equal(0, k.TradeCount);
|
||||||
|
Assert.Equal(0m, k.NetPnl);
|
||||||
|
Assert.Equal(0m, k.ProfitFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EquityCurve_is_cumulative_and_time_ordered()
|
||||||
|
{
|
||||||
|
var curve = TradeAnalytics.EquityCurve(new[]
|
||||||
|
{
|
||||||
|
Rec(5m, T0.AddMinutes(2)),
|
||||||
|
Rec(10m, T0.AddMinutes(1)),
|
||||||
|
Rec(-3m, T0.AddMinutes(3))
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(3, curve.Count);
|
||||||
|
Assert.Equal(10m, curve[0].Cumulative); // frühester zuerst
|
||||||
|
Assert.Equal(15m, curve[1].Cumulative);
|
||||||
|
Assert.Equal(12m, curve[2].Cumulative);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PnlByKey_groups_and_orders_desc()
|
||||||
|
{
|
||||||
|
var byModule = TradeAnalytics.PnlByKey(new[]
|
||||||
|
{
|
||||||
|
Rec(10m, T0, module: "A"), Rec(-2m, T0, module: "A"), Rec(5m, T0, module: "B")
|
||||||
|
}, t => t.ModuleName);
|
||||||
|
|
||||||
|
Assert.Equal(2, byModule.Count);
|
||||||
|
Assert.Equal("A", byModule[0].Key); // 8 vor 5
|
||||||
|
Assert.Equal(8m, byModule[0].Pnl);
|
||||||
|
Assert.Equal(2, byModule[0].Count);
|
||||||
|
Assert.Equal("B", byModule[1].Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PnlByDay_groups_by_calendar_day()
|
||||||
|
{
|
||||||
|
var byDay = TradeAnalytics.PnlByDay(new[]
|
||||||
|
{
|
||||||
|
Rec(4m, T0), Rec(6m, T0.AddHours(2)), Rec(-1m, T0.AddDays(1))
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(2, byDay.Count);
|
||||||
|
Assert.Equal(10m, byDay[0].Pnl); // Tag 1: 4+6
|
||||||
|
Assert.Equal(-1m, byDay[1].Pnl); // Tag 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WindowSummary_pnl_winrate_count()
|
||||||
|
{
|
||||||
|
var (pnl, winRate, count) = TradeAnalytics.WindowSummary(new[]
|
||||||
|
{
|
||||||
|
Rec(3m, T0), Rec(-1m, T0), Rec(2m, T0), Rec(-4m, T0)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(0m, pnl);
|
||||||
|
Assert.Equal(50m, winRate);
|
||||||
|
Assert.Equal(4, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user