Add genetic backtest to worker

This commit is contained in:
2025-11-09 03:32:08 +07:00
parent 7dba29c66f
commit 7e08e63dd1
30 changed files with 5056 additions and 232 deletions

View File

@@ -0,0 +1,122 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions.Shared;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Backtests;
/// <summary>
/// Adapter that wraps BacktestExecutor to implement IBacktester interface.
/// Used in compute workers where Backtester (with SignalR dependencies) is not available.
/// Only implements methods needed for genetic algorithm execution.
/// </summary>
public class BacktestExecutorAdapter : IBacktester
{
private readonly BacktestExecutor _executor;
private readonly IExchangeService _exchangeService;
private readonly ILogger<BacktestExecutorAdapter> _logger;
public BacktestExecutorAdapter(
BacktestExecutor executor,
IExchangeService exchangeService,
ILogger<BacktestExecutorAdapter> logger)
{
_executor = executor;
_exchangeService = exchangeService;
_logger = logger;
}
public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool save = false,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
// Load candles using ExchangeService
var candles = await _exchangeService.GetCandlesInflux(
TradingExchanges.Evm,
config.Ticker,
startDate,
config.Timeframe,
endDate);
if (candles == null || candles.Count == 0)
{
throw new InvalidOperationException(
$"No candles found for {config.Ticker} on {config.Timeframe} from {startDate} to {endDate}");
}
// Execute using BacktestExecutor
var result = await _executor.ExecuteAsync(
config,
candles,
user,
save,
withCandles,
requestId,
metadata,
progressCallback: null);
return result;
}
public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config,
HashSet<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
// Execute using BacktestExecutor
var result = await _executor.ExecuteAsync(
config,
candles,
user,
save: false,
withCandles,
requestId,
metadata,
progressCallback: null);
return result;
}
// Methods not needed for compute worker - throw NotImplementedException
public Task<bool> DeleteBacktestAsync(string id) => throw new NotImplementedException("Not available in compute worker");
public bool DeleteBacktests() => throw new NotImplementedException("Not available in compute worker");
public IEnumerable<Backtest> GetBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
public Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
public Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") => throw new NotImplementedException("Not available in compute worker");
public Task<Backtest> GetBacktestByIdForUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
public Task<bool> DeleteBacktestByUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker");
public Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids) => throw new NotImplementedException("Not available in compute worker");
public bool DeleteBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
public Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, BacktestSortableColumn sortBy, string sortOrder = "desc", BacktestsFilter? filter = null) => throw new NotImplementedException("Not available in compute worker");
public Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker");
public Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter) => throw new NotImplementedException("Not available in compute worker");
public Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false) => throw new NotImplementedException("Not available in compute worker");
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) => throw new NotImplementedException("Not available in compute worker");
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker");
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
public Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
public Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest) => throw new NotImplementedException("Not available in compute worker");
public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
public Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) => throw new NotImplementedException("Not available in compute worker");
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
public Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker");
}

View File

@@ -15,18 +15,18 @@ namespace Managing.Application.Backtests;
/// <summary>
/// Service for creating and managing backtest jobs in the queue
/// </summary>
public class BacktestJobService
public class JobService
{
private readonly IBacktestJobRepository _jobRepository;
private readonly IJobRepository _jobRepository;
private readonly IBacktestRepository _backtestRepository;
private readonly IKaigenService _kaigenService;
private readonly ILogger<BacktestJobService> _logger;
private readonly ILogger<JobService> _logger;
public BacktestJobService(
IBacktestJobRepository jobRepository,
public JobService(
IJobRepository jobRepository,
IBacktestRepository backtestRepository,
IKaigenService kaigenService,
ILogger<BacktestJobService> logger)
ILogger<JobService> logger)
{
_jobRepository = jobRepository;
_backtestRepository = backtestRepository;
@@ -37,7 +37,7 @@ public class BacktestJobService
/// <summary>
/// Creates a single backtest job
/// </summary>
public async Task<BacktestJob> CreateJobAsync(
public async Task<Job> CreateJobAsync(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
@@ -63,10 +63,10 @@ public class BacktestJobService
try
{
var job = new BacktestJob
var job = new Job
{
UserId = user.Id,
Status = BacktestJobStatus.Pending,
Status = JobStatus.Pending,
JobType = JobType.Backtest,
Priority = priority,
ConfigJson = JsonSerializer.Serialize(config),
@@ -109,11 +109,11 @@ public class BacktestJobService
/// <summary>
/// Creates multiple backtest jobs from bundle variants
/// </summary>
public async Task<List<BacktestJob>> CreateBundleJobsAsync(
public async Task<List<Job>> CreateBundleJobsAsync(
BundleBacktestRequest bundleRequest,
List<RunBacktestRequest> backtestRequests)
{
var jobs = new List<BacktestJob>();
var jobs = new List<Job>();
var creditRequestId = (string?)null;
try
@@ -203,10 +203,10 @@ public class BacktestJobService
UseForDynamicStopLoss = backtestRequest.Config.UseForDynamicStopLoss
};
var job = new BacktestJob
var job = new Job
{
UserId = bundleRequest.User.Id,
Status = BacktestJobStatus.Pending,
Status = JobStatus.Pending,
JobType = JobType.Backtest,
Priority = 0, // All bundle jobs have same priority
ConfigJson = JsonSerializer.Serialize(backtestConfig),

View File

@@ -29,7 +29,7 @@ namespace Managing.Application.Backtests
private readonly IMessengerService _messengerService;
private readonly IKaigenService _kaigenService;
private readonly IHubContext<BacktestHub> _hubContext;
private readonly BacktestJobService _jobService;
private readonly JobService _jobService;
public Backtester(
IExchangeService exchangeService,
@@ -41,7 +41,7 @@ namespace Managing.Application.Backtests
IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext,
IServiceScopeFactory serviceScopeFactory,
BacktestJobService jobService)
JobService jobService)
{
_exchangeService = exchangeService;
_backtestRepository = backtestRepository;

View File

@@ -0,0 +1,149 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Backtests;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Backtests;
/// <summary>
/// Service for executing genetic algorithm requests without Orleans dependencies.
/// Extracted from GeneticBacktestGrain to be reusable in compute workers.
/// </summary>
public class GeneticExecutor
{
private readonly ILogger<GeneticExecutor> _logger;
private readonly IGeneticService _geneticService;
private readonly IAccountService _accountService;
private readonly IWebhookService _webhookService;
public GeneticExecutor(
ILogger<GeneticExecutor> logger,
IGeneticService geneticService,
IAccountService accountService,
IWebhookService webhookService)
{
_logger = logger;
_geneticService = geneticService;
_accountService = accountService;
_webhookService = webhookService;
}
/// <summary>
/// Executes a genetic algorithm request.
/// </summary>
/// <param name="geneticRequestId">The genetic request ID to process</param>
/// <param name="progressCallback">Optional callback for progress updates (0-100)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The genetic algorithm result</returns>
public async Task<GeneticAlgorithmResult> ExecuteAsync(
string geneticRequestId,
Func<int, Task> progressCallback = null,
CancellationToken cancellationToken = default)
{
try
{
// 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 == geneticRequestId);
if (request == null)
{
_logger.LogWarning("[GeneticExecutor] Request {RequestId} not found among pending/failed.",
geneticRequestId);
throw new InvalidOperationException($"Genetic request {geneticRequestId} not found");
}
_logger.LogInformation("[GeneticExecutor] Processing genetic request {RequestId} for user {UserId}",
request.RequestId, request.User.Id);
// Mark running
request.Status = GeneticRequestStatus.Running;
await _geneticService.UpdateGeneticRequestAsync(request);
// Load user accounts if not already loaded
if (request.User.Accounts == null || !request.User.Accounts.Any())
{
request.User.Accounts = (await _accountService.GetAccountsByUserAsync(request.User)).ToList();
}
// Create progress wrapper if callback provided
Func<int, Task> wrappedProgressCallback = null;
if (progressCallback != null)
{
wrappedProgressCallback = async (percentage) =>
{
try
{
await progressCallback(percentage);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error in progress callback for genetic request {RequestId}", geneticRequestId);
}
};
}
// Run GA
var result = await _geneticService.RunGeneticAlgorithm(request, cancellationToken);
// 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("[GeneticExecutor] Completed genetic request {RequestId}. Best Fitness: {BestFitness}",
request.RequestId, result.BestFitness);
// Send webhook notification if user has telegram channel
if (!string.IsNullOrEmpty(request.User?.TelegramChannel))
{
var message = $"✅ Genetic algorithm optimization completed for {request.Ticker} on {request.Timeframe}. " +
$"Request ID: {request.RequestId}. " +
$"Best Fitness: {result.BestFitness:F4}. " +
$"Generations: {request.Generations}, Population: {request.PopulationSize}.";
await _webhookService.SendMessage(message, request.User.TelegramChannel);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "[GeneticExecutor] Error processing genetic request {RequestId}", geneticRequestId);
// Try to mark as failed
try
{
var running = await _geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running);
var req = running.FirstOrDefault(r => r.RequestId == geneticRequestId);
if (req != null)
{
req.Status = GeneticRequestStatus.Failed;
req.ErrorMessage = ex.Message;
req.CompletedAt = DateTime.UtcNow;
await _geneticService.UpdateGeneticRequestAsync(req);
// Send webhook notification for failed genetic request
if (!string.IsNullOrEmpty(req.User?.TelegramChannel))
{
var message = $"❌ Genetic algorithm optimization failed for {req.Ticker} on {req.Timeframe}. " +
$"Request ID: {req.RequestId}. " +
$"Error: {ex.Message}";
await _webhookService.SendMessage(message, req.User.TelegramChannel);
}
}
}
catch (Exception updateEx)
{
_logger.LogError(updateEx, "[GeneticExecutor] Failed to update request status to Failed for {RequestId}", geneticRequestId);
}
throw;
}
}
}