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;
using Managing.Domain.Strategies;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application;
///
/// Service implementation for managing genetic algorithm requests
///
public class GeneticService : IGeneticService
{
private readonly IGeneticRepository _geneticRepository;
private readonly IBacktester _backtester;
private readonly ILogger _logger;
private readonly IMessengerService _messengerService;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IGrainFactory _grainFactory;
private readonly IJobRepository _jobRepository;
// Predefined parameter ranges for each indicator (matching backtestGenetic.tsx)
public static readonly Dictionary ParameterRanges = new()
{
// Trading Parameters only - indicator parameters are now handled by IndicatorParameterRanges
["stopLoss"] = (0.2, 50.0), // Minimum 0.2% to cover fees, no upper limit (set to 50% as practical max)
["leverage"] = (1.0, 10.0),
["cooldownPeriod"] = (5.0, 25.0),
["maxLossStreak"] = (0.0, 4.0),
["maxPositionTimeHours"] = (0, 48.0)
};
// Default indicator values per indicator type (matching CustomScenario.tsx)
public static readonly Dictionary> DefaultIndicatorValues = new()
{
[IndicatorType.RsiDivergence] = new() { ["period"] = 14.0 },
[IndicatorType.RsiDivergenceConfirm] = new() { ["period"] = 14.0 },
[IndicatorType.EmaCross] = new() { ["period"] = 14.0 },
[IndicatorType.EmaTrend] = new() { ["period"] = 14.0 },
[IndicatorType.StDev] = new() { ["period"] = 14.0 },
[IndicatorType.ThreeWhiteSoldiers] = new() { ["period"] = 14.0 },
[IndicatorType.MacdCross] = new()
{
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0,
["signalPeriods"] = 9.0
},
[IndicatorType.DualEmaCross] = new()
{
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
},
[IndicatorType.SuperTrend] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.SuperTrendCrossEma] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.ChandelierExit] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.StochRsiTrend] = new()
{
["period"] = 14.0,
["stochPeriods"] = 14.0,
["signalPeriods"] = 9.0,
["smoothPeriods"] = 3.0
},
[IndicatorType.Stc] = new()
{
["cyclePeriods"] = 10.0,
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
},
[IndicatorType.LaggingStc] = new()
{
["cyclePeriods"] = 10.0,
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
}
};
// Indicator-specific parameter ranges
public static readonly Dictionary>
IndicatorParameterRanges = new()
{
[IndicatorType.RsiDivergence] = new()
{
["period"] = (6.0, 70.0)
},
[IndicatorType.RsiDivergenceConfirm] = new()
{
["period"] = (6.0, 70.0)
},
[IndicatorType.EmaCross] = new()
{
["period"] = (10.0, 300.0)
},
[IndicatorType.EmaTrend] = new()
{
["period"] = (10.0, 300.0)
},
[IndicatorType.StDev] = new()
{
["period"] = (5.0, 50.0)
},
[IndicatorType.ThreeWhiteSoldiers] = new()
{
["period"] = (5.0, 50.0)
},
[IndicatorType.MacdCross] = new()
{
["fastPeriods"] = (10.0, 70.0),
["slowPeriods"] = (20.0, 120.0),
["signalPeriods"] = (5.0, 50.0)
},
[IndicatorType.DualEmaCross] = new()
{
["fastPeriods"] = (5.0, 300.0),
["slowPeriods"] = (5.0, 300.0)
},
[IndicatorType.SuperTrend] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.SuperTrendCrossEma] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.ChandelierExit] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.StochRsiTrend] = new()
{
["period"] = (5.0, 50.0),
["stochPeriods"] = (5.0, 30.0),
["signalPeriods"] = (3.0, 15.0),
["smoothPeriods"] = (1.0, 10.0)
},
[IndicatorType.Stc] = new()
{
["cyclePeriods"] = (5.0, 50.0),
["fastPeriods"] = (5.0, 70.0),
["slowPeriods"] = (10.0, 120.0)
},
[IndicatorType.LaggingStc] = new()
{
["cyclePeriods"] = (5.0, 30.0),
["fastPeriods"] = (5.0, 50.0),
["slowPeriods"] = (10.0, 100.0)
}
};
// Indicator type to parameter mapping
public static readonly Dictionary IndicatorParamMapping = new()
{
[IndicatorType.RsiDivergence] = ["period"],
[IndicatorType.RsiDivergenceConfirm] = ["period"],
[IndicatorType.EmaCross] = ["period"],
[IndicatorType.EmaTrend] = ["period"],
[IndicatorType.StDev] = ["period"],
[IndicatorType.ThreeWhiteSoldiers] = ["period"],
[IndicatorType.MacdCross] = ["fastPeriods", "slowPeriods", "signalPeriods"],
[IndicatorType.DualEmaCross] = ["fastPeriods", "slowPeriods"],
[IndicatorType.SuperTrend] = ["period", "multiplier"],
[IndicatorType.SuperTrendCrossEma] = ["period", "multiplier"],
[IndicatorType.ChandelierExit] = ["period", "multiplier"],
[IndicatorType.StochRsiTrend] = ["period", "stochPeriods", "signalPeriods", "smoothPeriods"],
[IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
[IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"]
};
public GeneticService(
IGeneticRepository geneticRepository,
IBacktester backtester,
ILogger logger,
IMessengerService messengerService,
IServiceScopeFactory serviceScopeFactory,
IGrainFactory grainFactory,
IJobRepository jobRepository)
{
_geneticRepository = geneticRepository;
_backtester = backtester;
_logger = logger;
_messengerService = messengerService;
_serviceScopeFactory = serviceScopeFactory;
_grainFactory = grainFactory;
_jobRepository = jobRepository;
}
public async Task CreateGeneticRequestAsync(
User user,
Ticker ticker,
Timeframe timeframe,
DateTime startDate,
DateTime endDate,
decimal balance,
int populationSize,
int generations,
double mutationRate,
GeneticSelectionMethod selectionMethod,
GeneticCrossoverMethod crossoverMethod,
GeneticMutationMethod mutationMethod,
int elitismPercentage,
double maxTakeProfit,
List eligibleIndicators)
{
var id = Guid.NewGuid().ToString();
var geneticRequest = new GeneticRequest(id)
{
Ticker = ticker,
Timeframe = timeframe,
StartDate = startDate,
EndDate = endDate,
Balance = balance,
PopulationSize = populationSize,
Generations = generations,
MutationRate = mutationRate,
SelectionMethod = selectionMethod,
CrossoverMethod = crossoverMethod,
MutationMethod = mutationMethod,
ElitismPercentage = elitismPercentage,
MaxTakeProfit = maxTakeProfit,
EligibleIndicators = eligibleIndicators,
Status = GeneticRequestStatus.Pending
};
_geneticRepository.InsertGeneticRequestForUser(user, geneticRequest);
// Create a single job for this genetic request that will run until completion
try
{
var job = new Job
{
UserId = user.Id,
Status = JobStatus.Pending,
JobType = JobType.Genetic,
Priority = 0,
ConfigJson = "{}", // Not needed for genetic jobs, GeneticRequestId is used
StartDate = startDate,
EndDate = endDate,
GeneticRequestId = id,
RetryCount = 0,
MaxRetries = 3,
IsRetryable = true
};
await _jobRepository.CreateAsync(job);
_logger.LogInformation("Created genetic job {JobId} for genetic request {RequestId}", job.Id, id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create job for genetic request {RequestId}", id);
throw;
}
return geneticRequest;
}
public IEnumerable GetGeneticRequestsByUser(User user)
{
return _geneticRepository.GetGeneticRequestsByUser(user);
}
public GeneticRequest GetGeneticRequestByIdForUser(User user, string id)
{
return _geneticRepository.GetGeneticRequestByIdForUser(user, id);
}
public async Task UpdateGeneticRequestAsync(GeneticRequest geneticRequest)
{
await _geneticRepository.UpdateGeneticRequestAsync(geneticRequest);
}
public void DeleteGeneticRequestByIdForUser(User user, string id)
{
_geneticRepository.DeleteGeneticRequestByIdForUser(user, id);
}
public Task> GetGeneticRequestsAsync(GeneticRequestStatus status)
{
return _geneticRepository.GetGeneticRequestsAsync(status);
}
///
/// Runs the genetic algorithm for a specific request
///
/// The genetic request to process
/// Cancellation token to stop the algorithm
/// The genetic algorithm result
public async Task RunGeneticAlgorithm(GeneticRequest request,
CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Starting genetic algorithm for request {RequestId}", request.RequestId);
// Update status to running
request.Status = GeneticRequestStatus.Running;
await UpdateGeneticRequestAsync(request);
// Create or resume chromosome for trading bot configuration
TradingBotChromosome chromosome;
Population population;
if (!string.IsNullOrEmpty(request.BestChromosome) && request.CurrentGeneration > 0)
{
// Resume from previous state (best chromosome only)
chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit);
var savedChromosome = JsonSerializer.Deserialize(request.BestChromosome);
if (savedChromosome != null)
{
chromosome.ReplaceGenes(0, savedChromosome.Select(g => new Gene(g)).ToArray());
}
population = new Population(request.PopulationSize, request.PopulationSize, chromosome);
_logger.LogInformation(
"Resuming genetic algorithm for request {RequestId} from generation {Generation} with best chromosome",
request.RequestId, request.CurrentGeneration);
}
else
{
// Start fresh
chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit);
population = new Population(request.PopulationSize, request.PopulationSize, chromosome);
_logger.LogInformation("Starting fresh genetic algorithm for request {RequestId}", request.RequestId);
}
// 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,
GetSelection(request.SelectionMethod),
GetCrossover(request.CrossoverMethod),
GetMutation(request.MutationMethod))
{
Termination = new OrTermination(
new GenerationNumberTermination(request.Generations),
new FitnessStagnationTermination(15)
),
MutationProbability = (float)request.MutationRate,
CrossoverProbability = 0.75f, // Fixed crossover rate as in frontend
TaskExecutor = new ParallelTaskExecutor
{
MinThreads = 2,
MaxThreads = maxParallelThreads
}
};
// Set the genetic algorithm reference in the fitness function
fitness.SetGeneticAlgorithm(ga);
// Custom termination condition that checks for cancellation
var originalTermination = ga.Termination;
ga.Termination = new GenerationNumberTermination(request.Generations);
// Add cancellation check in the generation event
// Run the genetic algorithm with periodic checks for cancellation
var generationCount = 0;
ga.GenerationRan += async (sender, e) =>
{
try
{
generationCount = ga.GenerationsNumber;
// Update progress every generation
var bestFitness = ga.BestChromosome?.Fitness ?? 0;
var bestChromosomeJson = (string?)null;
var bestIndividual = (string?)null;
if (ga.BestChromosome is TradingBotChromosome bestChromosome)
{
var genes = bestChromosome.GetGenes();
var geneValues = genes.Select(g =>
{
if (g.Value is double doubleValue) return doubleValue;
if (g.Value is int intValue) return (double)intValue;
return Convert.ToDouble(g.Value.ToString());
}).ToArray();
bestChromosomeJson = JsonSerializer.Serialize(geneValues);
bestIndividual = bestChromosome.ToString();
}
// Update ProgressInfo with current generation information
var progressInfo = JsonSerializer.Serialize(new
{
generation = generationCount,
best_fitness = bestFitness,
population_size = request.PopulationSize,
generations = request.Generations,
updated_at = DateTime.UtcNow
});
// Update the domain object for local use
request.CurrentGeneration = generationCount;
request.BestFitnessSoFar = bestFitness;
request.BestChromosome = bestChromosomeJson;
request.BestIndividual = bestIndividual;
request.ProgressInfo = progressInfo;
// Update the database with current generation progress using a new scope
// This prevents DbContext concurrency issues when running in parallel
await ServiceScopeHelpers.WithScopedService(
_serviceScopeFactory,
async geneticService =>
{
// Reload the request from the database in the new scope
// Use the user from the original request to get the request by ID
var dbRequest =
geneticService.GetGeneticRequestByIdForUser(request.User, request.RequestId);
if (dbRequest != null)
{
// Update the loaded request with current generation data
dbRequest.CurrentGeneration = generationCount;
dbRequest.BestFitnessSoFar = bestFitness;
dbRequest.BestChromosome = bestChromosomeJson;
dbRequest.BestIndividual = bestIndividual;
dbRequest.ProgressInfo = progressInfo;
// Save the update
await geneticService.UpdateGeneticRequestAsync(dbRequest);
}
});
_logger.LogDebug(
"Updated genetic request {RequestId} at generation {Generation} with fitness {Fitness}",
request.RequestId, generationCount, bestFitness);
// Check for cancellation
if (cancellationToken.IsCancellationRequested)
{
ga.Stop();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating genetic request {RequestId} at generation {Generation}",
request.RequestId, generationCount);
// Don't throw - continue with next generation
}
};
// Run the genetic algorithm
ga.Start();
// Check if the algorithm was cancelled
if (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Genetic algorithm cancelled for request {RequestId}", request.RequestId);
// Update request status to pending so it can be resumed
request.Status = GeneticRequestStatus.Pending;
await UpdateGeneticRequestAsync(request);
return new GeneticAlgorithmResult
{
BestFitness = request.BestFitnessSoFar ?? 0,
BestIndividual = request.BestIndividual ?? "unknown",
ProgressInfo = request.ProgressInfo,
CompletedAt = DateTime.UtcNow
};
}
// Get the best chromosome
var bestChromosome = ga.BestChromosome as TradingBotChromosome;
var bestFitness = ga.BestChromosome?.Fitness ?? 0;
_logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}",
request.RequestId, bestFitness);
// Update request with final results
request.Status = GeneticRequestStatus.Completed;
request.CompletedAt = DateTime.UtcNow;
request.BestFitness = bestFitness;
request.BestIndividual = bestChromosome?.ToString() ?? "unknown";
request.CurrentGeneration = ga.GenerationsNumber;
request.BestFitnessSoFar = bestFitness;
// Update BestChromosome if not already set
if (bestChromosome != null && string.IsNullOrEmpty(request.BestChromosome))
{
var genes = bestChromosome.GetGenes();
var geneValues = genes.Select(g =>
{
if (g.Value is double doubleValue) return doubleValue;
if (g.Value is int intValue) return (double)intValue;
return Convert.ToDouble(g.Value.ToString());
}).ToArray();
request.BestChromosome = JsonSerializer.Serialize(geneValues);
}
request.ProgressInfo = JsonSerializer.Serialize(new
{
generation = ga.GenerationsNumber,
best_fitness = bestFitness,
population_size = request.PopulationSize,
generations = request.Generations,
completed_at = DateTime.UtcNow
});
await UpdateGeneticRequestAsync(request);
_logger.LogInformation(
"Final update completed for genetic request {RequestId}. Generation: {Generation}, Best Fitness: {Fitness}",
request.RequestId, ga.GenerationsNumber, bestFitness);
// Send notification about the completed genetic algorithm
try
{
await _messengerService.SendGeneticAlgorithmNotification(request, bestFitness, bestChromosome);
}
catch (Exception notificationEx)
{
_logger.LogWarning(notificationEx,
"Failed to send genetic algorithm notification for request {RequestId}", request.RequestId);
}
return new GeneticAlgorithmResult
{
BestFitness = bestFitness,
BestIndividual = bestChromosome?.ToString() ?? "unknown",
ProgressInfo = request.ProgressInfo,
CompletedAt = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running genetic algorithm for request {RequestId}", request.RequestId);
// Update request with error
request.Status = GeneticRequestStatus.Failed;
request.ErrorMessage = ex.Message;
request.CompletedAt = DateTime.UtcNow;
await UpdateGeneticRequestAsync(request);
throw;
}
}
private ISelection GetSelection(GeneticSelectionMethod selectionMethod)
{
return selectionMethod switch
{
GeneticSelectionMethod.Elite => new EliteSelection(),
GeneticSelectionMethod.Roulette => new RouletteWheelSelection(),
GeneticSelectionMethod.StochasticUniversalSampling => new StochasticUniversalSamplingSelection(),
GeneticSelectionMethod.Tournament => new TournamentSelection(),
GeneticSelectionMethod.Truncation => new TruncationSelection(),
_ => new TournamentSelection()
};
}
private ICrossover GetCrossover(GeneticCrossoverMethod crossoverMethod)
{
return crossoverMethod switch
{
GeneticCrossoverMethod.AlternatingPosition => new AlternatingPositionCrossover(),
GeneticCrossoverMethod.CutAndSplice => new CutAndSpliceCrossover(),
GeneticCrossoverMethod.Cycle => new CycleCrossover(),
GeneticCrossoverMethod.OnePoint => new OnePointCrossover(),
GeneticCrossoverMethod.OrderBased => new OrderBasedCrossover(),
GeneticCrossoverMethod.Ordered => new OrderedCrossover(),
GeneticCrossoverMethod.PartiallyMapped => new PartiallyMappedCrossover(),
GeneticCrossoverMethod.PositionBased => new PositionBasedCrossover(),
GeneticCrossoverMethod.ThreeParent => new ThreeParentCrossover(),
GeneticCrossoverMethod.TwoPoint => new TwoPointCrossover(),
GeneticCrossoverMethod.Uniform => new UniformCrossover(),
GeneticCrossoverMethod.VotingRecombination => new VotingRecombinationCrossover(),
_ => new UniformCrossover()
};
}
private IMutation GetMutation(GeneticMutationMethod mutationMethod)
{
return mutationMethod switch
{
GeneticMutationMethod.Displacement => new DisplacementMutation(),
GeneticMutationMethod.FlipBit => new FlipBitMutation(),
GeneticMutationMethod.Insertion => new InsertionMutation(),
GeneticMutationMethod.PartialShuffle => new PartialShuffleMutation(),
GeneticMutationMethod.ReverseSequence => new ReverseSequenceMutation(),
GeneticMutationMethod.Twors => new TworsMutation(),
GeneticMutationMethod.Uniform => new UniformMutation(true),
_ => new UniformMutation(true)
};
}
}
///
/// Chromosome representing a trading bot configuration with predefined parameter ranges
///
public class TradingBotChromosome : ChromosomeBase
{
private readonly List _eligibleIndicators;
private readonly double _maxTakeProfit;
private readonly Random _random = new Random();
private int[] _indicatorSelectionPattern;
// Gene structure:
// 0-3: Trading parameters (takeProfit, stopLoss, cooldownPeriod, maxLossStreak)
// 4: Loopback period
// 5-4+N: Indicator selection (N = number of eligible indicators)
// 5+N+: Indicator parameters (period, fastPeriods, etc.)
public TradingBotChromosome(List eligibleIndicators, double maxTakeProfit)
: base(4 + 1 + eligibleIndicators.Count +
eligibleIndicators.Count * 8) // Trading params + loopback + indicator selection + indicator params
{
_eligibleIndicators = eligibleIndicators;
_maxTakeProfit = maxTakeProfit;
// Initialize genes with proper constraint handling
InitializeGenesWithConstraints();
}
public override Gene GenerateGene(int geneIndex)
{
if (geneIndex < 4)
{
// Trading parameters
return geneIndex switch
{
0 => new Gene(GetRandomInRange((0.9, _maxTakeProfit))), // Take profit (0.9% to max TP)
1 => new Gene(GetRandomInRange((0.2, 50.0))), // Stop loss (will be constrained in GetTradingBotConfig)
2 => new Gene(GetRandomIntInRange(GeneticService.ParameterRanges["cooldownPeriod"])), // Cooldown period
3 => new Gene(GetRandomIntInRange(GeneticService.ParameterRanges["maxLossStreak"])), // Max loss streak
_ => new Gene(0)
};
}
else if (geneIndex == 4)
{
// LoopbackPeriod gene (always between 5 and 20)
return new Gene(GetRandomIntInRange((5, 20)));
}
else if (geneIndex < 5 + _eligibleIndicators.Count)
{
// Indicator selection (0 = not selected, 1 = selected)
// Generate a random combination of up to 4 indicators
if (_indicatorSelectionPattern == null)
{
GenerateIndicatorSelectionPattern();
}
return new Gene(_indicatorSelectionPattern[geneIndex - 5]);
}
else
{
// Indicator parameters
var indicatorIndex = (geneIndex - (5 + _eligibleIndicators.Count)) / 8;
var paramIndex = (geneIndex - (5 + _eligibleIndicators.Count)) % 8;
if (indicatorIndex < _eligibleIndicators.Count)
{
var indicator = _eligibleIndicators[indicatorIndex];
var paramName = GetParameterName(paramIndex);
if (paramName != null && GeneticService.IndicatorParamMapping.ContainsKey(indicator))
{
var requiredParams = GeneticService.IndicatorParamMapping[indicator];
if (requiredParams.Contains(paramName))
{
// Use indicator-specific ranges only
if (GeneticService.IndicatorParameterRanges.ContainsKey(indicator) &&
GeneticService.IndicatorParameterRanges[indicator].ContainsKey(paramName))
{
var indicatorRange = GeneticService.IndicatorParameterRanges[indicator][paramName];
// 70% chance to use default value, 30% chance to use random value within indicator-specific range
if (_random.NextDouble() < 0.7)
{
var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]);
}
else
{
return new Gene(GetRandomInRange(indicatorRange));
}
}
else
{
// If no indicator-specific range is found, use default value only
var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]);
}
}
}
}
return new Gene(0);
}
}
public override IChromosome CreateNew()
{
return new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit);
}
public override IChromosome Clone()
{
var clone = new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit);
clone.ReplaceGenes(0, GetGenes());
// Copy the selection pattern to maintain consistency
if (_indicatorSelectionPattern != null)
{
clone._indicatorSelectionPattern = (int[])_indicatorSelectionPattern.Clone();
}
return clone;
}
public List GetSelectedIndicators()
{
var selected = new List();
var genes = GetGenes();
// Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators)
for (int i = 0; i < _eligibleIndicators.Count; i++)
{
if (genes[5 + i].Value.ToString() == "1")
{
var indicator = new LightIndicator(_eligibleIndicators[i].ToString(), _eligibleIndicators[i])
{
Type = _eligibleIndicators[i]
};
// Add parameters for this indicator
var baseIndex = 5 + _eligibleIndicators.Count + i * 8;
var paramName = GetParameterName(0); // period
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.Period = Convert.ToInt32(genes[baseIndex].Value);
}
paramName = GetParameterName(1); // fastPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.FastPeriods = Convert.ToInt32(genes[baseIndex + 1].Value);
}
paramName = GetParameterName(2); // slowPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SlowPeriods = Convert.ToInt32(genes[baseIndex + 2].Value);
}
paramName = GetParameterName(3); // signalPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SignalPeriods = Convert.ToInt32(genes[baseIndex + 3].Value);
}
paramName = GetParameterName(4); // multiplier
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.Multiplier = Convert.ToDouble(genes[baseIndex + 4].Value);
}
paramName = GetParameterName(5); // stochPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.StochPeriods = Convert.ToInt32(genes[baseIndex + 5].Value);
}
paramName = GetParameterName(6); // smoothPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SmoothPeriods = Convert.ToInt32(genes[baseIndex + 6].Value);
}
paramName = GetParameterName(7); // cyclePeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.CyclePeriods = Convert.ToInt32(genes[baseIndex + 7].Value);
}
selected.Add(indicator);
}
}
return selected;
}
public TradingBotConfig GetTradingBotConfig(GeneticRequest request)
{
var genes = GetGenes();
var selectedIndicators = GetSelectedIndicators();
// The selection pattern ensures at least one indicator is always selected
// No need for fallback logic
// Get take profit from chromosome (gene 0)
var takeProfit = Convert.ToDouble(genes[0].Value);
// Get stop loss from chromosome (gene 1)
var stopLoss = Convert.ToDouble(genes[1].Value);
// Enforce proper risk-reward constraints
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
// Generate a random stop loss between min and max
var randomStopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
// Use the random value instead of clamping the original
stopLoss = randomStopLoss;
// Log the generated values (for debugging)
Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators
var scenario = new LightScenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod)
{
Indicators = selectedIndicators
};
var mm = new MoneyManagement
{
Name = $"Genetic_{request.RequestId}_MM",
Timeframe = request.Timeframe,
StopLoss = Convert.ToDecimal(stopLoss),
TakeProfit = Convert.ToDecimal(takeProfit),
Leverage = 1.0m
};
mm.FormatPercentage();
return new TradingBotConfig
{
Name = $"Genetic_{request.RequestId}",
AccountName = request.User.Accounts.FirstOrDefault().Name,
Ticker = request.Ticker,
Timeframe = request.Timeframe,
BotTradingBalance = request.Balance,
IsForBacktest = true,
IsForWatchingOnly = false,
CooldownPeriod = Convert.ToInt32(genes[2].Value),
MaxLossStreak = Convert.ToInt32(genes[3].Value),
FlipPosition = false,
FlipOnlyWhenInProfit = true,
CloseEarlyWhenProfitable = true,
MaxPositionTimeHours = 0, // Always 0 to prevent early position cutting
UseSynthApi = false,
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false,
Scenario = scenario,
MoneyManagement = mm,
RiskManagement = new RiskManagement
{
RiskTolerance = RiskToleranceLevel.Moderate
}
};
}
private double GetRandomInRange((double min, double max) range)
{
return _random.NextDouble() * (range.max - range.min) + range.min;
}
private int GetRandomIntInRange((double min, double max) range)
{
return _random.Next((int)range.min, (int)range.max + 1);
}
private string GetParameterName(int index)
{
return index switch
{
0 => "period",
1 => "fastPeriods",
2 => "slowPeriods",
3 => "signalPeriods",
4 => "multiplier",
5 => "stochPeriods",
6 => "smoothPeriods",
7 => "cyclePeriods",
_ => null
};
}
private bool HasParameter(IndicatorType indicator, string paramName)
{
return GeneticService.IndicatorParamMapping.ContainsKey(indicator) &&
GeneticService.IndicatorParamMapping[indicator].Contains(paramName);
}
private void GenerateIndicatorSelectionPattern()
{
// Generate a random combination of up to 4 indicators
var selectedCount = _random.Next(1, Math.Min(5, _eligibleIndicators.Count + 1)); // 1 to 4 indicators
var selectedIndices = new HashSet();
// Randomly select indices
while (selectedIndices.Count < selectedCount)
{
selectedIndices.Add(_random.Next(_eligibleIndicators.Count));
}
// Create the selection pattern
_indicatorSelectionPattern = new int[_eligibleIndicators.Count];
for (int i = 0; i < _eligibleIndicators.Count; i++)
{
_indicatorSelectionPattern[i] = selectedIndices.Contains(i) ? 1 : 0;
}
}
///
/// Initializes genes with proper constraint handling for take profit and stop loss
///
private void InitializeGenesWithConstraints()
{
// Generate take profit first (gene 0)
var takeProfit = GetRandomInRange((0.9, _maxTakeProfit));
ReplaceGene(0, new Gene(takeProfit));
// Generate stop loss with proper constraints (gene 1)
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
var stopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
ReplaceGene(1, new Gene(stopLoss));
// Log the initial values (for debugging)
Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Initialize remaining genes normally
for (int i = 2; i < Length; i++)
{
ReplaceGene(i, GenerateGene(i));
}
}
}
///
/// Multi-objective fitness function for trading bot optimization
///
public class TradingBotFitness : IFitness
{
private readonly IServiceScopeFactory _serviceScopeFactory;
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,
HashSet candles,
ILogger logger)
{
_serviceScopeFactory = serviceScopeFactory;
_request = request;
_candles = candles;
_logger = logger;
}
public void SetGeneticAlgorithm(GeneticAlgorithm geneticAlgorithm)
{
_geneticAlgorithm = geneticAlgorithm;
}
public double Evaluate(IChromosome chromosome)
{
try
{
var tradingBotChromosome = chromosome as TradingBotChromosome;
if (tradingBotChromosome == null)
return 0;
var config = tradingBotChromosome.GetTradingBotConfig(_request);
// 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,
bundleRequestId: null, // Genetic algorithm doesn't use bundle requests
metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0,
_request.RequestId),
progressCallback: null,
cancellationToken: default
)
).GetAwaiter().GetResult();
}
finally
{
_dbSemaphore.Release();
}
// Calculate multi-objective fitness based on backtest results
var fitness = CalculateFitness(lightBacktest, config);
return fitness;
}
catch (Exception ex)
{
_logger.LogWarning("Fitness evaluation failed for chromosome: {Message}", ex.Message);
// Return low fitness for failed backtests
return 0;
}
}
private double CalculateFitness(LightBacktest lightBacktest, TradingBotConfig config)
{
if (lightBacktest == null)
return 0.1;
// Calculate base fitness from backtest score
var baseFitness = lightBacktest.Score;
// Return base fitness (no penalty for now)
return baseFitness;
}
}