using System.Text.Json; using Managing.Api.Models.Requests; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; 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; /// /// Controller for managing backtest operations. /// Provides endpoints for creating, retrieving, and deleting backtests. /// Returns complete backtest configurations for easy bot deployment. /// Requires authorization for access. /// [ApiController] [Authorize] [Route("[controller]")] [Produces("application/json")] public class BacktestController : BaseController { private readonly IHubContext _hubContext; private readonly IBacktester _backtester; private readonly IScenarioService _scenarioService; private readonly IAccountService _accountService; private readonly IMoneyManagementService _moneyManagementService; private readonly IGeneticService _geneticService; /// /// Initializes a new instance of the class. /// /// The SignalR hub context for real-time communication. /// The service for backtesting strategies. /// The service for managing scenarios. /// 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, IScenarioService scenarioService, IAccountService accountService, IMoneyManagementService moneyManagementService, IGeneticService geneticService, IUserService userService) : base(userService) { _hubContext = hubContext; _backtester = backtester; _scenarioService = scenarioService; _accountService = accountService; _moneyManagementService = moneyManagementService; _geneticService = geneticService; } /// /// Retrieves all backtests for the authenticated user. /// Each backtest includes the complete TradingBotConfig for easy bot deployment. /// /// A list of backtests with complete configurations. [HttpGet] public async Task>> Backtests() { var user = await GetUser(); var backtests = await _backtester.GetBacktestsByUserAsync(user); return Ok(backtests); } /// /// Retrieves a specific backtest by ID for the authenticated user. /// This endpoint will also populate the candles for visualization and includes /// the complete TradingBotConfig that can be used to start a new bot. /// /// The ID of the backtest to retrieve. /// The requested backtest with populated candle data and complete configuration. [HttpGet("{id}")] public async Task> Backtest(string id) { var user = await GetUser(); var backtest = await _backtester.GetBacktestByIdForUserAsync(user, id); if (backtest == null) { return NotFound($"Backtest with ID {id} not found or doesn't belong to the current user."); } return Ok(backtest); } /// /// Deletes a specific backtest by ID for the authenticated user. /// /// The ID of the backtest to delete. /// An ActionResult indicating the outcome of the operation. [HttpDelete] public async Task DeleteBacktest(string id) { var user = await GetUser(); var result = await _backtester.DeleteBacktestByUserAsync(user, id); return Ok(result); } /// /// Deletes multiple backtests by their IDs for the authenticated user. /// /// The request containing the array of backtest IDs to delete. /// An ActionResult indicating the outcome of the operation. [HttpDelete("multiple")] public async Task DeleteBacktests([FromBody] DeleteBacktestsRequest request) { var user = await GetUser(); return Ok(await _backtester.DeleteBacktestsByIdsForUserAsync(user, request.BacktestIds)); } /// /// Retrieves all backtests for a specific genetic request ID. /// This endpoint is used to view the results of a genetic algorithm optimization. /// /// The request ID to filter backtests by. /// A list of backtests associated with the specified request ID. [HttpGet] [Route("ByRequestId/{requestId}")] public async Task>> GetBacktestsByRequestId(string requestId) { if (string.IsNullOrEmpty(requestId)) { return BadRequest("Request ID is required"); } var backtests = await _backtester.GetBacktestsByRequestIdAsync(requestId); return Ok(backtests); } /// /// Retrieves paginated backtests for a specific genetic request ID. /// This endpoint is used to view the results of a genetic algorithm optimization with pagination support. /// /// The request ID to filter backtests by. /// Page number (defaults to 1) /// Number of items per page (defaults to 50, max 100) /// Field to sort by (defaults to "score") /// Sort order - "asc" or "desc" (defaults to "desc") /// A paginated list of backtests associated with the specified request ID. [HttpGet] [Route("ByRequestId/{requestId}/Paginated")] public async Task> GetBacktestsByRequestIdPaginated( string requestId, int page = 1, int pageSize = 50, string sortBy = "score", string sortOrder = "desc") { if (string.IsNullOrEmpty(requestId)) { return BadRequest("Request ID is required"); } if (page < 1) { return BadRequest("Page must be greater than 0"); } if (pageSize < 1 || pageSize > 100) { return BadRequest("Page size must be between 1 and 100"); } if (sortOrder != "asc" && sortOrder != "desc") { return BadRequest("Sort order must be 'asc' or 'desc'"); } var (backtests, totalCount) = await _backtester.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy, sortOrder); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedBacktestsResponse { Backtests = backtests.Select(b => new LightBacktestResponse { Id = b.Id, Config = b.Config, FinalPnl = b.FinalPnl, WinRate = b.WinRate, GrowthPercentage = b.GrowthPercentage, HodlPercentage = b.HodlPercentage, StartDate = b.StartDate, EndDate = b.EndDate, MaxDrawdown = b.MaxDrawdown, Fees = b.Fees, SharpeRatio = b.SharpeRatio, Score = b.Score, ScoreMessage = b.ScoreMessage }), TotalCount = totalCount, CurrentPage = page, PageSize = pageSize, TotalPages = totalPages, HasNextPage = page < totalPages, HasPreviousPage = page > 1 }; return Ok(response); } /// /// Retrieves paginated backtests for the authenticated user. /// /// Page number (defaults to 1) /// Number of items per page (defaults to 50, max 100) /// Field to sort by (defaults to "score") /// Sort order - "asc" or "desc" (defaults to "desc") /// A paginated list of backtests for the user. [HttpGet] [Route("Paginated")] public async Task> GetBacktestsPaginated( int page = 1, int pageSize = 50, string sortBy = "score", string sortOrder = "desc") { var user = await GetUser(); if (page < 1) { return BadRequest("Page must be greater than 0"); } if (pageSize < 1 || pageSize > 100) { return BadRequest("Page size must be between 1 and 100"); } if (sortOrder != "asc" && sortOrder != "desc") { return BadRequest("Sort order must be 'asc' or 'desc'"); } var (backtests, totalCount) = await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedBacktestsResponse { Backtests = backtests.Select(b => new LightBacktestResponse { Id = b.Id, Config = b.Config, FinalPnl = b.FinalPnl, WinRate = b.WinRate, GrowthPercentage = b.GrowthPercentage, HodlPercentage = b.HodlPercentage, StartDate = b.StartDate, EndDate = b.EndDate, MaxDrawdown = b.MaxDrawdown, Fees = b.Fees, SharpeRatio = b.SharpeRatio, Score = b.Score, ScoreMessage = b.ScoreMessage }), TotalCount = totalCount, CurrentPage = page, PageSize = pageSize, TotalPages = totalPages, HasNextPage = page < totalPages, HasPreviousPage = page > 1 }; return Ok(response); } /// /// Runs a backtest with the specified configuration. /// The returned backtest includes a complete TradingBotConfig that preserves all /// settings including nullable MaxPositionTimeHours for easy bot deployment. /// /// The backtest request containing configuration and parameters. /// The result of the backtest with complete configuration. [HttpPost] [Route("Run")] public async Task> Run([FromBody] RunBacktestRequest request) { if (request?.Config == null) { return BadRequest("Backtest configuration is required"); } if (string.IsNullOrEmpty(request.Config.AccountName)) { return BadRequest("Account name is required"); } if (string.IsNullOrEmpty(request.Config.ScenarioName) && request.Config.Scenario == null) { return BadRequest("Either scenario name or scenario object is required"); } if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { return BadRequest("Either money management name or money management object is required"); } try { Backtest backtestResult = null; var account = await _accountService.GetAccount(request.Config.AccountName, true, false); var user = await GetUser(); // Get money management MoneyManagement moneyManagement; if (!string.IsNullOrEmpty(request.Config.MoneyManagementName)) { moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName); if (moneyManagement == null) return BadRequest("Money management not found"); } else { moneyManagement = Map(request.Config.MoneyManagement); moneyManagement?.FormatPercentage(); } // Handle scenario - either from ScenarioRequest or ScenarioName Scenario scenario = null; if (request.Config.Scenario != null) { // Convert ScenarioRequest to Scenario domain object scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) { User = user }; // Convert IndicatorRequest objects to Indicator domain objects foreach (var indicatorRequest in request.Config.Scenario.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 = user }; scenario.AddIndicator(indicator); } } // Convert TradingBotConfigRequest to TradingBotConfig for backtest var backtestConfig = new TradingBotConfig { AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, ScenarioName = request.Config.ScenarioName, Scenario = scenario, // Use the converted scenario object Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, IsForBacktest = true, CooldownPeriod = request.Config.CooldownPeriod, MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, FlipPosition = request.Config.FlipPosition, // Computed based on BotType Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, UseSynthApi = request.Config.UseSynthApi, UseForPositionSizing = request.Config.UseForPositionSizing, UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss }; backtestResult = await _backtester.RunTradingBotBacktest( backtestConfig, request.StartDate, request.EndDate, user, request.Save, request.WithCandles, null); // No requestId for regular backtests await NotifyBacktesingSubscriberAsync(backtestResult); return Ok(backtestResult); } catch (Exception ex) { return StatusCode(500, $"Error running backtest: {ex.Message}"); } } /// /// 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. /// Display name for the bundle (required). /// The bundle backtest request with ID for tracking progress. [HttpPost] [Route("BacktestBundle")] public async Task> RunBundle([FromBody] RunBundleBacktestRequest request) { if (request?.Requests == null || !request.Requests.Any()) { return BadRequest("At least one backtest request is required"); } if (request.Requests.Count > 10) { return BadRequest("Maximum of 10 backtests allowed per bundle request"); } if (string.IsNullOrWhiteSpace(request.Name)) { return BadRequest("Bundle name is required"); } try { var user = await GetUser(); // Validate all requests before creating the bundle foreach (var req in request.Requests) { if (req?.Config == null) { return BadRequest("Invalid request: Configuration is required"); } if (string.IsNullOrEmpty(req.Config.AccountName)) { return BadRequest("Invalid request: Account name is required"); } if (string.IsNullOrEmpty(req.Config.ScenarioName) && req.Config.Scenario == null) { return BadRequest("Invalid request: Either scenario name or scenario object is required"); } if (string.IsNullOrEmpty(req.Config.MoneyManagementName) && req.Config.MoneyManagement == null) { return BadRequest( "Invalid request: Either money management name or money management object is required"); } } // Create the bundle backtest request var bundleRequest = new BundleBacktestRequest { User = user, BacktestRequestsJson = JsonSerializer.Serialize(request.Requests), TotalBacktests = request.Requests.Count, CompletedBacktests = 0, FailedBacktests = 0, Status = BundleBacktestRequestStatus.Pending, Name = request.Name }; _backtester.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 = _backtester.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 = _backtester.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(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 _backtester.DeleteBundleBacktestRequestByIdForUser(user, id); // Then, delete all related backtests var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(id); return Ok(new { BundleRequestDeleted = true, RelatedBacktestsDeleted = backtestsDeleted }); } /// /// Subscribes the client to real-time updates for a bundle backtest request via SignalR. /// The client will receive LightBacktestResponse objects as new backtests are generated. /// /// The bundle request ID to subscribe to. [HttpPost] [Route("Bundle/Subscribe")] // POST /Backtest/Bundle/Subscribe public async Task SubscribeToBundle([FromQuery] string requestId) { if (string.IsNullOrWhiteSpace(requestId)) return BadRequest("RequestId is required"); // Get the connection ID from the SignalR context (assume it's passed via header or query) var connectionId = HttpContext.Request.Headers["X-SignalR-ConnectionId"].ToString(); if (string.IsNullOrEmpty(connectionId)) return BadRequest("SignalR connection ID is required in X-SignalR-ConnectionId header"); // Add the connection to the SignalR group for this bundle await _hubContext.Groups.AddToGroupAsync(connectionId, $"bundle-{requestId}"); return Ok(new { Subscribed = true, RequestId = requestId }); } /// /// Unsubscribes the client from real-time updates for a bundle backtest request via SignalR. /// /// The bundle request ID to unsubscribe from. [HttpPost] [Route("Bundle/Unsubscribe")] // POST /Backtest/Bundle/Unsubscribe public async Task UnsubscribeFromBundle([FromQuery] string requestId) { if (string.IsNullOrWhiteSpace(requestId)) return BadRequest("RequestId is required"); var connectionId = HttpContext.Request.Headers["X-SignalR-ConnectionId"].ToString(); if (string.IsNullOrEmpty(connectionId)) return BadRequest("SignalR connection ID is required in X-SignalR-ConnectionId header"); await _hubContext.Groups.RemoveFromGroupAsync(connectionId, $"bundle-{requestId}"); return Ok(new { Unsubscribed = true, RequestId = requestId }); } /// /// Runs a genetic algorithm optimization with the specified configuration. /// This endpoint saves the genetic request to the database and returns the request ID. /// The actual genetic algorithm execution will be handled by a background service. /// /// The genetic algorithm request containing configuration and parameters. /// The genetic request with ID for tracking progress. [HttpPost] [Route("Genetic")] public async Task> RunGenetic([FromBody] RunGeneticRequest request) { if (request == null) { return BadRequest("Genetic request is required"); } if (request.EligibleIndicators == null || !request.EligibleIndicators.Any()) { return BadRequest("At least one eligible indicator is required"); } if (request.StartDate >= request.EndDate) { return BadRequest("Start date must be before end date"); } if (request.PopulationSize <= 0 || request.Generations <= 0) { return BadRequest("Population size and generations must be greater than 0"); } if (request.MutationRate < 0 || request.MutationRate > 1) { return BadRequest("Mutation rate must be between 0 and 1"); } try { var user = await GetUser(); // Create genetic request using the GeneticService directly var geneticRequest = _geneticService.CreateGeneticRequest( user, request.Ticker, request.Timeframe, request.StartDate, request.EndDate, request.Balance, request.PopulationSize, request.Generations, request.MutationRate, request.SelectionMethod, request.CrossoverMethod, request.MutationMethod, request.ElitismPercentage, request.MaxTakeProfit, request.EligibleIndicators); return Ok(geneticRequest); } catch (Exception ex) { return StatusCode(500, $"Error creating genetic request: {ex.Message}"); } } /// /// Retrieves all genetic requests for the authenticated user. /// /// A list of genetic requests with their current status. [HttpGet] [Route("Genetic")] public async Task>> GetGeneticRequests() { var user = await GetUser(); var geneticRequests = _geneticService.GetGeneticRequestsByUser(user); return Ok(geneticRequests); } /// /// Retrieves a specific genetic request by ID for the authenticated user. /// /// The ID of the genetic request to retrieve. /// The requested genetic request with current status and results. [HttpGet] [Route("Genetic/{id}")] public async Task> GetGeneticRequest(string id) { var user = await GetUser(); var geneticRequest = _geneticService.GetGeneticRequestByIdForUser(user, id); if (geneticRequest == null) { return NotFound($"Genetic request with ID {id} not found or doesn't belong to the current user."); } return Ok(geneticRequest); } /// /// Deletes a specific genetic request by ID for the authenticated user. /// Also deletes all related backtests associated with this genetic request. /// /// The ID of the genetic request to delete. /// An ActionResult indicating the outcome of the operation. [HttpDelete] [Route("Genetic/{id}")] 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 = await _backtester.DeleteBacktestsByRequestIdAsync(id); return Ok(new { GeneticRequestDeleted = true, RelatedBacktestsDeleted = backtestsDeleted }); } /// /// Notifies subscribers about the backtesting results via SignalR. /// /// The backtest result to notify subscribers about. private async Task NotifyBacktesingSubscriberAsync(Backtest backtesting) { if (backtesting != null) { await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting); } } public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest) { return new MoneyManagement { Name = moneyManagementRequest.Name, StopLoss = moneyManagementRequest.StopLoss, TakeProfit = moneyManagementRequest.TakeProfit, Leverage = moneyManagementRequest.Leverage, Timeframe = moneyManagementRequest.Timeframe }; } } /// /// Request model for running a backtest /// public class RunBacktestRequest { /// /// The trading bot configuration request to use for the backtest /// public TradingBotConfigRequest Config { get; set; } /// /// The start date for the backtest /// public DateTime StartDate { get; set; } /// /// The end date for the backtest /// public DateTime EndDate { get; set; } /// /// Whether to save the backtest results /// 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; }