diff --git a/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs b/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs index 49f65786..2e8cdfbf 100644 --- a/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs @@ -1,4 +1,5 @@ using Orleans; +using Orleans.Concurrency; namespace Managing.Application.Abstractions.Grains; @@ -15,5 +16,6 @@ public interface IBundleBacktestGrain : IGrainWithGuidKey /// The RequestId is determined by the grain's primary key /// /// Task representing the async operation + [OneWay] Task ProcessBundleRequestAsync(); -} +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Grains/IGeneticBacktestGrain.cs b/src/Managing.Application.Abstractions/Grains/IGeneticBacktestGrain.cs new file mode 100644 index 00000000..699f2d82 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IGeneticBacktestGrain.cs @@ -0,0 +1,19 @@ +using Orleans; +using Orleans.Concurrency; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for Genetic Backtest operations. +/// Uses the genetic request ID as the primary key (string). +/// The grain processes a single genetic request identified by its primary key. +/// +public interface IGeneticBacktestGrain : IGrainWithStringKey +{ + /// + /// Processes the genetic backtest request for this grain's RequestId. + /// The RequestId is determined by the grain's primary key. + /// + [OneWay] + Task ProcessGeneticRequestAsync(); +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs index 8a23f1da..0f962f73 100644 --- a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs @@ -64,7 +64,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain // Create a fresh TradingBotBase instance for this backtest var tradingBot = await CreateTradingBotInstance(config); - tradingBot.Account = user.Accounts.First(a => a.Name == config.AccountName); + tradingBot.Account = user.Accounts.First(); var totalCandles = candles.Count; var currentCandle = 0; diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index 50c72218..4f8d05ff 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using GeneticSharp; +using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Core; @@ -26,6 +27,7 @@ public class GeneticService : IGeneticService private readonly ILogger _logger; private readonly IMessengerService _messengerService; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IGrainFactory _grainFactory; // Predefined parameter ranges for each indicator (matching backtestGenetic.tsx) public static readonly Dictionary ParameterRanges = new() @@ -193,13 +195,15 @@ public class GeneticService : IGeneticService IBacktester backtester, ILogger logger, IMessengerService messengerService, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IGrainFactory grainFactory) { _geneticRepository = geneticRepository; _backtester = backtester; _logger = logger; _messengerService = messengerService; _serviceScopeFactory = serviceScopeFactory; + _grainFactory = grainFactory; } public GeneticRequest CreateGeneticRequest( @@ -240,6 +244,18 @@ public class GeneticService : IGeneticService }; _geneticRepository.InsertGeneticRequestForUser(user, geneticRequest); + + // Trigger Orleans grain to process this request asynchronously + try + { + var grain = _grainFactory.GetGrain(id); + _ = grain.ProcessGeneticRequestAsync(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to trigger GeneticBacktestGrain for request {RequestId}", id); + } + return geneticRequest; } @@ -888,7 +904,7 @@ public class TradingBotFitness : IFitness // Run backtest using scoped service to avoid DbContext concurrency issues var lightBacktest = ServiceScopeHelpers.WithScopedService( _serviceScopeFactory, - backtester => backtester.RunTradingBotBacktest( + async backtester => await backtester.RunTradingBotBacktest( config, _request.StartDate, _request.EndDate, @@ -896,12 +912,9 @@ public class TradingBotFitness : IFitness true, false, // Don't include candles _request.RequestId, - new - { - generation = currentGeneration - } + new GeneticBacktestMetadata(currentGeneration, _request.RequestId) ) - ).Result; + ).GetAwaiter().GetResult(); // Calculate multi-objective fitness based on backtest results var fitness = CalculateFitness(lightBacktest, config); @@ -912,7 +925,7 @@ public class TradingBotFitness : IFitness { _logger.LogWarning("Fitness evaluation failed for chromosome: {Message}", ex.Message); // Return low fitness for failed backtests - return 0.1; + return 0; } } diff --git a/src/Managing.Application/Grains/GeneticBacktestGrain.cs b/src/Managing.Application/Grains/GeneticBacktestGrain.cs new file mode 100644 index 00000000..3a1cdad2 --- /dev/null +++ b/src/Managing.Application/Grains/GeneticBacktestGrain.cs @@ -0,0 +1,90 @@ +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Services; +using Managing.Core; +using Managing.Domain.Accounts; +using Managing.Domain.Backtests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orleans.Concurrency; + +namespace Managing.Application.Grains; + +/// +/// Stateless worker grain for processing genetic backtest requests. +/// Uses the genetic request ID (string) as the primary key. +/// +[StatelessWorker] +public class GeneticBacktestGrain : Grain, IGeneticBacktestGrain +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + public GeneticBacktestGrain( + ILogger logger, + IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + public async Task ProcessGeneticRequestAsync() + { + var requestId = this.GetPrimaryKeyString(); + + try + { + using var scope = _scopeFactory.CreateScope(); + var geneticService = scope.ServiceProvider.GetRequiredService(); + + // Load the request by status lists and filter by ID (Pending first, then Failed for retries) + var pending = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Pending); + var failed = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Failed); + var request = pending.Concat(failed).FirstOrDefault(r => r.RequestId == requestId); + + if (request == null) + { + _logger.LogWarning("[GeneticBacktestGrain] Request {RequestId} not found among pending/failed.", + requestId); + return; + } + + // Mark running + request.Status = GeneticRequestStatus.Running; + await geneticService.UpdateGeneticRequestAsync(request); + + request.User.Accounts = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, + async accountService => (await accountService.GetAccountsByUserAsync(request.User)).ToList()); + + // Run GA + var result = await geneticService.RunGeneticAlgorithm(request, CancellationToken.None); + + // Update final state + request.Status = GeneticRequestStatus.Completed; + request.CompletedAt = DateTime.UtcNow; + request.BestFitness = result.BestFitness; + request.BestIndividual = result.BestIndividual; + request.ProgressInfo = result.ProgressInfo; + await geneticService.UpdateGeneticRequestAsync(request); + + _logger.LogInformation("[GeneticBacktestGrain] Completed request {RequestId}", requestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "[GeneticBacktestGrain] Error processing request {RequestId}", requestId); + try + { + using var scope = _scopeFactory.CreateScope(); + var geneticService = scope.ServiceProvider.GetRequiredService(); + var running = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running); + var req = running.FirstOrDefault(r => r.RequestId == requestId) ?? new GeneticRequest(requestId); + req.Status = GeneticRequestStatus.Failed; + req.ErrorMessage = ex.Message; + req.CompletedAt = DateTime.UtcNow; + await geneticService.UpdateGeneticRequestAsync(req); + } + catch + { + } + } + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/GeneticBacktestMetadata.cs b/src/Managing.Domain/Backtests/GeneticBacktestMetadata.cs new file mode 100644 index 00000000..1481e2df --- /dev/null +++ b/src/Managing.Domain/Backtests/GeneticBacktestMetadata.cs @@ -0,0 +1,26 @@ +using Orleans; + +namespace Managing.Domain.Backtests; + +/// +/// Metadata class for genetic algorithm backtests. +/// This class is designed to be Orleans-serializable. +/// +[GenerateSerializer] +public class GeneticBacktestMetadata +{ + [Id(0)] public int Generation { get; set; } + [Id(1)] public string RequestId { get; set; } = string.Empty; + [Id(2)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public GeneticBacktestMetadata() + { + } + + public GeneticBacktestMetadata(int generation, string requestId = null) + { + Generation = generation; + RequestId = requestId ?? string.Empty; + CreatedAt = DateTime.UtcNow; + } +} diff --git a/src/Managing.Domain/Backtests/LightBacktest.cs b/src/Managing.Domain/Backtests/LightBacktest.cs index 8e3d9974..a0bdbbf0 100644 --- a/src/Managing.Domain/Backtests/LightBacktest.cs +++ b/src/Managing.Domain/Backtests/LightBacktest.cs @@ -23,4 +23,5 @@ public class LightBacktest [Id(10)] public double? SharpeRatio { get; set; } [Id(11)] public double Score { get; set; } [Id(12)] public string ScoreMessage { get; set; } = string.Empty; + [Id(13)] public object Metadata { get; set; } } \ No newline at end of file