using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTraderSharp; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTrader.Web.Endpoints { public static class CopyTradingEndpoints { public static IEndpointRouteBuilder MapCopyTradingEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/modules/copytrading"); // 1. Master-Trader abrufen group.MapGet("/traders", (IServiceProvider sp, CopyTradingState copyState) => { var repo = sp.GetService(); List traders; try { traders = repo?.GetAll() ?? copyState.Traders.Values.OrderBy(t => t.Id).ToList(); if (traders.Count == 0 && copyState.Traders.Count > 0) traders = copyState.Traders.Values.OrderBy(t => t.Id).ToList(); } catch { traders = copyState.Traders.Values.OrderBy(t => t.Id).ToList(); } var list = traders.Select(t => new { id = t.Id, displayName = t.DisplayName, walletAddress = t.WalletAddress, category = t.Category, description = t.Description, reasoning = t.Reasoning, isActive = t.IsActive, isHidden = t.IsHidden, autoPauseEnabled = t.AutoPauseEnabled, makerEntry = t.MakerEntry, assignedAccountIds = t.AssignedAccountIds?.ToList() ?? new List(), totalTrades = t.TotalTrades, winningTrades = t.WinningTrades, winrate30t = t.Winrate30t, totalPnl = t.TotalPnl, copyPnl30d = t.CopyPnl30d, copyProfitFactor = t.CopyProfitFactor, copyAvgPnlPerTrade = t.CopyAvgPnlPerTrade, copyTradeCount30d = t.CopyTradeCount30d }).ToList(); return Results.Ok(list); }); // 2. Master-Trader anlegen / aktualisieren group.MapPost("/traders", (IServiceProvider sp, CopyTradingState copyState, TerminalLogger logger, TrackedTraderInput input) => { var repo = sp.GetService(); int id = input.Id; if (id <= 0) { int maxId = copyState.Traders.Count > 0 ? copyState.Traders.Keys.Max() : 0; if (repo != null) { try { var existing = repo.GetAll(); if (existing.Count > 0) maxId = Math.Max(maxId, existing.Max(t => t.Id)); } catch { } } id = maxId + 1; } var trader = new TrackedTrader { Id = id, DisplayName = input.DisplayName ?? $"Trader #{id}", WalletAddress = (input.WalletAddress ?? string.Empty).Trim(), Category = string.IsNullOrWhiteSpace(input.Category) ? "NEW_BIG_BET" : input.Category.Trim(), Description = input.Description ?? string.Empty, Reasoning = input.Reasoning ?? string.Empty, IsActive = input.IsActive, IsHidden = input.IsHidden, AutoPauseEnabled = input.AutoPauseEnabled, MakerEntry = input.MakerEntry, AssignedAccountIds = input.AssignedAccountIds != null ? new HashSet(input.AssignedAccountIds) : new HashSet() }; // Wenn bereits vorhanden, bestehende Stats übernehmen if (copyState.Traders.TryGetValue(id, out var existingTrader)) { trader.TotalTrades = existingTrader.TotalTrades; trader.WinningTrades = existingTrader.WinningTrades; trader.Winrate30t = existingTrader.Winrate30t; trader.TotalPnl = existingTrader.TotalPnl; trader.CopyPnl30d = existingTrader.CopyPnl30d; trader.CopyProfitFactor = existingTrader.CopyProfitFactor; trader.CopyAvgPnlPerTrade = existingTrader.CopyAvgPnlPerTrade; trader.CopyTradeCount30d = existingTrader.CopyTradeCount30d; } var failed = PersistenceGuard.Write(repo != null, () => repo!.Upsert(trader), logger, $"Master-Trader #{id} ({trader.DisplayName})"); if (failed != null) return failed; // Erst nach erfolgreichem Schreiben in den Hauptspeicher (#534). copyState.Traders[id] = trader; return Results.Ok(new { success = true, trader = new { id = trader.Id, displayName = trader.DisplayName, walletAddress = trader.WalletAddress, category = trader.Category, description = trader.Description, reasoning = trader.Reasoning, isActive = trader.IsActive, isHidden = trader.IsHidden, autoPauseEnabled = trader.AutoPauseEnabled, makerEntry = trader.MakerEntry, assignedAccountIds = trader.AssignedAccountIds.ToList() } }); }); // 3. Master-Trader löschen group.MapDelete("/traders/{id:int}", (IServiceProvider sp, CopyTradingState copyState, TerminalLogger logger, int id) => { var repo = sp.GetService(); var failed = PersistenceGuard.Write(repo != null, () => repo!.Delete(id), logger, $"Loeschen von Master-Trader #{id}"); if (failed != null) return failed; copyState.Traders.TryRemove(id, out _); return Results.Ok(new { success = true, id }); }); // 4. Offene Trades group.MapGet("/open-trades", (TradingState state, CopyTradingState copyState) => { var openTrades = new List(); foreach (var acc in state.Accounts.Values.OrderBy(a => a.AccountId)) { foreach (var p in acc.OpenPositions.Values.OrderByDescending(p => p.OpenedAt)) { decimal unrealized = p.CurrentValueUsd - p.AmountUsd; decimal pct = p.AmountUsd > 0m ? 100m * unrealized / p.AmountUsd : 0m; string traderName = p.SourceTraderName; if (string.IsNullOrEmpty(traderName) && p.SourceTraderId > 0) { if (copyState.Traders.TryGetValue(p.SourceTraderId, out var tr)) traderName = tr.DisplayName; else traderName = $"#{p.SourceTraderId}"; } openTrades.Add(new { account = (string.IsNullOrEmpty(acc.Name) ? $"#{acc.AccountId}" : acc.Name) + (acc.IsDemo ? " (Demo)" : ""), accountId = acc.AccountId, isDemo = acc.IsDemo, sourceTraderId = p.SourceTraderId, trader = string.IsNullOrEmpty(traderName) ? "—" : traderName, market = p.MarketQuestion ?? "—", outcome = p.Outcome ?? "—", side = p.Side ?? "BUY", entryPrice = p.EntryPrice, currentPrice = p.CurrentPrice, size = p.Size, amountUsd = p.AmountUsd, currentValueUsd = p.CurrentValueUsd, unrealizedPnl = unrealized, unrealizedPct = pct, status = p.ExitPending ? "Exit läuft" : "Offen", openedAt = p.OpenedAt.ToString("yyyy-MM-dd HH:mm") }); } } return Results.Ok(new { count = openTrades.Count, totalAmount = openTrades.Count > 0 ? state.Accounts.Values.SelectMany(a => a.OpenPositions.Values).Sum(p => p.AmountUsd) : 0m, totalUnrealized = openTrades.Count > 0 ? state.Accounts.Values.SelectMany(a => a.OpenPositions.Values).Sum(p => p.CurrentValueUsd - p.AmountUsd) : 0m, trades = openTrades }); }); // 5. Geschlossene Trades (mit Filter & Paginierung) group.MapGet("/closed-trades", ( IServiceProvider sp, TradingState state, CopyTradingState copyState, int? traderId, string? result, string? search, int? page, int? pageSize) => { var repo = sp.GetService(); List trades; try { trades = repo?.Find(_ => true) ?? new List(); } catch { trades = new List(); } var query = trades.AsEnumerable(); if (traderId.HasValue && traderId.Value > 0) { query = query.Where(t => t.SourceTraderId == traderId.Value); } if (!string.IsNullOrWhiteSpace(result)) { if (result.Equals("Gewinner", StringComparison.OrdinalIgnoreCase)) query = query.Where(t => t.RealizedPnl > 0); else if (result.Equals("Verlierer", StringComparison.OrdinalIgnoreCase)) query = query.Where(t => t.RealizedPnl < 0); } if (!string.IsNullOrWhiteSpace(search)) { string q = search.Trim(); query = query.Where(t => (t.MarketQuestion != null && t.MarketQuestion.Contains(q, StringComparison.OrdinalIgnoreCase)) || (t.Outcome != null && t.Outcome.Contains(q, StringComparison.OrdinalIgnoreCase))); } var filtered = query.OrderByDescending(t => t.ClosedAt).ToList(); int totalCount = filtered.Count; decimal totalPnl = filtered.Sum(t => t.RealizedPnl); int winCount = filtered.Count(t => t.RealizedPnl > 0); decimal winrate = totalCount > 0 ? 100m * winCount / totalCount : 0m; int curPage = Math.Max(1, page.GetValueOrDefault(1)); int curPageSize = Math.Clamp(pageSize.GetValueOrDefault(50), 10, 200); int totalPages = (int)Math.Ceiling(totalCount / (double)curPageSize); var items = filtered .Skip((curPage - 1) * curPageSize) .Take(curPageSize) .Select(t => { string accName = $"#{t.AccountId}"; if (state.Accounts.TryGetValue(t.AccountId, out var acc) && !string.IsNullOrEmpty(acc.Name)) accName = acc.Name + (t.IsDemo ? " (Demo)" : ""); string traderName = $"#{t.SourceTraderId}"; if (copyState.Traders.TryGetValue(t.SourceTraderId, out var tr) && !string.IsNullOrEmpty(tr.DisplayName)) traderName = tr.DisplayName; return new { tradeId = t.TradeId, account = accName, accountId = t.AccountId, isDemo = t.IsDemo, sourceTraderId = t.SourceTraderId, trader = traderName, market = t.MarketQuestion ?? "—", outcome = t.Outcome ?? "—", side = t.Side ?? "—", entryPrice = t.EntryPrice, exitPrice = t.ExitPrice, size = t.Size, realizedPnl = t.RealizedPnl, pnlPercent = t.PnlPercent, totalFees = t.TotalFees, openedAt = t.OpenedAt.ToString("yyyy-MM-dd HH:mm"), closedAt = t.ClosedAt.ToString("yyyy-MM-dd HH:mm"), exitReason = t.ExitReason }; }) .ToList(); return Results.Ok(new { summary = new { totalTrades = totalCount, totalPnl, winCount, winRate = winrate }, totalCount, page = curPage, pageSize = curPageSize, totalPages, items }); }); // 6. Account-Einstellungen abrufen group.MapGet("/settings/{accountId:int}", (IServiceProvider sp, CopyTradingState copyState, int accountId) => { var repo = sp.GetService(); CopyTradingAccountSettings? settings = null; try { settings = repo?.Get(accountId); } catch { } if (settings == null) settings = copyState.GetAccountSettings(accountId); return Results.Ok(settings); }); // 7. Account-Einstellungen speichern group.MapPost("/settings/{accountId:int}", ( IServiceProvider sp, CopyTradingState copyState, TerminalLogger logger, int accountId, CopyTradingAccountSettings input) => { input.AccountId = accountId; var repo = sp.GetService(); var failed = PersistenceGuard.Write(repo != null, () => repo!.Upsert(input), logger, $"CopyTrading-Einstellungen fuer Konto {accountId}"); if (failed != null) return failed; copyState.AccountSettings[accountId] = input; return Results.Ok(new { success = true, settings = input }); }); return routes; } public record TrackedTraderInput( int Id, string? DisplayName, string? WalletAddress, string? Category, string? Description, string? Reasoning, bool IsActive, bool IsHidden, bool AutoPauseEnabled, bool MakerEntry, List? AssignedAccountIds ); } }