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(); } /// /// 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), 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 }); 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), 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 }); 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); } 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); } 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.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 }); 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); } 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.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), 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 }); return (mappedBacktests, totalCount); } // Bundle backtest methods public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) { bundleRequest.User = user; var entity = PostgreSqlMappers.Map(bundleRequest); // Set the UserId by finding the user entity var userEntity = _context.Users.FirstOrDefault(u => u.Name == user.Name); if (userEntity != null) { entity.UserId = userEntity.Id; } _context.BundleBacktestRequests.Add(entity); _context.SaveChanges(); } public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest) { bundleRequest.User = user; var entity = PostgreSqlMappers.Map(bundleRequest); // Set the UserId by finding the user entity var userEntity = await _context.Users.FirstOrDefaultAsync(u => u.Name == user.Name); 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.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.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 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); } }