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 { public static class DashboardEndpoints { public record SetTradingModeRequest(string ModeType, string TargetMode); public static IEndpointRouteBuilder MapDashboardEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/dashboard"); group.MapGet("/summary", (TradingState state, IServiceProvider sp) => { var vpn = sp.GetService(); var watchdog = sp.GetService(); var accounts = state.Accounts.Values.ToList(); decimal totalAvailable = accounts.Sum(a => a.AvailableBalance); decimal totalBalance = accounts.Sum(a => a.TotalBalance); return Results.Ok(new { globalTradingPaused = state.GlobalTradingPaused, liveTradingMode = state.LiveTradingMode.ToString(), demoTradingMode = state.DemoTradingMode.ToString(), alchemyHealthy = state.IsAlchemyHealthy, globalPnl = state.GlobalPnl, vpnConnected = vpn?.IsConnected ?? false, vpnEnabled = vpn != null, watchdogActive = watchdog != null, accountCount = accounts.Count, totalUsdcBalance = totalAvailable, totalPortfolioValue = totalBalance }); }); group.MapPost("/trading-mode", (SetTradingModeRequest req, TradingState state, TerminalLogger logger) => { if (req.ModeType.Equals("live", System.StringComparison.OrdinalIgnoreCase)) { if (System.Enum.TryParse(req.TargetMode, true, out var parsed)) { state.LiveTradingMode = parsed; logger.Info($"🌐 WebUI: Live-Trading-Modus auf '{parsed}' geändert."); return Results.Ok(new { success = true, currentMode = state.LiveTradingMode.ToString() }); } } else if (req.ModeType.Equals("demo", System.StringComparison.OrdinalIgnoreCase)) { if (System.Enum.TryParse(req.TargetMode, true, out var parsed)) { state.DemoTradingMode = parsed; logger.Info($"🌐 WebUI: Demo-Trading-Modus auf '{parsed}' geändert."); return Results.Ok(new { success = true, currentMode = state.DemoTradingMode.ToString() }); } } else if (req.ModeType.Equals("pause", System.StringComparison.OrdinalIgnoreCase)) { state.GlobalTradingPaused = !state.GlobalTradingPaused; logger.Warning($"🌐 WebUI: GlobalTradingPaused auf '{state.GlobalTradingPaused}' umgeschaltet."); return Results.Ok(new { success = true, isPaused = state.GlobalTradingPaused }); } return Results.BadRequest(new { error = "Ungültiger Modus oder Parametertyp." }); }); // Trading beenden (#563) - das WebUI-Gegenstueck zu "Beenden" im Desktop, beschraenkt auf das // Trading. Ein Ende der ganzen Anwendung naehme den Webserver mit; danach waere das WebUI nicht // mehr erreichbar, und nur ein Zugang zur Maschine braechte es zurueck. // // Beide Modi auf Inaktiv und die Pause an: Damit halten CopyTrading (Engine, Monitor, // Verkaufsleitern) und ResolutionFarming (#569) an, wie beim Ende der Anwendung. Offene Orders // an der Boerse werden bewusst NICHT storniert. Das tut auch das Beenden der App nicht, und // ein Eingriff in die CLOB-Integration gehoert nicht in einen Notschalter. Wieder // aufgenommen wird nur ausdruecklich ueber die Modus-Schalter. // // "confirmed" ist Pflicht: Ein Aufruf ohne Absicht, etwa aus einem alten Skript oder einem // doppelt abgeschickten Formular, soll das Trading nicht beenden. group.MapPost("/stop-trading", (StopTradingRequest? req, TradingState state, TerminalLogger logger) => { if (req?.Confirmed != true) return Results.BadRequest(new { error = "Bestätigung fehlt – das Trading wurde nicht beendet." }); string liveBefore = state.LiveTradingMode.ToString(); string demoBefore = state.DemoTradingMode.ToString(); bool pausedBefore = state.GlobalTradingPaused; state.LiveTradingMode = TradingMode.Inactive; state.DemoTradingMode = TradingMode.Inactive; state.GlobalTradingPaused = true; logger.Warning($"🛑 WebUI: Trading beendet (Live {liveBefore} → Inactive, Demo {demoBefore} → Inactive, " + $"Pause {(pausedBefore ? "war schon an" : "an")}). Anwendung und WebUI laufen weiter; " + "offene Orders an der Börse bleiben stehen."); return Results.Ok(new { success = true, liveTradingMode = state.LiveTradingMode.ToString(), demoTradingMode = state.DemoTradingMode.ToString(), globalTradingPaused = state.GlobalTradingPaused, stoppedAt = DateTime.UtcNow }); }); group.MapGet("/overview-widgets", (IServiceProvider sp) => { var tradeLog = sp.GetService(); DateTime now = DateTime.UtcNow; DateTime today = now.Date; DateTime week = now.AddDays(-7); DateTime month = now.AddDays(-30); // 1. Modul-KPIs List monthTrades; try { monthTrades = tradeLog?.Find(t => t.ClosedAt >= month) ?? new List(); } catch { monthTrades = new List(); } var moduleNames = monthTrades.Select(t => t.ModuleName).Where(m => !string.IsNullOrEmpty(m)).Distinct().OrderBy(m => m).ToList(); var kpiTiles = new List(); 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 dayTrades; try { dayTrades = tradeLog?.Find(t => t.ClosedAt >= since24h) ?? new List(); } catch { dayTrades = new List(); } 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(); 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(); 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(); 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; } public record StopTradingRequest(bool Confirmed); } }