diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 1efe2a1..d806dfb 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -12,6 +12,7 @@ using Managing.Domain.Scenarios; using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Strategies.Base; +using Managing.Domain.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -409,11 +410,17 @@ public class DataController : ControllerBase // Get all strategies for the specified user var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName)); - // Convert to detailed view model with additional information - var tasks = userStrategies.Select(strategy => MapStrategyToViewModelAsync(strategy)); - var result = await Task.WhenAll(tasks); + // Get all positions for all strategies in a single database call to avoid DbContext concurrency issues + var strategyIdentifiers = userStrategies.Select(s => s.Identifier).ToList(); + var allPositions = await _tradingService.GetPositionsByInitiatorIdentifiersAsync(strategyIdentifiers); + var positionsByIdentifier = allPositions.GroupBy(p => p.InitiatorIdentifier) + .ToDictionary(g => g.Key, g => g.ToList()); - return Ok(result.ToList()); + // Convert to detailed view model with additional information + var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy, positionsByIdentifier)) + .ToList(); + + return Ok(result); } /// @@ -439,6 +446,56 @@ public class DataController : ControllerBase return Ok(result); } + /// + /// Maps a trading bot to a strategy view model with detailed statistics using pre-fetched positions + /// + /// 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) + { + // Calculate ROI percentage based on PnL relative to account value + decimal pnl = strategy.Pnl; + + // If we had initial investment amount, we could calculate ROI like: + decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account + decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0; + + // Calculate volume statistics + decimal totalVolume = strategy.Volume; + decimal volumeLast24h = strategy.Volume; + + // Calculate win/loss statistics + (int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses); + + int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0; + // Calculate ROI for last 24h + decimal roiLast24h = strategy.Roi; + + // Get positions for this strategy from pre-fetched data + var positions = positionsByIdentifier.TryGetValue(strategy.Identifier, out var strategyPositions) + ? strategyPositions + : new List(); + + return new UserStrategyDetailsViewModel + { + Name = strategy.Name, + State = strategy.Status, + PnL = pnl, + ROIPercentage = roi, + ROILast24H = roiLast24h, + Runtime = strategy.StartupTime, + WinRate = winRate, + TotalVolumeTraded = totalVolume, + VolumeLast24H = volumeLast24h, + Wins = wins, + Losses = losses, + Positions = positions, + Identifier = strategy.Identifier, + WalletBalances = new Dictionary(), + }; + } + /// /// Maps a trading bot to a strategy view model with detailed statistics /// diff --git a/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs b/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs index 8ba0e92..c04735a 100644 --- a/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/ITradingRepository.cs @@ -26,6 +26,7 @@ public interface ITradingRepository Task> GetPositionsAsync(PositionInitiator positionInitiator); Task> GetPositionsByStatusAsync(PositionStatus positionStatus); Task> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier); + Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers); Task> GetAllPositionsAsync(); Task UpdateScenarioAsync(Scenario scenario); diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index bc40927..81d8717 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -37,6 +37,7 @@ public interface ITradingService Task> GetBrokerPositions(Account account); Task> GetAllDatabasePositionsAsync(); Task> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier); + Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers); Task InitPrivyWallet(string publicAddress); // Synth API integration methods diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index a11e42a..3037f4e 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -260,6 +260,11 @@ public class TradingService : ITradingService return await _tradingRepository.GetPositionsByInitiatorIdentifierAsync(initiatorIdentifier); } + public async Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers) + { + return await _tradingRepository.GetPositionsByInitiatorIdentifiersAsync(initiatorIdentifiers); + } + private async Task ManageTrader(TraderFollowup a, List tickers) { var shortAddress = a.Account.Address.Substring(0, 6); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs index a163de9..3a77c03 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlTradingRepository.cs @@ -268,17 +268,26 @@ public class PostgreSqlTradingRepository : ITradingRepository public async Task GetPositionByIdentifierAsync(Guid identifier) { - var position = await _context.Positions - .AsNoTracking() - .Include(p => p.User) - .Include(p => p.OpenTrade) - .Include(p => p.StopLossTrade) - .Include(p => p.TakeProfit1Trade) - .Include(p => p.TakeProfit2Trade) - .FirstOrDefaultAsync(p => p.Identifier == identifier) - .ConfigureAwait(false); + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var position = await _context.Positions + .AsNoTracking() + .Include(p => p.User) + .Include(p => p.OpenTrade) + .Include(p => p.StopLossTrade) + .Include(p => p.TakeProfit1Trade) + .Include(p => p.TakeProfit2Trade) + .FirstOrDefaultAsync(p => p.Identifier == identifier) + .ConfigureAwait(false); - return PostgreSqlMappers.Map(position); + return PostgreSqlMappers.Map(position); + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } } public IEnumerable GetPositions(PositionInitiator positionInitiator) @@ -413,6 +422,28 @@ public class PostgreSqlTradingRepository : ITradingRepository return PostgreSqlMappers.Map(positions); } + public async Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers) + { + var identifiersList = initiatorIdentifiers.ToList(); + if (!identifiersList.Any()) + { + return Enumerable.Empty(); + } + + var positions = await _context.Positions + .AsNoTracking() + .Include(p => p.User) + .Include(p => p.OpenTrade) + .Include(p => p.StopLossTrade) + .Include(p => p.TakeProfit1Trade) + .Include(p => p.TakeProfit2Trade) + .Where(p => identifiersList.Contains(p.InitiatorIdentifier)) + .ToListAsync() + .ConfigureAwait(false); + + return PostgreSqlMappers.Map(positions); + } + public async Task> GetAllPositionsAsync() { var positions = await _context.Positions