using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Scenarios; using Managing.Domain.Users; using Microsoft.AspNetCore.SignalR; 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 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 IGrainFactory _grainFactory; public Backtester( IExchangeService exchangeService, IBacktestRepository backtestRepository, ILogger logger, IScenarioService scenarioService, IAccountService accountService, IMessengerService messengerService, IKaigenService kaigenService, IHubContext hubContext, IGrainFactory grainFactory) { _exchangeService = exchangeService; _backtestRepository = backtestRepository; _logger = logger; _scenarioService = scenarioService; _accountService = accountService; _messengerService = messengerService; _kaigenService = kaigenService; _hubContext = hubContext; _grainFactory = grainFactory; } /// /// Runs a trading bot backtest with the specified configuration and date range. /// Automatically handles different bot types based on config.BotType. /// /// 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 (optional) /// Whether to save the backtest results /// 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, DateTime startDate, DateTime endDate, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null) { string creditRequestId = null; // Debit user credits before starting the backtest if (user != null) { try { creditRequestId = await _kaigenService.DebitUserCreditsAsync(user, 3); _logger.LogInformation( "Successfully debited credits for user {UserName} with request ID {RequestId}", user.Name, creditRequestId); } catch (Exception ex) { _logger.LogError(ex, "Failed to debit credits for user {UserName}. Backtest will not proceed.", user.Name); throw new Exception($"Failed to debit credits: {ex.Message}"); } } try { var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata); } catch (Exception ex) { // If backtest fails and we debited credits, attempt to refund if (user != null && !string.IsNullOrEmpty(creditRequestId)) { try { var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, user); if (refundSuccess) { _logger.LogError( "Successfully refunded credits for user {UserName} after backtest failure: {message}", user.Name, ex.Message); } else { _logger.LogError("Failed to refund credits for user {UserName} after backtest failure", user.Name); } } catch (Exception refundEx) { _logger.LogError(refundEx, "Error during refund attempt for user {UserName}", user.Name); } } throw; } } /// /// 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) { return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata); } /// /// Core backtesting logic - handles the actual backtest execution with pre-loaded candles /// private async Task RunBacktestWithCandles( TradingBotConfig config, HashSet candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null) { // Ensure this is a backtest configuration if (!config.IsForBacktest) { throw new InvalidOperationException("Backtest configuration must have IsForBacktest set to true"); } // Validate that scenario and indicators are properly loaded if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName)) { throw new InvalidOperationException( "Backtest configuration must include either Scenario object or ScenarioName"); } if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName)) { var fullScenario = await _scenarioService.GetScenarioByNameAndUserAsync(config.ScenarioName, user); config.Scenario = LightScenario.FromScenario(fullScenario); } // Create a clean copy of the config to avoid Orleans serialization issues var cleanConfig = CreateCleanConfigForOrleans(config); // Create Orleans grain for backtesting var backtestGrain = _grainFactory.GetGrain(Guid.NewGuid()); // Run the backtest using the Orleans grain and return LightBacktest directly return await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId, metadata); } private async Task GetAccountFromConfig(TradingBotConfig config) { return await _accountService.GetAccountByAccountName(config.AccountName, false, false); } private HashSet GetCandles(Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, startDate, timeframe, endDate).Result; if (candles == null || candles.Count == 0) throw new Exception($"No candles for {ticker} on {timeframe} timeframe"); return candles; } /// /// Creates a clean copy of the trading bot config for Orleans serialization /// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues /// private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig) { // Since we're now using LightScenario in TradingBotConfig, we can just return the original config // The conversion to LightScenario is already done when loading the scenario return originalConfig; } 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(string requestId) { var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); return backtests; } public async Task> GetBacktestsByRequestIdAsync(string requestId) { var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId); return backtests; } public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string 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( string 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(string 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 (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder); return (backtests, totalCount); } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = await _backtestRepository.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); return (backtests, totalCount); } // Bundle backtest methods public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) { _backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest); } 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, string id) { return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); } public async Task GetBundleBacktestRequestByIdForUserAsync(User user, string 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, string id) { _backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id); } public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string 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); } } }