WebUI: Module, Analytics und Buchhaltung als eigene Ansichten
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:
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PolyTrader.Core.Analytics;
|
||||
using PolyTraderSharp.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests.WebUi
|
||||
{
|
||||
public class AnalyticsEndpointsTests
|
||||
{
|
||||
private List<TradeRecord> CreateSampleTrades()
|
||||
{
|
||||
return new List<TradeRecord>
|
||||
{
|
||||
new TradeRecord
|
||||
{
|
||||
Id = "trade-1",
|
||||
AccountId = 1,
|
||||
ModuleName = "CopyTrading",
|
||||
MarketQuestion = "Will Bitcoin hit 100k in 2026?",
|
||||
Outcome = "Yes",
|
||||
Side = "BUY",
|
||||
IsDemo = false,
|
||||
EntryPrice = 0.40m,
|
||||
ExitPrice = 0.80m,
|
||||
Size = 100m,
|
||||
RealizedPnl = 40m,
|
||||
PnlPercent = 100m,
|
||||
ClosedAt = DateTime.UtcNow.AddDays(-2)
|
||||
},
|
||||
new TradeRecord
|
||||
{
|
||||
Id = "trade-2",
|
||||
AccountId = 1,
|
||||
ModuleName = "CopyTrading",
|
||||
MarketQuestion = "Will Ethereum hit 5k in 2026?",
|
||||
Outcome = "No",
|
||||
Side = "BUY",
|
||||
IsDemo = false,
|
||||
EntryPrice = 0.50m,
|
||||
ExitPrice = 0.20m,
|
||||
Size = 50m,
|
||||
RealizedPnl = -15m,
|
||||
PnlPercent = -60m,
|
||||
ClosedAt = DateTime.UtcNow.AddDays(-1)
|
||||
},
|
||||
new TradeRecord
|
||||
{
|
||||
Id = "trade-3",
|
||||
AccountId = 2,
|
||||
ModuleName = "ResolutionFarming",
|
||||
MarketQuestion = "Fed interest rate cut in Q1?",
|
||||
Outcome = "Yes",
|
||||
Side = "BUY",
|
||||
IsDemo = true,
|
||||
EntryPrice = 0.90m,
|
||||
ExitPrice = 1.00m,
|
||||
Size = 200m,
|
||||
RealizedPnl = 20m,
|
||||
PnlPercent = 11.1m,
|
||||
ClosedAt = DateTime.UtcNow.AddDays(-5)
|
||||
},
|
||||
new TradeRecord
|
||||
{
|
||||
Id = "trade-4",
|
||||
AccountId = 2,
|
||||
ModuleName = "ResolutionFarming",
|
||||
MarketQuestion = "US GDP growth above 2%?",
|
||||
Outcome = "Yes",
|
||||
Side = "BUY",
|
||||
IsDemo = false,
|
||||
EntryPrice = 0.92m,
|
||||
ExitPrice = 1.00m,
|
||||
Size = 100m,
|
||||
RealizedPnl = 8m,
|
||||
PnlPercent = 8.7m,
|
||||
ClosedAt = DateTime.UtcNow.AddDays(-15)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_FilterByAccount_ReturnsOnlyMatchingTrades()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
int filterAccId = 1;
|
||||
|
||||
var filtered = trades.Where(t => t.AccountId == filterAccId).ToList();
|
||||
|
||||
Assert.Equal(2, filtered.Count);
|
||||
Assert.All(filtered, t => Assert.Equal(1, t.AccountId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_FilterByModule_ReturnsOnlyMatchingModuleTrades()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
string selectedModule = "ResolutionFarming";
|
||||
|
||||
var filtered = trades.Where(t => string.Equals(t.ModuleName, selectedModule, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
Assert.Equal(2, filtered.Count);
|
||||
Assert.All(filtered, t => Assert.Equal("ResolutionFarming", t.ModuleName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_FilterByMode_SeparatesLiveAndDemo()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
|
||||
var liveTrades = trades.Where(t => !t.IsDemo).ToList();
|
||||
var demoTrades = trades.Where(t => t.IsDemo).ToList();
|
||||
|
||||
Assert.Equal(3, liveTrades.Count);
|
||||
var singleDemo = Assert.Single(demoTrades);
|
||||
Assert.Equal("trade-3", singleDemo.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_FilterByDays_AppliesTimeWindow()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
var since7d = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
var recentTrades = trades.Where(t => t.ClosedAt >= since7d).ToList();
|
||||
|
||||
Assert.Equal(3, recentTrades.Count);
|
||||
Assert.DoesNotContain(recentTrades, t => t.Id == "trade-4"); // Id trade-4 is 15 days ago
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_SearchFilter_MatchesMarketQuestionOrOutcome()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
string query = "Bitcoin";
|
||||
|
||||
var matched = trades.Where(t =>
|
||||
(t.MarketQuestion != null && t.MarketQuestion.Contains(query, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(t.Outcome != null && t.Outcome.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
).ToList();
|
||||
|
||||
var match = Assert.Single(matched);
|
||||
Assert.Equal("trade-1", match.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_WinLossFilter_SeparatesProfitableAndLosingTrades()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
|
||||
var winners = trades.Where(t => t.RealizedPnl > 0).ToList();
|
||||
var losers = trades.Where(t => t.RealizedPnl < 0).ToList();
|
||||
|
||||
Assert.Equal(3, winners.Count);
|
||||
var singleLoser = Assert.Single(losers);
|
||||
Assert.Equal("trade-2", singleLoser.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_ComputeKpis_ReturnsAccurateMetrics()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
var kpis = TradeAnalytics.ComputeKpis(trades);
|
||||
|
||||
Assert.Equal(4, kpis.TradeCount);
|
||||
Assert.Equal(53m, kpis.NetPnl); // 40 - 15 + 20 + 8 = 53
|
||||
Assert.Equal(75m, kpis.WinRatePct); // 3 wins out of 4 = 75%
|
||||
Assert.Equal(13.25m, kpis.AvgPnlPerTrade); // 53 / 4 = 13.25
|
||||
Assert.True(kpis.ProfitFactor > 4.5m); // Gross wins: 68, Gross loss: 15 -> 4.53
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analytics_Charts_CalculatesSeriesProperly()
|
||||
{
|
||||
var trades = CreateSampleTrades();
|
||||
|
||||
var equityCurve = TradeAnalytics.EquityCurve(trades).ToList();
|
||||
Assert.NotEmpty(equityCurve);
|
||||
Assert.Equal(53m, equityCurve.Last().Cumulative);
|
||||
|
||||
var pnlByModule = TradeAnalytics.PnlByKey(trades, t => t.ModuleName).ToList();
|
||||
Assert.Equal(2, pnlByModule.Count);
|
||||
var copyTradingPnl = pnlByModule.First(m => m.Key == "CopyTrading");
|
||||
Assert.Equal(25m, copyTradingPnl.Pnl); // 40 - 15 = 25
|
||||
var resFarmingPnl = pnlByModule.First(m => m.Key == "ResolutionFarming");
|
||||
Assert.Equal(28m, resFarmingPnl.Pnl); // 20 + 8 = 28
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user