using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Domain.Users; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.Agents; public class AgentService : IAgentService { private readonly IAgentBalanceRepository _agentBalanceRepository; private readonly IAgentSummaryRepository _agentSummaryRepository; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ICacheService _cacheService; private readonly ILogger _logger; public AgentService( IAgentBalanceRepository agentBalanceRepository, IAgentSummaryRepository agentSummaryRepository, IServiceScopeFactory serviceScopeFactory, ICacheService cacheService, ILogger logger) { _agentBalanceRepository = agentBalanceRepository; _agentSummaryRepository = agentSummaryRepository; _serviceScopeFactory = serviceScopeFactory; _cacheService = cacheService; _logger = logger; } public async Task GetAgentBalances(string agentName, DateTime start, DateTime? end = null) { // Get userId from agentName by looking up the user var userId = await ServiceScopeHelpers.WithScopedService(_serviceScopeFactory, async (userService) => { var user = await userService.GetUserByAgentName(agentName); return user?.Id; }); if (!userId.HasValue) { // Return empty result if user not found return new AgentBalanceHistory { UserId = 0, AgentName = agentName, AgentBalances = new List() }; } // Use the UserId-based method internally var result = await GetAgentBalancesByUserId(userId.Value, start, end); // Override the AgentName to use the requested agentName instead of the default result.AgentName = agentName; return result; } public async Task GetAgentBalancesByUserId(int userId, DateTime start, DateTime? end = null) { var effectiveEnd = end ?? DateTime.UtcNow; string cacheKey = $"AgentBalancesByUserId_{userId}_{start:yyyyMMdd}_{effectiveEnd:yyyyMMdd}"; // Check if the balances are already cached var cachedBalances = _cacheService.GetValue(cacheKey); if (cachedBalances != null) { return cachedBalances; } var balances = await _agentBalanceRepository.GetAgentBalancesByUserId(userId, start, end); // Check if we need to calculate fresh balance data var needsFreshData = await ShouldCalculateFreshBalanceData(userId, balances, effectiveEnd); if (needsFreshData) { _logger.LogInformation("Calculating fresh balance data for user {UserId} - last balance is missing or older than 2 minutes", userId); var freshBalance = await CalculateFreshBalanceData(userId); if (freshBalance != null) { // Insert the fresh balance data into InfluxDB _agentBalanceRepository.InsertAgentBalance(freshBalance); // Update the AgentSummary TotalBalance with the fresh balance data await UpdateAgentSummaryTotalBalanceAsync(userId, freshBalance.TotalBalanceValue); // Add the fresh balance to our results if it falls within the requested time range if (freshBalance.Time >= start && freshBalance.Time <= effectiveEnd) { balances.Add(freshBalance); } } } // Create a single AgentBalanceHistory with all balances var result = new AgentBalanceHistory { UserId = userId, AgentName = $"User_{userId}", AgentBalances = balances.OrderBy(b => b.Time).ToList() }; // Cache the results for 5 minutes _cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5)); return result; } public async Task SaveOrUpdateAgentSummary(AgentSummary agentSummary) { try { if (string.IsNullOrEmpty(agentSummary.AgentName)) { agentSummary.AgentName = await ServiceScopeHelpers.WithScopedService(_serviceScopeFactory, async (userService) => (await userService.GetUserByIdAsync(agentSummary.UserId)).AgentName); } // Use the injected AgentSummaryRepository to save or update await _agentSummaryRepository.SaveOrUpdateAsync(agentSummary); _logger.LogInformation("AgentSummary saved/updated for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); } catch (Exception ex) { _logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); throw; } } public async Task> GetAllAgentSummaries() { return await _agentSummaryRepository.GetAllAsync(); } public async Task> GetAllOnlineAgents() { var agentSummaries = await _agentSummaryRepository.GetAllAgentWithRunningBots(); return agentSummaries.Select(a => a.AgentName); } public async Task UpdateAgentSummaryNameAsync(int userId, string agentName) { try { await _agentSummaryRepository.UpdateAgentNameAsync(userId, agentName); _logger.LogInformation("Agent name updated for user {UserId} to {AgentName}", userId, agentName); } catch (Exception ex) { _logger.LogError(ex, "Error updating agent name for user {UserId} to {AgentName}", userId, agentName); throw; } } public async Task GetTotalAgentCount() { return await _agentSummaryRepository.GetTotalAgentCount(); } public async Task IncrementBacktestCountAsync(int userId) { try { await _agentSummaryRepository.IncrementBacktestCountAsync(userId); _logger.LogInformation("Backtest count incremented for user {UserId}", userId); } catch (Exception ex) { _logger.LogError(ex, "Error incrementing backtest count for user {UserId}", userId); throw; } } public async Task UpdateAgentSummaryTotalBalanceAsync(int userId, decimal totalBalance) { try { await _agentSummaryRepository.UpdateTotalBalanceAsync(userId, totalBalance); _logger.LogInformation("Total balance updated for user {UserId} to {TotalBalance}", userId, totalBalance); } catch (Exception ex) { _logger.LogError(ex, "Error updating total balance for user {UserId} to {TotalBalance}", userId, totalBalance); throw; } } /// /// Determines if fresh balance data should be calculated based on the last balance timestamp /// private Task ShouldCalculateFreshBalanceData(int userId, IList existingBalances, DateTime effectiveEnd) { try { // If no balances exist, we need fresh data if (!existingBalances.Any()) { _logger.LogDebug("No existing balances found for user {UserId}, calculating fresh data", userId); return Task.FromResult(true); } // Get the most recent balance var lastBalance = existingBalances.OrderByDescending(b => b.Time).First(); var timeSinceLastBalance = effectiveEnd - lastBalance.Time; // If the last balance is older than 2 minutes, calculate fresh data if (timeSinceLastBalance > TimeSpan.FromMinutes(2)) { _logger.LogDebug("Last balance for user {UserId} is {TimeAgo} old, calculating fresh data", userId, timeSinceLastBalance); return Task.FromResult(true); } return Task.FromResult(false); } catch (Exception ex) { _logger.LogError(ex, "Error checking if fresh balance data is needed for user {UserId}", userId); // Default to calculating fresh data on error to ensure we have current information return Task.FromResult(true); } } /// /// Calculates fresh balance data for a user by aggregating from positions and account balances /// private async Task CalculateFreshBalanceData(int userId) { try { // Get all positions for this user's bots as initiator var positions = await ServiceScopeHelpers.WithScopedService>( _serviceScopeFactory, async tradingService => { var userPositions = await tradingService.GetPositionByUserIdAsync(userId); return userPositions.Where(p => p.IsValidForMetrics()).ToList(); }); // Calculate PnL from positions var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0); var totalFees = positions.Sum(p => p.CalculateTotalFees()); var netPnL = totalPnL - totalFees; // Calculate USDC wallet value and USDC in positions decimal usdcWalletValue = 0; decimal usdcInPositionsValue = 0; decimal totalBalance = 0; try { // Get user and accounts var user = await ServiceScopeHelpers.WithScopedService( _serviceScopeFactory, async userService => await userService.GetUserByIdAsync(userId)); if (user == null) { _logger.LogError("User {UserId} not found for balance calculation", userId); return null; } var userAccounts = await ServiceScopeHelpers.WithScopedService>( _serviceScopeFactory, async accountService => await accountService.GetAccountsByUserAsync(user, hideSecrets: true, true)); // Calculate USDC wallet value from all accounts foreach (var account in userAccounts) { var balances = await ServiceScopeHelpers.WithScopedService>( _serviceScopeFactory, async exchangeService => await exchangeService.GetBalances(account)); var usdcBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC"); usdcWalletValue += usdcBalance?.Amount ?? 0; } // Calculate USDC in open positions foreach (var position in positions.Where(p => p.IsOpen())) { var positionUsd = position.Open.Price * position.Open.Quantity; var realized = position.ProfitAndLoss?.Realized ?? 0; usdcInPositionsValue += positionUsd + realized; } totalBalance = usdcWalletValue + usdcInPositionsValue; } catch (Exception ex) { _logger.LogError(ex, "Error calculating wallet balances for user {UserId}", userId); // Continue with zero values if balance calculation fails } // Calculate bots allocation USD value var activeStrategies = await ServiceScopeHelpers.WithScopedService>( _serviceScopeFactory, async botService => { var userBots = await botService.GetBotsByUser(userId); return userBots.Where(b => b.Status == BotStatus.Running).ToList(); }); var botsAllocationUsdValue = 0m; if (activeStrategies.Any()) { var botIds = activeStrategies.Select(b => b.Identifier); var botConfigs = await ServiceScopeHelpers.WithScopedService>( _serviceScopeFactory, async botService => await botService.GetBotConfigsByIdsAsync(botIds)); botsAllocationUsdValue = botConfigs.Sum(config => config.BotTradingBalance); } var freshBalance = new AgentBalance { UserId = userId, TotalBalanceValue = totalBalance, UsdcWalletValue = usdcWalletValue, UsdcInPositionsValue = usdcInPositionsValue, BotsAllocationUsdValue = botsAllocationUsdValue, PnL = netPnL, Time = DateTime.UtcNow }; _logger.LogDebug("Calculated fresh balance data for user {UserId}: TotalBalance={TotalBalance}, UsdcWallet={UsdcWallet}, UsdcInPositions={UsdcInPositions}, BotsAllocation={BotsAllocation}, PnL={PnL}", userId, totalBalance, usdcWalletValue, usdcInPositionsValue, botsAllocationUsdValue, netPnL); return freshBalance; } catch (Exception ex) { _logger.LogError(ex, "Error calculating fresh balance data for user {UserId}", userId); return null; } } }