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:
@@ -1,9 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PolyTrader.Core.Analytics;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTrader.Modules.Supervisor.Persistence;
|
||||
using PolyTraderSharp;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Web.Endpoints
|
||||
@@ -71,6 +78,177 @@ namespace PolyTrader.Web.Endpoints
|
||||
return Results.BadRequest(new { error = "Ungültiger Modus oder Parametertyp." });
|
||||
});
|
||||
|
||||
group.MapGet("/overview-widgets", (IServiceProvider sp) =>
|
||||
{
|
||||
var tradeLog = sp.GetService<ITradeLogRepository>();
|
||||
DateTime now = DateTime.UtcNow;
|
||||
DateTime today = now.Date;
|
||||
DateTime week = now.AddDays(-7);
|
||||
DateTime month = now.AddDays(-30);
|
||||
|
||||
// 1. Modul-KPIs
|
||||
List<TradeRecord> monthTrades;
|
||||
try { monthTrades = tradeLog?.Find(t => t.ClosedAt >= month) ?? new List<TradeRecord>(); }
|
||||
catch { monthTrades = new List<TradeRecord>(); }
|
||||
|
||||
var moduleNames = monthTrades.Select(t => t.ModuleName).Where(m => !string.IsNullOrEmpty(m)).Distinct().OrderBy(m => m).ToList();
|
||||
var kpiTiles = new List<object>();
|
||||
|
||||
foreach (var module in moduleNames)
|
||||
{
|
||||
var moduleTrades = monthTrades.Where(t => t.ModuleName == module).ToList();
|
||||
decimal todayPnl = moduleTrades.Where(t => t.ClosedAt >= today).Sum(t => t.RealizedPnl);
|
||||
var k7 = TradeAnalytics.ComputeKpis(moduleTrades.Where(t => t.ClosedAt >= week));
|
||||
var k30 = TradeAnalytics.ComputeKpis(moduleTrades);
|
||||
|
||||
kpiTiles.Add(new
|
||||
{
|
||||
moduleName = module,
|
||||
todayPnl,
|
||||
pnl7d = k7.NetPnl,
|
||||
winRate7d = k7.WinRatePct,
|
||||
count7d = k7.TradeCount,
|
||||
pnl30d = k30.NetPnl,
|
||||
winRate30d = k30.WinRatePct,
|
||||
text = $"Heute: {todayPnl:+0.00;-0.00} USDC\n7T: {k7.NetPnl:+0.00;-0.00} · {k7.WinRatePct:0}% · {k7.TradeCount} Tr.\n30T: {k30.NetPnl:+0.00;-0.00} · {k30.WinRatePct:0}%",
|
||||
isPositive = k30.NetPnl >= 0
|
||||
});
|
||||
}
|
||||
|
||||
decimal totalTodayPnl = monthTrades.Where(t => t.ClosedAt >= today).Sum(t => t.RealizedPnl);
|
||||
var totalK7 = TradeAnalytics.ComputeKpis(monthTrades.Where(t => t.ClosedAt >= week));
|
||||
var totalK30 = TradeAnalytics.ComputeKpis(monthTrades);
|
||||
kpiTiles.Add(new
|
||||
{
|
||||
moduleName = "Gesamt",
|
||||
todayPnl = totalTodayPnl,
|
||||
pnl7d = totalK7.NetPnl,
|
||||
winRate7d = totalK7.WinRatePct,
|
||||
count7d = totalK7.TradeCount,
|
||||
pnl30d = totalK30.NetPnl,
|
||||
winRate30d = totalK30.WinRatePct,
|
||||
text = $"Heute: {totalTodayPnl:+0.00;-0.00} USDC\n7T: {totalK7.NetPnl:+0.00;-0.00} · {totalK7.WinRatePct:0}% · {totalK7.TradeCount} Tr.\n30T: {totalK30.NetPnl:+0.00;-0.00} · {totalK30.WinRatePct:0}%",
|
||||
isPositive = totalK30.NetPnl >= 0
|
||||
});
|
||||
|
||||
// 2. Auffällige Trades (24h)
|
||||
DateTime since24h = now.AddDays(-1);
|
||||
List<TradeRecord> dayTrades;
|
||||
try { dayTrades = tradeLog?.Find(t => t.ClosedAt >= since24h) ?? new List<TradeRecord>(); }
|
||||
catch { dayTrades = new List<TradeRecord>(); }
|
||||
|
||||
var notable = dayTrades
|
||||
.OrderByDescending(t => Math.Abs(t.RealizedPnl))
|
||||
.Take(25)
|
||||
.Select(t => new
|
||||
{
|
||||
module = t.ModuleName ?? "",
|
||||
market = t.MarketQuestion ?? "",
|
||||
pnl = t.RealizedPnl,
|
||||
pnlPct = t.PnlPercent,
|
||||
closedAt = t.ClosedAt
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 3. Warnungen & Fehler (heute)
|
||||
var alerts = new List<object>();
|
||||
try
|
||||
{
|
||||
string logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
string path = Path.Combine(logsDir, $"{AppTimeZone.Today:yyyy-MM-dd}.jsonl");
|
||||
if (File.Exists(path))
|
||||
{
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
var p = LogJson.ParseLine(line);
|
||||
if (p == null) continue;
|
||||
if (p.Level != "Error" && p.Level != "Warning") continue;
|
||||
alerts.Add(new
|
||||
{
|
||||
time = DateTimeOffset.TryParse(p.Time, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dt) ? dt.ToString("HH:mm:ss") : p.Time,
|
||||
level = p.Level,
|
||||
message = (p.Message ?? string.Empty).Replace("\r", " ").Replace("\n", " ").Trim()
|
||||
});
|
||||
}
|
||||
alerts.Reverse();
|
||||
}
|
||||
else
|
||||
{
|
||||
var logger = sp.GetService<TerminalLogger>();
|
||||
if (logger != null)
|
||||
{
|
||||
var memAlerts = logger.GetHistory(TimeSpan.FromHours(24))
|
||||
.Where(e => e.Level == LogLevel.Warning || e.Level == LogLevel.Error)
|
||||
.Select(e => new
|
||||
{
|
||||
time = e.Timestamp.ToString("HH:mm:ss"),
|
||||
level = e.Level.ToString(),
|
||||
message = e.Message.Replace("\r", " ").Replace("\n", " ").Trim()
|
||||
})
|
||||
.Reverse()
|
||||
.Take(100)
|
||||
.ToList();
|
||||
alerts.AddRange(memAlerts);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* stiller Rückzug */ }
|
||||
|
||||
// 4. Supervisor-KI (letzter Bericht)
|
||||
var supervisorReports = sp.GetService<ISupervisorReportRepository>();
|
||||
object? supervisorSnippet = null;
|
||||
if (supervisorReports != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var last = supervisorReports.GetRecent(1).FirstOrDefault();
|
||||
if (last != null)
|
||||
{
|
||||
supervisorSnippet = new
|
||||
{
|
||||
available = true,
|
||||
createdAt = last.CreatedAt.ToString("dd.MM. HH:mm"),
|
||||
profile = last.Profile,
|
||||
model = last.Model,
|
||||
answer = last.Answer
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
supervisorSnippet = new
|
||||
{
|
||||
available = false,
|
||||
answer = "Noch kein Supervisor-Bericht vorhanden. (OpenRouter-Key setzen und im Supervisor-Fenster eine Analyse starten.)"
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
supervisorSnippet = new
|
||||
{
|
||||
available = false,
|
||||
answer = $"Supervisor-Berichte nicht verfügbar: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
supervisorSnippet = new
|
||||
{
|
||||
available = false,
|
||||
answer = "Supervisor-Modul nicht geladen oder nicht lizenziert."
|
||||
};
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
moduleKpis = kpiTiles,
|
||||
notableTrades = notable,
|
||||
alerts = alerts.Take(100),
|
||||
supervisorSnippet
|
||||
});
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user