Add genetic backtest to worker
This commit is contained in:
122
src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
Normal file
122
src/Managing.Application/Backtests/BacktestExecutorAdapter.cs
Normal 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");
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
149
src/Managing.Application/Backtests/GeneticExecutor.cs
Normal file
149
src/Managing.Application/Backtests/GeneticExecutor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user