using System.Diagnostics; using Exilion.TradingAtomics; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Shared; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Users; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; public class PostgreSqlBacktestRepository : IBacktestRepository { private readonly ManagingDbContext _context; public PostgreSqlBacktestRepository(ManagingDbContext context) { _context = context; } // User-specific operations public void InsertBacktestForUser(User user, Backtest result) { ValidateBacktestData(result); result.User = user; var entity = PostgreSqlMappers.Map(result); _context.Backtests.Add(entity); _context.SaveChanges(); } public async Task InsertBacktestForUserAsync(User user, Backtest result) { ValidateBacktestData(result); result.User = user; var entity = PostgreSqlMappers.Map(result); _context.Backtests.Add(entity); await _context.SaveChangesAsync(); } /// /// Validates that all numeric fields in the backtest are of the correct type /// private void ValidateBacktestData(Backtest backtest) { // Ensure FinalPnl is a valid decimal if (backtest.FinalPnl.GetType() != typeof(decimal)) { throw new InvalidOperationException( $"FinalPnl must be of type decimal, but got {backtest.FinalPnl.GetType().Name}"); } // Ensure other numeric fields are correct if (backtest.GrowthPercentage.GetType() != typeof(decimal)) { throw new InvalidOperationException( $"GrowthPercentage must be of type decimal, but got {backtest.GrowthPercentage.GetType().Name}"); } if (backtest.HodlPercentage.GetType() != typeof(decimal)) { throw new InvalidOperationException( $"HodlPercentage must be of type decimal, but got {backtest.HodlPercentage.GetType().Name}"); } if (backtest.Score.GetType() != typeof(double)) { throw new InvalidOperationException( $"Score must be of type double, but got {backtest.Score.GetType().Name}"); } if (backtest.WinRate.GetType() != typeof(int)) { throw new InvalidOperationException( $"WinRate must be of type int, but got {backtest.WinRate.GetType().Name}"); } } public IEnumerable GetBacktestsByUser(User user) { var entities = _context.Backtests .AsNoTracking() .Include(b => b.User) .Where(b => b.UserId == user.Id) .ToList(); return entities.Select(PostgreSqlMappers.Map); } public async Task> GetBacktestsByUserAsync(User user) { var entities = await _context.Backtests .AsNoTracking() .Include(b => b.User) .Where(b => b.UserId == user.Id) .ToListAsync() .ConfigureAwait(false); return entities.Select(PostgreSqlMappers.Map); } public IEnumerable GetBacktestsByRequestId(Guid requestId) { var entities = _context.Backtests .AsNoTracking() .Where(b => b.RequestId == requestId) .ToList(); return entities.Select(PostgreSqlMappers.Map); } public async Task> GetBacktestsByRequestIdAsync(Guid requestId) { var entities = await _context.Backtests .AsNoTracking() .Where(b => b.RequestId == requestId) .ToListAsync() .ConfigureAwait(false); return entities.Select(PostgreSqlMappers.Map); } public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var stopwatch = Stopwatch.StartNew(); var baseQuery = _context.Backtests .AsNoTracking() .Where(b => b.RequestId == requestId); var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = baseQuery.Count(); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting IQueryable sortedQuery = sortBy.ToLower() switch { "score" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), "finalpnl" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), "winrate" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), "growthpercentage" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), "hodlpercentage" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) }; var afterSortMs = stopwatch.ElapsedMilliseconds; var entities = sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); var afterToListMs = stopwatch.ElapsedMilliseconds; Console.WriteLine( $"[PostgreSqlBacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms"); var mappedBacktests = entities.Select(entity => new LightBacktest { Id = entity.Identifier, Config = JsonConvert.DeserializeObject(entity.ConfigJson), Ticker = entity.Ticker, FinalPnl = entity.FinalPnl, WinRate = entity.WinRate, GrowthPercentage = entity.GrowthPercentage, HodlPercentage = entity.HodlPercentage, StartDate = entity.StartDate, EndDate = entity.EndDate, MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson) ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.MaxDrawdown : null, Fees = entity.Fees, SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson) ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.SharpeRatio != null ? (double?)JsonConvert.DeserializeObject(entity.StatisticsJson).SharpeRatio : null : null, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty, InitialBalance = entity.InitialBalance, NetPnl = entity.NetPnl, PositionCount = entity.PositionCount }); return (mappedBacktests, totalCount); } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync( Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var stopwatch = Stopwatch.StartNew(); var baseQuery = _context.Backtests .AsNoTracking() .Where(b => b.RequestId == requestId); var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = await baseQuery.CountAsync().ConfigureAwait(false); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting IQueryable sortedQuery = sortBy.ToLower() switch { "score" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), "finalpnl" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), "winrate" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), "growthpercentage" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), "hodlpercentage" => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) }; var afterSortMs = stopwatch.ElapsedMilliseconds; var entities = await sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync() .ConfigureAwait(false); var afterToListMs = stopwatch.ElapsedMilliseconds; Console.WriteLine( $"[PostgreSqlBacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms"); var mappedBacktests = entities.Select(entity => new LightBacktest { Id = entity.Identifier, Config = JsonConvert.DeserializeObject(entity.ConfigJson), Ticker = entity.Ticker, FinalPnl = entity.FinalPnl, WinRate = entity.WinRate, GrowthPercentage = entity.GrowthPercentage, HodlPercentage = entity.HodlPercentage, StartDate = entity.StartDate, EndDate = entity.EndDate, MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson) ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.MaxDrawdown : null, Fees = entity.Fees, SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson) ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.SharpeRatio != null ? (double?)JsonConvert.DeserializeObject(entity.StatisticsJson).SharpeRatio : null : null, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty, InitialBalance = entity.InitialBalance, NetPnl = entity.NetPnl, PositionCount = entity.PositionCount }); return (mappedBacktests, totalCount); } public Backtest GetBacktestByIdForUser(User user, string id) { var entity = _context.Backtests .AsNoTracking() .Include(b => b.User) .FirstOrDefault(b => b.Identifier == id && b.UserId == user.Id); return entity != null ? PostgreSqlMappers.Map(entity) : null; } public async Task GetBacktestByIdForUserAsync(User user, string id) { var entity = await _context.Backtests .AsNoTracking() .Include(b => b.User) .FirstOrDefaultAsync(b => b.Identifier == id && b.UserId == user.Id) .ConfigureAwait(false); return entity != null ? PostgreSqlMappers.Map(entity) : null; } public void DeleteBacktestByIdForUser(User user, string id) { var entity = _context.Backtests .AsTracking() .FirstOrDefault(b => b.Identifier == id && b.UserId == user.Id); if (entity != null) { _context.Backtests.Remove(entity); _context.SaveChanges(); } } public async Task DeleteBacktestByIdForUserAsync(User user, string id) { var entity = await _context.Backtests .AsTracking() .FirstOrDefaultAsync(b => b.Identifier == id && b.UserId == user.Id) .ConfigureAwait(false); if (entity != null) { _context.Backtests.Remove(entity); await _context.SaveChangesAsync().ConfigureAwait(false); } } public void DeleteBacktestsByIdsForUser(User user, IEnumerable ids) { var entities = _context.Backtests .AsTracking() .Where(b => b.UserId == user.Id && ids.Contains(b.Identifier)) .ToList(); if (entities.Any()) { _context.Backtests.RemoveRange(entities); _context.SaveChanges(); } } public async Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable ids) { var entities = await _context.Backtests .AsTracking() .Where(b => b.UserId == user.Id && ids.Contains(b.Identifier)) .ToListAsync() .ConfigureAwait(false); if (entities.Any()) { _context.Backtests.RemoveRange(entities); await _context.SaveChangesAsync().ConfigureAwait(false); } } public void DeleteAllBacktestsForUser(User user) { var entities = _context.Backtests .AsTracking() .Where(b => b.UserId == user.Id) .ToList(); if (entities.Any()) { _context.Backtests.RemoveRange(entities); _context.SaveChanges(); } } public void DeleteBacktestsByRequestId(Guid requestId) { var entities = _context.Backtests .AsTracking() .Where(b => b.RequestId == requestId) .ToList(); if (entities.Any()) { _context.Backtests.RemoveRange(entities); _context.SaveChanges(); } } public async Task DeleteBacktestsByRequestIdAsync(Guid requestId) { var entities = await _context.Backtests .AsTracking() .Where(b => b.RequestId == requestId) .ToListAsync() .ConfigureAwait(false); if (entities.Any()) { _context.Backtests.RemoveRange(entities); await _context.SaveChangesAsync().ConfigureAwait(false); } } public async Task DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter) { var baseQuery = _context.Backtests .AsTracking() .Where(b => b.UserId == user.Id); if (filter != null) { if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameLike = $"%{filter.NameContains.Trim()}%"; baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); } if (filter.ScoreMin.HasValue) baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); if (filter.ScoreMax.HasValue) baseQuery = baseQuery.Where(b => b.Score <= filter.ScoreMax.Value); if (filter.WinrateMin.HasValue) baseQuery = baseQuery.Where(b => b.WinRate >= filter.WinrateMin.Value); if (filter.WinrateMax.HasValue) baseQuery = baseQuery.Where(b => b.WinRate <= filter.WinrateMax.Value); if (filter.MaxDrawdownMax.HasValue) baseQuery = baseQuery.Where(b => b.MaxDrawdown <= filter.MaxDrawdownMax.Value); if (filter.Tickers != null && filter.Tickers.Any()) { var tickerArray = filter.Tickers.ToArray(); baseQuery = baseQuery.Where(b => tickerArray.Contains(b.Ticker)); } if (filter.Indicators != null && filter.Indicators.Any()) { foreach (var ind in filter.Indicators) { var token = "," + ind + ","; baseQuery = baseQuery.Where(b => ("," + b.IndicatorsCsv + ",").Contains(token)); } } if (filter.DurationMin.HasValue) baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); if (filter.DurationMax.HasValue) baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); if (filter.TradingType.HasValue) baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value); } var entities = await baseQuery.ToListAsync().ConfigureAwait(false); var count = entities.Count; if (count > 0) { _context.Backtests.RemoveRange(entities); await _context.SaveChangesAsync().ConfigureAwait(false); } return count; } public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated( User user, int page, int pageSize, BacktestSortableColumn sortBy = BacktestSortableColumn.Score, string sortOrder = "desc", BacktestsFilter? filter = null) { var stopwatch = Stopwatch.StartNew(); var baseQuery = _context.Backtests .AsNoTracking() .Where(b => b.UserId == user.Id); if (filter != null) { if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameLike = $"%{filter.NameContains.Trim()}%"; baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); } if (filter.ScoreMin.HasValue) baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); if (filter.ScoreMax.HasValue) baseQuery = baseQuery.Where(b => b.Score <= filter.ScoreMax.Value); if (filter.WinrateMin.HasValue) baseQuery = baseQuery.Where(b => b.WinRate >= filter.WinrateMin.Value); if (filter.WinrateMax.HasValue) baseQuery = baseQuery.Where(b => b.WinRate <= filter.WinrateMax.Value); if (filter.MaxDrawdownMax.HasValue) baseQuery = baseQuery.Where(b => b.MaxDrawdown <= filter.MaxDrawdownMax.Value); if (filter.Tickers != null && filter.Tickers.Any()) { var tickerArray = filter.Tickers.ToArray(); baseQuery = baseQuery.Where(b => tickerArray.Contains(b.Ticker)); } if (filter.Indicators != null && filter.Indicators.Any()) { foreach (var ind in filter.Indicators) { var token = "," + ind + ","; baseQuery = baseQuery.Where(b => ("," + b.IndicatorsCsv + ",").Contains(token)); } } if (filter.DurationMin.HasValue) baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); if (filter.DurationMax.HasValue) baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); if (filter.TradingType.HasValue) baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value); } var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = baseQuery.Count(); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting IQueryable sortedQuery = sortBy switch { BacktestSortableColumn.Score => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), BacktestSortableColumn.FinalPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), BacktestSortableColumn.NetPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.NetPnl) : baseQuery.OrderBy(b => b.NetPnl), BacktestSortableColumn.WinRate => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), BacktestSortableColumn.GrowthPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), BacktestSortableColumn.HodlPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), BacktestSortableColumn.Duration => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Duration) : baseQuery.OrderBy(b => b.Duration), BacktestSortableColumn.Timeframe => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Timeframe) : baseQuery.OrderBy(b => b.Timeframe), BacktestSortableColumn.IndicatorsCount => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.IndicatorsCount) : baseQuery.OrderBy(b => b.IndicatorsCount), BacktestSortableColumn.MaxDrawdown => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.MaxDrawdown) : baseQuery.OrderBy(b => b.MaxDrawdown), BacktestSortableColumn.Fees => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Fees) : baseQuery.OrderBy(b => b.Fees), BacktestSortableColumn.SharpeRatio => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.SharpeRatio) : baseQuery.OrderBy(b => b.SharpeRatio), BacktestSortableColumn.Ticker => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Ticker) : baseQuery.OrderBy(b => b.Ticker), BacktestSortableColumn.Name => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Name) : baseQuery.OrderBy(b => b.Name), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) }; var afterSortMs = stopwatch.ElapsedMilliseconds; var entities = sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); var afterToListMs = stopwatch.ElapsedMilliseconds; Console.WriteLine( $"[PostgreSqlBacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms"); var mappedBacktests = entities.Select(entity => new LightBacktest { Id = entity.Identifier, Config = JsonConvert.DeserializeObject(entity.ConfigJson), Ticker = entity.Ticker, FinalPnl = entity.FinalPnl, WinRate = entity.WinRate, GrowthPercentage = entity.GrowthPercentage, HodlPercentage = entity.HodlPercentage, StartDate = entity.StartDate, EndDate = entity.EndDate, MaxDrawdown = entity.MaxDrawdown, Fees = entity.Fees, SharpeRatio = (double?)entity.SharpeRatio, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty, InitialBalance = entity.InitialBalance, NetPnl = entity.NetPnl, PositionCount = entity.PositionCount }); return (mappedBacktests, totalCount); } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( User user, int page, int pageSize, BacktestSortableColumn sortBy = BacktestSortableColumn.Score, string sortOrder = "desc", BacktestsFilter? filter = null) { var stopwatch = Stopwatch.StartNew(); var baseQuery = _context.Backtests .AsNoTracking() .Where(b => b.UserId == user.Id); if (filter != null) { if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameLike = $"%{filter.NameContains.Trim()}%"; baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); } if (filter.ScoreMin.HasValue) baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); if (filter.ScoreMax.HasValue) baseQuery = baseQuery.Where(b => b.Score <= filter.ScoreMax.Value); if (filter.WinrateMin.HasValue) baseQuery = baseQuery.Where(b => b.WinRate >= filter.WinrateMin.Value); if (filter.WinrateMax.HasValue) baseQuery = baseQuery.Where(b => b.WinRate <= filter.WinrateMax.Value); if (filter.MaxDrawdownMax.HasValue) baseQuery = baseQuery.Where(b => b.MaxDrawdown <= filter.MaxDrawdownMax.Value); if (filter.Tickers != null && filter.Tickers.Any()) { var tickerArray = filter.Tickers.ToArray(); baseQuery = baseQuery.Where(b => tickerArray.Contains(b.Ticker)); } if (filter.Indicators != null && filter.Indicators.Any()) { foreach (var ind in filter.Indicators) { var token = "," + ind + ","; baseQuery = baseQuery.Where(b => ("," + b.IndicatorsCsv + ",").Contains(token)); } } if (filter.DurationMin.HasValue) baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); if (filter.DurationMax.HasValue) baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); if (filter.TradingType.HasValue) baseQuery = baseQuery.Where(b => b.TradingType == (int)filter.TradingType.Value); } var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = await baseQuery.CountAsync().ConfigureAwait(false); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting IQueryable sortedQuery = sortBy switch { BacktestSortableColumn.Score => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), BacktestSortableColumn.FinalPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), BacktestSortableColumn.NetPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.NetPnl) : baseQuery.OrderBy(b => b.NetPnl), BacktestSortableColumn.WinRate => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), BacktestSortableColumn.GrowthPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), BacktestSortableColumn.HodlPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), BacktestSortableColumn.Duration => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Duration) : baseQuery.OrderBy(b => b.Duration), BacktestSortableColumn.Timeframe => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Timeframe) : baseQuery.OrderBy(b => b.Timeframe), BacktestSortableColumn.IndicatorsCount => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.IndicatorsCount) : baseQuery.OrderBy(b => b.IndicatorsCount), BacktestSortableColumn.MaxDrawdown => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.MaxDrawdown) : baseQuery.OrderBy(b => b.MaxDrawdown), BacktestSortableColumn.Fees => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Fees) : baseQuery.OrderBy(b => b.Fees), BacktestSortableColumn.SharpeRatio => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.SharpeRatio) : baseQuery.OrderBy(b => b.SharpeRatio), BacktestSortableColumn.Ticker => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Ticker) : baseQuery.OrderBy(b => b.Ticker), BacktestSortableColumn.Name => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Name) : baseQuery.OrderBy(b => b.Name), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) }; var afterSortMs = stopwatch.ElapsedMilliseconds; var entities = await sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync() .ConfigureAwait(false); var afterToListMs = stopwatch.ElapsedMilliseconds; Console.WriteLine( $"[PostgreSqlBacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms"); var mappedBacktests = entities.Select(entity => new LightBacktest { Id = entity.Identifier, Config = JsonConvert.DeserializeObject(entity.ConfigJson), Ticker = entity.Ticker, FinalPnl = entity.FinalPnl, WinRate = entity.WinRate, GrowthPercentage = entity.GrowthPercentage, HodlPercentage = entity.HodlPercentage, StartDate = entity.StartDate, EndDate = entity.EndDate, MaxDrawdown = entity.MaxDrawdown, Fees = entity.Fees, SharpeRatio = (double?)entity.SharpeRatio, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty, InitialBalance = entity.InitialBalance, NetPnl = entity.NetPnl, PositionCount = entity.PositionCount }); return (mappedBacktests, totalCount); } // Bundle backtest methods public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) { bundleRequest.User = user; // Set the UserId by finding the user entity var userEntity = _context.Users.FirstOrDefault(u => u.Name == user.Name); if (userEntity != null) { // Check for existing bundle requests with the same name for this user var maxVersion = _context.BundleBacktestRequests .Where(b => b.UserId == userEntity.Id && b.Name == bundleRequest.Name) .Max(b => (int?)b.Version); // Increment version if a bundle with the same name exists bundleRequest.Version = (maxVersion ?? 0) + 1; } var entity = PostgreSqlMappers.Map(bundleRequest); if (userEntity != null) { entity.UserId = userEntity.Id; } _context.BundleBacktestRequests.Add(entity); _context.SaveChanges(); } public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest) { bundleRequest.User = user; // Set the UserId by finding the user entity var userEntity = await _context.Users.FirstOrDefaultAsync(u => u.Name == user.Name); if (userEntity != null) { // Check for existing bundle requests with the same name for this user var maxVersion = await _context.BundleBacktestRequests .Where(b => b.UserId == userEntity.Id && b.Name == bundleRequest.Name) .MaxAsync(b => (int?)b.Version); // Increment version if a bundle with the same name exists bundleRequest.Version = (maxVersion ?? 0) + 1; } var entity = PostgreSqlMappers.Map(bundleRequest); if (userEntity != null) { entity.UserId = userEntity.Id; } await _context.BundleBacktestRequests.AddAsync(entity); await _context.SaveChangesAsync(); } public IEnumerable GetBundleBacktestRequestsByUser(User user) { var entities = _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .Where(b => b.UserId == user.Id) .OrderByDescending(b => b.CreatedAt) .ToList(); return entities.Select(PostgreSqlMappers.Map); } public async Task> GetBundleBacktestRequestsByUserAsync(User user) { var entities = await _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .Where(b => b.UserId == user.Id) .OrderByDescending(b => b.CreatedAt) .ToListAsync() .ConfigureAwait(false); return entities.Select(PostgreSqlMappers.Map); } public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) { var entity = _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .FirstOrDefault(b => b.RequestId == id && b.UserId == user.Id); return entity != null ? PostgreSqlMappers.Map(entity) : null; } public async Task GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) { var entity = await _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .FirstOrDefaultAsync(b => b.RequestId == id && b.UserId == user.Id) .ConfigureAwait(false); return entity != null ? PostgreSqlMappers.Map(entity) : null; } public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) { var entity = _context.BundleBacktestRequests .AsTracking() .FirstOrDefault(b => b.RequestId == bundleRequest.RequestId); if (entity != null) { // Update the entity properties entity.Status = bundleRequest.Status; entity.CompletedAt = bundleRequest.CompletedAt; entity.CompletedBacktests = bundleRequest.CompletedBacktests; entity.FailedBacktests = bundleRequest.FailedBacktests; entity.ErrorMessage = bundleRequest.ErrorMessage; entity.ProgressInfo = bundleRequest.ProgressInfo; entity.CurrentBacktest = bundleRequest.CurrentBacktest; entity.EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds; entity.Version = bundleRequest.Version; // Preserve the version entity.UpdatedAt = DateTime.UtcNow; // Serialize Results to JSON if (bundleRequest.Results != null && bundleRequest.Results.Any()) { try { entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results); } catch { entity.ResultsJson = "[]"; } } else { entity.ResultsJson = "[]"; } _context.SaveChanges(); } } public async Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest) { var entity = await _context.BundleBacktestRequests .AsTracking() .FirstOrDefaultAsync(b => b.RequestId == bundleRequest.RequestId) .ConfigureAwait(false); if (entity != null) { // Update the entity properties entity.Status = bundleRequest.Status; entity.CompletedAt = bundleRequest.CompletedAt; entity.CompletedBacktests = bundleRequest.CompletedBacktests; entity.FailedBacktests = bundleRequest.FailedBacktests; entity.ErrorMessage = bundleRequest.ErrorMessage; entity.ProgressInfo = bundleRequest.ProgressInfo; entity.CurrentBacktest = bundleRequest.CurrentBacktest; entity.EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds; entity.Version = bundleRequest.Version; // Preserve the version entity.UpdatedAt = DateTime.UtcNow; // Serialize Results to JSON if (bundleRequest.Results != null && bundleRequest.Results.Any()) { try { entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results); } catch { entity.ResultsJson = "[]"; } } else { entity.ResultsJson = "[]"; } await _context.SaveChangesAsync().ConfigureAwait(false); } } public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) { var entity = _context.BundleBacktestRequests .AsTracking() .FirstOrDefault(b => b.RequestId == id && b.UserId == user.Id); if (entity != null) { _context.BundleBacktestRequests.Remove(entity); _context.SaveChanges(); } } public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) { var entity = await _context.BundleBacktestRequests .AsTracking() .FirstOrDefaultAsync(b => b.RequestId == id && b.UserId == user.Id) .ConfigureAwait(false); if (entity != null) { _context.BundleBacktestRequests.Remove(entity); await _context.SaveChangesAsync().ConfigureAwait(false); } } public async Task DeleteBundleBacktestRequestByIdAsync(Guid id) { var entity = await _context.BundleBacktestRequests .AsTracking() .FirstOrDefaultAsync(b => b.RequestId == id) .ConfigureAwait(false); if (entity != null) { _context.BundleBacktestRequests.Remove(entity); await _context.SaveChangesAsync().ConfigureAwait(false); } } public IEnumerable GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) { var entities = _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .Where(b => b.Status == status) .ToList(); return entities.Select(PostgreSqlMappers.Map); } public async Task> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) { var entities = await _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .Where(b => b.Status == status) .ToListAsync() .ConfigureAwait(false); return entities.Select(PostgreSqlMappers.Map); } public (IEnumerable BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated( int page, int pageSize, BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt, string sortOrder = "desc", BundleBacktestRequestsFilter? filter = null) { var baseQuery = _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .AsQueryable(); // Apply filters if (filter != null) { if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameLike = $"%{filter.NameContains.Trim()}%"; baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); } if (filter.Status.HasValue) { baseQuery = baseQuery.Where(b => b.Status == filter.Status.Value); } if (filter.UserId.HasValue) { baseQuery = baseQuery.Where(b => b.UserId == filter.UserId.Value); } if (!string.IsNullOrWhiteSpace(filter.UserNameContains)) { var userNameLike = $"%{filter.UserNameContains.Trim()}%"; baseQuery = baseQuery.Where(b => b.User != null && EF.Functions.ILike(b.User.Name, userNameLike)); } if (filter.TotalBacktestsMin.HasValue) { baseQuery = baseQuery.Where(b => b.TotalBacktests >= filter.TotalBacktestsMin.Value); } if (filter.TotalBacktestsMax.HasValue) { baseQuery = baseQuery.Where(b => b.TotalBacktests <= filter.TotalBacktestsMax.Value); } if (filter.CompletedBacktestsMin.HasValue) { baseQuery = baseQuery.Where(b => b.CompletedBacktests >= filter.CompletedBacktestsMin.Value); } if (filter.CompletedBacktestsMax.HasValue) { baseQuery = baseQuery.Where(b => b.CompletedBacktests <= filter.CompletedBacktestsMax.Value); } if (filter.ProgressPercentageMin.HasValue) { var minProgress = filter.ProgressPercentageMin.Value; baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 && (double)b.CompletedBacktests / b.TotalBacktests * 100 >= minProgress); } if (filter.ProgressPercentageMax.HasValue) { var maxProgress = filter.ProgressPercentageMax.Value; baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 && (double)b.CompletedBacktests / b.TotalBacktests * 100 <= maxProgress); } if (filter.CreatedAtFrom.HasValue) { baseQuery = baseQuery.Where(b => b.CreatedAt >= filter.CreatedAtFrom.Value); } if (filter.CreatedAtTo.HasValue) { baseQuery = baseQuery.Where(b => b.CreatedAt <= filter.CreatedAtTo.Value); } } var totalCount = baseQuery.Count(); // Apply sorting IQueryable sortedQuery = sortBy switch { BundleBacktestRequestSortableColumn.RequestId => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.RequestId) : baseQuery.OrderBy(b => b.RequestId), BundleBacktestRequestSortableColumn.Name => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Name) : baseQuery.OrderBy(b => b.Name), BundleBacktestRequestSortableColumn.Status => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Status) : baseQuery.OrderBy(b => b.Status), BundleBacktestRequestSortableColumn.CreatedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CreatedAt) : baseQuery.OrderBy(b => b.CreatedAt), BundleBacktestRequestSortableColumn.CompletedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CompletedAt ?? DateTime.MinValue) : baseQuery.OrderBy(b => b.CompletedAt ?? DateTime.MaxValue), BundleBacktestRequestSortableColumn.TotalBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.TotalBacktests) : baseQuery.OrderBy(b => b.TotalBacktests), BundleBacktestRequestSortableColumn.CompletedBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CompletedBacktests) : baseQuery.OrderBy(b => b.CompletedBacktests), BundleBacktestRequestSortableColumn.FailedBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FailedBacktests) : baseQuery.OrderBy(b => b.FailedBacktests), BundleBacktestRequestSortableColumn.ProgressPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0) : baseQuery.OrderBy(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0), BundleBacktestRequestSortableColumn.UserId => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.UserId ?? int.MaxValue) : baseQuery.OrderBy(b => b.UserId ?? int.MinValue), BundleBacktestRequestSortableColumn.UserName => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.User != null ? b.User.Name : string.Empty) : baseQuery.OrderBy(b => b.User != null ? b.User.Name : string.Empty), BundleBacktestRequestSortableColumn.UpdatedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.UpdatedAt) : baseQuery.OrderBy(b => b.UpdatedAt), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CreatedAt) : baseQuery.OrderBy(b => b.CreatedAt) }; var entities = sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); var mappedRequests = entities.Select(PostgreSqlMappers.Map); return (mappedRequests, totalCount); } public async Task<(IEnumerable BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync( int page, int pageSize, BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt, string sortOrder = "desc", BundleBacktestRequestsFilter? filter = null) { var baseQuery = _context.BundleBacktestRequests .AsNoTracking() .Include(b => b.User) .AsQueryable(); // Apply filters if (filter != null) { if (!string.IsNullOrWhiteSpace(filter.NameContains)) { var nameLike = $"%{filter.NameContains.Trim()}%"; baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); } if (filter.Status.HasValue) { baseQuery = baseQuery.Where(b => b.Status == filter.Status.Value); } if (filter.UserId.HasValue) { baseQuery = baseQuery.Where(b => b.UserId == filter.UserId.Value); } if (!string.IsNullOrWhiteSpace(filter.UserNameContains)) { var userNameLike = $"%{filter.UserNameContains.Trim()}%"; baseQuery = baseQuery.Where(b => b.User != null && EF.Functions.ILike(b.User.Name, userNameLike)); } if (filter.TotalBacktestsMin.HasValue) { baseQuery = baseQuery.Where(b => b.TotalBacktests >= filter.TotalBacktestsMin.Value); } if (filter.TotalBacktestsMax.HasValue) { baseQuery = baseQuery.Where(b => b.TotalBacktests <= filter.TotalBacktestsMax.Value); } if (filter.CompletedBacktestsMin.HasValue) { baseQuery = baseQuery.Where(b => b.CompletedBacktests >= filter.CompletedBacktestsMin.Value); } if (filter.CompletedBacktestsMax.HasValue) { baseQuery = baseQuery.Where(b => b.CompletedBacktests <= filter.CompletedBacktestsMax.Value); } if (filter.ProgressPercentageMin.HasValue) { var minProgress = filter.ProgressPercentageMin.Value; baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 && (double)b.CompletedBacktests / b.TotalBacktests * 100 >= minProgress); } if (filter.ProgressPercentageMax.HasValue) { var maxProgress = filter.ProgressPercentageMax.Value; baseQuery = baseQuery.Where(b => b.TotalBacktests > 0 && (double)b.CompletedBacktests / b.TotalBacktests * 100 <= maxProgress); } if (filter.CreatedAtFrom.HasValue) { baseQuery = baseQuery.Where(b => b.CreatedAt >= filter.CreatedAtFrom.Value); } if (filter.CreatedAtTo.HasValue) { baseQuery = baseQuery.Where(b => b.CreatedAt <= filter.CreatedAtTo.Value); } } var totalCount = await baseQuery.CountAsync().ConfigureAwait(false); // Apply sorting IQueryable sortedQuery = sortBy switch { BundleBacktestRequestSortableColumn.RequestId => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.RequestId) : baseQuery.OrderBy(b => b.RequestId), BundleBacktestRequestSortableColumn.Name => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Name) : baseQuery.OrderBy(b => b.Name), BundleBacktestRequestSortableColumn.Status => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Status) : baseQuery.OrderBy(b => b.Status), BundleBacktestRequestSortableColumn.CreatedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CreatedAt) : baseQuery.OrderBy(b => b.CreatedAt), BundleBacktestRequestSortableColumn.CompletedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CompletedAt ?? DateTime.MinValue) : baseQuery.OrderBy(b => b.CompletedAt ?? DateTime.MaxValue), BundleBacktestRequestSortableColumn.TotalBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.TotalBacktests) : baseQuery.OrderBy(b => b.TotalBacktests), BundleBacktestRequestSortableColumn.CompletedBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CompletedBacktests) : baseQuery.OrderBy(b => b.CompletedBacktests), BundleBacktestRequestSortableColumn.FailedBacktests => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FailedBacktests) : baseQuery.OrderBy(b => b.FailedBacktests), BundleBacktestRequestSortableColumn.ProgressPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0) : baseQuery.OrderBy(b => b.TotalBacktests > 0 ? (double)b.CompletedBacktests / b.TotalBacktests : 0), BundleBacktestRequestSortableColumn.UserId => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.UserId ?? int.MaxValue) : baseQuery.OrderBy(b => b.UserId ?? int.MinValue), BundleBacktestRequestSortableColumn.UserName => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.User != null ? b.User.Name : string.Empty) : baseQuery.OrderBy(b => b.User != null ? b.User.Name : string.Empty), BundleBacktestRequestSortableColumn.UpdatedAt => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.UpdatedAt) : baseQuery.OrderBy(b => b.UpdatedAt), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.CreatedAt) : baseQuery.OrderBy(b => b.CreatedAt) }; var entities = await sortedQuery .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync() .ConfigureAwait(false); var mappedRequests = entities.Select(PostgreSqlMappers.Map); return (mappedRequests, totalCount); } public async Task GetBundleBacktestRequestsSummaryAsync() { // Use ADO.NET directly for aggregation queries to avoid EF Core mapping issues var connection = _context.Database.GetDbConnection(); await connection.OpenAsync(); try { var statusCounts = new List(); var totalRequests = 0; // Query 1: Status summary // Note: Status is stored as text in PostgreSQL, not as integer var statusSummarySql = @" SELECT ""Status"", COUNT(*) as Count FROM ""BundleBacktestRequests"" GROUP BY ""Status"" ORDER BY ""Status"""; using (var command = connection.CreateCommand()) { command.CommandText = statusSummarySql; using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { var statusString = reader.GetString(0); var count = reader.GetInt32(1); // Parse the string status to enum if (Enum.TryParse(statusString, ignoreCase: true, out var status)) { statusCounts.Add(new BundleBacktestRequestStatusCountResult { Status = status, Count = count }); } } } } // Query 2: Total count var totalCountSql = @" SELECT COUNT(*) as Count FROM ""BundleBacktestRequests"""; using (var command = connection.CreateCommand()) { command.CommandText = totalCountSql; var result = await command.ExecuteScalarAsync(); totalRequests = result != null ? Convert.ToInt32(result) : 0; } return new BundleBacktestRequestSummary { StatusCounts = statusCounts.Select(s => new BundleBacktestRequestStatusCount { Status = s.Status, Count = s.Count }).ToList(), TotalRequests = totalRequests }; } finally { await connection.CloseAsync(); } } private class BundleBacktestRequestStatusCountResult { public BundleBacktestRequestStatus Status { get; set; } public int Count { get; set; } } }