WebUI: Module, Analytics und Buchhaltung als eigene Ansichten
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s

Das WebGUI konnte bisher Dashboard, Konten, Jobs, Logs und Einstellungen.
Die vier Strategie- und Analysemodule waren nur in der Avalonia-Oberflaeche
erreichbar - wer den Server aus der Ferne bediente, sah sie nicht.

Fuenf neue Endpunktgruppen holen sie herein:

  /api/analytics                  KPIs, Equity-Kurve, PnL je Modul und Tag,
                                  durchsuchbare Trade-Historie mit Seitenlauf
  /api/modules/copytrading        Master-Trader (CRUD), offene und
                                  geschlossene Trades, Risikolimits je Konto
  /api/modules/resolutionfarming  Scanner-Kandidaten, Positionen, Historie,
                                  Farming-Parameter je Konto
  /api/modules/supervisor         Profile, Agentenfragen, Berichtsarchiv,
                                  Signal-Dossiers, Counterfactuals
  /api/modules/accounting         BWA-Periodenabrechnung, Ledger-Journal,
                                  PDF- und CSV-Export, Ingest-Laeufe

Dazu im Bestand:
- Dashboard bekommt /overview-widgets: Modul-KPIs (heute/7T/30T), auffaellige
  Trades der letzten 24 Stunden, Warnungen und Fehler des Tages, den letzten
  Supervisor-Bericht.
- Die Kontenliste zeigt 3-Tage-PnL, Winrate, Gesamtergebnis und die
  beteiligten Module statt nur Salden.
- Jobs lassen sich ueber /toggle und /state ein- und ausschalten, nicht nur
  ausloesen.

Die Oberflaeche bleibt ohne Fremdbibliotheken: alle Diagramme sind Inline-SVG,
kein CDN, kein npm. Alle neuen Pfade liegen unter /api/ und damit hinter der
bestehenden Token-Pruefung.

Zwei Fehler nebenbei behoben:

- AccountingReportService.ResolveCurrency lief in eine NullReferenceException,
  wenn der FX-Dienst nicht gesetzt war. Betrifft jeden EUR-Aufruf ohne
  geladene EZB-Kurse.
- Ein Master-Trader, dessen Name ein Apostroph enthaelt, hatte einen toten
  Loeschen-Knopf: escapeHtml laesst ' unberuehrt, und im onclick-Attribut
  beendete das Zeichen das JavaScript-Literal vorzeitig. Laengerer Text mit
  Anfuehrungszeichen konnte darueber eigenen Code einschleusen. Neu setzt
  jsArg() den Wert als JSON-Literal ein; geprueft mit
  O'Brien "quote" <b>bold</b> \ backslash.

McpLightServerTests weicht jetzt aus, wenn HttpListener mangels URL-ACL nicht
binden darf, statt den Lauf scheitern zu lassen.

Geprueft auf der Linux-Test-VM (Ubuntu 26.04, .NET 10): Build ohne Fehler,
533 von 533 Tests bestanden, alle 11 Ansichten und 15 Unteransichten geladen,
alle Endpunkte mit 200 beantwortet, Schreibwege bis in die Datenbank
nachverfolgt, PDF- und CSV-Export gueltig, SSE-Logstrom live.

Offene Befunde aus der Abnahme liegen als #532 (Kopfleiste laeuft unter
1623 px ueber), #533 (EUR-PDF ohne EZB-Kurse liefert Nullbetraege) und #534
(Einstellungen melden Erfolg trotz gescheitertem Speichern) im Bugtracker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-10 09:17:49 +02:00
co-authored by Claude Opus 5
parent 78a2f85b94
commit dcc764dec1
27 changed files with 6493 additions and 39 deletions
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Analytics;
using PolyTrader.Web.Endpoints;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;
namespace PolyTrader.Tests.WebUi
{
public class DashboardAndJobsEndpointsTests
{
[Fact]
public void JobStatusRow_ToggleIsEnabled_WorksProperly()
{
var job = new JobStatusRow
{
JobName = "TestJob",
IsEnabled = true
};
job.IsEnabled = !job.IsEnabled;
Assert.False(job.IsEnabled);
job.IsEnabled = !job.IsEnabled;
Assert.True(job.IsEnabled);
}
[Fact]
public void AccountOverviewRow_CalculatesPerformanceCorrectly()
{
var trades = new List<TradeRecord>
{
new TradeRecord
{
AccountId = 1,
ModuleName = "CopyTrading",
MarketQuestion = "Test Market 1",
RealizedPnl = 50m,
ClosedAt = DateTime.UtcNow.AddDays(-1)
},
new TradeRecord
{
AccountId = 1,
ModuleName = "CopyTrading",
MarketQuestion = "Test Market 2",
RealizedPnl = -20m,
ClosedAt = DateTime.UtcNow.AddDays(-2)
},
new TradeRecord
{
AccountId = 1,
ModuleName = "ResolutionFarming",
MarketQuestion = "Old Market",
RealizedPnl = 100m,
ClosedAt = DateTime.UtcNow.AddDays(-10)
}
};
var since3d = DateTime.UtcNow.AddDays(-3);
var (pnl3d, win3d, count3d) = TradeAnalytics.WindowSummary(trades.Where(t => t.ClosedAt >= since3d));
Assert.Equal(30m, pnl3d);
Assert.Equal(50m, win3d); // 1 out of 2 trades won
Assert.Equal(2, count3d);
decimal overallPnl = trades.Sum(t => t.RealizedPnl);
Assert.Equal(130m, overallPnl);
string modules = trades
.Select(t => t.ModuleName)
.Where(m => !string.IsNullOrEmpty(m))
.Distinct().OrderBy(m => m)
.Aggregate((a, b) => a + ", " + b);
Assert.Equal("CopyTrading, ResolutionFarming", modules);
}
[Fact]
public void TradeAnalytics_ComputeKpis_HandlesModuleSet()
{
var trades = new List<TradeRecord>
{
new TradeRecord { ModuleName = "CopyTrading", RealizedPnl = 25m, ClosedAt = DateTime.UtcNow.AddDays(-2) },
new TradeRecord { ModuleName = "CopyTrading", RealizedPnl = -10m, ClosedAt = DateTime.UtcNow.AddDays(-1) },
new TradeRecord { ModuleName = "CopyTrading", RealizedPnl = 15m, ClosedAt = DateTime.UtcNow.Date }
};
var kpis = TradeAnalytics.ComputeKpis(trades);
Assert.Equal(3, kpis.TradeCount);
Assert.Equal(30m, kpis.NetPnl);
Assert.True(kpis.WinRatePct > 60m);
Assert.Equal(40m / 10m, kpis.ProfitFactor); // gross win 40 / gross loss 10 = 4.0
}
}
}