diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index bee78768..2bd27326 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -816,7 +816,7 @@ public class BacktestController : BaseController // Get all jobs for this bundle using var serviceScope = _serviceScopeFactory.CreateScope(); - var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); + var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleGuid); var response = new BundleBacktestStatusResponse @@ -824,10 +824,10 @@ public class BacktestController : BaseController BundleRequestId = bundleGuid, Status = bundleRequest.Status.ToString(), TotalJobs = jobs.Count(), - CompletedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Completed), - FailedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Failed), - RunningJobs = jobs.Count(j => j.Status == BacktestJobStatus.Running), - PendingJobs = jobs.Count(j => j.Status == BacktestJobStatus.Pending), + CompletedJobs = jobs.Count(j => j.Status == JobStatus.Completed), + FailedJobs = jobs.Count(j => j.Status == JobStatus.Failed), + RunningJobs = jobs.Count(j => j.Status == JobStatus.Running), + PendingJobs = jobs.Count(j => j.Status == JobStatus.Pending), ProgressPercentage = bundleRequest.ProgressPercentage, CreatedAt = bundleRequest.CreatedAt, CompletedAt = bundleRequest.CompletedAt, diff --git a/src/Managing.Api/Controllers/JobController.cs b/src/Managing.Api/Controllers/JobController.cs index 7b6c0528..0d6c7102 100644 --- a/src/Managing.Api/Controllers/JobController.cs +++ b/src/Managing.Api/Controllers/JobController.cs @@ -68,7 +68,7 @@ public class JobController : BaseController /// The job ID to query /// The job status and result if completed [HttpGet("{jobId}")] - public async Task> GetJobStatus(string jobId) + public async Task> GetJobStatus(string jobId) { if (!await IsUserAdmin()) { @@ -82,7 +82,7 @@ public class JobController : BaseController } using var serviceScope = _serviceScopeFactory.CreateScope(); - var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); + var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var job = await jobRepository.GetByIdAsync(jobGuid); if (job == null) @@ -90,7 +90,7 @@ public class JobController : BaseController return NotFound($"Job with ID {jobId} not found."); } - var response = new BacktestJobStatusResponse + var response = new JobStatusResponse { JobId = job.Id, Status = job.Status.ToString(), @@ -99,7 +99,7 @@ public class JobController : BaseController StartedAt = job.StartedAt, CompletedAt = job.CompletedAt, ErrorMessage = job.ErrorMessage, - Result = job.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson) + Result = job.Status == JobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson) ? JsonSerializer.Deserialize(job.ResultJson) : null }; @@ -156,16 +156,16 @@ public class JobController : BaseController } // Parse status filter - BacktestJobStatus? statusFilter = null; + JobStatus? statusFilter = null; if (!string.IsNullOrEmpty(status)) { - if (Enum.TryParse(status, true, out var parsedStatus)) + if (Enum.TryParse(status, true, out var parsedStatus)) { statusFilter = parsedStatus; } else { - return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames())}"); + return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames())}"); } } @@ -195,7 +195,7 @@ public class JobController : BaseController } using var serviceScope = _serviceScopeFactory.CreateScope(); - var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); + var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var (jobs, totalCount) = await jobRepository.GetPaginatedAsync( page, @@ -257,7 +257,7 @@ public class JobController : BaseController } using var serviceScope = _serviceScopeFactory.CreateScope(); - var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); + var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var summary = await jobRepository.GetSummaryAsync(); diff --git a/src/Managing.Api/Models/Responses/BacktestJobStatusResponse.cs b/src/Managing.Api/Models/Responses/BacktestJobStatusResponse.cs index 059787f1..3b6203ae 100644 --- a/src/Managing.Api/Models/Responses/BacktestJobStatusResponse.cs +++ b/src/Managing.Api/Models/Responses/BacktestJobStatusResponse.cs @@ -3,9 +3,9 @@ using Managing.Domain.Backtests; namespace Managing.Api.Models.Responses; /// -/// Response model for backtest job status +/// Response model for job status /// -public class BacktestJobStatusResponse +public class JobStatusResponse { public Guid JobId { get; set; } public string Status { get; set; } = string.Empty; diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestJobRepository.cs b/src/Managing.Application.Abstractions/Repositories/IJobRepository.cs similarity index 77% rename from src/Managing.Application.Abstractions/Repositories/IBacktestJobRepository.cs rename to src/Managing.Application.Abstractions/Repositories/IJobRepository.cs index e6b135ea..4219e529 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestJobRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IJobRepository.cs @@ -4,14 +4,14 @@ using static Managing.Common.Enums; namespace Managing.Application.Abstractions.Repositories; /// -/// Repository interface for managing backtest jobs in the queue system +/// Repository interface for managing jobs in the queue system /// -public interface IBacktestJobRepository +public interface IJobRepository { /// - /// Creates a new backtest job + /// Creates a new job /// - Task CreateAsync(BacktestJob job); + Task CreateAsync(Job job); /// /// Claims the next available job using PostgreSQL advisory locks. @@ -19,33 +19,33 @@ public interface IBacktestJobRepository /// /// The ID of the worker claiming the job /// Optional job type filter. If null, claims any job type. - Task ClaimNextJobAsync(string workerId, JobType? jobType = null); + Task ClaimNextJobAsync(string workerId, JobType? jobType = null); /// /// Updates an existing job /// - Task UpdateAsync(BacktestJob job); + Task UpdateAsync(Job job); /// /// Gets all jobs for a specific bundle request /// - Task> GetByBundleRequestIdAsync(Guid bundleRequestId); + Task> GetByBundleRequestIdAsync(Guid bundleRequestId); /// /// Gets all jobs for a specific user /// - Task> GetByUserIdAsync(int userId); + Task> GetByUserIdAsync(int userId); /// /// Gets a job by its ID /// - Task GetByIdAsync(Guid jobId); + Task GetByIdAsync(Guid jobId); /// /// Gets stale jobs (jobs that are Running but haven't sent a heartbeat in the specified timeout) /// /// Number of minutes since last heartbeat to consider stale - Task> GetStaleJobsAsync(int timeoutMinutes = 5); + Task> GetStaleJobsAsync(int timeoutMinutes = 5); /// /// Resets stale jobs back to Pending status @@ -55,12 +55,12 @@ public interface IBacktestJobRepository /// /// Gets all running jobs assigned to a specific worker /// - Task> GetRunningJobsByWorkerIdAsync(string workerId); + Task> GetRunningJobsByWorkerIdAsync(string workerId); /// /// Gets all jobs for a specific genetic request ID /// - Task> GetByGeneticRequestIdAsync(string geneticRequestId); + Task> GetByGeneticRequestIdAsync(string geneticRequestId); /// /// Gets paginated jobs with optional filters and sorting @@ -75,12 +75,12 @@ public interface IBacktestJobRepository /// Optional worker ID filter /// Optional bundle request ID filter /// Tuple of jobs and total count - Task<(IEnumerable Jobs, int TotalCount)> GetPaginatedAsync( + Task<(IEnumerable Jobs, int TotalCount)> GetPaginatedAsync( int page, int pageSize, string sortBy = "CreatedAt", string sortOrder = "desc", - BacktestJobStatus? status = null, + JobStatus? status = null, JobType? jobType = null, int? userId = null, string? workerId = null, @@ -109,7 +109,7 @@ public class JobSummary /// public class JobStatusCount { - public BacktestJobStatus Status { get; set; } + public JobStatus Status { get; set; } public int Count { get; set; } } @@ -127,7 +127,7 @@ public class JobTypeCount /// public class JobStatusTypeCount { - public BacktestJobStatus Status { get; set; } + public JobStatus Status { get; set; } public JobType JobType { get; set; } public int Count { get; set; } } diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs index 5f217281..ed6fef5c 100644 --- a/src/Managing.Application.Tests/BotsTests.cs +++ b/src/Managing.Application.Tests/BotsTests.cs @@ -43,12 +43,12 @@ namespace Managing.Application.Tests var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger(); var backtestLogger = TradingBaseTests.CreateBacktesterLogger(); ILoggerFactory loggerFactory = new NullLoggerFactory(); - var backtestJobLogger = loggerFactory.CreateLogger(); + var backtestJobLogger = loggerFactory.CreateLogger(); var botService = new Mock().Object; var agentService = new Mock().Object; var _scopeFactory = new Mock(); - var backtestJobRepository = new Mock().Object; - var backtestJobService = new BacktestJobService(backtestJobRepository, backtestRepository, kaigenService, backtestJobLogger); + var backtestJobRepository = new Mock().Object; + var backtestJobService = new JobService(backtestJobRepository, backtestRepository, kaigenService, backtestJobLogger); _backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger, scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, _scopeFactory.Object, backtestJobService); diff --git a/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs b/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs new file mode 100644 index 00000000..eba12943 --- /dev/null +++ b/src/Managing.Application/Backtests/BacktestExecutorAdapter.cs @@ -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; + +/// +/// 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. +/// +public class BacktestExecutorAdapter : IBacktester +{ + private readonly BacktestExecutor _executor; + private readonly IExchangeService _exchangeService; + private readonly ILogger _logger; + + public BacktestExecutorAdapter( + BacktestExecutor executor, + IExchangeService exchangeService, + ILogger logger) + { + _executor = executor; + _exchangeService = exchangeService; + _logger = logger; + } + + public async Task 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 RunTradingBotBacktest( + TradingBotConfig config, + HashSet 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 DeleteBacktestAsync(string id) => throw new NotImplementedException("Not available in compute worker"); + public bool DeleteBacktests() => throw new NotImplementedException("Not available in compute worker"); + public IEnumerable GetBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker"); + public Task> GetBacktestsByUserAsync(User user) => throw new NotImplementedException("Not available in compute worker"); + public IEnumerable GetBacktestsByRequestId(Guid requestId) => throw new NotImplementedException("Not available in compute worker"); + public Task> GetBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker"); + public (IEnumerable 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 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 GetBacktestByIdForUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker"); + public Task DeleteBacktestByUserAsync(User user, string id) => throw new NotImplementedException("Not available in compute worker"); + public Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable ids) => throw new NotImplementedException("Not available in compute worker"); + public bool DeleteBacktestsByUser(User user) => throw new NotImplementedException("Not available in compute worker"); + public (IEnumerable 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 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 DeleteBacktestsByRequestIdAsync(Guid requestId) => throw new NotImplementedException("Not available in compute worker"); + public Task 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 GetBundleBacktestRequestsByUser(User user) => throw new NotImplementedException("Not available in compute worker"); + public Task> 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 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 GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker"); + public Task> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status) => throw new NotImplementedException("Not available in compute worker"); +} + diff --git a/src/Managing.Application/Backtests/BacktestJobService.cs b/src/Managing.Application/Backtests/BacktestJobService.cs index 09fbf5f9..f6810f0d 100644 --- a/src/Managing.Application/Backtests/BacktestJobService.cs +++ b/src/Managing.Application/Backtests/BacktestJobService.cs @@ -15,18 +15,18 @@ namespace Managing.Application.Backtests; /// /// Service for creating and managing backtest jobs in the queue /// -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 _logger; + private readonly ILogger _logger; - public BacktestJobService( - IBacktestJobRepository jobRepository, + public JobService( + IJobRepository jobRepository, IBacktestRepository backtestRepository, IKaigenService kaigenService, - ILogger logger) + ILogger logger) { _jobRepository = jobRepository; _backtestRepository = backtestRepository; @@ -37,7 +37,7 @@ public class BacktestJobService /// /// Creates a single backtest job /// - public async Task CreateJobAsync( + public async Task 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 /// /// Creates multiple backtest jobs from bundle variants /// - public async Task> CreateBundleJobsAsync( + public async Task> CreateBundleJobsAsync( BundleBacktestRequest bundleRequest, List backtestRequests) { - var jobs = new List(); + var jobs = new List(); 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), diff --git a/src/Managing.Application/Backtests/Backtester.cs b/src/Managing.Application/Backtests/Backtester.cs index 7319111e..f0242c84 100644 --- a/src/Managing.Application/Backtests/Backtester.cs +++ b/src/Managing.Application/Backtests/Backtester.cs @@ -29,7 +29,7 @@ namespace Managing.Application.Backtests private readonly IMessengerService _messengerService; private readonly IKaigenService _kaigenService; private readonly IHubContext _hubContext; - private readonly BacktestJobService _jobService; + private readonly JobService _jobService; public Backtester( IExchangeService exchangeService, @@ -41,7 +41,7 @@ namespace Managing.Application.Backtests IKaigenService kaigenService, IHubContext hubContext, IServiceScopeFactory serviceScopeFactory, - BacktestJobService jobService) + JobService jobService) { _exchangeService = exchangeService; _backtestRepository = backtestRepository; diff --git a/src/Managing.Application/Backtests/GeneticExecutor.cs b/src/Managing.Application/Backtests/GeneticExecutor.cs new file mode 100644 index 00000000..4202febb --- /dev/null +++ b/src/Managing.Application/Backtests/GeneticExecutor.cs @@ -0,0 +1,149 @@ +using Managing.Application.Abstractions.Services; +using Managing.Domain.Backtests; +using Microsoft.Extensions.Logging; + +namespace Managing.Application.Backtests; + +/// +/// Service for executing genetic algorithm requests without Orleans dependencies. +/// Extracted from GeneticBacktestGrain to be reusable in compute workers. +/// +public class GeneticExecutor +{ + private readonly ILogger _logger; + private readonly IGeneticService _geneticService; + private readonly IAccountService _accountService; + private readonly IWebhookService _webhookService; + + public GeneticExecutor( + ILogger logger, + IGeneticService geneticService, + IAccountService accountService, + IWebhookService webhookService) + { + _logger = logger; + _geneticService = geneticService; + _accountService = accountService; + _webhookService = webhookService; + } + + /// + /// Executes a genetic algorithm request. + /// + /// The genetic request ID to process + /// Optional callback for progress updates (0-100) + /// Cancellation token + /// The genetic algorithm result + public async Task ExecuteAsync( + string geneticRequestId, + Func 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 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; + } + } +} + diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index 4f8d05ff..fe5eeece 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -347,7 +347,7 @@ public class GeneticService : IGeneticService CrossoverProbability = 0.75f, // Fixed crossover rate as in frontend TaskExecutor = new ParallelTaskExecutor { - MinThreads = 4, + MinThreads = 2, MaxThreads = Environment.ProcessorCount } }; diff --git a/src/Managing.Application/Workers/BacktestComputeWorker.cs b/src/Managing.Application/Workers/BacktestComputeWorker.cs index 87377df1..4d2c04bb 100644 --- a/src/Managing.Application/Workers/BacktestComputeWorker.cs +++ b/src/Managing.Application/Workers/BacktestComputeWorker.cs @@ -77,10 +77,10 @@ public class BacktestComputeWorker : BackgroundService try { using var scope = _scopeFactory.CreateScope(); - var jobRepository = scope.ServiceProvider.GetRequiredService(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); - // Try to claim a job - var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId); + // Try to claim a backtest job (exclude genetic jobs) + var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId, JobType.Backtest); if (job == null) { @@ -114,11 +114,11 @@ public class BacktestComputeWorker : BackgroundService } private async Task ProcessJobAsync( - BacktestJob job, + Job job, CancellationToken cancellationToken) { using var scope = _scopeFactory.CreateScope(); - var jobRepository = scope.ServiceProvider.GetRequiredService(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); var executor = scope.ServiceProvider.GetRequiredService(); var userService = scope.ServiceProvider.GetRequiredService(); var exchangeService = scope.ServiceProvider.GetRequiredService(); @@ -184,7 +184,7 @@ public class BacktestComputeWorker : BackgroundService progressCallback: progressCallback); // Update job with result - job.Status = BacktestJobStatus.Completed; + job.Status = JobStatus.Completed; job.ProgressPercentage = 100; job.ResultJson = JsonSerializer.Serialize(result); job.CompletedAt = DateTime.UtcNow; @@ -207,24 +207,7 @@ public class BacktestComputeWorker : BackgroundService _logger.LogError(ex, "Error processing backtest job {JobId}", job.Id); SentrySdk.CaptureException(ex); - // Update job status to failed - try - { - job.Status = BacktestJobStatus.Failed; - job.ErrorMessage = ex.Message; - job.CompletedAt = DateTime.UtcNow; - await jobRepository.UpdateAsync(job); - - // Update bundle request if this is part of a bundle - if (job.BundleRequestId.HasValue) - { - await UpdateBundleRequestProgress(job.BundleRequestId.Value, scope.ServiceProvider); - } - } - catch (Exception updateEx) - { - _logger.LogError(updateEx, "Error updating job {JobId} status to failed", job.Id); - } + await HandleJobFailure(job, ex, jobRepository, scope.ServiceProvider); } } @@ -233,14 +216,15 @@ public class BacktestComputeWorker : BackgroundService try { var backtestRepository = serviceProvider.GetRequiredService(); - var jobRepository = serviceProvider.GetRequiredService(); + var jobRepository = serviceProvider.GetRequiredService(); var userService = serviceProvider.GetRequiredService(); + var webhookService = serviceProvider.GetRequiredService(); // Get all jobs for this bundle var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleRequestId); - var completedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Completed); - var failedJobs = jobs.Count(j => j.Status == BacktestJobStatus.Failed); - var runningJobs = jobs.Count(j => j.Status == BacktestJobStatus.Running); + var completedJobs = jobs.Count(j => j.Status == JobStatus.Completed); + var failedJobs = jobs.Count(j => j.Status == JobStatus.Failed); + var runningJobs = jobs.Count(j => j.Status == JobStatus.Running); var totalJobs = jobs.Count(); if (totalJobs == 0) @@ -265,6 +249,8 @@ public class BacktestComputeWorker : BackgroundService return; } + var previousStatus = bundleRequest.Status; + // Update bundle request progress bundleRequest.CompletedBacktests = completedJobs; bundleRequest.FailedBacktests = failedJobs; @@ -298,7 +284,7 @@ public class BacktestComputeWorker : BackgroundService // Update results list from completed jobs var completedJobResults = jobs - .Where(j => j.Status == BacktestJobStatus.Completed && !string.IsNullOrEmpty(j.ResultJson)) + .Where(j => j.Status == JobStatus.Completed && !string.IsNullOrEmpty(j.ResultJson)) .Select(j => { try @@ -318,6 +304,28 @@ public class BacktestComputeWorker : BackgroundService await backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest); + // Send webhook notification if bundle request just completed + if (previousStatus != BundleBacktestRequestStatus.Completed && + bundleRequest.Status == BundleBacktestRequestStatus.Completed && + !string.IsNullOrEmpty(user.TelegramChannel)) + { + var message = $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully. " + + $"Completed: {completedJobs}/{totalJobs} backtests" + + (failedJobs > 0 ? $", Failed: {failedJobs}" : "") + + $". Results: {completedJobResults.Count} backtest(s) generated."; + + await webhookService.SendMessage(message, user.TelegramChannel); + } + else if (previousStatus != BundleBacktestRequestStatus.Failed && + bundleRequest.Status == BundleBacktestRequestStatus.Failed && + !string.IsNullOrEmpty(user.TelegramChannel)) + { + var message = $"❌ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) failed. " + + $"All {totalJobs} backtests failed. Error: {bundleRequest.ErrorMessage}"; + + await webhookService.SendMessage(message, user.TelegramChannel); + } + _logger.LogInformation( "Updated bundle request {BundleRequestId} progress: {Completed}/{Total} completed, {Failed} failed, {Running} running", bundleRequestId, completedJobs, totalJobs, failedJobs, runningJobs); @@ -337,13 +345,58 @@ public class BacktestComputeWorker : BackgroundService await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); // Check every minute using var scope = _scopeFactory.CreateScope(); - var jobRepository = scope.ServiceProvider.GetRequiredService(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); - var resetCount = await jobRepository.ResetStaleJobsAsync(_options.StaleJobTimeoutMinutes); + // Get stale jobs for this worker + var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId); + var staleJobs = runningJobs + .Where(j => j.JobType == JobType.Backtest && + (j.LastHeartbeat == null || + j.LastHeartbeat < DateTime.UtcNow.AddMinutes(-_options.StaleJobTimeoutMinutes))) + .ToList(); - if (resetCount > 0) + foreach (var job in staleJobs) { - _logger.LogInformation("Reset {Count} stale backtest jobs back to Pending status", resetCount); + // If it's stale but retryable, reset to pending with retry count + if (job.RetryCount < job.MaxRetries) + { + job.Status = JobStatus.Pending; + job.RetryCount++; + var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes); + job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes); + job.ErrorMessage = $"Worker timeout - retry {job.RetryCount}/{job.MaxRetries}"; + job.FailureCategory = FailureCategory.SystemError; + _logger.LogWarning( + "Stale job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}", + job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter); + } + else + { + // Exceeded retries - mark as failed + job.Status = JobStatus.Failed; + job.ErrorMessage = "Worker timeout - exceeded max retries"; + job.FailureCategory = FailureCategory.SystemError; + job.IsRetryable = false; + job.CompletedAt = DateTime.UtcNow; + + // Notify permanent failure + await NotifyPermanentFailure(job, new TimeoutException("Worker timeout"), scope.ServiceProvider); + + // Update bundle request if this is part of a bundle + if (job.BundleRequestId.HasValue) + { + await UpdateBundleRequestProgress(job.BundleRequestId.Value, scope.ServiceProvider); + } + } + + job.AssignedWorkerId = null; + job.LastHeartbeat = null; + await jobRepository.UpdateAsync(job); + } + + if (staleJobs.Count > 0) + { + _logger.LogInformation("Processed {Count} stale backtest jobs", staleJobs.Count); } } catch (Exception ex) @@ -362,7 +415,7 @@ public class BacktestComputeWorker : BackgroundService await Task.Delay(TimeSpan.FromSeconds(_options.HeartbeatIntervalSeconds), cancellationToken); using var scope = _scopeFactory.CreateScope(); - var jobRepository = scope.ServiceProvider.GetRequiredService(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); // Update heartbeat for all jobs assigned to this worker var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId); @@ -380,6 +433,118 @@ public class BacktestComputeWorker : BackgroundService } } + private async Task HandleJobFailure( + Job job, + Exception ex, + IJobRepository jobRepository, + IServiceProvider serviceProvider) + { + try + { + // Categorize the failure + var failureCategory = CategorizeFailure(ex); + var isRetryable = IsRetryableFailure(ex, failureCategory); + + // Check if we should retry + if (isRetryable && job.RetryCount < job.MaxRetries) + { + // Calculate exponential backoff: 2^retryCount minutes, capped at MaxRetryDelayMinutes + var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes); + job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes); + job.RetryCount++; + job.Status = JobStatus.Pending; // Reset to pending for retry + job.AssignedWorkerId = null; // Allow any worker to pick it up + job.ErrorMessage = $"Retry {job.RetryCount}/{job.MaxRetries}: {ex.Message}"; + job.FailureCategory = failureCategory; + job.IsRetryable = true; + + _logger.LogWarning( + "Job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}. Error: {Error}", + job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter, ex.Message); + } + else + { + // Permanent failure - mark as failed + job.Status = JobStatus.Failed; + job.ErrorMessage = ex.Message; + job.FailureCategory = failureCategory; + job.IsRetryable = false; + job.CompletedAt = DateTime.UtcNow; + + _logger.LogError( + "Job {JobId} failed permanently after {RetryCount} retries. Error: {Error}", + job.Id, job.RetryCount, ex.Message); + + // Send notification for permanent failure + await NotifyPermanentFailure(job, ex, serviceProvider); + + // Update bundle request if this is part of a bundle + if (job.BundleRequestId.HasValue) + { + await UpdateBundleRequestProgress(job.BundleRequestId.Value, serviceProvider); + } + } + + job.LastHeartbeat = DateTime.UtcNow; + await jobRepository.UpdateAsync(job); + } + catch (Exception updateEx) + { + _logger.LogError(updateEx, "Failed to update job {JobId} status after failure", job.Id); + } + } + + private FailureCategory CategorizeFailure(Exception ex) + { + return ex switch + { + TimeoutException => FailureCategory.Transient, + TaskCanceledException => FailureCategory.Transient, + HttpRequestException => FailureCategory.Transient, + InvalidOperationException when ex.Message.Contains("candles") || ex.Message.Contains("No candles") => FailureCategory.DataError, + InvalidOperationException when ex.Message.Contains("User") || ex.Message.Contains("not found") => FailureCategory.UserError, + OutOfMemoryException => FailureCategory.SystemError, + _ => FailureCategory.Unknown + }; + } + + private bool IsRetryableFailure(Exception ex, FailureCategory category) + { + // Don't retry user errors or data errors (missing candles, invalid config) + if (category == FailureCategory.UserError || category == FailureCategory.DataError) + return false; + + // Retry transient and system errors + return category == FailureCategory.Transient || category == FailureCategory.SystemError; + } + + private async Task NotifyPermanentFailure( + Job job, + Exception ex, + IServiceProvider serviceProvider) + { + try + { + var webhookService = serviceProvider.GetRequiredService(); + const string alertsChannel = "2676086723"; + + var jobTypeName = job.JobType == JobType.Genetic ? "Genetic" : "Backtest"; + var message = $"🚨 **{jobTypeName} Job Failed Permanently**\n" + + $"Job ID: `{job.Id}`\n" + + $"User ID: {job.UserId}\n" + + $"Retry Attempts: {job.RetryCount}/{job.MaxRetries}\n" + + $"Failure Category: {job.FailureCategory}\n" + + $"Error: {ex.Message}\n" + + $"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; + + await webhookService.SendMessage(message, alertsChannel); + } + catch (Exception notifyEx) + { + _logger.LogError(notifyEx, "Failed to send permanent failure notification for job {JobId}", job.Id); + } + } + public override void Dispose() { _semaphore?.Dispose(); @@ -418,5 +583,15 @@ public class BacktestComputeWorkerOptions /// Timeout in minutes for considering a job stale /// public int StaleJobTimeoutMinutes { get; set; } = 5; + + /// + /// Default maximum retry attempts for failed jobs + /// + public int DefaultMaxRetries { get; set; } = 3; + + /// + /// Maximum retry delay in minutes (cap for exponential backoff) + /// + public int MaxRetryDelayMinutes { get; set; } = 60; } diff --git a/src/Managing.Application/Workers/GeneticComputeWorker.cs b/src/Managing.Application/Workers/GeneticComputeWorker.cs new file mode 100644 index 00000000..e822d171 --- /dev/null +++ b/src/Managing.Application/Workers/GeneticComputeWorker.cs @@ -0,0 +1,429 @@ +using System.Text.Json; +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Application.Backtests; +using Managing.Domain.Backtests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using static Managing.Common.Enums; + +namespace Managing.Application.Workers; + +/// +/// Background worker that processes genetic algorithm jobs from the queue. +/// Polls for pending genetic jobs, claims them using advisory locks, and processes them. +/// +public class GeneticComputeWorker : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly GeneticComputeWorkerOptions _options; + private readonly SemaphoreSlim _semaphore; + + public GeneticComputeWorker( + IServiceScopeFactory scopeFactory, + ILogger logger, + IOptions options) + { + _scopeFactory = scopeFactory; + _logger = logger; + _options = options.Value; + _semaphore = new SemaphoreSlim(_options.MaxConcurrentGenetics, _options.MaxConcurrentGenetics); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation( + "GeneticComputeWorker starting. WorkerId: {WorkerId}, MaxConcurrent: {MaxConcurrent}, PollInterval: {PollInterval}s", + _options.WorkerId, _options.MaxConcurrentGenetics, _options.JobPollIntervalSeconds); + + // Background task for stale job recovery + var staleJobRecoveryTask = Task.Run(() => StaleJobRecoveryLoop(stoppingToken), stoppingToken); + + // Background task for heartbeat updates + var heartbeatTask = Task.Run(() => HeartbeatLoop(stoppingToken), stoppingToken); + + // Main job processing loop + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessJobsAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GeneticComputeWorker main loop"); + SentrySdk.CaptureException(ex); + } + + await Task.Delay(TimeSpan.FromSeconds(_options.JobPollIntervalSeconds), stoppingToken); + } + + _logger.LogInformation("GeneticComputeWorker stopping"); + } + + private async Task ProcessJobsAsync(CancellationToken cancellationToken) + { + // Check if we have capacity + if (!await _semaphore.WaitAsync(0, cancellationToken)) + { + // At capacity, skip this iteration + return; + } + + try + { + using var scope = _scopeFactory.CreateScope(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); + + // Try to claim a genetic job + var job = await jobRepository.ClaimNextJobAsync(_options.WorkerId, JobType.Genetic); + + if (job == null) + { + // No jobs available, release semaphore + _semaphore.Release(); + return; + } + + _logger.LogInformation("Claimed genetic job {JobId} for worker {WorkerId}", job.Id, _options.WorkerId); + + // Process the job asynchronously (don't await, let it run in background) + // Create a new scope for the job processing to ensure proper lifetime management + _ = Task.Run(async () => + { + try + { + await ProcessJobAsync(job, cancellationToken); + } + finally + { + _semaphore.Release(); + } + }, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error claiming or processing genetic job"); + _semaphore.Release(); + throw; + } + } + + private async Task ProcessJobAsync( + Job job, + CancellationToken cancellationToken) + { + using var scope = _scopeFactory.CreateScope(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); + var geneticExecutor = scope.ServiceProvider.GetRequiredService(); + + try + { + _logger.LogInformation( + "Processing genetic job {JobId} (GeneticRequestId: {GeneticRequestId}, UserId: {UserId})", + job.Id, job.GeneticRequestId, job.UserId); + + if (string.IsNullOrEmpty(job.GeneticRequestId)) + { + throw new InvalidOperationException("GeneticRequestId is required for genetic jobs"); + } + + // Progress callback to update job progress + Func progressCallback = async (percentage) => + { + try + { + job.ProgressPercentage = percentage; + job.LastHeartbeat = DateTime.UtcNow; + await jobRepository.UpdateAsync(job); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error updating job progress for job {JobId}", job.Id); + } + }; + + // Execute the genetic algorithm + var result = await geneticExecutor.ExecuteAsync( + job.GeneticRequestId, + progressCallback, + cancellationToken); + + // Update job with result + job.Status = JobStatus.Completed; + job.ProgressPercentage = 100; + job.ResultJson = JsonSerializer.Serialize(new + { + BestFitness = result.BestFitness, + BestIndividual = result.BestIndividual, + ProgressInfo = result.ProgressInfo + }); + job.CompletedAt = DateTime.UtcNow; + job.LastHeartbeat = DateTime.UtcNow; + + await jobRepository.UpdateAsync(job); + + _logger.LogInformation( + "Completed genetic job {JobId}. Best Fitness: {BestFitness}", + job.Id, result.BestFitness); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing genetic job {JobId}", job.Id); + SentrySdk.CaptureException(ex); + + await HandleJobFailure(job, ex, jobRepository, scope.ServiceProvider); + } + } + + private async Task StaleJobRecoveryLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); // Check every minute + + using var scope = _scopeFactory.CreateScope(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); + + // Reset stale genetic jobs only + var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId); + var staleJobs = runningJobs + .Where(j => j.JobType == JobType.Genetic && + (j.LastHeartbeat == null || + j.LastHeartbeat < DateTime.UtcNow.AddMinutes(-_options.StaleJobTimeoutMinutes))) + .ToList(); + + foreach (var job in staleJobs) + { + // If it's stale but retryable, reset to pending with retry count + if (job.RetryCount < job.MaxRetries) + { + job.Status = JobStatus.Pending; + job.RetryCount++; + var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes); + job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes); + job.ErrorMessage = $"Worker timeout - retry {job.RetryCount}/{job.MaxRetries}"; + job.FailureCategory = FailureCategory.SystemError; + _logger.LogWarning( + "Stale job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}", + job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter); + } + else + { + // Exceeded retries - mark as failed + job.Status = JobStatus.Failed; + job.ErrorMessage = "Worker timeout - exceeded max retries"; + job.FailureCategory = FailureCategory.SystemError; + job.IsRetryable = false; + job.CompletedAt = DateTime.UtcNow; + + // Notify permanent failure + await NotifyPermanentFailure(job, new TimeoutException("Worker timeout"), scope.ServiceProvider); + } + + job.AssignedWorkerId = null; + job.LastHeartbeat = null; + await jobRepository.UpdateAsync(job); + } + + if (staleJobs.Count > 0) + { + _logger.LogInformation("Processed {Count} stale genetic jobs", staleJobs.Count); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in stale job recovery loop"); + } + } + } + + private async Task HeartbeatLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_options.HeartbeatIntervalSeconds), cancellationToken); + + using var scope = _scopeFactory.CreateScope(); + var jobRepository = scope.ServiceProvider.GetRequiredService(); + + // Update heartbeat for all genetic jobs assigned to this worker + var runningJobs = await jobRepository.GetRunningJobsByWorkerIdAsync(_options.WorkerId); + var geneticJobs = runningJobs.Where(j => j.JobType == JobType.Genetic); + + foreach (var job in geneticJobs) + { + job.LastHeartbeat = DateTime.UtcNow; + await jobRepository.UpdateAsync(job); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in heartbeat loop"); + } + } + } + + private async Task HandleJobFailure( + Job job, + Exception ex, + IJobRepository jobRepository, + IServiceProvider serviceProvider) + { + try + { + // Categorize the failure + var failureCategory = CategorizeFailure(ex); + var isRetryable = IsRetryableFailure(ex, failureCategory); + + // Check if we should retry + if (isRetryable && job.RetryCount < job.MaxRetries) + { + // Calculate exponential backoff: 2^retryCount minutes, capped at MaxRetryDelayMinutes + var backoffMinutes = Math.Min(Math.Pow(2, job.RetryCount), _options.MaxRetryDelayMinutes); + job.RetryAfter = DateTime.UtcNow.AddMinutes(backoffMinutes); + job.RetryCount++; + job.Status = JobStatus.Pending; // Reset to pending for retry + job.AssignedWorkerId = null; // Allow any worker to pick it up + job.ErrorMessage = $"Retry {job.RetryCount}/{job.MaxRetries}: {ex.Message}"; + job.FailureCategory = failureCategory; + job.IsRetryable = true; + + _logger.LogWarning( + "Job {JobId} will be retried (attempt {RetryCount}/{MaxRetries}) after {RetryAfter}. Error: {Error}", + job.Id, job.RetryCount, job.MaxRetries, job.RetryAfter, ex.Message); + } + else + { + // Permanent failure - mark as failed + job.Status = JobStatus.Failed; + job.ErrorMessage = ex.Message; + job.FailureCategory = failureCategory; + job.IsRetryable = false; + job.CompletedAt = DateTime.UtcNow; + + _logger.LogError( + "Job {JobId} failed permanently after {RetryCount} retries. Error: {Error}", + job.Id, job.RetryCount, ex.Message); + + // Send notification for permanent failure + await NotifyPermanentFailure(job, ex, serviceProvider); + } + + job.LastHeartbeat = DateTime.UtcNow; + await jobRepository.UpdateAsync(job); + } + catch (Exception updateEx) + { + _logger.LogError(updateEx, "Failed to update job {JobId} status after failure", job.Id); + } + } + + private FailureCategory CategorizeFailure(Exception ex) + { + return ex switch + { + TimeoutException => FailureCategory.Transient, + TaskCanceledException => FailureCategory.Transient, + HttpRequestException => FailureCategory.Transient, + InvalidOperationException when ex.Message.Contains("candles") || ex.Message.Contains("data") => FailureCategory.DataError, + InvalidOperationException when ex.Message.Contains("User") || ex.Message.Contains("not found") => FailureCategory.UserError, + OutOfMemoryException => FailureCategory.SystemError, + _ => FailureCategory.Unknown + }; + } + + private bool IsRetryableFailure(Exception ex, FailureCategory category) + { + // Don't retry user errors or data errors (missing candles, invalid config) + if (category == FailureCategory.UserError || category == FailureCategory.DataError) + return false; + + // Retry transient and system errors + return category == FailureCategory.Transient || category == FailureCategory.SystemError; + } + + private async Task NotifyPermanentFailure( + Job job, + Exception ex, + IServiceProvider serviceProvider) + { + try + { + var webhookService = serviceProvider.GetRequiredService(); + const string alertsChannel = "2676086723"; + + var jobTypeName = job.JobType == JobType.Genetic ? "Genetic" : "Backtest"; + var message = $"🚨 **{jobTypeName} Job Failed Permanently**\n" + + $"Job ID: `{job.Id}`\n" + + $"User ID: {job.UserId}\n" + + $"Retry Attempts: {job.RetryCount}/{job.MaxRetries}\n" + + $"Failure Category: {job.FailureCategory}\n" + + $"Error: {ex.Message}\n" + + $"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; + + await webhookService.SendMessage(message, alertsChannel); + } + catch (Exception notifyEx) + { + _logger.LogError(notifyEx, "Failed to send permanent failure notification for job {JobId}", job.Id); + } + } + + public override void Dispose() + { + _semaphore?.Dispose(); + base.Dispose(); + } +} + +/// +/// Configuration options for GeneticComputeWorker +/// +public class GeneticComputeWorkerOptions +{ + public const string SectionName = "GeneticComputeWorker"; + + /// + /// Unique identifier for this worker instance + /// + public string WorkerId { get; set; } = Environment.MachineName + "-genetic"; + + /// + /// Maximum number of concurrent genetic algorithm jobs to process + /// + public int MaxConcurrentGenetics { get; set; } = 2; + + /// + /// Interval in seconds between job polling attempts + /// + public int JobPollIntervalSeconds { get; set; } = 5; + + /// + /// Interval in seconds between heartbeat updates + /// + public int HeartbeatIntervalSeconds { get; set; } = 30; + + /// + /// Timeout in minutes for considering a job stale + /// + public int StaleJobTimeoutMinutes { get; set; } = 10; + + /// + /// Default maximum retry attempts for failed jobs + /// + public int DefaultMaxRetries { get; set; } = 3; + + /// + /// Maximum retry delay in minutes (cap for exponential backoff) + /// + public int MaxRetryDelayMinutes { get; set; } = 60; +} + diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 80cdbdc0..4deb48fb 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -400,7 +400,7 @@ public static class ApiBootstrap // Processors services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -443,7 +443,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Managing.Bootstrap/ComputeBootstrap.cs b/src/Managing.Bootstrap/ComputeBootstrap.cs index c011dbc9..93eedbed 100644 --- a/src/Managing.Bootstrap/ComputeBootstrap.cs +++ b/src/Managing.Bootstrap/ComputeBootstrap.cs @@ -1,3 +1,4 @@ +using Managing.Application; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; @@ -57,7 +58,6 @@ public static class ComputeBootstrap // Services not needed for compute worker (depend on IBacktester/Orleans) // services.AddScoped(); // Requires IBacktester // services.AddScoped(); // Requires IBacktester - // services.AddScoped(); // Requires IBacktester // services.AddScoped(); // May require Orleans // services.AddScoped(); // May require Orleans // services.AddScoped(); // May require Orleans @@ -66,6 +66,13 @@ public static class ComputeBootstrap // Processors // Note: IBacktester not needed for compute worker - BacktestExecutor is used directly services.AddTransient(); + services.AddTransient(); + + // Adapter to make BacktestExecutor implement IBacktester (needed for GeneticService) + services.AddTransient(); + + // Genetic service (needed for GeneticExecutor) + services.AddScoped(); services.AddTransient(); services.AddTransient(); @@ -120,7 +127,7 @@ public static class ComputeBootstrap // PostgreSql Repositories services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 7525321b..0932e2ae 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -564,6 +564,11 @@ public static class Enums /// /// Genetic algorithm backtest job /// - GeneticBacktest + GeneticBacktest, + + /// + /// Genetic algorithm request processing job + /// + Genetic } } \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/BacktestJob.cs b/src/Managing.Domain/Backtests/BacktestJob.cs index 030458c1..bca06428 100644 --- a/src/Managing.Domain/Backtests/BacktestJob.cs +++ b/src/Managing.Domain/Backtests/BacktestJob.cs @@ -5,16 +5,16 @@ using static Managing.Common.Enums; namespace Managing.Domain.Backtests; /// -/// Represents a single backtest job in the queue system. -/// Can be a standalone backtest or part of a bundle backtest request. +/// Represents a single job in the queue system. +/// Can be a standalone backtest, genetic algorithm, or part of a bundle backtest request. /// -public class BacktestJob +public class Job { - public BacktestJob() + public Job() { Id = Guid.NewGuid(); CreatedAt = DateTime.UtcNow; - Status = BacktestJobStatus.Pending; + Status = JobStatus.Pending; ProgressPercentage = 0; } @@ -39,7 +39,7 @@ public class BacktestJob /// Current status of the job /// [Required] - public BacktestJobStatus Status { get; set; } + public JobStatus Status { get; set; } /// /// Priority of the job (higher = more important) @@ -122,12 +122,37 @@ public class BacktestJob /// Optional genetic request ID if this is a genetic backtest job /// public string? GeneticRequestId { get; set; } + + /// + /// Number of times this job has been retried + /// + public int RetryCount { get; set; } = 0; + + /// + /// Maximum number of retry attempts allowed + /// + public int MaxRetries { get; set; } = 3; + + /// + /// When the job should be retried next (for exponential backoff) + /// + public DateTime? RetryAfter { get; set; } + + /// + /// Whether the failure is retryable (transient vs permanent) + /// + public bool IsRetryable { get; set; } = true; + + /// + /// Failure category for better error handling + /// + public FailureCategory? FailureCategory { get; set; } } /// -/// Status of a backtest job +/// Status of a job /// -public enum BacktestJobStatus +public enum JobStatus { /// /// Job is pending and waiting to be claimed by a worker @@ -155,3 +180,34 @@ public enum BacktestJobStatus Cancelled } +/// +/// Category of failure for better error handling and retry logic +/// +public enum FailureCategory +{ + /// + /// Transient failures: network issues, timeouts, temporary service unavailability + /// + Transient, + + /// + /// Data errors: missing candles, invalid data format + /// + DataError, + + /// + /// System errors: out of memory, database errors, infrastructure issues + /// + SystemError, + + /// + /// User errors: invalid input, configuration errors + /// + UserError, + + /// + /// Unknown or unclassified errors + /// + Unknown +} + diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.Designer.cs new file mode 100644 index 00000000..a24feb0c --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.Designer.cs @@ -0,0 +1,1720 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251108195534_RenameBacktestJobsToJobs")] + partial class RenameBacktestJobsToJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitialBalance") + .HasColumnType("decimal(18,8)"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.HasIndex("UserId", "Name", "Version"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedWorkerId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BundleRequestId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailureCategory") + .HasColumnType("integer"); + + b.Property("GeneticRequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("JobType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("LastHeartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("ProgressPercentage") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultJson") + .HasColumnType("jsonb"); + + b.Property("RetryAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BundleRequestId") + .HasDatabaseName("idx_bundle_request"); + + b.HasIndex("GeneticRequestId") + .HasDatabaseName("idx_genetic_request"); + + b.HasIndex("AssignedWorkerId", "Status") + .HasDatabaseName("idx_assigned_worker"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("idx_user_status"); + + b.HasIndex("Status", "JobType", "Priority", "CreatedAt") + .HasDatabaseName("idx_status_jobtype_priority_created"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WhitelistAccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddedWallet") + .IsRequired() + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("ExternalEthereumAccount") + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("IsWhitelisted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PrivyCreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivyId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwitterAccount") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EmbeddedWallet") + .IsUnique(); + + b.HasIndex("ExternalEthereumAccount"); + + b.HasIndex("PrivyId") + .IsUnique(); + + b.HasIndex("TwitterAccount"); + + b.ToTable("WhitelistAccounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.cs b/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.cs new file mode 100644 index 00000000..9e472d76 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108195534_RenameBacktestJobsToJobs.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class RenameBacktestJobsToJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BacktestJobs_Users_UserId", + table: "BacktestJobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_BacktestJobs", + table: "BacktestJobs"); + + migrationBuilder.RenameTable( + name: "BacktestJobs", + newName: "Jobs"); + + migrationBuilder.AddColumn( + name: "FailureCategory", + table: "Jobs", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsRetryable", + table: "Jobs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MaxRetries", + table: "Jobs", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "RetryAfter", + table: "Jobs", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryCount", + table: "Jobs", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddPrimaryKey( + name: "PK_Jobs", + table: "Jobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Jobs", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "FailureCategory", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "IsRetryable", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "MaxRetries", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "RetryAfter", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "RetryCount", + table: "Jobs"); + + migrationBuilder.RenameTable( + name: "Jobs", + newName: "BacktestJobs"); + + migrationBuilder.AddPrimaryKey( + name: "PK_BacktestJobs", + table: "BacktestJobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BacktestJobs_Users_UserId", + table: "BacktestJobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.Designer.cs new file mode 100644 index 00000000..39a3f91c --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.Designer.cs @@ -0,0 +1,1720 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251108202235_RenameJobsTableToLowercase")] + partial class RenameJobsTableToLowercase + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitialBalance") + .HasColumnType("decimal(18,8)"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.HasIndex("UserId", "Name", "Version"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedWorkerId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BundleRequestId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailureCategory") + .HasColumnType("integer"); + + b.Property("GeneticRequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("JobType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("LastHeartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("ProgressPercentage") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ResultJson") + .HasColumnType("jsonb"); + + b.Property("RetryAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BundleRequestId") + .HasDatabaseName("idx_bundle_request"); + + b.HasIndex("GeneticRequestId") + .HasDatabaseName("idx_genetic_request"); + + b.HasIndex("AssignedWorkerId", "Status") + .HasDatabaseName("idx_assigned_worker"); + + b.HasIndex("UserId", "Status") + .HasDatabaseName("idx_user_status"); + + b.HasIndex("Status", "JobType", "Priority", "CreatedAt") + .HasDatabaseName("idx_status_jobtype_priority_created"); + + b.ToTable("jobs", "public"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WhitelistAccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddedWallet") + .IsRequired() + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("ExternalEthereumAccount") + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("IsWhitelisted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PrivyCreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivyId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwitterAccount") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EmbeddedWallet") + .IsUnique(); + + b.HasIndex("ExternalEthereumAccount"); + + b.HasIndex("PrivyId") + .IsUnique(); + + b.HasIndex("TwitterAccount"); + + b.ToTable("WhitelistAccounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.cs b/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.cs new file mode 100644 index 00000000..3e933b7b --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251108202235_RenameJobsTableToLowercase.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class RenameJobsTableToLowercase : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Jobs", + table: "Jobs"); + + migrationBuilder.EnsureSchema( + name: "public"); + + migrationBuilder.RenameTable( + name: "Jobs", + newName: "jobs", + newSchema: "public"); + + migrationBuilder.AddPrimaryKey( + name: "PK_jobs", + schema: "public", + table: "jobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_jobs_Users_UserId", + schema: "public", + table: "jobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_jobs_Users_UserId", + schema: "public", + table: "jobs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_jobs", + schema: "public", + table: "jobs"); + + migrationBuilder.RenameTable( + name: "jobs", + schema: "public", + newName: "Jobs"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Jobs", + table: "Jobs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Users_UserId", + table: "Jobs", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 327db65b..32188f55 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -734,10 +734,16 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("ErrorMessage") .HasColumnType("text"); + b.Property("FailureCategory") + .HasColumnType("integer"); + b.Property("GeneticRequestId") .HasMaxLength(255) .HasColumnType("character varying(255)"); + b.Property("IsRetryable") + .HasColumnType("boolean"); + b.Property("JobType") .ValueGeneratedOnAdd() .HasColumnType("integer") @@ -746,6 +752,9 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("LastHeartbeat") .HasColumnType("timestamp with time zone"); + b.Property("MaxRetries") + .HasColumnType("integer"); + b.Property("Priority") .ValueGeneratedOnAdd() .HasColumnType("integer") @@ -763,6 +772,12 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("ResultJson") .HasColumnType("jsonb"); + b.Property("RetryAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + b.Property("StartDate") .HasColumnType("timestamp with time zone"); @@ -792,7 +807,7 @@ namespace Managing.Infrastructure.Databases.Migrations b.HasIndex("Status", "JobType", "Priority", "CreatedAt") .HasDatabaseName("idx_status_jobtype_priority_created"); - b.ToTable("BacktestJobs"); + b.ToTable("jobs", "public"); }); modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/JobEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/JobEntity.cs index bb5d6453..7e82f625 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/JobEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/JobEntity.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Managing.Infrastructure.Databases.PostgreSql.Entities; -[Table("BacktestJobs")] +[Table("Jobs")] public class JobEntity { [Key] @@ -16,7 +16,7 @@ public class JobEntity public int UserId { get; set; } [Required] - public int Status { get; set; } // BacktestJobStatus enum as int + public int Status { get; set; } // JobStatus enum as int [Required] public int JobType { get; set; } // JobType enum as int @@ -61,6 +61,16 @@ public class JobEntity [MaxLength(255)] public string? GeneticRequestId { get; set; } + public int RetryCount { get; set; } = 0; + + public int MaxRetries { get; set; } = 3; + + public DateTime? RetryAfter { get; set; } + + public bool IsRetryable { get; set; } = true; + + public int? FailureCategory { get; set; } // FailureCategory enum as int + // Navigation property public UserEntity? User { get; set; } } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs index 1cc35f64..e1af51e6 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlJobRepository.cs @@ -7,7 +7,7 @@ using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; -public class PostgreSqlJobRepository : IBacktestJobRepository +public class PostgreSqlJobRepository : IJobRepository { private readonly ManagingDbContext _context; private readonly ILogger _logger; @@ -20,7 +20,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository _logger = logger; } - public async Task CreateAsync(BacktestJob job) + public async Task CreateAsync(Job job) { var entity = MapToEntity(job); _context.Jobs.Add(entity); @@ -28,10 +28,9 @@ public class PostgreSqlJobRepository : IBacktestJobRepository return MapToDomain(entity); } - public async Task ClaimNextJobAsync(string workerId, JobType? jobType = null) + public async Task ClaimNextJobAsync(string workerId, JobType? jobType = null) { // Use execution strategy to support retry with transactions - // FOR UPDATE SKIP LOCKED ensures only one worker can claim a specific job var strategy = _context.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () => @@ -42,10 +41,10 @@ public class PostgreSqlJobRepository : IBacktestJobRepository { // Build SQL query with optional job type filter var sql = @" - SELECT * FROM ""BacktestJobs"" + SELECT * FROM ""Jobs"" WHERE ""Status"" = {0}"; - var parameters = new List { (int)BacktestJobStatus.Pending }; + var parameters = new List { (int)JobStatus.Pending }; if (jobType.HasValue) { @@ -70,7 +69,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository } // Update the job status atomically - job.Status = (int)BacktestJobStatus.Running; + job.Status = (int)JobStatus.Running; job.AssignedWorkerId = workerId; job.StartedAt = DateTime.UtcNow; job.LastHeartbeat = DateTime.UtcNow; @@ -89,7 +88,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository }); } - public async Task UpdateAsync(BacktestJob job) + public async Task UpdateAsync(Job job) { // Use AsTracking() to enable change tracking since DbContext uses NoTracking by default var entity = await _context.Jobs @@ -115,11 +114,16 @@ public class PostgreSqlJobRepository : IBacktestJobRepository entity.RequestId = job.RequestId; entity.GeneticRequestId = job.GeneticRequestId; entity.Priority = job.Priority; + entity.RetryCount = job.RetryCount; + entity.MaxRetries = job.MaxRetries; + entity.RetryAfter = job.RetryAfter; + entity.IsRetryable = job.IsRetryable; + entity.FailureCategory = job.FailureCategory.HasValue ? (int)job.FailureCategory.Value : null; await _context.SaveChangesAsync(); } - public async Task> GetByBundleRequestIdAsync(Guid bundleRequestId) + public async Task> GetByBundleRequestIdAsync(Guid bundleRequestId) { var entities = await _context.Jobs .Where(j => j.BundleRequestId == bundleRequestId) @@ -128,7 +132,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository return entities.Select(MapToDomain); } - public async Task> GetByUserIdAsync(int userId) + public async Task> GetByUserIdAsync(int userId) { var entities = await _context.Jobs .Where(j => j.UserId == userId) @@ -140,16 +144,16 @@ public class PostgreSqlJobRepository : IBacktestJobRepository /// /// Gets all running jobs assigned to a specific worker /// - public async Task> GetRunningJobsByWorkerIdAsync(string workerId) + public async Task> GetRunningJobsByWorkerIdAsync(string workerId) { var entities = await _context.Jobs - .Where(j => j.AssignedWorkerId == workerId && j.Status == (int)BacktestJobStatus.Running) + .Where(j => j.AssignedWorkerId == workerId && j.Status == (int)JobStatus.Running) .ToListAsync(); return entities.Select(MapToDomain); } - public async Task> GetByGeneticRequestIdAsync(string geneticRequestId) + public async Task> GetByGeneticRequestIdAsync(string geneticRequestId) { var entities = await _context.Jobs .Where(j => j.GeneticRequestId == geneticRequestId) @@ -158,12 +162,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository return entities.Select(MapToDomain); } - public async Task<(IEnumerable Jobs, int TotalCount)> GetPaginatedAsync( + public async Task<(IEnumerable Jobs, int TotalCount)> GetPaginatedAsync( int page, int pageSize, string sortBy = "CreatedAt", string sortOrder = "desc", - BacktestJobStatus? status = null, + JobStatus? status = null, JobType? jobType = null, int? userId = null, string? workerId = null, @@ -235,7 +239,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository return (jobs, totalCount); } - public async Task GetByIdAsync(Guid jobId) + public async Task GetByIdAsync(Guid jobId) { var entity = await _context.Jobs .FirstOrDefaultAsync(j => j.Id == jobId); @@ -243,12 +247,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository return entity != null ? MapToDomain(entity) : null; } - public async Task> GetStaleJobsAsync(int timeoutMinutes = 5) + public async Task> GetStaleJobsAsync(int timeoutMinutes = 5) { var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes); var entities = await _context.Jobs - .Where(j => j.Status == (int)BacktestJobStatus.Running && + .Where(j => j.Status == (int)JobStatus.Running && (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) .ToListAsync(); @@ -262,13 +266,13 @@ public class PostgreSqlJobRepository : IBacktestJobRepository // Use AsTracking() to enable change tracking since DbContext uses NoTracking by default var staleJobs = await _context.Jobs .AsTracking() - .Where(j => j.Status == (int)BacktestJobStatus.Running && + .Where(j => j.Status == (int)JobStatus.Running && (j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold)) .ToListAsync(); foreach (var job in staleJobs) { - job.Status = (int)BacktestJobStatus.Pending; + job.Status = (int)JobStatus.Pending; job.AssignedWorkerId = null; job.LastHeartbeat = null; } @@ -299,7 +303,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository // Query 1: Status summary var statusSummarySql = @" SELECT ""Status"", COUNT(*) as Count - FROM ""BacktestJobs"" + FROM ""Jobs"" GROUP BY ""Status"" ORDER BY ""Status"""; @@ -322,7 +326,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository // Query 2: Job type summary var jobTypeSummarySql = @" SELECT ""JobType"", COUNT(*) as Count - FROM ""BacktestJobs"" + FROM ""Jobs"" GROUP BY ""JobType"" ORDER BY ""JobType"""; @@ -345,7 +349,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository // Query 3: Status + Job type summary var statusTypeSummarySql = @" SELECT ""Status"", ""JobType"", COUNT(*) as Count - FROM ""BacktestJobs"" + FROM ""Jobs"" GROUP BY ""Status"", ""JobType"" ORDER BY ""Status"", ""JobType"""; @@ -369,7 +373,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository // Query 4: Total count var totalCountSql = @" SELECT COUNT(*) as Count - FROM ""BacktestJobs"""; + FROM ""Jobs"""; using (var command = connection.CreateCommand()) { @@ -382,7 +386,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository { StatusCounts = statusCounts.Select(s => new JobStatusCount { - Status = (BacktestJobStatus)s.Status, + Status = (JobStatus)s.Status, Count = s.Count }).ToList(), JobTypeCounts = jobTypeCounts.Select(j => new JobTypeCount @@ -392,7 +396,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository }).ToList(), StatusTypeCounts = statusTypeCounts.Select(st => new JobStatusTypeCount { - Status = (BacktestJobStatus)st.Status, + Status = (JobStatus)st.Status, JobType = (JobType)st.JobType, Count = st.Count }).ToList(), @@ -430,7 +434,7 @@ public class PostgreSqlJobRepository : IBacktestJobRepository public int Count { get; set; } } - private static JobEntity MapToEntity(BacktestJob job) + private static JobEntity MapToEntity(Job job) { return new JobEntity { @@ -452,18 +456,23 @@ public class PostgreSqlJobRepository : IBacktestJobRepository ResultJson = job.ResultJson, ErrorMessage = job.ErrorMessage, RequestId = job.RequestId, - GeneticRequestId = job.GeneticRequestId + GeneticRequestId = job.GeneticRequestId, + RetryCount = job.RetryCount, + MaxRetries = job.MaxRetries, + RetryAfter = job.RetryAfter, + IsRetryable = job.IsRetryable, + FailureCategory = job.FailureCategory.HasValue ? (int)job.FailureCategory.Value : null }; } - private static BacktestJob MapToDomain(JobEntity entity) + private static Job MapToDomain(JobEntity entity) { - return new BacktestJob + return new Job { Id = entity.Id, BundleRequestId = entity.BundleRequestId, UserId = entity.UserId, - Status = (BacktestJobStatus)entity.Status, + Status = (JobStatus)entity.Status, JobType = (JobType)entity.JobType, Priority = entity.Priority, ConfigJson = entity.ConfigJson, @@ -478,7 +487,12 @@ public class PostgreSqlJobRepository : IBacktestJobRepository ResultJson = entity.ResultJson, ErrorMessage = entity.ErrorMessage, RequestId = entity.RequestId, - GeneticRequestId = entity.GeneticRequestId + GeneticRequestId = entity.GeneticRequestId, + RetryCount = entity.RetryCount, + MaxRetries = entity.MaxRetries, + RetryAfter = entity.RetryAfter, + IsRetryable = entity.IsRetryable, + FailureCategory = entity.FailureCategory.HasValue ? (FailureCategory)entity.FailureCategory.Value : null }; } } diff --git a/src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx b/src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx index 7e68dbd3..5e9c62a3 100644 --- a/src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx +++ b/src/Managing.WebApp/src/pages/adminPage/jobs/jobsSettings.tsx @@ -19,6 +19,7 @@ const JobsSettings: React.FC = () => { const [workerIdFilter, setWorkerIdFilter] = useState('') const [bundleRequestIdFilter, setBundleRequestIdFilter] = useState('') const [filtersOpen, setFiltersOpen] = useState(false) + const [showTable, setShowTable] = useState(false) const jobClient = new JobClient({}, apiUrl) @@ -56,9 +57,10 @@ const JobsSettings: React.FC = () => { bundleRequestIdFilter || null ) }, + enabled: showTable, // Only fetch when table is shown staleTime: 10000, // 10 seconds gcTime: 5 * 60 * 1000, - refetchInterval: 5000, // Auto-refresh every 5 seconds + refetchInterval: showTable ? 5000 : false, // Auto-refresh only when table is shown }) const jobs = jobsData?.jobs || [] @@ -316,7 +318,50 @@ const JobsSettings: React.FC = () => { )} - {filtersOpen && ( + {/* Load Table Button */} + {!showTable && ( +
+
+
+
+

Jobs List

+

Click the button below to load and view the jobs table

+
+ +
+
+
+ )} + + {showTable && ( + <> + {/* Hide Table Button */} +
+
+
+

Jobs List

+ +
+
+
+ + {filtersOpen && (
@@ -416,92 +461,96 @@ const JobsSettings: React.FC = () => {
)} - {error && ( -
- Error loading jobs: {(error as any)?.message || 'Unknown error'} -
+ {error && ( +
+ Error loading jobs: {(error as any)?.message || 'Unknown error'} +
+ )} + + + )} - - {/* Bottom Menu Bar */} - -
  • - { - e.preventDefault() - setFiltersOpen(!filtersOpen) - }} - className={filtersOpen ? 'active' : ''} - > - - - - -
  • -
  • - { - e.preventDefault() - refetch() - }} - > - - - - -
  • -
  • - { - e.preventDefault() - clearFilters() - }} - > - - - - -
  • -
    + {showTable && ( + +
  • + { + e.preventDefault() + setFiltersOpen(!filtersOpen) + }} + className={filtersOpen ? 'active' : ''} + > + + + + +
  • +
  • + { + e.preventDefault() + refetch() + }} + > + + + + +
  • +
  • + { + e.preventDefault() + clearFilters() + }} + > + + + + +
  • +
    + )}
    ) } diff --git a/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx b/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx index b1aa7a4f..fad38a75 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx @@ -265,7 +265,7 @@ const BundleRequestModal: React.FC = ({ const successMessage = asTemplate ? 'Template saved successfully!' : 'Bundle backtest request created successfully!'; - new Toast(successMessage, true); + new Toast(successMessage, false); onClose(); } catch (error) { const errorMessage = asTemplate diff --git a/src/Managing.Workers.Api/Program.cs b/src/Managing.Workers.Api/Program.cs index 26a8bda1..287e34f8 100644 --- a/src/Managing.Workers.Api/Program.cs +++ b/src/Managing.Workers.Api/Program.cs @@ -12,6 +12,8 @@ var host = Host.CreateDefaultBuilder(args) config.SetBasePath(AppContext.BaseDirectory); config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true) + .AddJsonFile("appsettings.SandboxLocal.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.ProductionLocal.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .AddUserSecrets(); }) @@ -50,6 +52,7 @@ var host = Host.CreateDefaultBuilder(args) { options.EnableDetailedErrors(); options.EnableSensitiveDataLogging(); + options.LogTo(Console.WriteLine, LogLevel.Information); // Enable SQL logging to debug table name issues } options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); @@ -63,21 +66,47 @@ var host = Host.CreateDefaultBuilder(args) services.Configure( configuration.GetSection(BacktestComputeWorkerOptions.SectionName)); - // Override WorkerId from environment variable if provided + // Get task slot from CapRover ({{.Task.Slot}}) or environment variable + // This identifies which instance of the worker is running + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + + // Override WorkerId from environment variable if provided, otherwise use task slot var workerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? configuration["BacktestComputeWorker:WorkerId"] ?? - Environment.MachineName; + $"{Environment.MachineName}-{taskSlot}"; services.Configure(options => { options.WorkerId = workerId; }); - // Register the compute worker if enabled - var isWorkerEnabled = configuration.GetValue("WorkerBacktestCompute", false); - if (isWorkerEnabled) + // Configure GeneticComputeWorker options + services.Configure( + configuration.GetSection(GeneticComputeWorkerOptions.SectionName)); + + // Override Genetic WorkerId from environment variable if provided, otherwise use task slot + var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? + configuration["GeneticComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-genetic-{taskSlot}"; + services.Configure(options => + { + options.WorkerId = geneticWorkerId; + }); + + // Register the backtest compute worker if enabled + var isBacktestWorkerEnabled = configuration.GetValue("WorkerBacktestCompute", false); + if (isBacktestWorkerEnabled) { services.AddHostedService(); } + + // Register the genetic compute worker if enabled + var isGeneticWorkerEnabled = configuration.GetValue("WorkerGeneticCompute", false); + if (isGeneticWorkerEnabled) + { + services.AddHostedService(); + } }) .ConfigureLogging((hostingContext, logging) => { @@ -89,18 +118,41 @@ var host = Host.CreateDefaultBuilder(args) // Log worker status var logger = host.Services.GetRequiredService>(); -var isWorkerEnabled = host.Services.GetRequiredService().GetValue("WorkerBacktestCompute", false); +var config = host.Services.GetRequiredService(); -if (isWorkerEnabled) +var isBacktestWorkerEnabled = config.GetValue("WorkerBacktestCompute", false); +var isGeneticWorkerEnabled = config.GetValue("WorkerGeneticCompute", false); + +if (isBacktestWorkerEnabled) { + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + var backtestWorkerId = Environment.GetEnvironmentVariable("WORKER_ID") ?? + config["BacktestComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-{taskSlot}"; logger.LogInformation("BacktestComputeWorker is enabled and will be started."); - logger.LogInformation("Worker ID: {WorkerId}", Environment.GetEnvironmentVariable("WORKER_ID") ?? - host.Services.GetRequiredService()["BacktestComputeWorker:WorkerId"] ?? - Environment.MachineName); + logger.LogInformation("Backtest Worker ID: {WorkerId} (Task Slot: {TaskSlot})", backtestWorkerId, taskSlot); } else { - logger.LogWarning("BacktestComputeWorker is disabled via configuration. No jobs will be processed."); + logger.LogWarning("BacktestComputeWorker is disabled via configuration. No backtest jobs will be processed."); +} + +if (isGeneticWorkerEnabled) +{ + var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? + Environment.GetEnvironmentVariable("CAPROVER_TASK_SLOT") ?? + "0"; + var geneticWorkerId = Environment.GetEnvironmentVariable("GENETIC_WORKER_ID") ?? + config["GeneticComputeWorker:WorkerId"] ?? + $"{Environment.MachineName}-genetic-{taskSlot}"; + logger.LogInformation("GeneticComputeWorker is enabled and will be started."); + logger.LogInformation("Genetic Worker ID: {WorkerId} (Task Slot: {TaskSlot})", geneticWorkerId, taskSlot); +} +else +{ + logger.LogWarning("GeneticComputeWorker is disabled via configuration. No genetic jobs will be processed."); } try diff --git a/src/Managing.Workers.Api/appsettings.Development.json b/src/Managing.Workers.Api/appsettings.Development.json index 066cc5e6..52671f2a 100644 --- a/src/Managing.Workers.Api/appsettings.Development.json +++ b/src/Managing.Workers.Api/appsettings.Development.json @@ -7,12 +7,18 @@ }, "WorkerBacktestCompute": true, "BacktestComputeWorker": { - "WorkerId": "local-worker-1", "MaxConcurrentBacktests": 6, "JobPollIntervalSeconds": 5, "HeartbeatIntervalSeconds": 30, "StaleJobTimeoutMinutes": 5 }, + "WorkerGeneticCompute": true, + "GeneticComputeWorker": { + "MaxConcurrentGenetics": 2, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 10 + }, "PostgreSql": { "ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres" }, diff --git a/src/Managing.Workers.Api/appsettings.ProductionLocal.json b/src/Managing.Workers.Api/appsettings.ProductionLocal.json new file mode 100644 index 00000000..68569778 --- /dev/null +++ b/src/Managing.Workers.Api/appsettings.ProductionLocal.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WorkerBacktestCompute": true, + "BacktestComputeWorker": { + "MaxConcurrentBacktests": 6, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 5 + }, + "WorkerGeneticCompute": true, + "GeneticComputeWorker": { + "MaxConcurrentGenetics": 2, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 10 + }, + "PostgreSql": { + "ConnectionString": "Host=kaigen-db.kaigen.managing.live;Port=5432;Database=managing;Username=postgres;Password=2ab5423dcca4aa2d" + }, + "InfluxDb": { + "Url": "https://influx-db.kaigen.managing.live", + "Organization": "managing-org", + "Token": "ROvQoZ1Dg5jiKDFxB0saEGqHC3rsLkUNlPL6_AFbOcpNjMieIv8v58yA4v5tFU9sX9LLvXEToPvUrxqQEMaWDw==" + }, + "Sentry": { + "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1" + }, + "N8n": { + "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951", + "IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11", + "Username": "managing-api", + "Password": "T259836*PdiV2@%!eR%Qf4" + }, + "Kaigen": { + "BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app", + "DebitEndpoint": "/api/credits/debit", + "RefundEndpoint": "/api/credits/refund" + } +} + diff --git a/src/Managing.Workers.Api/appsettings.SandboxLocal.json b/src/Managing.Workers.Api/appsettings.SandboxLocal.json new file mode 100644 index 00000000..c620ecef --- /dev/null +++ b/src/Managing.Workers.Api/appsettings.SandboxLocal.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WorkerBacktestCompute": true, + "BacktestComputeWorker": { + "MaxConcurrentBacktests": 6, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 5 + }, + "WorkerGeneticCompute": true, + "GeneticComputeWorker": { + "MaxConcurrentGenetics": 2, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 10 + }, + "PostgreSql": { + "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37" + }, + "InfluxDb": { + "Url": "https://influx-db.apps.managing.live", + "Organization": "managing-org", + "Token": "zODh8Hn8sN5VwpVJH0HAwDpCJPE4oB5IUg8L4Q0T67KM1Rta6PoM0nATUzf1ddkyWx_VledooZXfFIddahbL9Q==" + }, + "Sentry": { + "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1" + }, + "N8n": { + "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951", + "IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11", + "Username": "managing-api", + "Password": "T259836*PdiV2@%!eR%Qf4" + }, + "Kaigen": { + "BaseUrl": "https://kaigen-back-kaigen-stage.up.railway.app", + "DebitEndpoint": "/api/credits/debit", + "RefundEndpoint": "/api/credits/refund" + } +} + diff --git a/src/Managing.Workers.Api/appsettings.json b/src/Managing.Workers.Api/appsettings.json index 796fe915..54613c53 100644 --- a/src/Managing.Workers.Api/appsettings.json +++ b/src/Managing.Workers.Api/appsettings.json @@ -7,12 +7,18 @@ }, "WorkerBacktestCompute": true, "BacktestComputeWorker": { - "WorkerId": "worker-1", "MaxConcurrentBacktests": 6, "JobPollIntervalSeconds": 5, "HeartbeatIntervalSeconds": 30, "StaleJobTimeoutMinutes": 5 }, + "WorkerGeneticCompute": true, + "GeneticComputeWorker": { + "MaxConcurrentGenetics": 2, + "JobPollIntervalSeconds": 5, + "HeartbeatIntervalSeconds": 30, + "StaleJobTimeoutMinutes": 10 + }, "PostgreSql": { "ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37" },