From 6f49f2659f621a74f2cf10e21e393791fbf55989 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 21 Jul 2025 17:03:27 +0700 Subject: [PATCH] Add bundle backtest --- .../Controllers/BacktestController.cs | 68 ++++--- src/Managing.Api/Controllers/BotController.cs | 1 + .../Controllers/DataController.cs | 1 + .../GetCandlesWithIndicatorsRequest.cs | 5 +- .../Models/Requests/StartBotRequest.cs | 1 + .../Models/Requests/UpdateBotConfigRequest.cs | 1 + .../Services/IBacktester.cs | 8 + .../BundleBacktestWorker.cs | 158 +++++++++++---- .../Backtesting/Backtester.cs | 31 +++ .../Backtests/BundleBacktestRequest.cs | 8 +- .../Backtests}/IndicatorRequest.cs | 2 +- .../Backtests}/MoneyManagementRequest.cs | 2 +- .../Backtests}/RunBacktestRequest.cs | 13 +- .../Backtests}/ScenarioRequest.cs | 2 +- .../Bots}/TradingBotConfigRequest.cs | 3 +- .../BacktestRepository.cs | 59 +----- .../Collections/BundleBacktestRequestDto.cs | 3 +- .../MongoDb/MongoMappers.cs | 47 +++++ .../src/generated/ManagingApi.ts | 184 ++++++++++++++++++ .../src/generated/ManagingApiTypes.ts | 27 +++ 20 files changed, 492 insertions(+), 132 deletions(-) rename src/{Managing.Api/Models/Requests => Managing.Domain/Backtests}/IndicatorRequest.cs (97%) rename src/{Managing.Api/Models/Requests => Managing.Domain/Backtests}/MoneyManagementRequest.cs (90%) rename src/{Managing.Api/Models/Requests => Managing.Domain/Backtests}/RunBacktestRequest.cs (79%) rename src/{Managing.Api/Models/Requests => Managing.Domain/Backtests}/ScenarioRequest.cs (93%) rename src/{Managing.Api/Models/Requests => Managing.Domain/Bots}/TradingBotConfigRequest.cs (98%) diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index d897815..d03de06 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -1,7 +1,6 @@ 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; @@ -12,6 +11,7 @@ using Managing.Domain.Strategies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using MoneyManagementRequest = Managing.Domain.Backtests.MoneyManagementRequest; namespace Managing.Api.Controllers; @@ -33,7 +33,6 @@ 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. @@ -52,7 +51,6 @@ public class BacktestController : BaseController IAccountService accountService, IMoneyManagementService moneyManagementService, IGeneticService geneticService, - IBacktestRepository backtestRepository, IUserService userService) : base(userService) { _hubContext = hubContext; @@ -61,7 +59,6 @@ public class BacktestController : BaseController _accountService = accountService; _moneyManagementService = moneyManagementService; _geneticService = geneticService; - _backtestRepository = backtestRepository; } /// @@ -153,8 +150,8 @@ public class BacktestController : BaseController [HttpGet] [Route("ByRequestId/{requestId}/Paginated")] public async Task> GetBacktestsByRequestIdPaginated( - string requestId, - int page = 1, + string requestId, + int page = 1, int pageSize = 50, string sortBy = "score", string sortOrder = "desc") @@ -179,10 +176,11 @@ public class BacktestController : BaseController return BadRequest("Sort order must be 'asc' or 'desc'"); } - var (backtests, totalCount) = _backtester.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder); - + var (backtests, totalCount) = + _backtester.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder); + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); - + var response = new PaginatedBacktestsResponse { Backtests = backtests.Select(b => new LightBacktestResponse @@ -410,10 +408,12 @@ public class BacktestController : BaseController /// This endpoint creates a request that will be processed by a background worker. /// /// The list of backtest requests to execute. + /// Display name for the bundle (required). /// 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, + [FromQuery] string name) { if (requests == null || !requests.Any()) { @@ -425,10 +425,15 @@ public class BacktestController : BaseController return BadRequest("Maximum of 10 backtests allowed per bundle request"); } + if (string.IsNullOrWhiteSpace(name)) + { + return BadRequest("Bundle name is required"); + } + try { var user = await GetUser(); - + // Validate all requests before creating the bundle foreach (var request in requests) { @@ -449,7 +454,8 @@ public class BacktestController : BaseController if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { - return BadRequest("Invalid request: Either money management name or money management object is required"); + return BadRequest( + "Invalid request: Either money management name or money management object is required"); } } @@ -461,10 +467,11 @@ public class BacktestController : BaseController TotalBacktests = requests.Count, CompletedBacktests = 0, FailedBacktests = 0, - Status = BundleBacktestRequestStatus.Pending + Status = BundleBacktestRequestStatus.Pending, + Name = name }; - _backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest); + _backtester.InsertBundleBacktestRequestForUser(user, bundleRequest); return Ok(bundleRequest); } @@ -483,7 +490,7 @@ public class BacktestController : BaseController public async Task>> GetBundleBacktestRequests() { var user = await GetUser(); - var bundleRequests = _backtestRepository.GetBundleBacktestRequestsByUser(user); + var bundleRequests = _backtester.GetBundleBacktestRequestsByUser(user); return Ok(bundleRequests); } @@ -497,7 +504,7 @@ public class BacktestController : BaseController public async Task> GetBundleBacktestRequest(string id) { var user = await GetUser(); - var bundleRequest = _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); + var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, id); if (bundleRequest == null) { @@ -518,16 +525,17 @@ public class BacktestController : BaseController public async Task DeleteBundleBacktestRequest(string id) { var user = await GetUser(); - + // First, delete the bundle request - _backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id); - + _backtester.DeleteBundleBacktestRequestByIdForUser(user, id); + // Then, delete all related backtests var backtestsDeleted = _backtester.DeleteBacktestsByRequestId(id); - - return Ok(new { - BundleRequestDeleted = true, - RelatedBacktestsDeleted = backtestsDeleted + + return Ok(new + { + BundleRequestDeleted = true, + RelatedBacktestsDeleted = backtestsDeleted }); } @@ -641,21 +649,21 @@ public class BacktestController : BaseController public async Task DeleteGeneticRequest(string id) { var user = await GetUser(); - + // First, delete the genetic request _geneticService.DeleteGeneticRequestByIdForUser(user, id); - + // Then, delete all related backtests var backtestsDeleted = _backtester.DeleteBacktestsByRequestId(id); - - return Ok(new { - GeneticRequestDeleted = true, - RelatedBacktestsDeleted = backtestsDeleted + + return Ok(new + { + GeneticRequestDeleted = true, + RelatedBacktestsDeleted = backtestsDeleted }); } - /// /// Notifies subscribers about the backtesting results via SignalR. /// diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index d3cabe1..506f55a 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -5,6 +5,7 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; using Managing.Common; +using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index a58bd5b..50fc803 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -4,6 +4,7 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; +using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Scenarios; diff --git a/src/Managing.Api/Models/Requests/GetCandlesWithIndicatorsRequest.cs b/src/Managing.Api/Models/Requests/GetCandlesWithIndicatorsRequest.cs index 2ddd5d8..d56f546 100644 --- a/src/Managing.Api/Models/Requests/GetCandlesWithIndicatorsRequest.cs +++ b/src/Managing.Api/Models/Requests/GetCandlesWithIndicatorsRequest.cs @@ -1,3 +1,4 @@ +using Managing.Domain.Backtests; using static Managing.Common.Enums; namespace Managing.Api.Models.Requests; @@ -7,8 +8,6 @@ namespace Managing.Api.Models.Requests; /// public class GetCandlesWithIndicatorsRequest { - - /// /// The ticker symbol. /// @@ -33,4 +32,4 @@ public class GetCandlesWithIndicatorsRequest /// Optional scenario for calculating indicators. /// public ScenarioRequest Scenario { get; set; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/StartBotRequest.cs b/src/Managing.Api/Models/Requests/StartBotRequest.cs index 42bedc8..5bd7b30 100644 --- a/src/Managing.Api/Models/Requests/StartBotRequest.cs +++ b/src/Managing.Api/Models/Requests/StartBotRequest.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Managing.Domain.Bots; namespace Managing.Api.Models.Requests { diff --git a/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs index 26bc075..76c817b 100644 --- a/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs +++ b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; namespace Managing.Api.Models.Requests; diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 9192d77..316fb86 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -62,6 +62,14 @@ namespace Managing.Application.Abstractions.Services bool DeleteBacktestsByRequestId(string requestId); (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); + // 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 index 09f44fa..5306c26 100644 --- a/src/Managing.Application.Workers/BundleBacktestWorker.cs +++ b/src/Managing.Application.Workers/BundleBacktestWorker.cs @@ -1,8 +1,11 @@ using System.Text.Json; -using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Workers.Abstractions; using Managing.Domain.Backtests; +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; +using Managing.Domain.Strategies; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -13,12 +16,11 @@ namespace Managing.Application.Workers; /// public class BundleBacktestWorker : BaseWorker { - private readonly IBacktestRepository _backtestRepository; + // Removed direct repository usage for bundle requests private readonly IBacktester _backtester; private static readonly WorkerType _workerType = WorkerType.BundleBacktest; public BundleBacktestWorker( - IBacktestRepository backtestRepository, IBacktester backtester, ILogger logger, IWorkerService workerService) : base( @@ -27,7 +29,6 @@ public class BundleBacktestWorker : BaseWorker TimeSpan.FromMinutes(1), workerService) { - _backtestRepository = backtestRepository; _backtester = backtester; } @@ -36,7 +37,7 @@ public class BundleBacktestWorker : BaseWorker try { // Get pending bundle backtest requests - var pendingRequests = _backtestRepository.GetPendingBundleBacktestRequests(); + var pendingRequests = _backtester.GetPendingBundleBacktestRequests(); foreach (var bundleRequest in pendingRequests) { @@ -61,10 +62,12 @@ public class BundleBacktestWorker : BaseWorker // Update status to running bundleRequest.Status = BundleBacktestRequestStatus.Running; - _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + _backtester.UpdateBundleBacktestRequest(bundleRequest); - // Deserialize the backtest requests as dynamic objects - var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); + // Deserialize the backtest requests as strongly-typed objects + var backtestRequests = + JsonSerializer.Deserialize>( + bundleRequest.BacktestRequestsJson); if (backtestRequests == null) { throw new InvalidOperationException("Failed to deserialize backtest requests"); @@ -78,28 +81,27 @@ public class BundleBacktestWorker : BaseWorker try { - var requestElement = backtestRequests[i]; - + var runBacktestRequest = backtestRequests[i]; // Update current backtest being processed bundleRequest.CurrentBacktest = $"Backtest {i + 1} of {backtestRequests.Count}"; - _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + _backtester.UpdateBundleBacktestRequest(bundleRequest); - // Convert JSON element to domain model and run backtest - await RunSingleBacktest(requestElement, bundleRequest.RequestId, cancellationToken); + // Run the backtest directly with the strongly-typed request + await RunSingleBacktest(runBacktestRequest, bundleRequest, i, cancellationToken); // Update progress bundleRequest.CompletedBacktests++; - _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + _backtester.UpdateBundleBacktestRequest(bundleRequest); - _logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}", + _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}", + _logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}", i + 1, bundleRequest.RequestId); bundleRequest.FailedBacktests++; - _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + _backtester.UpdateBundleBacktestRequest(bundleRequest); } } @@ -121,36 +123,120 @@ public class BundleBacktestWorker : BaseWorker bundleRequest.CompletedAt = DateTime.UtcNow; bundleRequest.CurrentBacktest = null; - _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + _backtester.UpdateBundleBacktestRequest(bundleRequest); - _logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}", + _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); + _backtester.UpdateBundleBacktestRequest(bundleRequest); } } - private async Task RunSingleBacktest(JsonElement requestElement, string bundleRequestId, CancellationToken cancellationToken) + // Change RunSingleBacktest to accept RunBacktestRequest directly + private async Task RunSingleBacktest(RunBacktestRequest runBacktestRequest, BundleBacktestRequest bundleRequest, + int index, 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 + if (runBacktestRequest == null || runBacktestRequest.Config == null) + { + _logger.LogError("Invalid RunBacktestRequest in bundle (null config)"); + return; + } + + // Map MoneyManagement + MoneyManagement moneyManagement = null; + if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName)) + { + // In worker context, we cannot resolve by name (no user/db), so skip or set null + // Optionally, log a warning + _logger.LogWarning("MoneyManagementName provided but cannot resolve in worker context: {Name}", + (string)runBacktestRequest.Config.MoneyManagementName); + } + else if (runBacktestRequest.Config.MoneyManagement != null) + { + var mmReq = runBacktestRequest.Config.MoneyManagement; + moneyManagement = new MoneyManagement + { + Name = mmReq.Name, + Timeframe = mmReq.Timeframe, + StopLoss = mmReq.StopLoss, + TakeProfit = mmReq.TakeProfit, + Leverage = mmReq.Leverage + }; + moneyManagement.FormatPercentage(); + } + + // Map Scenario + Scenario scenario = null; + if (runBacktestRequest.Config.Scenario != null) + { + var sReq = runBacktestRequest.Config.Scenario; + scenario = new Scenario(sReq.Name, sReq.LoopbackPeriod) + { + User = null // No user context in worker + }; + foreach (var indicatorRequest in sReq.Indicators) + { + var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) + { + SignalType = indicatorRequest.SignalType, + MinimumHistory = indicatorRequest.MinimumHistory, + Period = indicatorRequest.Period, + FastPeriods = indicatorRequest.FastPeriods, + SlowPeriods = indicatorRequest.SlowPeriods, + SignalPeriods = indicatorRequest.SignalPeriods, + Multiplier = indicatorRequest.Multiplier, + SmoothPeriods = indicatorRequest.SmoothPeriods, + StochPeriods = indicatorRequest.StochPeriods, + CyclePeriods = indicatorRequest.CyclePeriods, + User = null // No user context in worker + }; + scenario.AddIndicator(indicator); + } + } + + // Map TradingBotConfig + var backtestConfig = new TradingBotConfig + { + AccountName = runBacktestRequest.Config.AccountName, + MoneyManagement = moneyManagement, + Ticker = runBacktestRequest.Config.Ticker, + ScenarioName = runBacktestRequest.Config.ScenarioName, + Scenario = scenario, + Timeframe = runBacktestRequest.Config.Timeframe, + IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly, + BotTradingBalance = runBacktestRequest.Config.BotTradingBalance, + IsForBacktest = true, + CooldownPeriod = runBacktestRequest.Config.CooldownPeriod, + MaxLossStreak = runBacktestRequest.Config.MaxLossStreak, + MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit, + FlipPosition = runBacktestRequest.Config.FlipPosition, + Name = $"{bundleRequest.Name} #{index + 1}", + CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable, + UseSynthApi = runBacktestRequest.Config.UseSynthApi, + UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing, + UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering, + UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss + }; + + // Run the backtest (no user context) + var result = await _backtester.RunTradingBotBacktest( + backtestConfig, + runBacktestRequest.StartDate, + runBacktestRequest.EndDate, + null, // No user context in worker + runBacktestRequest.Save, + runBacktestRequest.WithCandles, + bundleRequest.RequestId // Use bundleRequestId as requestId for traceability + ); + + _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 94ec5f6..daaf256 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -573,5 +573,36 @@ namespace Managing.Application.Backtesting _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder); return (backtests, totalCount); } + + // Bundle backtest methods + public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) + { + _backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest); + } + + public IEnumerable GetBundleBacktestRequestsByUser(User user) + { + return _backtestRepository.GetBundleBacktestRequestsByUser(user); + } + + public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) + { + return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); + } + + public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) + { + _backtestRepository.UpdateBundleBacktestRequest(bundleRequest); + } + + public void DeleteBundleBacktestRequestByIdForUser(User user, string id) + { + _backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id); + } + + public IEnumerable GetPendingBundleBacktestRequests() + { + return _backtestRepository.GetPendingBundleBacktestRequests(); + } } } \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs index dc8fc18..3a55d89 100644 --- a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs +++ b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs @@ -59,6 +59,12 @@ public class BundleBacktestRequest [Required] public BundleBacktestRequestStatus Status { get; set; } + /// + /// Display name for the bundle backtest request + /// + [Required] + public string Name { get; set; } + /// /// The list of backtest requests to execute (serialized as JSON) /// @@ -143,4 +149,4 @@ public enum BundleBacktestRequestStatus /// Request was cancelled /// Cancelled -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/IndicatorRequest.cs b/src/Managing.Domain/Backtests/IndicatorRequest.cs similarity index 97% rename from src/Managing.Api/Models/Requests/IndicatorRequest.cs rename to src/Managing.Domain/Backtests/IndicatorRequest.cs index 1a11600..e64779a 100644 --- a/src/Managing.Api/Models/Requests/IndicatorRequest.cs +++ b/src/Managing.Domain/Backtests/IndicatorRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using static Managing.Common.Enums; -namespace Managing.Api.Models.Requests; +namespace Managing.Domain.Backtests; /// /// Request model for indicator configuration without user information diff --git a/src/Managing.Api/Models/Requests/MoneyManagementRequest.cs b/src/Managing.Domain/Backtests/MoneyManagementRequest.cs similarity index 90% rename from src/Managing.Api/Models/Requests/MoneyManagementRequest.cs rename to src/Managing.Domain/Backtests/MoneyManagementRequest.cs index eef3162..e401f03 100644 --- a/src/Managing.Api/Models/Requests/MoneyManagementRequest.cs +++ b/src/Managing.Domain/Backtests/MoneyManagementRequest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using Managing.Common; -namespace Managing.Api.Models.Requests; +namespace Managing.Domain.Backtests; public class MoneyManagementRequest { diff --git a/src/Managing.Api/Models/Requests/RunBacktestRequest.cs b/src/Managing.Domain/Backtests/RunBacktestRequest.cs similarity index 79% rename from src/Managing.Api/Models/Requests/RunBacktestRequest.cs rename to src/Managing.Domain/Backtests/RunBacktestRequest.cs index 80892d5..7bd130c 100644 --- a/src/Managing.Api/Models/Requests/RunBacktestRequest.cs +++ b/src/Managing.Domain/Backtests/RunBacktestRequest.cs @@ -1,6 +1,7 @@ -using Managing.Domain.MoneyManagements; +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; -namespace Managing.Api.Models.Requests; +namespace Managing.Domain.Backtests; /// /// Request model for running a backtest @@ -37,6 +38,12 @@ public class RunBacktestRequest /// public bool Save { get; set; } = false; + /// + /// Whether to include candles and indicators values in the response. + /// Set to false to reduce response size dramatically. + /// + public bool WithCandles { get; set; } = false; + /// /// The name of the money management to use (optional if MoneyManagement is provided) /// @@ -46,4 +53,4 @@ public class RunBacktestRequest /// The money management details (optional if MoneyManagementName is provided) /// public MoneyManagement? MoneyManagement { get; set; } -} +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/ScenarioRequest.cs b/src/Managing.Domain/Backtests/ScenarioRequest.cs similarity index 93% rename from src/Managing.Api/Models/Requests/ScenarioRequest.cs rename to src/Managing.Domain/Backtests/ScenarioRequest.cs index 40d3fc3..547d8fe 100644 --- a/src/Managing.Api/Models/Requests/ScenarioRequest.cs +++ b/src/Managing.Domain/Backtests/ScenarioRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Managing.Api.Models.Requests; +namespace Managing.Domain.Backtests; /// /// Request model for scenario configuration without user information diff --git a/src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs b/src/Managing.Domain/Bots/TradingBotConfigRequest.cs similarity index 98% rename from src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs rename to src/Managing.Domain/Bots/TradingBotConfigRequest.cs index 95e98f8..a75c1d5 100644 --- a/src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs +++ b/src/Managing.Domain/Bots/TradingBotConfigRequest.cs @@ -1,7 +1,8 @@ using System.ComponentModel.DataAnnotations; +using Managing.Domain.Backtests; using static Managing.Common.Enums; -namespace Managing.Api.Models.Requests; +namespace Managing.Domain.Bots; /// /// Simplified trading bot configuration request with only primary properties diff --git a/src/Managing.Infrastructure.Database/BacktestRepository.cs b/src/Managing.Infrastructure.Database/BacktestRepository.cs index 7a5f2c7..5c5ac2d 100644 --- a/src/Managing.Infrastructure.Database/BacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/BacktestRepository.cs @@ -314,7 +314,7 @@ public class BacktestRepository : IBacktestRepository public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) { bundleRequest.User = user; - var dto = MapToDto(bundleRequest); + var dto = MongoMappers.Map(bundleRequest); _bundleBacktestRepository.InsertOne(dto); } @@ -324,7 +324,7 @@ public class BacktestRepository : IBacktestRepository .Where(b => b.User.Name == user.Name) .ToList(); - return bundleRequests.Select(MapToDomain); + return bundleRequests.Select(MongoMappers.Map); } public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) @@ -333,7 +333,7 @@ public class BacktestRepository : IBacktestRepository if (bundleRequest != null && bundleRequest.User.Name == user.Name) { - return MapToDomain(bundleRequest); + return MongoMappers.Map(bundleRequest); } return null; @@ -341,7 +341,7 @@ public class BacktestRepository : IBacktestRepository public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest) { - var dto = MapToDto(bundleRequest); + var dto = MongoMappers.Map(bundleRequest); _bundleBacktestRepository.ReplaceOne(dto); } @@ -361,55 +361,6 @@ public class BacktestRepository : IBacktestRepository .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 - }; + return pendingRequests.Select(MongoMappers.Map); } } \ 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 index f9c7c2f..dc0e2d1 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/Collections/BundleBacktestRequestDto.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/BundleBacktestRequestDto.cs @@ -19,4 +19,5 @@ public class BundleBacktestRequestDto : Document public string? ProgressInfo { get; set; } public string? CurrentBacktest { get; set; } public int? EstimatedTimeRemainingSeconds { get; set; } -} \ No newline at end of file + public string Name { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index 9ad7eab..de3a818 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -1108,4 +1108,51 @@ public static class MongoMappers } #endregion + + #region BundleBacktestRequests + + public static BundleBacktestRequestDto Map(BundleBacktestRequest domain) + { + if (domain == null) return null; + return new BundleBacktestRequestDto + { + RequestId = domain.RequestId, + User = 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, + Name = domain.Name + }; + } + + public static BundleBacktestRequest Map(BundleBacktestRequestDto dto) + { + if (dto == null) return null; + return new BundleBacktestRequest + { + RequestId = dto.RequestId, + User = 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, + Name = dto.Name + }; + } + + #endregion } \ No newline at end of file diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 66afb40..f685c1b 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -716,6 +716,163 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + backtest_RunBundle(name: string | null | undefined, requests: RunBacktestRequest[]): Promise { + let url_ = this.baseUrl + "/Backtest/Bundle?"; + if (name !== undefined && name !== null) + url_ += "name=" + encodeURIComponent("" + name) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(requests); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_RunBundle(_response); + }); + } + + protected processBacktest_RunBundle(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequest; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + backtest_GetBundleBacktestRequests(): Promise { + let url_ = this.baseUrl + "/Backtest/Bundle"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_GetBundleBacktestRequests(_response); + }); + } + + protected processBacktest_GetBundleBacktestRequests(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequest[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + backtest_GetBundleBacktestRequest(id: string): Promise { + let url_ = this.baseUrl + "/Backtest/Bundle/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_GetBundleBacktestRequest(_response); + }); + } + + protected processBacktest_GetBundleBacktestRequest(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequest; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + backtest_DeleteBundleBacktestRequest(id: string): Promise { + let url_ = this.baseUrl + "/Backtest/Bundle/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "DELETE", + headers: { + "Accept": "application/octet-stream" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_DeleteBundleBacktestRequest(_response); + }); + } + + protected processBacktest_DeleteBundleBacktestRequest(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + backtest_RunGenetic(request: RunGeneticRequest): Promise { let url_ = this.baseUrl + "/Backtest/Genetic"; url_ = url_.replace(/[?&]$/, ""); @@ -3880,6 +4037,33 @@ export interface MoneyManagementRequest { leverage: number; } +export interface BundleBacktestRequest { + requestId: string; + user: User; + createdAt: Date; + completedAt?: Date | null; + status: BundleBacktestRequestStatus; + name: string; + backtestRequestsJson: string; + results?: Backtest[] | null; + totalBacktests: number; + completedBacktests: number; + failedBacktests: number; + progressPercentage?: number; + errorMessage?: string | null; + progressInfo?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; +} + +export enum BundleBacktestRequestStatus { + Pending = "Pending", + Running = "Running", + Completed = "Completed", + Failed = "Failed", + Cancelled = "Cancelled", +} + export interface GeneticRequest { requestId: string; user: User; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index c0c772c..e01647a 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -686,6 +686,33 @@ export interface MoneyManagementRequest { leverage: number; } +export interface BundleBacktestRequest { + requestId: string; + user: User; + createdAt: Date; + completedAt?: Date | null; + status: BundleBacktestRequestStatus; + name: string; + backtestRequestsJson: string; + results?: Backtest[] | null; + totalBacktests: number; + completedBacktests: number; + failedBacktests: number; + progressPercentage?: number; + errorMessage?: string | null; + progressInfo?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; +} + +export enum BundleBacktestRequestStatus { + Pending = "Pending", + Running = "Running", + Completed = "Completed", + Failed = "Failed", + Cancelled = "Cancelled", +} + export interface GeneticRequest { requestId: string; user: User;