From e43a1af5ef5acd340f375225980b0c6ec3c73c4b Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 11 Jul 2025 14:11:41 +0700 Subject: [PATCH] Fix genetic backend --- .cursor/rules/fullstack.mdc | 1 + src/Managing.Api.Workers/appsettings.Oda.json | 8 +- .../Controllers/BacktestController.cs | 19 + .../Repositories/IBacktestRepository.cs | 1 + .../Services/IBacktester.cs | 1 + .../Services/IGeneticService.cs | 3 +- .../GeneticAlgorithmWorker.cs | 2 +- .../Backtesting/Backtester.cs | 13 +- src/Managing.Application/GeneticService.cs | 505 +++++++++++------- .../Backtests/GeneticRequest.cs | 15 + .../BacktestRepository.cs | 9 + .../MongoDb/Collections/GeneticRequestDto.cs | 4 + .../src/generated/ManagingApi.ts | 42 ++ .../src/generated/ManagingApiTypes.ts | 4 + .../backtestPage/backtestGeneticBundle.tsx | 120 ++++- 15 files changed, 542 insertions(+), 205 deletions(-) diff --git a/.cursor/rules/fullstack.mdc b/.cursor/rules/fullstack.mdc index 806d53c..0d754c0 100644 --- a/.cursor/rules/fullstack.mdc +++ b/.cursor/rules/fullstack.mdc @@ -105,5 +105,6 @@ Key Principles - Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build - Do not reference new react library if a component already exist in mollecules or atoms - After finishing the editing, build the project + - you have to pass from controller -> application -> repository, do not inject repository inside controllers Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components. diff --git a/src/Managing.Api.Workers/appsettings.Oda.json b/src/Managing.Api.Workers/appsettings.Oda.json index b943704..e758eb2 100644 --- a/src/Managing.Api.Workers/appsettings.Oda.json +++ b/src/Managing.Api.Workers/appsettings.Oda.json @@ -25,10 +25,10 @@ "Uri": "http://localhost:9200" }, "AllowedHosts": "*", - "WorkerPricesFifteenMinutes": true, - "WorkerPricesOneHour": true, - "WorkerPricesFourHours": true, - "WorkerPricesOneDay": true, + "WorkerPricesFifteenMinutes": false, + "WorkerPricesOneHour": false, + "WorkerPricesFourHours": false, + "WorkerPricesOneDay": false, "WorkerPricesFiveMinutes": false, "WorkerFee": false, "WorkerPositionManager": false, diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 0dbdb73..53ffa0e 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -103,6 +103,25 @@ public class BacktestController : BaseController return Ok(_backtester.DeleteBacktestByUser(user, id)); } + /// + /// Retrieves all backtests for a specific genetic request ID. + /// This endpoint is used to view the results of a genetic algorithm optimization. + /// + /// The request ID to filter backtests by. + /// A list of backtests associated with the specified request ID. + [HttpGet] + [Route("ByRequestId/{requestId}")] + public async Task>> GetBacktestsByRequestId(string requestId) + { + if (string.IsNullOrEmpty(requestId)) + { + return BadRequest("Request ID is required"); + } + + var backtests = _backtester.GetBacktestsByRequestId(requestId); + return Ok(backtests); + } + /// /// Runs a backtest with the specified configuration. /// The returned backtest includes a complete TradingBotConfig that preserves all diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs index 899cfa0..c3873b7 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs @@ -7,6 +7,7 @@ public interface IBacktestRepository { void InsertBacktestForUser(User user, Backtest result); IEnumerable GetBacktestsByUser(User user); + IEnumerable GetBacktestsByRequestId(string requestId); Backtest GetBacktestByIdForUser(User user, string id); void DeleteBacktestByIdForUser(User user, string id); void DeleteAllBacktestsForUser(User user); diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index d0399f0..d50b2f4 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -49,6 +49,7 @@ namespace Managing.Application.Abstractions.Services bool DeleteBacktest(string id); bool DeleteBacktests(); IEnumerable GetBacktestsByUser(User user); + IEnumerable GetBacktestsByRequestId(string requestId); Backtest GetBacktestByIdForUser(User user, string id); bool DeleteBacktestByUser(User user, string id); bool DeleteBacktestsByUser(User user); diff --git a/src/Managing.Application.Abstractions/Services/IGeneticService.cs b/src/Managing.Application.Abstractions/Services/IGeneticService.cs index cf0a308..d53790c 100644 --- a/src/Managing.Application.Abstractions/Services/IGeneticService.cs +++ b/src/Managing.Application.Abstractions/Services/IGeneticService.cs @@ -79,6 +79,7 @@ public interface IGeneticService /// Runs the genetic algorithm for a specific request /// /// The genetic request to process + /// Cancellation token to stop the algorithm /// The genetic algorithm result - Task RunGeneticAlgorithm(GeneticRequest request); + Task RunGeneticAlgorithm(GeneticRequest request, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Managing.Application.Workers/GeneticAlgorithmWorker.cs b/src/Managing.Application.Workers/GeneticAlgorithmWorker.cs index 7fc4968..ffbe564 100644 --- a/src/Managing.Application.Workers/GeneticAlgorithmWorker.cs +++ b/src/Managing.Application.Workers/GeneticAlgorithmWorker.cs @@ -70,7 +70,7 @@ public class GeneticAlgorithmWorker : BaseWorker _geneticService.UpdateGeneticRequest(request); // Run genetic algorithm using the service - var results = await _geneticService.RunGeneticAlgorithm(request); + var results = await _geneticService.RunGeneticAlgorithm(request, cancellationToken); // Update request with results request.Status = GeneticRequestStatus.Completed; diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 8a0f2fd..eb6f473 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -80,8 +80,7 @@ namespace Managing.Application.Backtesting bool withCandles = false, string requestId = null) { - var account = await GetAccountFromConfig(config); - var candles = GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate); + var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId); @@ -165,7 +164,7 @@ namespace Managing.Application.Backtesting }; } - private List GetCandles(Account account, Ticker ticker, Timeframe timeframe, + private List GetCandles(Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, @@ -400,6 +399,12 @@ namespace Managing.Application.Backtesting return backtests; } + public IEnumerable GetBacktestsByRequestId(string requestId) + { + var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); + return backtests; + } + public Backtest GetBacktestByIdForUser(User user, string id) { var backtest = _backtestRepository.GetBacktestByIdForUser(user, id); @@ -462,7 +467,5 @@ namespace Managing.Application.Backtesting return false; } } - - } } \ No newline at end of file diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index 91227e3..9b3f5fc 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -42,104 +42,127 @@ public class GeneticService : IGeneticService [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.MacdCross] = new() + { + ["fastPeriods"] = 12.0, + ["slowPeriods"] = 26.0, + ["signalPeriods"] = 9.0 }, - [IndicatorType.DualEmaCross] = new() { - ["fastPeriods"] = 12.0, - ["slowPeriods"] = 26.0 + [IndicatorType.DualEmaCross] = new() + { + ["fastPeriods"] = 12.0, + ["slowPeriods"] = 26.0 }, - [IndicatorType.SuperTrend] = new() { - ["period"] = 14.0, - ["multiplier"] = 3.0 + [IndicatorType.SuperTrend] = new() + { + ["period"] = 14.0, + ["multiplier"] = 3.0 }, - [IndicatorType.SuperTrendCrossEma] = 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.ChandelierExit] = new() + { + ["period"] = 14.0, + ["multiplier"] = 3.0 }, - [IndicatorType.StochRsiTrend] = new() { - ["period"] = 14.0, - ["stochPeriods"] = 14.0, - ["signalPeriods"] = 9.0, - ["smoothPeriods"] = 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.Stc] = new() + { + ["cyclePeriods"] = 10.0, + ["fastPeriods"] = 12.0, + ["slowPeriods"] = 26.0 }, - [IndicatorType.LaggingStc] = 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) - } - }; + 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() @@ -236,8 +259,10 @@ public class GeneticService : IGeneticService /// 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) + public async Task RunGeneticAlgorithm(GeneticRequest request, + CancellationToken cancellationToken = default) { try { @@ -247,15 +272,39 @@ public class GeneticService : IGeneticService request.Status = GeneticRequestStatus.Running; UpdateGeneticRequest(request); - // Create chromosome for trading bot configuration - var chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit); - + // 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 var fitness = new TradingBotFitness(_backtester, request); - + // Create genetic algorithm with better configuration var ga = new GeneticAlgorithm( - new Population(request.PopulationSize, request.PopulationSize, chromosome), + population, fitness, GetSelection(request.SelectionMethod), new UniformCrossover(), @@ -266,14 +315,73 @@ public class GeneticService : IGeneticService CrossoverProbability = 0.7f // Fixed crossover rate as in frontend }; + // 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 5 generations + if (generationCount % 5 == 0) + { + 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.Value; + var bestFitness = ga.BestChromosome?.Fitness ?? 0; - _logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}", + _logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}", request.RequestId, bestFitness); // Update request with results @@ -289,7 +397,7 @@ public class GeneticService : IGeneticService generations = request.Generations, completed_at = DateTime.UtcNow }); - + UpdateGeneticRequest(request); return new GeneticAlgorithmResult @@ -303,13 +411,13 @@ public class GeneticService : IGeneticService 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; } } @@ -345,11 +453,18 @@ public class TradingBotChromosome : ChromosomeBase // 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 + 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) @@ -366,7 +481,12 @@ public class TradingBotChromosome : ChromosomeBase _ => new Gene(0) }; } - else if (geneIndex < 8) + 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)); @@ -374,46 +494,46 @@ public class TradingBotChromosome : ChromosomeBase else { // Indicator parameters - var indicatorIndex = (geneIndex - 8) / 8; - var paramIndex = (geneIndex - 8) % 8; - + 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]); - } - } - } + + 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); } } @@ -434,70 +554,70 @@ public class TradingBotChromosome : ChromosomeBase { var selected = new List(); var genes = GetGenes(); - + for (int i = 0; i < 4; i++) // Check first 4 indicator slots { - if (genes[4 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count) + if (genes[5 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count) { var indicator = new GeneticIndicator { Type = _eligibleIndicators[i] }; - + // Add parameters for this indicator - var baseIndex = 8 + i * 8; + 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; } @@ -505,7 +625,7 @@ public class TradingBotChromosome : ChromosomeBase { var genes = GetGenes(); var selectedIndicators = GetSelectedIndicators(); - + // Ensure we have at least one indicator if (!selectedIndicators.Any()) { @@ -514,25 +634,28 @@ public class TradingBotChromosome : ChromosomeBase // 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", 1); - + var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod); + foreach (var geneticIndicator in selectedIndicators) { var indicator = ScenarioHelpers.BuildIndicator( @@ -547,14 +670,14 @@ public class TradingBotChromosome : ChromosomeBase smoothPeriods: geneticIndicator.SmoothPeriods, cyclePeriods: geneticIndicator.CyclePeriods ); - + scenario.AddIndicator(indicator); } return new TradingBotConfig { Name = $"Genetic_{request.RequestId}", - AccountName = "genetic_account", + AccountName = "Oda-embedded", Ticker = request.Ticker, Timeframe = request.Timeframe, BotTradingBalance = request.Balance, @@ -612,11 +735,11 @@ public class TradingBotChromosome : ChromosomeBase }; } - private bool HasParameter(IndicatorType indicator, string paramName) - { - return GeneticService.IndicatorParamMapping.ContainsKey(indicator) && - GeneticService.IndicatorParamMapping[indicator].Contains(paramName); - } + private bool HasParameter(IndicatorType indicator, string paramName) + { + return GeneticService.IndicatorParamMapping.ContainsKey(indicator) && + GeneticService.IndicatorParamMapping[indicator].Contains(paramName); + } } /// @@ -658,21 +781,21 @@ public class TradingBotFitness : IFitness return 0; var config = tradingBotChromosome.GetTradingBotConfig(_request); - + // Run backtest var backtest = _backtester.RunTradingBotBacktest( config, _request.StartDate, _request.EndDate, _request.User, - false, // Don't save individual backtests + true, // Don't save individual backtests false, // Don't include candles _request.RequestId ).Result; // Calculate multi-objective fitness based on backtest results var fitness = CalculateMultiObjectiveFitness(backtest, config); - + return fitness; } catch (Exception) @@ -688,30 +811,32 @@ public class TradingBotFitness : IFitness return 0.1; var stats = backtest.Statistics; - - // 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; - + + // 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 +} \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/GeneticRequest.cs b/src/Managing.Domain/Backtests/GeneticRequest.cs index 9ffcc26..d7f4176 100644 --- a/src/Managing.Domain/Backtests/GeneticRequest.cs +++ b/src/Managing.Domain/Backtests/GeneticRequest.cs @@ -154,6 +154,21 @@ public class GeneticRequest /// Progress information (JSON serialized) /// public string? ProgressInfo { get; set; } + + /// + /// The best chromosome found so far (JSON serialized) + /// + public string? BestChromosome { get; set; } + + /// + /// Current generation number when the algorithm was stopped + /// + public int CurrentGeneration { get; set; } + + /// + /// The best fitness score achieved so far + /// + public double? BestFitnessSoFar { get; set; } } /// diff --git a/src/Managing.Infrastructure.Database/BacktestRepository.cs b/src/Managing.Infrastructure.Database/BacktestRepository.cs index bf103a6..eda9a55 100644 --- a/src/Managing.Infrastructure.Database/BacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/BacktestRepository.cs @@ -32,6 +32,15 @@ public class BacktestRepository : IBacktestRepository return backtests.Select(b => MongoMappers.Map(b)); } + public IEnumerable GetBacktestsByRequestId(string requestId) + { + var backtests = _backtestRepository.AsQueryable() + .Where(b => b.RequestId == requestId) + .ToList(); + + return backtests.Select(b => MongoMappers.Map(b)); + } + public Backtest GetBacktestByIdForUser(User user, string id) { var backtest = _backtestRepository.FindById(id); diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/GeneticRequestDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/GeneticRequestDto.cs index d3c0278..b066459 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/Collections/GeneticRequestDto.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/GeneticRequestDto.cs @@ -27,5 +27,9 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections public string? BestIndividual { get; set; } public string? ErrorMessage { get; set; } public string? ProgressInfo { get; set; } + public string? BestChromosome { get; set; } + public double? BestFitnessSoFar { get; set; } + public int CurrentGeneration { get; set; } + public DateTime? UpdatedAt { get; set; } } } \ No newline at end of file diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index ea1f447..53bf8c2 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -498,6 +498,44 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + backtest_GetBacktestsByRequestId(requestId: string): Promise { + let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}"; + if (requestId === undefined || requestId === null) + throw new Error("The parameter 'requestId' must be defined."); + url_ = url_.replace("{requestId}", encodeURIComponent("" + requestId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_GetBacktestsByRequestId(_response); + }); + } + + protected processBacktest_GetBacktestsByRequestId(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Backtest[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + backtest_Run(request: RunBacktestRequest): Promise { let url_ = this.baseUrl + "/Backtest/Run"; url_ = url_.replace(/[?&]$/, ""); @@ -3251,6 +3289,7 @@ export interface Backtest { user: User; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; + requestId?: string | null; } export interface TradingBotConfig { @@ -3691,6 +3730,9 @@ export interface GeneticRequest { bestIndividual?: string | null; errorMessage?: string | null; progressInfo?: string | null; + bestChromosome?: string | null; + currentGeneration?: number; + bestFitnessSoFar?: number | null; } export enum GeneticRequestStatus { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index d52dcdf..912822e 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -236,6 +236,7 @@ export interface Backtest { user: User; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; + requestId?: string | null; } export interface TradingBotConfig { @@ -676,6 +677,9 @@ export interface GeneticRequest { bestIndividual?: string | null; errorMessage?: string | null; progressInfo?: string | null; + bestChromosome?: string | null; + currentGeneration?: number; + bestFitnessSoFar?: number | null; } export enum GeneticRequestStatus { diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx index d9e308e..5e14162 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx @@ -4,6 +4,7 @@ import {useQuery} from '@tanstack/react-query' import useApiUrlStore from '../../app/store/apiStore' import { + type Backtest, BacktestClient, type GeneticRequest, IndicatorType, @@ -55,6 +56,10 @@ const BacktestGeneticBundle: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false) const [selectedIndicators, setSelectedIndicators] = useState(ALL_INDICATORS) const [geneticRequests, setGeneticRequests] = useState([]) + const [selectedRequest, setSelectedRequest] = useState(null) + const [isViewModalOpen, setIsViewModalOpen] = useState(false) + const [backtests, setBacktests] = useState([]) + const [isLoadingBacktests, setIsLoadingBacktests] = useState(false) // Form setup const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm({ @@ -187,6 +192,30 @@ const BacktestGeneticBundle: React.FC = () => { } } + // Handle view details + const handleViewDetails = async (request: GeneticRequest) => { + setSelectedRequest(request) + setIsViewModalOpen(true) + setIsLoadingBacktests(true) + + try { + const backtestsData = await backtestClient.backtest_GetBacktestsByRequestId(request.requestId) + setBacktests(backtestsData) + } catch (error) { + console.error('Error fetching backtests:', error) + new Toast('Failed to load backtest details', false) + } finally { + setIsLoadingBacktests(false) + } + } + + // Close view modal + const closeViewModal = () => { + setIsViewModalOpen(false) + setSelectedRequest(null) + setBacktests([]) + } + return (
@@ -433,10 +462,7 @@ const BacktestGeneticBundle: React.FC = () => {
)} + + {/* View Details Modal */} + {isViewModalOpen && selectedRequest && ( +
+
+

+ Genetic Request Details - {selectedRequest.requestId.slice(0, 8)}... +

+ +
+
+ Ticker: {selectedRequest.ticker} +
+
+ Timeframe: {selectedRequest.timeframe} +
+
+ Status: + + {selectedRequest.status} + +
+
+ Created: {new Date(selectedRequest.createdAt).toLocaleString()} +
+ {selectedRequest.completedAt && ( +
+ Completed: {new Date(selectedRequest.completedAt).toLocaleString()} +
+ )} +
+ +
+

Backtest Results ({backtests.length})

+ {isLoadingBacktests ? ( +
+ +
+ ) : backtests.length > 0 ? ( +
+ + + + + + + + + + + + + + {backtests.map((backtest) => ( + + + + + + + + + + ))} + +
IDFinal PnLWin RateGrowth %ScorePositionsCreated
{backtest.id.slice(0, 8)}...= 0 ? 'text-success' : 'text-error'}> + ${backtest.finalPnl.toFixed(2)} + {backtest.winRate}%= 0 ? 'text-success' : 'text-error'}> + {backtest.growthPercentage.toFixed(2)}% + {backtest.score.toFixed(2)}{backtest.positions?.length || 0}{new Date(backtest.startDate).toLocaleDateString()}
+
+ ) : ( +
+ No backtest results found for this request. +
+ )} +
+ +
+ +
+
+
+ )}
) }