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