diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index 5ed3bbdc..b3b0e278 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -2,9 +2,11 @@ using System.Text.Json; using GeneticSharp; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; +using Managing.Application.Backtests; using Managing.Core; using Managing.Domain.Backtests; using Managing.Domain.Bots; +using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Risk; using Managing.Domain.Scenarios; @@ -346,10 +348,50 @@ public class GeneticService : IGeneticService _logger.LogInformation("Starting fresh genetic algorithm for request {RequestId}", request.RequestId); } - // Create fitness function first - var fitness = new TradingBotFitness(_serviceScopeFactory, request, _logger); + // Load candles once at the beginning to avoid repeated database queries + // This significantly reduces database connections during genetic algorithm execution + _logger.LogInformation("Loading candles for genetic algorithm {RequestId}: {Ticker} on {Timeframe} from {StartDate} to {EndDate}", + request.RequestId, request.Ticker, request.Timeframe, request.StartDate, request.EndDate); + + HashSet candles; + try + { + candles = await ServiceScopeHelpers.WithScopedService>( + _serviceScopeFactory, + async candleRepository => await candleRepository.GetCandles( + TradingExchanges.Evm, // Default exchange for genetic algorithms + request.Ticker, + request.Timeframe, + request.StartDate, + request.EndDate + ) + ); + + if (candles == null || candles.Count == 0) + { + throw new InvalidOperationException( + $"No candles found for {request.Ticker} on {request.Timeframe} from {request.StartDate} to {request.EndDate}"); + } + + _logger.LogInformation("Loaded {CandleCount} candles for genetic algorithm {RequestId}", + candles.Count, request.RequestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load candles for genetic algorithm {RequestId}", request.RequestId); + throw; + } + + // Create fitness function with pre-loaded candles to avoid database queries during evaluation + var fitness = new TradingBotFitness(_serviceScopeFactory, request, candles, _logger); // Create genetic algorithm with better configuration + // Limit parallelism to prevent database connection pool exhaustion + // Each fitness evaluation creates a database connection, so we need to limit + // the number of parallel evaluations to stay within connection pool limits + // Default PostgreSQL pool is 20 connections, so we limit to 4 threads per genetic job + // This leaves room for other operations and multiple concurrent genetic jobs + var maxParallelThreads = Math.Min(4, Environment.ProcessorCount); var ga = new GeneticAlgorithm( population, fitness, @@ -366,7 +408,7 @@ public class GeneticService : IGeneticService TaskExecutor = new ParallelTaskExecutor { MinThreads = 2, - MaxThreads = Environment.ProcessorCount + MaxThreads = maxParallelThreads } }; @@ -963,12 +1005,18 @@ public class TradingBotFitness : IFitness private readonly GeneticRequest _request; private GeneticAlgorithm _geneticAlgorithm; private readonly ILogger _logger; + private readonly HashSet _candles; + private static readonly SemaphoreSlim _dbSemaphore = new SemaphoreSlim(4, 4); // Limit concurrent DB operations - public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request, + public TradingBotFitness( + IServiceScopeFactory serviceScopeFactory, + GeneticRequest request, + HashSet candles, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; _request = request; + _candles = candles; _logger = logger; } @@ -987,23 +1035,30 @@ public class TradingBotFitness : IFitness var config = tradingBotChromosome.GetTradingBotConfig(_request); - // Get current generation number (default to 0 if not available) - var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0; - - // Run backtest using scoped service to avoid DbContext concurrency issues - var lightBacktest = ServiceScopeHelpers.WithScopedService( - _serviceScopeFactory, - async backtester => await backtester.RunTradingBotBacktest( - config, - _request.StartDate, - _request.EndDate, - _request.User, - true, - false, // Don't include candles - _request.RequestId, - new GeneticBacktestMetadata(currentGeneration, _request.RequestId) - ) - ).GetAwaiter().GetResult(); + // Run backtest synchronously using BacktestExecutor (no job creation, no DB writes) + // Use semaphore to limit concurrent database operations (for indicator calculations) + // This prevents connection pool exhaustion when running large populations + _dbSemaphore.Wait(); + LightBacktest lightBacktest; + try + { + lightBacktest = ServiceScopeHelpers.WithScopedService( + _serviceScopeFactory, + async executor => await executor.ExecuteAsync( + config, + _candles, + _request.User, + save: false, // Don't save backtest results for genetic algorithm + withCandles: false, + requestId: _request.RequestId, + metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0, _request.RequestId) + ) + ).GetAwaiter().GetResult(); + } + finally + { + _dbSemaphore.Release(); + } // Calculate multi-objective fitness based on backtest results var fitness = CalculateFitness(lightBacktest, config);