using System; using MongoDB.Driver; using PolyTraderSharp.Extensions; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using PolyTraderSharp.Models; namespace PolyTraderSharp.Services { public class TraderAnalyticsJob : BackgroundService { private readonly TradingState _state; private readonly TerminalLogger _logger; private readonly IMongoDatabase _db; private readonly JobStatusRow _jobStatus; public TraderAnalyticsJob(TradingState state, TerminalLogger logger, IMongoDatabase db, JobManager jobManager) { _state = state; _logger = logger; _db = db; _jobStatus = new JobStatusRow { JobName = "Trader Analytics", Description = "Analysiert Master-Trader-Performance pro Account (letzte 30 Trades, 7D Volumen).", StatusText = "Pending Initial Delay..." }; _jobStatus.ManualTriggerAction = async () => { _jobStatus.StatusText = "Running (Manual)..."; await RunAnalyticsAsync(); _jobStatus.StatusText = "Idle"; _jobStatus.LastRun = DateTime.Now; }; jobManager.RegisterJob(_jobStatus); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Initial wait so the application can start smoothly await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); while (!stoppingToken.IsCancellationRequested) { if (_jobStatus.IsEnabled) { try { _jobStatus.StatusText = "Running (Scheduled)..."; await RunAnalyticsAsync(); _jobStatus.LastRun = DateTime.Now; } catch (Exception ex) { _logger.Error($"Error in TraderAnalyticsJob: {ex.Message}"); _jobStatus.StatusText = "Error!"; } finally { if (_jobStatus.StatusText != "Error!") _jobStatus.StatusText = "Idle"; } } else { _jobStatus.StatusText = "Paused"; } _jobStatus.NextRun = DateTime.Now.AddHours(6); await Task.Delay(TimeSpan.FromHours(6), stoppingToken); } } private Task RunAnalyticsAsync() { return Task.Run(() => { try { _logger.Info("🔄 Starte Trader Analytics (7D / Letzte 30 Trades)..."); var closedTradesColl = _db.GetCollection("closed_trades"); // Ensure indexes closedTradesColl.EnsureIndex(x => x.AccountId); closedTradesColl.EnsureIndex(x => x.SourceTraderId); DateTime sevenDaysAgo = DateTime.UtcNow.AddDays(-7); foreach (var acc in _state.Accounts.Values) { var results = new List(); // Find all master traders that this account has copied successfully in their entire history // Or we just find MTs that were copied in the last 7 days? // The requirement says: "Welche Trades ... in den letzten 7 Tagen kopiert ... und wie hoch war die Winrate der letzten 30 Trades" // Thus we only care about MTs that had at least 1 trade in the last 7 days! int accId = acc.AccountId; var recentMTs = closedTradesColl.LiteFind(x => x.AccountId == accId && x.ClosedAt >= sevenDaysAgo) .Select(x => x.SourceTraderId) .Distinct() .Where(id => id != 0) // Ignore orphaned historical trades (API resolved/auto-redeem before ID tracking patch) .ToList(); foreach (var mtId in recentMTs) { var mtInfo = _state.Traders.Values.FirstOrDefault(t => t.Id == mtId); string name = mtInfo?.DisplayName ?? $"MT #{mtId}"; string address = mtInfo?.WalletAddress ?? ""; // 1. Trades im 7D Fenster zählen int trades7D = closedTradesColl.LiteFind(x => x.AccountId == accId && x.SourceTraderId == mtId && x.ClosedAt >= sevenDaysAgo).Count(); // 2. Letzte 30 Trades holen var last30 = closedTradesColl.LiteFind(x => x.AccountId == accId && x.SourceTraderId == mtId) .OrderByDescending(x => x.ClosedAt) .Take(30) .ToList(); if (last30.Count == 0) continue; decimal pnl30T = last30.Sum(x => x.RealizedPnl); int wins = last30.Count(x => x.RealizedPnl > 0); // Exakt 0 ist kein Win, nur > 0 decimal winrate = ((decimal)wins / last30.Count) * 100m; results.Add(new TraderAnalyticsResult { AccountId = acc.AccountId, SourceTraderId = mtId, SourceTraderName = name, SourceTraderAddress = address, Winrate30T = winrate, Pnl30T = pnl30T, Trades7D = trades7D }); } // Save to cache _state.TraderAnalyticsCache[acc.AccountId] = results; } _logger.Info("✅ Trader Analytics erfolgreich abgeschlossen und im Cache aktualisiert."); } catch (Exception ex) { _logger.Error($"TraderAnalyticsJob Exception: {ex}"); } }); } } }