diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index d806dfb..92199fb 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -345,7 +345,7 @@ public class DataController : ControllerBase // Calculate PnL for each bot once and store in a list of tuples var botsWithPnL = activeBots - .Select(bot => new { Bot = bot, PnL = bot.Pnl }) + .Select(bot => new { Bot = bot, PnL = bot.Pnl, agentName = bot.User.AgentName }) .OrderByDescending(item => item.PnL) .Take(3) .ToList(); @@ -357,7 +357,8 @@ public class DataController : ControllerBase .Select(item => new StrategyPerformance { StrategyName = item.Bot.Name, - PnL = item.PnL + PnL = item.PnL, + AgentName = item.agentName, }) .ToList() }; @@ -452,7 +453,8 @@ public class DataController : ControllerBase /// The trading bot to map /// Pre-fetched positions grouped by initiator identifier /// A view model with detailed strategy information - private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy, Dictionary> positionsByIdentifier) + private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy, + Dictionary> positionsByIdentifier) { // Calculate ROI percentage based on PnL relative to account value decimal pnl = strategy.Pnl; @@ -473,8 +475,8 @@ public class DataController : ControllerBase decimal roiLast24h = strategy.Roi; // Get positions for this strategy from pre-fetched data - var positions = positionsByIdentifier.TryGetValue(strategy.Identifier, out var strategyPositions) - ? strategyPositions + var positions = positionsByIdentifier.TryGetValue(strategy.Identifier, out var strategyPositions) + ? strategyPositions : new List(); return new UserStrategyDetailsViewModel @@ -645,6 +647,7 @@ public class DataController : ControllerBase Losses = agentSummary.Losses, ActiveStrategiesCount = agentSummary.ActiveStrategiesCount, TotalVolume = agentSummary.TotalVolume, + TotalBalance = agentSummary.TotalBalance, }; agentSummaryViewModels.Add(agentSummaryViewModel); diff --git a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs index 56d970b..72dba06 100644 --- a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs +++ b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs @@ -42,6 +42,11 @@ namespace Managing.Api.Models.Responses /// Total volume traded by this agent in USD /// public decimal TotalVolume { get; set; } + + /// + /// Total balance including USDC and open position values (without leverage, including PnL) + /// + public decimal TotalBalance { get; set; } } /// diff --git a/src/Managing.Api/Models/Responses/TopStrategiesViewModel.cs b/src/Managing.Api/Models/Responses/TopStrategiesViewModel.cs index 03789ac..4a9aaf0 100644 --- a/src/Managing.Api/Models/Responses/TopStrategiesViewModel.cs +++ b/src/Managing.Api/Models/Responses/TopStrategiesViewModel.cs @@ -9,11 +9,13 @@ namespace Managing.Api.Models.Responses /// Name of the strategy bot /// public string StrategyName { get; set; } - + /// /// Profit and Loss value of the strategy /// public decimal PnL { get; set; } + + public string AgentName { get; set; } } /// @@ -25,17 +27,17 @@ namespace Managing.Api.Models.Responses /// Name of the strategy bot /// public string StrategyName { get; set; } - + /// /// Return on Investment percentage of the strategy /// public decimal Roi { get; set; } - + /// /// Profit and Loss value of the strategy /// public decimal PnL { get; set; } - + /// /// Volume traded by the strategy /// @@ -52,7 +54,7 @@ namespace Managing.Api.Models.Responses /// public List TopStrategies { get; set; } = new List(); } - + /// /// View model containing the top performing strategies by ROI /// @@ -63,4 +65,4 @@ namespace Managing.Api.Models.Responses /// public List TopStrategiesByRoi { get; set; } = new List(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IAccountService.cs b/src/Managing.Application.Abstractions/Services/IAccountService.cs index 4d42ebf..e18a921 100644 --- a/src/Managing.Application.Abstractions/Services/IAccountService.cs +++ b/src/Managing.Application.Abstractions/Services/IAccountService.cs @@ -9,13 +9,13 @@ public interface IAccountService Task CreateAccount(User user, Account account); bool DeleteAccount(User user, string name); IEnumerable GetAccountsByUser(User user, bool hideSecrets = true); - Task> GetAccountsByUserAsync(User user, bool hideSecrets = true); + Task> GetAccountsByUserAsync(User user, bool hideSecrets = true, bool getBalance = false); Task> GetAccounts(bool hideSecrets, bool getBalance); Task> GetAccountsAsync(bool hideSecrets, bool getBalance); Task GetAccount(string name, bool hideSecrets, bool getBalance); public Task GetAccountByUser(User user, string name, bool hideSecrets, bool getBalance); public Task GetAccountByKey(string key, bool hideSecrets, bool getBalance); - + /// /// Gets an account by name directly from the repository. /// @@ -24,7 +24,7 @@ public interface IAccountService /// Whether to fetch the current balance /// The found account or null if not found Task GetAccountByAccountName(string accountName, bool hideSecrets = true, bool getBalance = false); - + IEnumerable GetAccountsBalancesByUser(User user, bool hideSecrets = true); Task> GetAccountsBalancesByUserAsync(User user, bool hideSecrets = true); Task GetGmxClaimableSummaryAsync(User user, string accountName); diff --git a/src/Managing.Application.Abstractions/Services/IUserService.cs b/src/Managing.Application.Abstractions/Services/IUserService.cs index 520e0be..0a42672 100644 --- a/src/Managing.Application.Abstractions/Services/IUserService.cs +++ b/src/Managing.Application.Abstractions/Services/IUserService.cs @@ -11,5 +11,6 @@ public interface IUserService Task UpdateTelegramChannel(User user, string telegramChannel); Task GetUserByName(string name); Task GetUserByAgentName(string agentName); + Task GetUserByIdAsync(int userId); Task> GetAllUsersAsync(); } \ No newline at end of file diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index 55b01a5..1afa7b3 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -173,13 +173,14 @@ public class AccountService : IAccountService return GetAccountsByUserAsync(user, hideSecrets).Result; } - public async Task> GetAccountsByUserAsync(User user, bool hideSecrets = true) + public async Task> GetAccountsByUserAsync(User user, bool hideSecrets = true, + bool getBalance = false) { var cacheKey = $"user-account-{user.Name}"; // For now, we'll get fresh data since caching async operations requires more complex logic // This can be optimized later with proper async caching - return await GetAccountsAsync(user, hideSecrets, false); + return await GetAccountsAsync(user, hideSecrets, getBalance); } private async Task> GetAccountsAsync(User user, bool hideSecrets, bool getBalance) diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs index 30f9847..f90f069 100644 --- a/src/Managing.Application/Bots/Grains/AgentGrain.cs +++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs @@ -14,6 +14,10 @@ public class AgentGrain : Grain, IAgentGrain, IRemindable private readonly ILogger _logger; private readonly IBotService _botService; private readonly IAgentService _agentService; + private readonly IExchangeService _exchangeService; + private readonly IUserService _userService; + private readonly IAccountService _accountService; + private readonly ITradingService _tradingService; private const string _updateSummaryReminderName = "UpdateAgentSummary"; public AgentGrain( @@ -21,12 +25,20 @@ public class AgentGrain : Grain, IAgentGrain, IRemindable IPersistentState state, ILogger logger, IBotService botService, - IAgentService agentService) + IAgentService agentService, + IExchangeService exchangeService, + IUserService userService, + IAccountService accountService, + ITradingService tradingService) { _state = state; _logger = logger; _botService = botService; _agentService = agentService; + _exchangeService = exchangeService; + _userService = userService; + _accountService = accountService; + _tradingService = tradingService; } public override Task OnActivateAsync(CancellationToken cancellationToken) @@ -122,6 +134,43 @@ public class AgentGrain : Grain, IAgentGrain, IRemindable runtime = bots.Max(b => b.StartupTime); } + // Calculate total balance (USDC + open positions value) + decimal totalBalance = 0; + try + { + var userId = (int)this.GetPrimaryKeyLong(); + var user = await _userService.GetUserByIdAsync(userId); + + if (user != null) + { + var userAccounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, true); + + foreach (var account in userAccounts) + { + // Get USDC balance + var usdcBalances = await _exchangeService.GetBalances(account); + var usdcBalance = usdcBalances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC")?.Amount ?? + 0; + totalBalance += usdcBalance; + } + + // Get positions for all bots using their GUIDs as InitiatorIdentifier + var botPositions = + await _tradingService.GetPositionsByInitiatorIdentifiersAsync(_state.State.BotIds); + + foreach (var position in botPositions.Where(p => !p.IsFinished())) + { + totalBalance += position.Open.Price * position.Open.Quantity; + totalBalance += position.ProfitAndLoss?.Realized ?? 0; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calculating total balance for agent {UserId}", this.GetPrimaryKeyLong()); + totalBalance = 0; // Set to 0 if calculation fails + } + var summary = new AgentSummary { UserId = (int)this.GetPrimaryKeyLong(), @@ -133,6 +182,7 @@ public class AgentGrain : Grain, IAgentGrain, IRemindable Runtime = runtime, ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Running), TotalVolume = totalVolume, + TotalBalance = totalBalance, }; // Save summary to database diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index 707e5d1..3e7516e 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -251,4 +251,17 @@ public class UserService : IUserService { return await _userRepository.GetAllUsersAsync(); } + + public async Task GetUserByIdAsync(int userId) + { + var allUsers = await _userRepository.GetAllUsersAsync(); + var user = allUsers.FirstOrDefault(u => u.Id == userId); + + if (user == null) + { + throw new Exception($"User with ID {userId} not found"); + } + + return user; + } } \ No newline at end of file diff --git a/src/Managing.Domain/Statistics/AgentSummary.cs b/src/Managing.Domain/Statistics/AgentSummary.cs index b56f794..615987a 100644 --- a/src/Managing.Domain/Statistics/AgentSummary.cs +++ b/src/Managing.Domain/Statistics/AgentSummary.cs @@ -44,4 +44,7 @@ public class AgentSummary [Id(12)] public decimal TotalVolume { get; set; } + + [Id(13)] + public decimal TotalBalance { get; set; } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs index 02ef88e..e87aa00 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/AgentSummaryRepository.cs @@ -193,7 +193,8 @@ public class AgentSummaryRepository : IAgentSummaryRepository CreatedAt = domain.CreatedAt, UpdatedAt = domain.UpdatedAt, ActiveStrategiesCount = domain.ActiveStrategiesCount, - TotalVolume = domain.TotalVolume + TotalVolume = domain.TotalVolume, + TotalBalance = domain.TotalBalance }; } @@ -208,6 +209,7 @@ public class AgentSummaryRepository : IAgentSummaryRepository entity.Runtime = domain.Runtime; entity.ActiveStrategiesCount = domain.ActiveStrategiesCount; entity.TotalVolume = domain.TotalVolume; + entity.TotalBalance = domain.TotalBalance; } private static AgentSummary MapToDomain(AgentSummaryEntity entity) @@ -226,6 +228,7 @@ public class AgentSummaryRepository : IAgentSummaryRepository UpdatedAt = entity.UpdatedAt, ActiveStrategiesCount = entity.ActiveStrategiesCount, TotalVolume = entity.TotalVolume, + TotalBalance = entity.TotalBalance, User = PostgreSqlMappers.Map(entity.User) }; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs index af9a258..67681af 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/AgentSummaryEntity.cs @@ -14,6 +14,7 @@ public class AgentSummaryEntity public DateTime UpdatedAt { get; set; } public int ActiveStrategiesCount { get; set; } public decimal TotalVolume { get; set; } + public decimal TotalBalance { get; set; } // Navigation property public UserEntity User { get; set; }