Setup bundle for backtest

This commit is contained in:
2025-07-21 00:01:13 +07:00
parent 4c07d7323f
commit 0870edee61
8 changed files with 543 additions and 72 deletions

View File

@@ -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;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
@@ -31,6 +33,7 @@ public class BacktestController : BaseController
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IGeneticService _geneticService; private readonly IGeneticService _geneticService;
private readonly IBacktestRepository _backtestRepository;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BacktestController"/> class. /// Initializes a new instance of the <see cref="BacktestController"/> class.
@@ -41,6 +44,7 @@ public class BacktestController : BaseController
/// <param name="accountService">The service for account management.</param> /// <param name="accountService">The service for account management.</param>
/// <param name="moneyManagementService">The service for money management strategies.</param> /// <param name="moneyManagementService">The service for money management strategies.</param>
/// <param name="geneticService">The service for genetic algorithm operations.</param> /// <param name="geneticService">The service for genetic algorithm operations.</param>
/// <param name="backtestRepository">The repository for backtest operations.</param>
public BacktestController( public BacktestController(
IHubContext<BotHub> hubContext, IHubContext<BotHub> hubContext,
IBacktester backtester, IBacktester backtester,
@@ -48,6 +52,7 @@ public class BacktestController : BaseController
IAccountService accountService, IAccountService accountService,
IMoneyManagementService moneyManagementService, IMoneyManagementService moneyManagementService,
IGeneticService geneticService, IGeneticService geneticService,
IBacktestRepository backtestRepository,
IUserService userService) : base(userService) IUserService userService) : base(userService)
{ {
_hubContext = hubContext; _hubContext = hubContext;
@@ -56,6 +61,7 @@ public class BacktestController : BaseController
_accountService = accountService; _accountService = accountService;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_geneticService = geneticService; _geneticService = geneticService;
_backtestRepository = backtestRepository;
} }
/// <summary> /// <summary>
@@ -400,14 +406,14 @@ public class BacktestController : BaseController
} }
/// <summary> /// <summary>
/// Runs multiple backtests in a bundle with the specified configurations. /// Creates a bundle backtest request with the specified configurations.
/// This endpoint receives a list of backtest requests and will execute them all. /// This endpoint creates a request that will be processed by a background worker.
/// </summary> /// </summary>
/// <param name="requests">The list of backtest requests to execute.</param> /// <param name="requests">The list of backtest requests to execute.</param>
/// <returns>A list of backtest results.</returns> /// <returns>The bundle backtest request with ID for tracking progress.</returns>
[HttpPost] [HttpPost]
[Route("Bundle")] [Route("Bundle")]
public async Task<ActionResult<IEnumerable<object>>> RunBundle([FromBody] List<RunBacktestRequest> requests) public async Task<ActionResult<BundleBacktestRequest>> RunBundle([FromBody] List<RunBacktestRequest> requests)
{ {
if (requests == null || !requests.Any()) if (requests == null || !requests.Any())
{ {
@@ -419,92 +425,110 @@ public class BacktestController : BaseController
return BadRequest("Maximum of 10 backtests allowed per bundle request"); return BadRequest("Maximum of 10 backtests allowed per bundle request");
} }
var user = await GetUser(); try
var results = new List<object>();
foreach (var request in requests)
{ {
try var user = await GetUser();
// Validate all requests before creating the bundle
foreach (var request in requests)
{ {
// Validate individual request
if (request?.Config == null) if (request?.Config == null)
{ {
results.Add(new return BadRequest("Invalid request: Configuration is required");
{
Id = Guid.NewGuid().ToString(),
Status = "Error",
Message = "Invalid request: Configuration is required"
});
continue;
} }
if (string.IsNullOrEmpty(request.Config.AccountName)) if (string.IsNullOrEmpty(request.Config.AccountName))
{ {
results.Add(new return BadRequest("Invalid request: Account name is required");
{
Id = Guid.NewGuid().ToString(),
Status = "Error",
Message = "Invalid request: Account name is required"
});
continue;
} }
if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null) if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null)
{ {
results.Add(new return BadRequest("Invalid request: Either scenario name or scenario object is required");
{
Id = Guid.NewGuid().ToString(),
Status = "Error",
Message = "Invalid request: Either scenario name or scenario object is required"
});
continue;
} }
if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null)
{ {
results.Add(new return BadRequest("Invalid request: Either money management name or money management object is required");
{
Id = Guid.NewGuid().ToString(),
Status = "Error",
Message = "Invalid request: Either money management name or money management object is required"
});
continue;
} }
// 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 User = user,
{ BacktestRequestsJson = JsonSerializer.Serialize(requests),
Id = Guid.NewGuid().ToString(), TotalBacktests = requests.Count,
Status = "Error", CompletedBacktests = 0,
Message = $"Error processing backtest: {ex.Message}" 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}");
}
}
/// <summary>
/// Retrieves all bundle backtest requests for the authenticated user.
/// </summary>
/// <returns>A list of bundle backtest requests with their current status.</returns>
[HttpGet]
[Route("Bundle")]
public async Task<ActionResult<IEnumerable<BundleBacktestRequest>>> GetBundleBacktestRequests()
{
var user = await GetUser();
var bundleRequests = _backtestRepository.GetBundleBacktestRequestsByUser(user);
return Ok(bundleRequests);
}
/// <summary>
/// Retrieves a specific bundle backtest request by ID for the authenticated user.
/// </summary>
/// <param name="id">The ID of the bundle backtest request to retrieve.</param>
/// <returns>The requested bundle backtest request with current status and results.</returns>
[HttpGet]
[Route("Bundle/{id}")]
public async Task<ActionResult<BundleBacktestRequest>> 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);
}
/// <summary>
/// Deletes a specific bundle backtest request by ID for the authenticated user.
/// Also deletes all related backtests associated with this bundle request.
/// </summary>
/// <param name="id">The ID of the bundle backtest request to delete.</param>
/// <returns>An ActionResult indicating the outcome of the operation.</returns>
[HttpDelete]
[Route("Bundle/{id}")]
public async Task<ActionResult> 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
});
} }
/// <summary> /// <summary>
@@ -630,6 +654,8 @@ public class BacktestController : BaseController
}); });
} }
/// <summary> /// <summary>
/// Notifies subscribers about the backtesting results via SignalR. /// Notifies subscribers about the backtesting results via SignalR.
/// </summary> /// </summary>

View File

@@ -15,4 +15,12 @@ public interface IBacktestRepository
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids); void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
void DeleteAllBacktestsForUser(User user); void DeleteAllBacktestsForUser(User user);
void DeleteBacktestsByRequestId(string requestId); void DeleteBacktestsByRequestId(string requestId);
// Bundle backtest methods
void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user);
BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id);
void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest);
void DeleteBundleBacktestRequestByIdForUser(User user, string id);
IEnumerable<BundleBacktestRequest> GetPendingBundleBacktestRequests();
} }

View File

@@ -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;
/// <summary>
/// Worker for processing bundle backtest requests
/// </summary>
public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
{
private readonly IBacktestRepository _backtestRepository;
private readonly IBacktester _backtester;
private static readonly WorkerType _workerType = WorkerType.BundleBacktest;
public BundleBacktestWorker(
IBacktestRepository backtestRepository,
IBacktester backtester,
ILogger<BundleBacktestWorker> 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<List<JsonElement>>(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
}
}

View File

@@ -137,6 +137,11 @@ public static class WorkersBootstrap
services.AddHostedService<GeneticAlgorithmWorker>(); services.AddHostedService<GeneticAlgorithmWorker>();
} }
if (configuration.GetValue<bool>("WorkerBundleBacktest", false))
{
services.AddHostedService<BundleBacktestWorker>();
}
return services; return services;
} }

View File

@@ -383,7 +383,8 @@ public static class Enums
BotManager, BotManager,
FundingRatesWatcher, FundingRatesWatcher,
BalanceTracking, BalanceTracking,
GeneticAlgorithm GeneticAlgorithm,
BundleBacktest
} }
public enum WorkflowUsage public enum WorkflowUsage

View File

@@ -0,0 +1,146 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Users;
namespace Managing.Domain.Backtests;
/// <summary>
/// Domain model for bundle backtest requests
/// </summary>
public class BundleBacktestRequest
{
public BundleBacktestRequest()
{
RequestId = Guid.NewGuid().ToString();
CreatedAt = DateTime.UtcNow;
Status = BundleBacktestRequestStatus.Pending;
Results = new List<Backtest>();
BacktestRequestsJson = string.Empty;
}
/// <summary>
/// Constructor that allows setting a specific ID
/// </summary>
/// <param name="requestId">The specific ID to use</param>
public BundleBacktestRequest(string requestId)
{
RequestId = requestId;
CreatedAt = DateTime.UtcNow;
Status = BundleBacktestRequestStatus.Pending;
Results = new List<Backtest>();
BacktestRequestsJson = string.Empty;
}
/// <summary>
/// Unique identifier for the bundle backtest request
/// </summary>
[Required]
public string RequestId { get; set; }
/// <summary>
/// The user who created this request
/// </summary>
[Required]
public User User { get; set; }
/// <summary>
/// When the request was created
/// </summary>
[Required]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the request was completed (if completed)
/// </summary>
public DateTime? CompletedAt { get; set; }
/// <summary>
/// Current status of the bundle backtest request
/// </summary>
[Required]
public BundleBacktestRequestStatus Status { get; set; }
/// <summary>
/// The list of backtest requests to execute (serialized as JSON)
/// </summary>
[Required]
public string BacktestRequestsJson { get; set; } = string.Empty;
/// <summary>
/// The results of the bundle backtest execution
/// </summary>
public List<Backtest> Results { get; set; } = new();
/// <summary>
/// Total number of backtests in the bundle
/// </summary>
[Required]
public int TotalBacktests { get; set; }
/// <summary>
/// Number of backtests completed so far
/// </summary>
[Required]
public int CompletedBacktests { get; set; }
/// <summary>
/// Number of backtests that failed
/// </summary>
[Required]
public int FailedBacktests { get; set; }
/// <summary>
/// Progress percentage (0-100)
/// </summary>
public double ProgressPercentage => TotalBacktests > 0 ? (double)CompletedBacktests / TotalBacktests * 100 : 0;
/// <summary>
/// Error message if the request failed
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// Progress information (JSON serialized)
/// </summary>
public string? ProgressInfo { get; set; }
/// <summary>
/// Current backtest being processed
/// </summary>
public string? CurrentBacktest { get; set; }
/// <summary>
/// Estimated time remaining in seconds
/// </summary>
public int? EstimatedTimeRemainingSeconds { get; set; }
}
/// <summary>
/// Status of a bundle backtest request
/// </summary>
public enum BundleBacktestRequestStatus
{
/// <summary>
/// Request is pending execution
/// </summary>
Pending,
/// <summary>
/// Request is currently being processed
/// </summary>
Running,
/// <summary>
/// Request completed successfully
/// </summary>
Completed,
/// <summary>
/// Request failed with an error
/// </summary>
Failed,
/// <summary>
/// Request was cancelled
/// </summary>
Cancelled
}

View File

@@ -12,10 +12,14 @@ namespace Managing.Infrastructure.Databases;
public class BacktestRepository : IBacktestRepository public class BacktestRepository : IBacktestRepository
{ {
private readonly IMongoRepository<BacktestDto> _backtestRepository; private readonly IMongoRepository<BacktestDto> _backtestRepository;
private readonly IMongoRepository<BundleBacktestRequestDto> _bundleBacktestRepository;
public BacktestRepository(IMongoRepository<BacktestDto> backtestRepository) public BacktestRepository(
IMongoRepository<BacktestDto> backtestRepository,
IMongoRepository<BundleBacktestRequestDto> bundleBacktestRepository)
{ {
_backtestRepository = backtestRepository; _backtestRepository = backtestRepository;
_bundleBacktestRepository = bundleBacktestRepository;
} }
// User-specific operations // User-specific operations
@@ -305,4 +309,107 @@ public class BacktestRepository : IBacktestRepository
return (mappedBacktests, (int)totalCount); 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<BundleBacktestRequest> 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<BundleBacktestRequest> GetPendingBundleBacktestRequests()
{
var pendingRequests = _bundleBacktestRepository.AsQueryable()
.Where(b => b.Status == BundleBacktestRequestStatus.Pending)
.ToList();
return pendingRequests.Select(MapToDomain);
}
/// <summary>
/// Maps a domain model to DTO
/// </summary>
/// <param name="domain">The domain model</param>
/// <returns>The DTO</returns>
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
};
}
/// <summary>
/// Maps a DTO to domain model
/// </summary>
/// <param name="dto">The DTO</param>
/// <returns>The domain model</returns>
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
};
}
} }

View File

@@ -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; }
}