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 _logger; private readonly IExchangeService _exchangeService; private readonly IScenarioService _scenarioService; private readonly IAccountService _accountService; private readonly IMessengerService _messengerService; private readonly IKaigenService _kaigenService; private readonly IHubContext _hubContext; private readonly JobService _jobService; public Backtester( IExchangeService exchangeService, IBacktestRepository backtestRepository, ILogger logger, IScenarioService scenarioService, IAccountService accountService, IMessengerService messengerService, IKaigenService kaigenService, IHubContext 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; } /// /// Creates a backtest job and returns immediately (fire-and-forget pattern). /// The job will be processed by compute workers. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The start date for the backtest /// The end date for the backtest /// The user running the backtest (required) /// Whether to save the backtest results /// Whether to include candles and indicators values in the response (ignored, always false for jobs) /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) /// A lightweight backtest response with job ID (result will be available later via GetJobStatus) public async Task 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" }; } /// /// Runs a trading bot backtest with pre-loaded candles. /// Automatically handles different bot types based on config.BotType. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The candles to use for backtesting /// The user running the backtest (optional) /// Whether to include candles and indicators values in the response /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) /// The lightweight backtest results public async Task RunTradingBotBacktest( TradingBotConfig config, HashSet 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); } // Removed RunBacktestWithCandles - backtests now run via compute workers // This method is kept for backward compatibility but should not be called directly private async Task> GetCandles(Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, startDate, timeframe, endDate); if (candles == null || candles.Count == 0) throw new Exception( $"No candles for {ticker} on {timeframe} timeframe for start {startDate} to end {endDate}"); return candles; } // Removed CreateCleanConfigForOrleans - no longer needed with job queue approach private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) { try { if (backtest.Score > 60) { await _messengerService.SendBacktestNotification(backtest); } } catch (Exception ex) { _logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id); } } public async Task 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 GetBacktestsByUser(User user) { var backtests = _backtestRepository.GetBacktestsByUser(user).ToList(); return backtests; } public async Task> GetBacktestsByUserAsync(User user) { var backtests = await _backtestRepository.GetBacktestsByUserAsync(user); return backtests; } public IEnumerable GetBacktestsByRequestId(Guid requestId) { var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); return backtests; } public async Task> GetBacktestsByRequestIdAsync(Guid requestId) { var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId); return backtests; } public (IEnumerable 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 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 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 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 DeleteBacktestsByIdsForUserAsync(User user, IEnumerable 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 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 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 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 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); } } } /// /// Generates individual backtest requests from variant configuration /// private async Task> GenerateBacktestRequestsFromVariants( BundleBacktestRequest bundleRequest) { try { // Deserialize the variant configurations var universalConfig = JsonSerializer.Deserialize(bundleRequest.UniversalConfigJson); var dateTimeRanges = JsonSerializer.Deserialize>(bundleRequest.DateTimeRangesJson); var moneyManagementVariants = JsonSerializer.Deserialize>(bundleRequest.MoneyManagementVariantsJson); var tickerVariants = JsonSerializer.Deserialize>(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(); } // 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(); } var backtestRequests = new List(); 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(); } } public IEnumerable GetBundleBacktestRequestsByUser(User user) { return _backtestRepository.GetBundleBacktestRequestsByUser(user); } public async Task> GetBundleBacktestRequestsByUserAsync(User user) { return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user); } public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) { return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); } public async Task 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 GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status) { // Use the repository method to get all bundles, then filter by status return _backtestRepository.GetBundleBacktestRequestsByStatus(status); } public async Task> GetBundleBacktestRequestsByStatusAsync( BundleBacktestRequestStatus status) { return await _backtestRepository.GetBundleBacktestRequestsByStatusAsync(status); } /// /// Sends a LightBacktestResponse to all SignalR subscribers of a bundle request. /// public async Task SendBundleBacktestUpdateAsync(string requestId, LightBacktestResponse response) { if (string.IsNullOrWhiteSpace(requestId) || response == null) return; await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response); } // Removed TriggerBundleBacktestGrain methods - bundle backtests now use job queue } }