Files
managing-apps/src/Managing.Application/Agents/AgentService.cs

353 lines
14 KiB
C#

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<AgentService> _logger;
public AgentService(
IAgentBalanceRepository agentBalanceRepository,
IAgentSummaryRepository agentSummaryRepository,
IServiceScopeFactory serviceScopeFactory,
ICacheService cacheService,
ILogger<AgentService> logger)
{
_agentBalanceRepository = agentBalanceRepository;
_agentSummaryRepository = agentSummaryRepository;
_serviceScopeFactory = serviceScopeFactory;
_cacheService = cacheService;
_logger = logger;
}
public async Task<AgentBalanceHistory> GetAgentBalances(string agentName, DateTime start,
DateTime? end = null)
{
// Get userId from agentName by looking up the user
var userId = await ServiceScopeHelpers.WithScopedService<IUserService, int?>(_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<AgentBalance>()
};
}
// 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<AgentBalanceHistory> 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<AgentBalanceHistory>(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<IUserService, string>(_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<IEnumerable<AgentSummary>> GetAllAgentSummaries()
{
return await _agentSummaryRepository.GetAllAsync();
}
public async Task<IEnumerable<string>> 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<int> 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;
}
}
/// <summary>
/// Determines if fresh balance data should be calculated based on the last balance timestamp
/// </summary>
private Task<bool> ShouldCalculateFreshBalanceData(int userId, IList<AgentBalance> 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);
}
}
/// <summary>
/// Calculates fresh balance data for a user by aggregating from positions and account balances
/// </summary>
private async Task<AgentBalance?> CalculateFreshBalanceData(int userId)
{
try
{
// Get all positions for this user's bots as initiator
var positions = await ServiceScopeHelpers.WithScopedService<ITradingService, List<Position>>(
_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<IUserService, User>(
_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<IAccountService, IEnumerable<Account>>(
_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<IExchangeService, List<Balance>>(
_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<IBotService, List<Bot>>(
_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<IBotService, IEnumerable<TradingBotConfig>>(
_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;
}
}
}