diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index 986dff6..91227e3 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -6,6 +6,7 @@ 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; @@ -21,6 +22,144 @@ public class GeneticService : IGeneticService private readonly IBacktester _backtester; private readonly ILogger _logger; + // 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"] = (5.0, 50.0) + }, + [IndicatorType.RsiDivergenceConfirm] = new() { + ["period"] = (5.0, 50.0) + }, + [IndicatorType.EmaCross] = new() { + ["period"] = (5.0, 200.0) + }, + [IndicatorType.EmaTrend] = new() { + ["period"] = (5.0, 200.0) + }, + [IndicatorType.StDev] = new() { + ["period"] = (5.0, 50.0) + }, + [IndicatorType.ThreeWhiteSoldiers] = new() { + ["period"] = (5.0, 50.0) + }, + [IndicatorType.MacdCross] = new() { + ["fastPeriods"] = (10.0, 50.0), + ["slowPeriods"] = (20.0, 100.0), + ["signalPeriods"] = (5.0, 20.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, 30.0), + ["fastPeriods"] = (5.0, 50.0), + ["slowPeriods"] = (10.0, 100.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, @@ -46,7 +185,7 @@ public class GeneticService : IGeneticService double maxTakeProfit, List eligibleIndicators) { - var id = Guid.NewGuid().ToString(); // Generate unique GUID + var id = Guid.NewGuid().ToString(); var geneticRequest = new GeneticRequest(id) { Ticker = ticker, @@ -104,13 +243,17 @@ public class GeneticService : IGeneticService { _logger.LogInformation("Starting genetic algorithm for request {RequestId}", request.RequestId); + // Update status to running + request.Status = GeneticRequestStatus.Running; + UpdateGeneticRequest(request); + // Create chromosome for trading bot configuration - var chromosome = new TradingBotChromosome(request.EligibleIndicators); + var chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit); // Create fitness function var fitness = new TradingBotFitness(_backtester, request); - // Create genetic algorithm + // Create genetic algorithm with better configuration var ga = new GeneticAlgorithm( new Population(request.PopulationSize, request.PopulationSize, chromosome), fitness, @@ -118,7 +261,9 @@ public class GeneticService : IGeneticService new UniformCrossover(), GetMutation(request.MutationRate)) { - Termination = new GenerationNumberTermination(request.Generations) + Termination = new GenerationNumberTermination(request.Generations), + MutationProbability = (float)request.MutationRate, + CrossoverProbability = 0.7f // Fixed crossover rate as in frontend }; // Run the genetic algorithm @@ -131,22 +276,40 @@ public class GeneticService : IGeneticService _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); + return new GeneticAlgorithmResult { BestFitness = bestFitness, BestIndividual = bestChromosome?.ToString() ?? "unknown", - ProgressInfo = JsonSerializer.Serialize(new - { - generation = ga.GenerationsNumber, - best_fitness = bestFitness, - population_size = request.PopulationSize, - generations = request.Generations - }) + 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; } } @@ -157,7 +320,7 @@ public class GeneticService : IGeneticService { "tournament" => new TournamentSelection(), "roulette" => new RouletteWheelSelection(), - "rank" => new RankSelection(), + "fitness-weighted" => new RankSelection(), // Use rank selection as approximation _ => new TournamentSelection() }; } @@ -169,69 +332,224 @@ public class GeneticService : IGeneticService } /// -/// Chromosome representing a trading bot configuration +/// 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(); - public TradingBotChromosome(List eligibleIndicators) : base(eligibleIndicators.Count + 5) + // 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 + 4 + eligibleIndicators.Count * 8) // Trading params + indicator selection + indicator params { _eligibleIndicators = eligibleIndicators; + _maxTakeProfit = maxTakeProfit; } public override Gene GenerateGene(int geneIndex) { - if (geneIndex < _eligibleIndicators.Count) + if (geneIndex < 4) { - // Gene represents whether an indicator is selected (0 or 1) + // 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 < 8) + { + // Indicator selection (0 = not selected, 1 = selected) return new Gene(_random.Next(2)); } else { - // Additional genes for other parameters - return geneIndex switch + // Indicator parameters + var indicatorIndex = (geneIndex - 8) / 8; + var paramIndex = (geneIndex - 8) % 8; + + if (indicatorIndex < _eligibleIndicators.Count) { - var i when i == _eligibleIndicators.Count => new Gene(_random.Next(1, 11)), // Stop loss percentage - var i when i == _eligibleIndicators.Count + 1 => new Gene(_random.Next(1, 21)), // Take profit percentage - var i when i == _eligibleIndicators.Count + 2 => new Gene(_random.Next(1, 101)), // Position size percentage - var i when i == _eligibleIndicators.Count + 3 => new Gene(_random.Next(1, 51)), // Max positions - var i when i == _eligibleIndicators.Count + 4 => new Gene(_random.Next(1, 11)), // Risk level - _ => new Gene(0) - }; + 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); + return new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit); } public override IChromosome Clone() { - var clone = new TradingBotChromosome(_eligibleIndicators); + var clone = new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit); clone.ReplaceGenes(0, GetGenes()); return clone; } - public List GetSelectedIndicators() + public List GetSelectedIndicators() { - var selected = new List(); - for (int i = 0; i < _eligibleIndicators.Count; i++) + var selected = new List(); + var genes = GetGenes(); + + for (int i = 0; i < 4; i++) // Check first 4 indicator slots { - if (GetGene(i).Value.ToString() == "1") + if (genes[4 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count) { - selected.Add(_eligibleIndicators[i]); + var indicator = new GeneticIndicator + { + Type = _eligibleIndicators[i] + }; + + // Add parameters for this indicator + var baseIndex = 8 + 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 selectedIndicators = GetSelectedIndicators(); 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)); + } + + // Build scenario using selected indicators + var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", 1); + + 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); + } return new TradingBotConfig { @@ -242,28 +560,83 @@ public class TradingBotChromosome : ChromosomeBase BotTradingBalance = request.Balance, IsForBacktest = true, IsForWatchingOnly = false, - CooldownPeriod = 0, - MaxLossStreak = 3, + 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 = new MoneyManagement { Name = $"Genetic_{request.RequestId}_MM", Timeframe = request.Timeframe, - StopLoss = Convert.ToDecimal(genes[_eligibleIndicators.Count].Value), - TakeProfit = Convert.ToDecimal(genes[_eligibleIndicators.Count + 1].Value), + StopLoss = Convert.ToDecimal(stopLoss), + TakeProfit = Convert.ToDecimal(takeProfit), Leverage = 1.0m }, RiskManagement = new RiskManagement { - RiskTolerance = (RiskToleranceLevel)Convert.ToInt32(genes[_eligibleIndicators.Count + 4].Value) + 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); + } } /// -/// Fitness function for trading bot optimization +/// 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 { @@ -297,8 +670,8 @@ public class TradingBotFitness : IFitness _request.RequestId ).Result; - // Calculate fitness based on backtest results - var fitness = CalculateFitness(backtest); + // Calculate multi-objective fitness based on backtest results + var fitness = CalculateMultiObjectiveFitness(backtest, config); return fitness; } @@ -309,29 +682,36 @@ public class TradingBotFitness : IFitness } } - private double CalculateFitness(Backtest backtest) + private double CalculateMultiObjectiveFitness(Backtest backtest, TradingBotConfig config) { - if (backtest == null || backtest.Score == null) + if (backtest == null || backtest.Statistics == null) return 0.1; - // Use the backtest score as the primary fitness metric - var baseFitness = backtest.Score; - - // Apply additional factors - var tradeCount = backtest.Positions?.Count ?? 0; - var winRate = backtest.WinRate; - var finalPnl = backtest.FinalPnl; - - // Penalize if no trades were made - if (tradeCount == 0) - return 0.1; - - // Bonus for good win rate - var winRateBonus = winRate > 0.6 ? 10 : 0; + var stats = backtest.Statistics; - // Bonus for positive PnL - var pnlBonus = finalPnl > 0 ? 20 : 0; - - return baseFitness + winRateBonus + pnlBonus; + // Multi-objective fitness function (matching frontend) + var pnlScore = Math.Max(0, (double)stats.TotalPnL / 1000); // Normalize PnL + var winRateScore = backtest.WinRate / 100.0; // Normalize win rate + var riskRewardScore = Math.Min(2, (double)stats.WinningTrades / Math.Max(1, Math.Abs((double)stats.LoosingTrades))); + var consistencyScore = 1 - Math.Abs((double)stats.TotalPnL - (double)backtest.FinalPnl) / Math.Max(1, Math.Abs((double)stats.TotalPnL)); + + // Risk-reward ratio bonus + var riskRewardRatio = (double)(config.MoneyManagement.TakeProfit / config.MoneyManagement.StopLoss); + var riskRewardBonus = Math.Min(0.2, (riskRewardRatio - 1.1) * 0.1); + + // Drawdown score (normalized to 0-1, where lower drawdown is better) + var maxDrawdownPc = Math.Abs((double)stats.MaxDrawdownPc); + var drawdownScore = Math.Max(0, 1 - (maxDrawdownPc / 50)); + + // Weighted combination + var fitness = + pnlScore * 0.3 + + winRateScore * 0.2 + + riskRewardScore * 0.2 + + consistencyScore * 0.1 + + riskRewardBonus * 0.1 + + drawdownScore * 0.1; + + return Math.Max(0, fitness); } } \ No newline at end of file