using System.Text.Json; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Orleans; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Orleans.Concurrency; namespace Managing.Application.Grains; /// /// Stateless worker grain for processing bundle backtest requests /// Uses the bundle request ID as the primary key (Guid) /// Implements IRemindable for automatic retry of failed bundles /// Uses custom compute placement with random fallback. /// [StatelessWorker] [TradingPlacement] // Use custom compute placement with random fallback public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable { private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; // Reminder configuration private const string RETRY_REMINDER_NAME = "BundleBacktestRetry"; private static readonly TimeSpan RETRY_INTERVAL = TimeSpan.FromMinutes(30); public BundleBacktestGrain( ILogger logger, IServiceScopeFactory scopeFactory) { _logger = logger; _scopeFactory = scopeFactory; } public async Task ProcessBundleRequestAsync() { // Get the RequestId from the grain's primary key var bundleRequestId = this.GetPrimaryKey(); try { // Create a new service scope to get fresh instances of services with scoped DbContext using var scope = _scopeFactory.CreateScope(); var backtester = scope.ServiceProvider.GetRequiredService(); var messengerService = scope.ServiceProvider.GetRequiredService(); // Get the specific bundle request by ID var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId); if (bundleRequest == null) { _logger.LogError("Bundle request {RequestId} not found", bundleRequestId); return; } // Process only this specific bundle request await ProcessBundleRequest(bundleRequest, backtester, messengerService); } catch (Exception ex) { _logger.LogError(ex, "Error in BundleBacktestGrain for request {RequestId}", bundleRequestId); throw; } } private async Task GetBundleRequestById(IBacktester backtester, Guid bundleRequestId) { try { // Get pending and failed bundle backtest requests for retry capability var pendingRequests = await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending); var failedRequests = await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Failed); var allRequests = pendingRequests.Concat(failedRequests); return allRequests.FirstOrDefault(r => r.RequestId == bundleRequestId); } catch (Exception ex) { _logger.LogError(ex, "Failed to get bundle request {RequestId}", bundleRequestId); return null; } } private async Task ProcessBundleRequest( BundleBacktestRequest bundleRequest, IBacktester backtester, IMessengerService messengerService) { try { _logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId); // Update status to running bundleRequest.Status = BundleBacktestRequestStatus.Running; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); // Deserialize the backtest requests as strongly-typed objects var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); if (backtestRequests == null) { throw new InvalidOperationException("Failed to deserialize backtest requests"); } // Process each backtest request sequentially for (int i = 0; i < backtestRequests.Count; i++) { await ProcessSingleBacktest(backtester, backtestRequests[i], bundleRequest, i); } // Update final status and send notifications await UpdateFinalStatus(bundleRequest, backtester, messengerService); _logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}", bundleRequest.RequestId, bundleRequest.Status); } catch (Exception ex) { _logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId); await HandleBundleRequestError(bundleRequest, backtester, ex); } } private async Task ProcessSingleBacktest( IBacktester backtester, RunBacktestRequest runBacktestRequest, BundleBacktestRequest bundleRequest, int index) { try { // Get total count from deserialized requests instead of string splitting var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); var totalCount = backtestRequests?.Count ?? 0; // Update current backtest being processed bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}"; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); bundleRequest.User.Accounts = await ServiceScopeHelpers.WithScopedService>( _scopeFactory, async service => { return (await service.GetAccountsByUserAsync(bundleRequest.User, true)).ToList(); }); // Run the backtest directly with the strongly-typed request var backtestId = await RunSingleBacktest(backtester, runBacktestRequest, bundleRequest, index); if (!string.IsNullOrEmpty(backtestId)) { bundleRequest.Results.Add(backtestId); } // Update progress bundleRequest.CompletedBacktests++; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); _logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}", index + 1, bundleRequest.RequestId); } catch (Exception ex) { _logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}", index + 1, bundleRequest.RequestId); bundleRequest.FailedBacktests++; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); } } private async Task RunSingleBacktest( IBacktester backtester, RunBacktestRequest runBacktestRequest, BundleBacktestRequest bundleRequest, int index) { if (runBacktestRequest?.Config == null) { _logger.LogError("Invalid RunBacktestRequest in bundle (null config)"); return string.Empty; } // Map MoneyManagement MoneyManagement moneyManagement = null; if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName)) { _logger.LogWarning("MoneyManagementName provided but cannot resolve in grain context: {Name}", runBacktestRequest.Config.MoneyManagementName); } else if (runBacktestRequest.Config.MoneyManagement != null) { var mmReq = runBacktestRequest.Config.MoneyManagement; moneyManagement = new MoneyManagement { Name = mmReq.Name, Timeframe = mmReq.Timeframe, StopLoss = mmReq.StopLoss, TakeProfit = mmReq.TakeProfit, Leverage = mmReq.Leverage }; moneyManagement.FormatPercentage(); } // Map Scenario LightScenario scenario = null; if (runBacktestRequest.Config.Scenario != null) { var sReq = runBacktestRequest.Config.Scenario; scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod) { Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type) { SignalType = i.SignalType, MinimumHistory = i.MinimumHistory, Period = i.Period, FastPeriods = i.FastPeriods, SlowPeriods = i.SlowPeriods, SignalPeriods = i.SignalPeriods, Multiplier = i.Multiplier, SmoothPeriods = i.SmoothPeriods, StochPeriods = i.StochPeriods, CyclePeriods = i.CyclePeriods }).ToList() ?? new List() }; } // Map TradingBotConfig var backtestConfig = new TradingBotConfig { AccountName = runBacktestRequest.Config.AccountName, MoneyManagement = moneyManagement, Ticker = runBacktestRequest.Config.Ticker, ScenarioName = runBacktestRequest.Config.ScenarioName, Scenario = scenario, Timeframe = runBacktestRequest.Config.Timeframe, IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly, BotTradingBalance = runBacktestRequest.Config.BotTradingBalance, IsForBacktest = true, CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1, MaxLossStreak = runBacktestRequest.Config.MaxLossStreak, MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit, FlipPosition = runBacktestRequest.Config.FlipPosition, Name = $"{bundleRequest.Name} #{index + 1}", CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable, UseSynthApi = runBacktestRequest.Config.UseSynthApi, UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing, UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering, UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss }; // Run the backtest var result = await backtester.RunTradingBotBacktest( backtestConfig, runBacktestRequest.StartDate, runBacktestRequest.EndDate, bundleRequest.User, true, runBacktestRequest.WithCandles, bundleRequest.RequestId.ToString() ); _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId); return result.Id; } private async Task UpdateFinalStatus( BundleBacktestRequest bundleRequest, IBacktester backtester, IMessengerService messengerService) { if (bundleRequest.FailedBacktests == 0) { bundleRequest.Status = BundleBacktestRequestStatus.Completed; await NotifyUser(bundleRequest, messengerService); } else if (bundleRequest.CompletedBacktests == 0) { bundleRequest.Status = BundleBacktestRequestStatus.Failed; bundleRequest.ErrorMessage = "All backtests failed"; } else { bundleRequest.Status = BundleBacktestRequestStatus.Completed; bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed"; await NotifyUser(bundleRequest, messengerService); } bundleRequest.CompletedAt = DateTime.UtcNow; bundleRequest.CurrentBacktest = null; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); // Unregister retry reminder since bundle completed await UnregisterRetryReminder(); } private async Task HandleBundleRequestError( BundleBacktestRequest bundleRequest, IBacktester backtester, Exception ex) { bundleRequest.Status = BundleBacktestRequestStatus.Failed; bundleRequest.ErrorMessage = ex.Message; bundleRequest.CompletedAt = DateTime.UtcNow; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); // Register retry reminder for failed bundle await RegisterRetryReminder(); } private async Task NotifyUser(BundleBacktestRequest bundleRequest, IMessengerService messengerService) { if (bundleRequest.User?.TelegramChannel != null) { var message = bundleRequest.FailedBacktests == 0 ? $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully." : $"⚠️ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed with {bundleRequest.FailedBacktests} failed backtests."; await messengerService.SendMessage(message, bundleRequest.User.TelegramChannel); } } #region IRemindable Implementation /// /// Handles reminder callbacks for automatic retry of failed bundle backtests /// public async Task ReceiveReminder(string reminderName, TickStatus status) { if (reminderName != RETRY_REMINDER_NAME) { _logger.LogWarning("Unknown reminder {ReminderName} received", reminderName); return; } var bundleRequestId = this.GetPrimaryKey(); _logger.LogInformation("Retry reminder triggered for bundle request {RequestId}", bundleRequestId); try { using var scope = _scopeFactory.CreateScope(); var backtester = scope.ServiceProvider.GetRequiredService(); // Get the bundle request var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId); if (bundleRequest == null) { _logger.LogWarning("Bundle request {RequestId} not found during retry", bundleRequestId); await UnregisterRetryReminder(); return; } // Check if bundle is still failed if (bundleRequest.Status != BundleBacktestRequestStatus.Failed) { _logger.LogInformation( "Bundle request {RequestId} is no longer failed (status: {Status}), unregistering reminder", bundleRequestId, bundleRequest.Status); await UnregisterRetryReminder(); return; } // Retry the bundle processing _logger.LogInformation("Retrying failed bundle request {RequestId}", bundleRequestId); // Reset status to pending for retry bundleRequest.Status = BundleBacktestRequestStatus.Pending; bundleRequest.ErrorMessage = null; bundleRequest.CurrentBacktest = null; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); // Process the bundle again await ProcessBundleRequestAsync(); } catch (Exception ex) { _logger.LogError(ex, "Error during bundle backtest retry for request {RequestId}", bundleRequestId); } } /// /// Registers a retry reminder for this bundle request /// private async Task RegisterRetryReminder() { try { await this.RegisterOrUpdateReminder(RETRY_REMINDER_NAME, RETRY_INTERVAL, RETRY_INTERVAL); _logger.LogInformation("Registered retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to register retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); } } /// /// Unregisters the retry reminder for this bundle request /// private async Task UnregisterRetryReminder() { try { var reminder = await this.GetReminder(RETRY_REMINDER_NAME); if (reminder != null) { await this.UnregisterReminder(reminder); _logger.LogInformation("Unregistered retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); } } catch (Exception ex) { _logger.LogError(ex, "Failed to unregister retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); } } #endregion }