Files
managing-apps/src/Managing.Application/Backtests/Backtester.cs
2025-11-10 11:50:20 +07:00

602 lines
26 KiB
C#

using System.Text.Json;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions.Shared;
using Managing.Application.Hubs;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using LightBacktestResponse = Managing.Domain.Backtests.LightBacktest; // Use the domain model for notification
namespace Managing.Application.Backtests
{
public class Backtester : IBacktester
{
private readonly IBacktestRepository _backtestRepository;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
private readonly IMessengerService _messengerService;
private readonly IKaigenService _kaigenService;
private readonly IHubContext<BacktestHub> _hubContext;
private readonly JobService _jobService;
public Backtester(
IExchangeService exchangeService,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger,
IScenarioService scenarioService,
IAccountService accountService,
IMessengerService messengerService,
IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext,
IServiceScopeFactory serviceScopeFactory,
JobService jobService)
{
_exchangeService = exchangeService;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
_accountService = accountService;
_messengerService = messengerService;
_kaigenService = kaigenService;
_hubContext = hubContext;
_serviceScopeFactory = serviceScopeFactory;
_jobService = jobService;
}
/// <summary>
/// Creates a backtest job and returns immediately (fire-and-forget pattern).
/// The job will be processed by compute workers.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="startDate">The start date for the backtest</param>
/// <param name="endDate">The end date for the backtest</param>
/// <param name="user">The user running the backtest (required)</param>
/// <param name="save">Whether to save the backtest results</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response (ignored, always false for jobs)</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>A lightweight backtest response with job ID (result will be available later via GetJobStatus)</returns>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool save = false,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User is required for job-based backtests");
}
// Create a job instead of running synchronously
var job = await _jobService.CreateJobAsync(
config,
startDate,
endDate,
user,
priority: 0,
requestId: requestId);
_logger.LogInformation(
"Created backtest job {JobId} for user {UserId}. Job will be processed by compute workers.",
job.Id, user.Id);
// Return a placeholder response with job ID
// The actual result will be available via GetJobStatus endpoint
return new LightBacktestResponse
{
Id = job.Id.ToString(),
Config = config,
Score = 0, // Placeholder, actual score will be available when job completes
ScoreMessage = $"Job {job.Id} is queued for processing"
};
}
/// <summary>
/// Runs a trading bot backtest with pre-loaded candles.
/// Automatically handles different bot types based on config.BotType.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional)</param>
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config,
HashSet<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
// This overload is deprecated - use the date range overload which creates a job
// For backward compatibility, create a job with the provided candles date range
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User is required");
}
var startDate = candles.Min(c => c.Date);
var endDate = candles.Max(c => c.Date);
return await RunTradingBotBacktest(config, startDate, endDate, user, false, withCandles, requestId, metadata);
}
public async Task<bool> DeleteBacktestAsync(string id)
{
try
{
await _backtestRepository.DeleteBacktestByIdForUserAsync(null, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteBacktests()
{
try
{
_backtestRepository.DeleteAllBacktestsForUser(null);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public IEnumerable<Backtest> GetBacktestsByUser(User user)
{
var backtests = _backtestRepository.GetBacktestsByUser(user).ToList();
return backtests;
}
public async Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user)
{
var backtests = await _backtestRepository.GetBacktestsByUserAsync(user);
return backtests;
}
public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId)
{
var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList();
return backtests;
}
public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId)
{
var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId);
return backtests;
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId,
int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
_backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder);
return (backtests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(
Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
await _backtestRepository.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy,
sortOrder);
return (backtests, totalCount);
}
public async Task<Backtest> GetBacktestByIdForUserAsync(User user, string id)
{
var backtest = await _backtestRepository.GetBacktestByIdForUserAsync(user, id);
if (backtest == null)
return null;
if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10)
{
try
{
var account = new Account
{ Name = backtest.Config.AccountName, Exchange = TradingExchanges.Evm };
var candles = await _exchangeService.GetCandlesInflux(
account.Exchange,
backtest.Config.Ticker,
backtest.StartDate,
backtest.Config.Timeframe,
backtest.EndDate);
if (candles != null && candles.Count > 0)
{
backtest.Candles = candles;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", id);
}
}
return backtest;
}
public async Task<bool> DeleteBacktestByUserAsync(User user, string id)
{
try
{
await _backtestRepository.DeleteBacktestByIdForUserAsync(user, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids)
{
try
{
await _backtestRepository.DeleteBacktestsByIdsForUserAsync(user, ids);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete backtests for user {UserName}", user.Name);
return false;
}
}
public bool DeleteBacktestsByUser(User user)
{
try
{
_backtestRepository.DeleteAllBacktestsForUser(user);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId)
{
try
{
await _backtestRepository.DeleteBacktestsByRequestIdAsync(requestId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete backtests for request ID {RequestId}", requestId);
return false;
}
}
public async Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter)
{
try
{
return await _backtestRepository.DeleteBacktestsByFiltersAsync(user, filter);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete backtests by filters for user {UserId}", user.Id);
throw;
}
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(
User user,
int page,
int pageSize,
BacktestSortableColumn sortBy,
string sortOrder = "desc",
BacktestsFilter? filter = null)
{
var (backtests, totalCount) =
_backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder, filter);
return (backtests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(
User user,
int page,
int pageSize,
BacktestSortableColumn sortBy,
string sortOrder = "desc",
BacktestsFilter? filter = null)
{
var (backtests, totalCount) =
await _backtestRepository.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder,
filter);
return (backtests, totalCount);
}
public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest,
bool saveAsTemplate = false)
{
await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest);
if (!saveAsTemplate)
{
// Generate backtest requests from variants (same logic as BundleBacktestGrain)
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
if (backtestRequests != null && backtestRequests.Any())
{
// Create jobs for all variants
await _jobService.CreateBundleJobsAsync(bundleRequest, backtestRequests);
_logger.LogInformation(
"Created {JobCount} backtest jobs for bundle request {BundleRequestId}",
backtestRequests.Count, bundleRequest.RequestId);
}
}
}
/// <summary>
/// Saves a bundle backtest request without creating jobs.
/// Use this when you want to return immediately and create jobs in a background task.
/// </summary>
public async Task SaveBundleBacktestRequestAsync(User user, BundleBacktestRequest bundleRequest)
{
await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest);
}
/// <summary>
/// Creates jobs for an existing bundle request asynchronously.
/// This method is intended to be called from a background task after the bundle request has been saved.
/// </summary>
public async Task CreateJobsForBundleRequestAsync(BundleBacktestRequest bundleRequest)
{
try
{
// Generate backtest requests from variants
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
if (backtestRequests != null && backtestRequests.Any())
{
// Create jobs for all variants
await _jobService.CreateBundleJobsAsync(bundleRequest, backtestRequests);
_logger.LogInformation(
"Created {JobCount} backtest jobs for bundle request {BundleRequestId}",
backtestRequests.Count, bundleRequest.RequestId);
}
else
{
_logger.LogWarning(
"No backtest requests generated for bundle request {BundleRequestId}",
bundleRequest.RequestId);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating jobs for bundle request {BundleRequestId}",
bundleRequest.RequestId);
throw;
}
}
/// <summary>
/// Generates individual backtest requests from variant configuration
/// </summary>
private async Task<List<RunBacktestRequest>> GenerateBacktestRequestsFromVariants(
BundleBacktestRequest bundleRequest)
{
try
{
// Deserialize the variant configurations
var universalConfig =
JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
var moneyManagementVariants =
JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null ||
tickerVariants == null)
{
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}",
bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
// Get the first account for the user
var accounts = await _accountService.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
var firstAccount = accounts.FirstOrDefault();
if (firstAccount == null)
{
_logger.LogError("No accounts found for user {UserId} in bundle request {RequestId}",
bundleRequest.User.Id, bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
var backtestRequests = new List<RunBacktestRequest>();
foreach (var dateRange in dateTimeRanges)
{
foreach (var mmVariant in moneyManagementVariants)
{
foreach (var ticker in tickerVariants)
{
var config = new TradingBotConfigRequest
{
AccountName = firstAccount.Name,
Ticker = ticker,
Timeframe = universalConfig.Timeframe,
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
BotTradingBalance = universalConfig.BotTradingBalance,
Name =
$"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
FlipPosition = universalConfig.FlipPosition,
CooldownPeriod = universalConfig.CooldownPeriod,
MaxLossStreak = universalConfig.MaxLossStreak,
Scenario = universalConfig.Scenario,
ScenarioName = universalConfig.ScenarioName,
MoneyManagement = mmVariant.MoneyManagement,
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
};
var backtestRequest = new RunBacktestRequest
{
Config = config,
StartDate = dateRange.StartDate,
EndDate = dateRange.EndDate,
Balance = universalConfig.BotTradingBalance,
WatchOnly = universalConfig.WatchOnly,
Save = universalConfig.Save,
WithCandles = false,
MoneyManagement = mmVariant.MoneyManagement != null
? new MoneyManagement
{
Name = mmVariant.MoneyManagement.Name,
Timeframe = mmVariant.MoneyManagement.Timeframe,
StopLoss = mmVariant.MoneyManagement.StopLoss,
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
Leverage = mmVariant.MoneyManagement.Leverage
}
: null
};
backtestRequests.Add(backtestRequest);
}
}
}
return backtestRequests;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}",
bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
{
return _backtestRepository.GetBundleBacktestRequestsByUser(user);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user)
{
return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user);
}
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id)
{
return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id);
}
public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id)
{
return await _backtestRepository.GetBundleBacktestRequestByIdForUserAsync(user, id);
}
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest)
{
_backtestRepository.UpdateBundleBacktestRequest(bundleRequest);
}
public async Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest)
{
await _backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest);
}
public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id)
{
_backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id);
}
public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id)
{
await _backtestRepository.DeleteBundleBacktestRequestByIdForUserAsync(user, id);
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status)
{
// Use the repository method to get all bundles, then filter by status
return _backtestRepository.GetBundleBacktestRequestsByStatus(status);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(
BundleBacktestRequestStatus status)
{
return await _backtestRepository.GetBundleBacktestRequestsByStatusAsync(status);
}
public (IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount) GetBundleBacktestRequestsPaginated(
int page,
int pageSize,
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
string sortOrder = "desc",
BundleBacktestRequestsFilter? filter = null)
{
var (bundleRequests, totalCount) =
_backtestRepository.GetBundleBacktestRequestsPaginated(page, pageSize, sortBy, sortOrder, filter);
return (bundleRequests, totalCount);
}
public async Task<(IEnumerable<BundleBacktestRequest> BundleRequests, int TotalCount)> GetBundleBacktestRequestsPaginatedAsync(
int page,
int pageSize,
BundleBacktestRequestSortableColumn sortBy = BundleBacktestRequestSortableColumn.CreatedAt,
string sortOrder = "desc",
BundleBacktestRequestsFilter? filter = null)
{
var (bundleRequests, totalCount) =
await _backtestRepository.GetBundleBacktestRequestsPaginatedAsync(page, pageSize, sortBy, sortOrder, filter);
return (bundleRequests, totalCount);
}
public async Task<BundleBacktestRequestSummary> GetBundleBacktestRequestsSummaryAsync()
{
return await _backtestRepository.GetBundleBacktestRequestsSummaryAsync();
}
/// <summary>
/// Sends a LightBacktestResponse to all SignalR subscribers of a bundle request.
/// </summary>
public async Task SendBundleBacktestUpdateAsync(string requestId, LightBacktestResponse response)
{
if (string.IsNullOrWhiteSpace(requestId) || response == null) return;
await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response);
}
}
}