353 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
} |