diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 8b86fd6..d897815 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -1,5 +1,7 @@ -using Managing.Api.Models.Requests; +using System.Text.Json; +using Managing.Api.Models.Requests; using Managing.Application.Abstractions; +using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Domain.Backtests; @@ -31,6 +33,7 @@ public class BacktestController : BaseController private readonly IAccountService _accountService; private readonly IMoneyManagementService _moneyManagementService; private readonly IGeneticService _geneticService; + private readonly IBacktestRepository _backtestRepository; /// /// Initializes a new instance of the class. @@ -41,6 +44,7 @@ public class BacktestController : BaseController /// The service for account management. /// The service for money management strategies. /// The service for genetic algorithm operations. + /// The repository for backtest operations. public BacktestController( IHubContext hubContext, IBacktester backtester, @@ -48,6 +52,7 @@ public class BacktestController : BaseController IAccountService accountService, IMoneyManagementService moneyManagementService, IGeneticService geneticService, + IBacktestRepository backtestRepository, IUserService userService) : base(userService) { _hubContext = hubContext; @@ -56,6 +61,7 @@ public class BacktestController : BaseController _accountService = accountService; _moneyManagementService = moneyManagementService; _geneticService = geneticService; + _backtestRepository = backtestRepository; } /// @@ -400,14 +406,14 @@ public class BacktestController : BaseController } /// - /// Runs multiple backtests in a bundle with the specified configurations. - /// This endpoint receives a list of backtest requests and will execute them all. + /// Creates a bundle backtest request with the specified configurations. + /// This endpoint creates a request that will be processed by a background worker. /// /// The list of backtest requests to execute. - /// A list of backtest results. + /// The bundle backtest request with ID for tracking progress. [HttpPost] [Route("Bundle")] - public async Task>> RunBundle([FromBody] List requests) + public async Task> RunBundle([FromBody] List requests) { if (requests == null || !requests.Any()) { @@ -419,92 +425,110 @@ public class BacktestController : BaseController return BadRequest("Maximum of 10 backtests allowed per bundle request"); } - var user = await GetUser(); - var results = new List(); - - foreach (var request in requests) + try { - try + var user = await GetUser(); + + // Validate all requests before creating the bundle + foreach (var request in requests) { - // Validate individual request if (request?.Config == null) { - results.Add(new - { - Id = Guid.NewGuid().ToString(), - Status = "Error", - Message = "Invalid request: Configuration is required" - }); - continue; + return BadRequest("Invalid request: Configuration is required"); } if (string.IsNullOrEmpty(request.Config.AccountName)) { - results.Add(new - { - Id = Guid.NewGuid().ToString(), - Status = "Error", - Message = "Invalid request: Account name is required" - }); - continue; + return BadRequest("Invalid request: Account name is required"); } if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null) { - results.Add(new - { - Id = Guid.NewGuid().ToString(), - Status = "Error", - Message = "Invalid request: Either scenario name or scenario object is required" - }); - continue; + return BadRequest("Invalid request: Either scenario name or scenario object is required"); } if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { - results.Add(new - { - Id = Guid.NewGuid().ToString(), - Status = "Error", - Message = "Invalid request: Either money management name or money management object is required" - }); - continue; + return BadRequest("Invalid request: Either money management name or money management object is required"); } - - // TODO: Implement actual backtest execution logic here - // For now, return a placeholder result - var placeholderResult = new - { - Id = Guid.NewGuid().ToString(), - Status = "Pending", - Message = "Bundle backtest - Application layer integration pending", - Config = new - { - AccountName = request.Config.AccountName, - Ticker = request.Config.Ticker, - ScenarioName = request.Config.ScenarioName, - Timeframe = request.Config.Timeframe, - BotTradingBalance = request.Config.BotTradingBalance, - Name = request.Config.Name ?? $"Bundle-Backtest-{DateTime.UtcNow:yyyyMMdd-HHmmss}" - }, - StartDate = request.StartDate, - EndDate = request.EndDate - }; - - results.Add(placeholderResult); } - catch (Exception ex) + + // Create the bundle backtest request + var bundleRequest = new BundleBacktestRequest { - results.Add(new - { - Id = Guid.NewGuid().ToString(), - Status = "Error", - Message = $"Error processing backtest: {ex.Message}" - }); - } + User = user, + BacktestRequestsJson = JsonSerializer.Serialize(requests), + TotalBacktests = requests.Count, + CompletedBacktests = 0, + FailedBacktests = 0, + Status = BundleBacktestRequestStatus.Pending + }; + + _backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest); + + return Ok(bundleRequest); + } + catch (Exception ex) + { + return StatusCode(500, $"Error creating bundle backtest request: {ex.Message}"); + } + } + + /// + /// Retrieves all bundle backtest requests for the authenticated user. + /// + /// A list of bundle backtest requests with their current status. + [HttpGet] + [Route("Bundle")] + public async Task>> GetBundleBacktestRequests() + { + var user = await GetUser(); + var bundleRequests = _backtestRepository.GetBundleBacktestRequestsByUser(user); + return Ok(bundleRequests); + } + + /// + /// Retrieves a specific bundle backtest request by ID for the authenticated user. + /// + /// The ID of the bundle backtest request to retrieve. + /// The requested bundle backtest request with current status and results. + [HttpGet] + [Route("Bundle/{id}")] + public async Task> GetBundleBacktestRequest(string id) + { + var user = await GetUser(); + var bundleRequest = _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); + + if (bundleRequest == null) + { + return NotFound($"Bundle backtest request with ID {id} not found or doesn't belong to the current user."); } - return Ok(results); + return Ok(bundleRequest); + } + + /// + /// Deletes a specific bundle backtest request by ID for the authenticated user. + /// Also deletes all related backtests associated with this bundle request. + /// + /// The ID of the bundle backtest request to delete. + /// An ActionResult indicating the outcome of the operation. + [HttpDelete] + [Route("Bundle/{id}")] + public async Task DeleteBundleBacktestRequest(string id) + { + var user = await GetUser(); + + // First, delete the bundle request + _backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id); + + // Then, delete all related backtests + var backtestsDeleted = _backtester.DeleteBacktestsByRequestId(id); + + return Ok(new { + BundleRequestDeleted = true, + RelatedBacktestsDeleted = backtestsDeleted + }); } /// @@ -630,6 +654,8 @@ public class BacktestController : BaseController }); } + + /// /// Notifies subscribers about the backtesting results via SignalR. /// diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs index d164a5f..ac5f789 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs @@ -15,4 +15,12 @@ public interface IBacktestRepository void DeleteBacktestsByIdsForUser(User user, IEnumerable ids); void DeleteAllBacktestsForUser(User user); void DeleteBacktestsByRequestId(string requestId); + + // Bundle backtest methods + void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); + IEnumerable GetBundleBacktestRequestsByUser(User user); + BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id); + void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest); + void DeleteBundleBacktestRequestByIdForUser(User user, string id); + IEnumerable GetPendingBundleBacktestRequests(); } \ No newline at end of file diff --git a/src/Managing.Application.Workers/BundleBacktestWorker.cs b/src/Managing.Application.Workers/BundleBacktestWorker.cs new file mode 100644 index 0000000..09f44fa --- /dev/null +++ b/src/Managing.Application.Workers/BundleBacktestWorker.cs @@ -0,0 +1,156 @@ +using System.Text.Json; +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Application.Workers.Abstractions; +using Managing.Domain.Backtests; +using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; + +namespace Managing.Application.Workers; + +/// +/// Worker for processing bundle backtest requests +/// +public class BundleBacktestWorker : BaseWorker +{ + private readonly IBacktestRepository _backtestRepository; + private readonly IBacktester _backtester; + private static readonly WorkerType _workerType = WorkerType.BundleBacktest; + + public BundleBacktestWorker( + IBacktestRepository backtestRepository, + IBacktester backtester, + ILogger logger, + IWorkerService workerService) : base( + _workerType, + logger, + TimeSpan.FromMinutes(1), + workerService) + { + _backtestRepository = backtestRepository; + _backtester = backtester; + } + + protected override async Task Run(CancellationToken cancellationToken) + { + try + { + // Get pending bundle backtest requests + var pendingRequests = _backtestRepository.GetPendingBundleBacktestRequests(); + + foreach (var bundleRequest in pendingRequests) + { + if (cancellationToken.IsCancellationRequested) + break; + + await ProcessBundleRequest(bundleRequest, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in BundleBacktestWorker"); + throw; + } + } + + private async Task ProcessBundleRequest(BundleBacktestRequest bundleRequest, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId); + + // Update status to running + bundleRequest.Status = BundleBacktestRequestStatus.Running; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + + // Deserialize the backtest requests as dynamic objects + var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); + if (backtestRequests == null) + { + throw new InvalidOperationException("Failed to deserialize backtest requests"); + } + + // Process each backtest request + for (int i = 0; i < backtestRequests.Count; i++) + { + if (cancellationToken.IsCancellationRequested) + break; + + try + { + var requestElement = backtestRequests[i]; + + // Update current backtest being processed + bundleRequest.CurrentBacktest = $"Backtest {i + 1} of {backtestRequests.Count}"; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + + // Convert JSON element to domain model and run backtest + await RunSingleBacktest(requestElement, bundleRequest.RequestId, cancellationToken); + + // Update progress + bundleRequest.CompletedBacktests++; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + + _logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}", + i + 1, bundleRequest.RequestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}", + i + 1, bundleRequest.RequestId); + bundleRequest.FailedBacktests++; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + } + } + + // Update final status + if (bundleRequest.FailedBacktests == 0) + { + bundleRequest.Status = BundleBacktestRequestStatus.Completed; + } + else if (bundleRequest.CompletedBacktests == 0) + { + bundleRequest.Status = BundleBacktestRequestStatus.Failed; + bundleRequest.ErrorMessage = "All backtests failed"; + } + else + { + bundleRequest.Status = BundleBacktestRequestStatus.Completed; + bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed"; + } + + bundleRequest.CompletedAt = DateTime.UtcNow; + bundleRequest.CurrentBacktest = null; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + + _logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}", + bundleRequest.RequestId, bundleRequest.Status); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId); + + bundleRequest.Status = BundleBacktestRequestStatus.Failed; + bundleRequest.ErrorMessage = ex.Message; + bundleRequest.CompletedAt = DateTime.UtcNow; + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + } + } + + private async Task RunSingleBacktest(JsonElement requestElement, string bundleRequestId, CancellationToken cancellationToken) + { + // For now, we'll use a simplified approach that simulates backtest execution + // In a real implementation, you would parse the JSON and convert to domain models + + // Simulate backtest processing time + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + + _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequestId); + + // TODO: Implement actual backtest execution by: + // 1. Parsing the JSON request element + // 2. Converting to TradingBotConfig domain model + // 3. Calling _backtester.RunTradingBotBacktest with proper parameters + // 4. Handling the results and saving to database if needed + } +} \ No newline at end of file diff --git a/src/Managing.Bootstrap/WorkersBootstrap.cs b/src/Managing.Bootstrap/WorkersBootstrap.cs index 348baf8..3052530 100644 --- a/src/Managing.Bootstrap/WorkersBootstrap.cs +++ b/src/Managing.Bootstrap/WorkersBootstrap.cs @@ -137,6 +137,11 @@ public static class WorkersBootstrap services.AddHostedService(); } + if (configuration.GetValue("WorkerBundleBacktest", false)) + { + services.AddHostedService(); + } + return services; } diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 0cfc47f..dbe08ba 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -383,7 +383,8 @@ public static class Enums BotManager, FundingRatesWatcher, BalanceTracking, - GeneticAlgorithm + GeneticAlgorithm, + BundleBacktest } public enum WorkflowUsage diff --git a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs new file mode 100644 index 0000000..dc8fc18 --- /dev/null +++ b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs @@ -0,0 +1,146 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Domain.Users; + +namespace Managing.Domain.Backtests; + +/// +/// Domain model for bundle backtest requests +/// +public class BundleBacktestRequest +{ + public BundleBacktestRequest() + { + RequestId = Guid.NewGuid().ToString(); + CreatedAt = DateTime.UtcNow; + Status = BundleBacktestRequestStatus.Pending; + Results = new List(); + BacktestRequestsJson = string.Empty; + } + + /// + /// Constructor that allows setting a specific ID + /// + /// The specific ID to use + public BundleBacktestRequest(string requestId) + { + RequestId = requestId; + CreatedAt = DateTime.UtcNow; + Status = BundleBacktestRequestStatus.Pending; + Results = new List(); + BacktestRequestsJson = string.Empty; + } + + /// + /// Unique identifier for the bundle backtest request + /// + [Required] + public string RequestId { get; set; } + + /// + /// The user who created this request + /// + [Required] + public User User { get; set; } + + /// + /// When the request was created + /// + [Required] + public DateTime CreatedAt { get; set; } + + /// + /// When the request was completed (if completed) + /// + public DateTime? CompletedAt { get; set; } + + /// + /// Current status of the bundle backtest request + /// + [Required] + public BundleBacktestRequestStatus Status { get; set; } + + /// + /// The list of backtest requests to execute (serialized as JSON) + /// + [Required] + public string BacktestRequestsJson { get; set; } = string.Empty; + + /// + /// The results of the bundle backtest execution + /// + public List Results { get; set; } = new(); + + /// + /// Total number of backtests in the bundle + /// + [Required] + public int TotalBacktests { get; set; } + + /// + /// Number of backtests completed so far + /// + [Required] + public int CompletedBacktests { get; set; } + + /// + /// Number of backtests that failed + /// + [Required] + public int FailedBacktests { get; set; } + + /// + /// Progress percentage (0-100) + /// + public double ProgressPercentage => TotalBacktests > 0 ? (double)CompletedBacktests / TotalBacktests * 100 : 0; + + /// + /// Error message if the request failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Progress information (JSON serialized) + /// + public string? ProgressInfo { get; set; } + + /// + /// Current backtest being processed + /// + public string? CurrentBacktest { get; set; } + + /// + /// Estimated time remaining in seconds + /// + public int? EstimatedTimeRemainingSeconds { get; set; } +} + +/// +/// Status of a bundle backtest request +/// +public enum BundleBacktestRequestStatus +{ + /// + /// Request is pending execution + /// + Pending, + + /// + /// Request is currently being processed + /// + Running, + + /// + /// Request completed successfully + /// + Completed, + + /// + /// Request failed with an error + /// + Failed, + + /// + /// Request was cancelled + /// + Cancelled +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/BacktestRepository.cs b/src/Managing.Infrastructure.Database/BacktestRepository.cs index e0782f7..7a5f2c7 100644 --- a/src/Managing.Infrastructure.Database/BacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/BacktestRepository.cs @@ -12,10 +12,14 @@ namespace Managing.Infrastructure.Databases; public class BacktestRepository : IBacktestRepository { private readonly IMongoRepository _backtestRepository; + private readonly IMongoRepository _bundleBacktestRepository; - public BacktestRepository(IMongoRepository backtestRepository) + public BacktestRepository( + IMongoRepository backtestRepository, + IMongoRepository bundleBacktestRepository) { _backtestRepository = backtestRepository; + _bundleBacktestRepository = bundleBacktestRepository; } // User-specific operations @@ -305,4 +309,107 @@ public class BacktestRepository : IBacktestRepository return (mappedBacktests, (int)totalCount); } + + // Bundle backtest methods + public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) + { + bundleRequest.User = user; + var dto = MapToDto(bundleRequest); + _bundleBacktestRepository.InsertOne(dto); + } + + public IEnumerable GetBundleBacktestRequestsByUser(User user) + { + var bundleRequests = _bundleBacktestRepository.AsQueryable() + .Where(b => b.User.Name == user.Name) + .ToList(); + + return bundleRequests.Select(MapToDomain); + } + + public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) + { + var bundleRequest = _bundleBacktestRepository.FindOne(b => b.RequestId == id); + + if (bundleRequest != null && bundleRequest.User.Name == user.Name) + { + return MapToDomain(bundleRequest); + } + + return null; + } + + public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) + { + var dto = MapToDto(bundleRequest); + _bundleBacktestRepository.ReplaceOne(dto); + } + + public void DeleteBundleBacktestRequestByIdForUser(User user, string id) + { + var bundleRequest = _bundleBacktestRepository.FindOne(b => b.RequestId == id); + + if (bundleRequest != null && bundleRequest.User.Name == user.Name) + { + _bundleBacktestRepository.DeleteById(bundleRequest.Id.ToString()); + } + } + + public IEnumerable GetPendingBundleBacktestRequests() + { + var pendingRequests = _bundleBacktestRepository.AsQueryable() + .Where(b => b.Status == BundleBacktestRequestStatus.Pending) + .ToList(); + + return pendingRequests.Select(MapToDomain); + } + + /// + /// Maps a domain model to DTO + /// + /// The domain model + /// The DTO + private static BundleBacktestRequestDto MapToDto(BundleBacktestRequest domain) + { + return new BundleBacktestRequestDto + { + RequestId = domain.RequestId, + User = MongoMappers.Map(domain.User), + CompletedAt = domain.CompletedAt, + Status = domain.Status, + BacktestRequestsJson = domain.BacktestRequestsJson, + TotalBacktests = domain.TotalBacktests, + CompletedBacktests = domain.CompletedBacktests, + FailedBacktests = domain.FailedBacktests, + ErrorMessage = domain.ErrorMessage, + ProgressInfo = domain.ProgressInfo, + CurrentBacktest = domain.CurrentBacktest, + EstimatedTimeRemainingSeconds = domain.EstimatedTimeRemainingSeconds + }; + } + + /// + /// Maps a DTO to domain model + /// + /// The DTO + /// The domain model + private static BundleBacktestRequest MapToDomain(BundleBacktestRequestDto dto) + { + return new BundleBacktestRequest + { + RequestId = dto.RequestId, + User = MongoMappers.Map(dto.User), + CreatedAt = dto.CreatedAt, + CompletedAt = dto.CompletedAt, + Status = dto.Status, + BacktestRequestsJson = dto.BacktestRequestsJson, + TotalBacktests = dto.TotalBacktests, + CompletedBacktests = dto.CompletedBacktests, + FailedBacktests = dto.FailedBacktests, + ErrorMessage = dto.ErrorMessage, + ProgressInfo = dto.ProgressInfo, + CurrentBacktest = dto.CurrentBacktest, + EstimatedTimeRemainingSeconds = dto.EstimatedTimeRemainingSeconds + }; + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/BundleBacktestRequestDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/BundleBacktestRequestDto.cs new file mode 100644 index 0000000..f9c7c2f --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/BundleBacktestRequestDto.cs @@ -0,0 +1,22 @@ +using Managing.Domain.Backtests; +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +[BsonCollection("BundleBacktestRequests")] +public class BundleBacktestRequestDto : Document +{ + public string RequestId { get; set; } = string.Empty; + public UserDto User { get; set; } = new(); + public DateTime? CompletedAt { get; set; } + public BundleBacktestRequestStatus Status { get; set; } + public string BacktestRequestsJson { get; set; } = string.Empty; + public int TotalBacktests { get; set; } + public int CompletedBacktests { get; set; } + public int FailedBacktests { get; set; } + public string? ErrorMessage { get; set; } + public string? ProgressInfo { get; set; } + public string? CurrentBacktest { get; set; } + public int? EstimatedTimeRemainingSeconds { get; set; } +} \ No newline at end of file