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