using System.Text.Json; using GeneticSharp; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Managing.Domain.Risk; using Managing.Domain.Scenarios; using Managing.Domain.Users; 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; // 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) { _geneticRepository = geneticRepository; _backtester = backtester; _logger = logger; _messengerService = messengerService; } public GeneticRequest CreateGeneticRequest( 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); return geneticRequest; } public IEnumerable GetGeneticRequestsByUser(User user) { return _geneticRepository.GetGeneticRequestsByUser(user); } public GeneticRequest GetGeneticRequestByIdForUser(User user, string id) { return _geneticRepository.GetGeneticRequestByIdForUser(user, id); } public void UpdateGeneticRequest(GeneticRequest geneticRequest) { _geneticRepository.UpdateGeneticRequest(geneticRequest); } public void DeleteGeneticRequestByIdForUser(User user, string id) { _geneticRepository.DeleteGeneticRequestByIdForUser(user, id); } public IEnumerable GetPendingGeneticRequests() { return _geneticRepository.GetPendingGeneticRequests(); } /// /// 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; UpdateGeneticRequest(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); } // Create fitness function first var fitness = new TradingBotFitness(_backtester, request); // Create genetic algorithm with better configuration var ga = new GeneticAlgorithm( population, fitness, GetSelection(request.SelectionMethod), GetCrossover(request.CrossoverMethod), GetMutation(request.MutationMethod)) { Termination = new GenerationNumberTermination(request.Generations), MutationProbability = (float)request.MutationRate, CrossoverProbability = 0.7f // Fixed crossover rate as in frontend }; // 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 += (sender, e) => { generationCount = ga.GenerationsNumber; // Update progress every generation var bestFitness = ga.BestChromosome?.Fitness ?? 0; request.CurrentGeneration = generationCount; request.BestFitnessSoFar = bestFitness; 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(); request.BestChromosome = JsonSerializer.Serialize(geneValues); } UpdateGeneticRequest(request); // Check for cancellation if (cancellationToken.IsCancellationRequested) { ga.Stop(); } }; // 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; UpdateGeneticRequest(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 results request.Status = GeneticRequestStatus.Completed; request.CompletedAt = DateTime.UtcNow; request.BestFitness = bestFitness; request.BestIndividual = bestChromosome?.ToString() ?? "unknown"; request.ProgressInfo = JsonSerializer.Serialize(new { generation = ga.GenerationsNumber, best_fitness = bestFitness, population_size = request.PopulationSize, generations = request.Generations, completed_at = DateTime.UtcNow }); UpdateGeneticRequest(request); // 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; UpdateGeneticRequest(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(); // Gene structure: // 0-2: Trading parameters (takeProfit, stopLoss, cooldownPeriod, maxLossStreak) // 3-6: Indicator selection (up to 4 indicators) // 7+: Indicator parameters (period, fastPeriods, etc.) public TradingBotChromosome(List eligibleIndicators, double maxTakeProfit) : base(4 + 1 + 4 + eligibleIndicators.Count * 8) // Trading params + loopback + indicator selection + indicator params { _eligibleIndicators = eligibleIndicators; _maxTakeProfit = maxTakeProfit; // Initialize all genes for (int i = 0; i < Length; i++) { ReplaceGene(i, GenerateGene(i)); } } 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(GeneticService.ParameterRanges["stopLoss"])), // Stop loss 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 < 9) { // Indicator selection (0 = not selected, 1 = selected) return new Gene(_random.Next(2)); } else { // Indicator parameters var indicatorIndex = (geneIndex - 9) / 8; var paramIndex = (geneIndex - 9) % 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()); return clone; } public List GetSelectedIndicators() { var selected = new List(); var genes = GetGenes(); for (int i = 0; i < 4; i++) // Check first 4 indicator slots { if (genes[5 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count) { var indicator = new GeneticIndicator { Type = _eligibleIndicators[i] }; // Add parameters for this indicator var baseIndex = 9 + 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(); // Ensure we have at least one indicator if (!selectedIndicators.Any()) { selectedIndicators.Add(new GeneticIndicator { Type = _eligibleIndicators[0] }); } // Get take profit from chromosome (gene 0) var takeProfit = Convert.ToDouble(genes[0].Value); // Calculate stop loss based on 1.1:1 risk-reward ratio (gene 1) var stopLoss = Convert.ToDouble(genes[1].Value); // Ensure minimum 1.1:1 risk-reward ratio and minimum 0.2% to cover fees var minStopLossForRR = takeProfit / 1.1; var minStopLossForFees = 0.2; // Minimum 0.2% to cover trading fees var minStopLoss = Math.Max(minStopLossForRR, minStopLossForFees); var maxStopLoss = takeProfit - 0.1; // Ensure SL is less than TP with some buffer // Adjust stop loss if it doesn't meet the constraints if (stopLoss > maxStopLoss || stopLoss < minStopLoss) { stopLoss = GetRandomInRange((minStopLoss, maxStopLoss)); } // Get loopback period from gene 4 var loopbackPeriod = Convert.ToInt32(genes[4].Value); // Build scenario using selected indicators var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod); foreach (var geneticIndicator in selectedIndicators) { var indicator = ScenarioHelpers.BuildIndicator( type: geneticIndicator.Type, name: $"Genetic_{geneticIndicator.Type}_{Guid.NewGuid():N}", period: geneticIndicator.Period, fastPeriods: geneticIndicator.FastPeriods, slowPeriods: geneticIndicator.SlowPeriods, signalPeriods: geneticIndicator.SignalPeriods, multiplier: geneticIndicator.Multiplier, stochPeriods: geneticIndicator.StochPeriods, smoothPeriods: geneticIndicator.SmoothPeriods, cyclePeriods: geneticIndicator.CyclePeriods ); scenario.AddIndicator(indicator); } 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 = "Oda-embedded", 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); } } /// /// Genetic indicator with parameters /// public class GeneticIndicator { public IndicatorType Type { get; set; } public int? Period { get; set; } public int? FastPeriods { get; set; } public int? SlowPeriods { get; set; } public int? SignalPeriods { get; set; } public double? Multiplier { get; set; } public int? StochPeriods { get; set; } public int? SmoothPeriods { get; set; } public int? CyclePeriods { get; set; } } /// /// Multi-objective fitness function for trading bot optimization /// public class TradingBotFitness : IFitness { private readonly IBacktester _backtester; private readonly GeneticRequest _request; private GeneticAlgorithm _geneticAlgorithm; public TradingBotFitness(IBacktester backtester, GeneticRequest request) { _backtester = backtester; _request = request; } 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); // Get current generation number (default to 0 if not available) var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0; // Run backtest var backtest = _backtester.RunTradingBotBacktest( config, _request.StartDate, _request.EndDate, _request.User, true, false, // Don't include candles _request.RequestId, new { generation = currentGeneration } ).Result; // Calculate multi-objective fitness based on backtest results var fitness = CalculateFitness(backtest, config); return fitness; } catch (Exception) { // Return low fitness for failed backtests return 0.1; } } private double CalculateFitness(Backtest backtest, TradingBotConfig config) { if (backtest == null || backtest.Statistics == null) return 0.1; // Use the comprehensive backtest score directly as fitness // The BacktestScorer already includes all important metrics with proper weighting return backtest.Score; } }