diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 4477877a..486114b2 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -1,5 +1,6 @@ using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; @@ -38,6 +39,7 @@ public class DataController : ControllerBase private readonly ITradingService _tradingService; private readonly IGrainFactory _grainFactory; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IBotService _botService; /// /// Initializes a new instance of the class. @@ -52,6 +54,7 @@ public class DataController : ControllerBase /// Service for trading operations. /// Orleans grain factory for accessing grains. /// Service scope factory for creating scoped services. + /// Service for bot operations. public DataController( IExchangeService exchangeService, IAccountService accountService, @@ -61,7 +64,8 @@ public class DataController : ControllerBase IMediator mediator, ITradingService tradingService, IGrainFactory grainFactory, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IBotService botService) { _exchangeService = exchangeService; _accountService = accountService; @@ -72,6 +76,7 @@ public class DataController : ControllerBase _tradingService = tradingService; _grainFactory = grainFactory; _serviceScopeFactory = serviceScopeFactory; + _botService = botService; } /// @@ -765,6 +770,86 @@ public class DataController : ControllerBase }; } + /// + /// Retrieves a paginated list of strategies (bots) excluding those with Saved status + /// + /// Page number (1-based, defaults to 1) + /// Number of items per page (defaults to 10, max 100) + /// Filter by name (partial match, case-insensitive) + /// Filter by ticker (partial match, case-insensitive) + /// Filter by agent name (partial match, case-insensitive) + /// Sort field (defaults to CreateDate) + /// Sort direction - "Asc" or "Desc" (defaults to "Desc") + /// A paginated list of strategies excluding Saved status bots + [HttpGet("GetStrategiesPaginated")] + public async Task>> GetStrategiesPaginated( + int pageNumber = 1, + int pageSize = 10, + string? name = null, + string? ticker = null, + string? agentName = null, + BotSortableColumn sortBy = BotSortableColumn.CreateDate, + string sortDirection = "Desc") + { + // Validate pagination parameters + if (pageNumber < 1) + { + return BadRequest("Page number must be greater than 0"); + } + + if (pageSize < 1 || pageSize > 100) + { + return BadRequest("Page size must be between 1 and 100"); + } + + // Validate sort direction + if (sortDirection != "Asc" && sortDirection != "Desc") + { + return BadRequest("Sort direction must be 'Asc' or 'Desc'"); + } + + try + { + // Get paginated bots excluding Saved status + var (bots, totalCount) = await _botService.GetBotsPaginatedAsync( + pageNumber, + pageSize, + null, // No specific status filter - we'll exclude Saved in the service call + name, + ticker, + agentName, + sortBy, + sortDirection); + + // Filter out Saved status bots + var filteredBots = bots.Where(bot => bot.Status != BotStatus.Saved).ToList(); + var filteredCount = totalCount - bots.Count(bot => bot.Status == BotStatus.Saved); + + // Map to response objects + var tradingBotResponses = MapBotsToTradingBotResponse(filteredBots); + + // Calculate pagination metadata + var totalPages = (int)Math.Ceiling((double)filteredCount / pageSize); + + var response = new PaginatedResponse + { + Items = tradingBotResponses.ToList(), + TotalCount = filteredCount, + PageNumber = pageNumber, + PageSize = pageSize, + TotalPages = totalPages, + HasPreviousPage = pageNumber > 1, + HasNextPage = pageNumber < totalPages + }; + + return Ok(response); + } + catch (Exception ex) + { + return StatusCode(500, $"Error retrieving strategies: {ex.Message}"); + } + } + /// /// Maps a Position domain object to a PositionViewModel /// @@ -789,4 +874,35 @@ public class DataController : ControllerBase Identifier = position.Identifier, }; } + + /// + /// Maps a collection of Bot entities to TradingBotResponse objects. + /// + /// The collection of bots to map + /// A list of TradingBotResponse objects + private static List MapBotsToTradingBotResponse(IEnumerable bots) + { + var list = new List(); + + foreach (var item in bots) + { + list.Add(new TradingBotResponse + { + Status = item.Status.ToString(), + WinRate = (item.TradeWins + item.TradeLosses) != 0 + ? item.TradeWins / (item.TradeWins + item.TradeLosses) + : 0, + ProfitAndLoss = item.Pnl, + Roi = item.Roi, + Identifier = item.Identifier.ToString(), + AgentName = item.User.AgentName, + CreateDate = item.CreateDate, + StartupTime = item.StartupTime, + Name = item.Name, + Ticker = item.Ticker, + }); + } + + return list; + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/InfluxDb/AgentBalanceRepository.cs b/src/Managing.Infrastructure.Database/InfluxDb/AgentBalanceRepository.cs index 0e3ecca4..ae74ca2f 100644 --- a/src/Managing.Infrastructure.Database/InfluxDb/AgentBalanceRepository.cs +++ b/src/Managing.Infrastructure.Database/InfluxDb/AgentBalanceRepository.cs @@ -46,11 +46,48 @@ public class AgentBalanceRepository : IAgentBalanceRepository { var results = await _influxDbRepository.QueryAsync(async query => { + var effectiveEnd = end ?? DateTime.UtcNow; + var timeRange = effectiveEnd - start; + + // Determine sampling interval based on time range to limit data points + string samplingInterval; + if (timeRange.TotalDays <= 1) + { + // Less than 1 day: 5-minute intervals (max ~288 points) + samplingInterval = "5m"; + } + else if (timeRange.TotalDays <= 7) + { + // 1-7 days: 30-minute intervals (max ~336 points) + samplingInterval = "30m"; + } + else if (timeRange.TotalDays <= 30) + { + // 1-30 days: 2-hour intervals (max ~360 points) + samplingInterval = "2h"; + } + else if (timeRange.TotalDays <= 90) + { + // 1-3 months: 6-hour intervals (max ~360 points) + samplingInterval = "6h"; + } + else if (timeRange.TotalDays <= 365) + { + // 3-12 months: 1-day intervals (max ~365 points) + samplingInterval = "1d"; + } + else + { + // More than 1 year: 7-day intervals (max ~52 points) + samplingInterval = "7d"; + } + var flux = $"from(bucket:\"{_balanceBucket}\") " + $"|> range(start: {start:s}Z" + (end.HasValue ? $", stop: {end.Value:s}Z" : "") + $") " + $"|> filter(fn: (r) => r[\"user_id\"] == \"{userId}\") " + + $"|> aggregateWindow(every: {samplingInterval}, fn: last, createEmpty: false) " + $"|> pivot(rowKey: [\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")"; var result = await query.QueryAsync(flux, _influxDbRepository.Organization); @@ -69,52 +106,4 @@ public class AgentBalanceRepository : IAgentBalanceRepository return results; } - - public async Task<(IList result, int totalCount)> GetAllAgentBalancesWithHistory( - DateTime start, DateTime? end) - { - var results = await _influxDbRepository.QueryAsync(async query => - { - // Get all balances within the time range, pivoted so each row is a full AgentBalanceDto - var flux = $@" - from(bucket: ""{_balanceBucket}"") - |> range(start: {start:s}Z{(end.HasValue ? $", stop: {end.Value:s}Z" : "")}) - |> filter(fn: (r) => r._measurement == ""agent_balance"") - |> pivot(rowKey: [""_time""], columnKey: [""_field""], valueColumn: ""_value"") - "; - - var balances = await query.QueryAsync(flux, _influxDbRepository.Organization); - - // Group balances by user ID - var agentGroups = balances - .GroupBy(b => b.UserId) - .Select(g => - { - var userBalances = g.Select(b => new AgentBalance - { - UserId = b.UserId, - TotalBalanceValue = b.TotalBalanceValue, - UsdcWalletValue = b.UsdcWalletValue, - UsdcInPositionsValue = b.UsdcInPositionsValue, - BotsAllocationUsdValue = b.BotsAllocationUsdValue, - PnL = b.PnL, - Time = b.Time - }).OrderBy(b => b.Time).ToList(); - - // Use a default agent name since we don't store it in AgentBalance anymore - var mostRecentAgentName = $"User_{g.Key}"; - - return new AgentBalanceHistory - { - UserId = g.Key, - AgentName = mostRecentAgentName, - AgentBalances = userBalances - }; - }).ToList(); - - return (agentGroups, agentGroups.Count); - }); - - return results; - } } \ No newline at end of file