using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Managing.Domain.Users; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.Bots.Grains; /// /// Orleans grain for live trading bot operations. /// Uses composition with TradingBotBase to maintain separation of concerns. /// This grain handles live trading scenarios with real-time market data and execution. /// public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { private readonly IPersistentState _state; private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private TradingBotBase? _tradingBot; private IDisposable? _timer; private string _reminderName = "RebootReminder"; public LiveTradingBotGrain( [PersistentState("live-trading-bot", "bot-store")] IPersistentState state, ILogger logger, IServiceScopeFactory scopeFactory) { _logger = logger; _scopeFactory = scopeFactory; _state = state; } public override async Task OnActivateAsync(CancellationToken cancellationToken) { _logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey()); await base.OnActivateAsync(cancellationToken); await ResumeBotIfRequiredAsync(); } public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) { _logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}", this.GetPrimaryKey(), reason.Description); StopAndDisposeTimer(); await base.OnDeactivateAsync(reason, cancellationToken); } public async Task CreateAsync(TradingBotConfig config, User user) { if (config == null || string.IsNullOrEmpty(config.Name)) { throw new InvalidOperationException("Bot configuration is not properly initialized"); } if (config.IsForBacktest) { throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting"); } // This is a new bot, so we can assume it's not registered or active. _state.State.Config = config; _state.State.User = user; _state.State.CreateDate = DateTime.UtcNow; _state.State.Identifier = this.GetPrimaryKey(); await _state.WriteStateAsync(); var botRegistry = GrainFactory.GetGrain(0); await botRegistry.RegisterBot(_state.State.Identifier, user.Id); // Register the bot with the user's agent var agentGrain = GrainFactory.GetGrain(user.Id); await agentGrain.RegisterBotAsync(_state.State.Identifier); await SaveBotAsync(BotStatus.Saved); _logger.LogInformation("LiveTradingBotGrain {GrainId} created successfully", this.GetPrimaryKey()); } private async Task ResumeBotIfRequiredAsync() { // Make the network call to the registry to get the source of truth var botRegistry = GrainFactory.GetGrain(0); var botId = this.GetPrimaryKey(); var botStatus = await botRegistry.GetBotStatus(botId); _logger.LogInformation("LiveTradingBotGrain {GrainId} activated. Registry status: {Status}", botId, botStatus); if (botStatus == BotStatus.Running && _tradingBot == null) { // Now, we can proceed with resuming the bot. await ResumeBotInternalAsync(); } } private async Task ResumeBotInternalAsync() { // Idempotency check if (_tradingBot != null) { return; } try { // Create and initialize trading bot instance _tradingBot = CreateTradingBotInstance(_state.State.Config); await _tradingBot.Start(); // Set startup time when bot actually starts running _state.State.StartupTime = DateTime.UtcNow; await _state.WriteStateAsync(); // Start the in-memory timer and persistent reminder RegisterAndStartTimer(); await RegisterReminder(); // Update both database and registry status await SaveBotAsync(BotStatus.Running); await UpdateBotRegistryStatus(BotStatus.Running); _logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to resume bot {GrainId}", this.GetPrimaryKey()); _tradingBot = null; // Clean up on failure await UpdateBotRegistryStatus(BotStatus.Stopped); throw; } } public async Task StartAsync() { var botRegistry = GrainFactory.GetGrain(0); var botId = this.GetPrimaryKey(); var status = await botRegistry.GetBotStatus(botId); // Check if already running if (status == BotStatus.Running && _tradingBot != null) { await RegisterReminder(); _logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey()); return; } try { // Resume the bot - this handles registry status update internally await ResumeBotInternalAsync(); _logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); // Ensure registry status is correct on failure await UpdateBotRegistryStatus(BotStatus.Stopped); throw; } } private async Task RegisterReminder() { var reminderPeriod = TimeSpan.FromMinutes(2); await this.RegisterOrUpdateReminder(_reminderName, reminderPeriod, reminderPeriod); } /// /// Starts the Orleans timer for periodic bot execution /// private void RegisterAndStartTimer() { if (_tradingBot == null) return; if (_timer != null) return; _timer = this.RegisterGrainTimer( async _ => await ExecuteBotCycle(), new GrainTimerCreationOptions { Period = TimeSpan.FromMinutes(1), DueTime = TimeSpan.FromMinutes(1), KeepAlive = true }); } public async Task StopAsync() { // The check is now against the registry status var botRegistry = GrainFactory.GetGrain(0); var botStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey()); if (botStatus == BotStatus.Stopped) { _logger.LogInformation("Bot {GrainId} is already stopped", this.GetPrimaryKey()); return; } try { StopAndDisposeTimer(); await UnregisterReminder(); // Sync state from the volatile TradingBotBase before destroying it SyncStateFromBase(); await _state.WriteStateAsync(); await SaveBotAsync(BotStatus.Stopped); _tradingBot = null; await UpdateBotRegistryStatus(BotStatus.Stopped); _logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to stop LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); throw; } } private void StopAndDisposeTimer() { if (_timer != null) { // Stop the timer _timer?.Dispose(); _timer = null; } } private async Task UnregisterReminder() { var reminder = await this.GetReminder(_reminderName); if (reminder != null) { await this.UnregisterReminder(reminder); } } /// /// Creates a TradingBotBase instance using composition /// private TradingBotBase CreateTradingBotInstance(TradingBotConfig config) { if (string.IsNullOrEmpty(config.AccountName)) { throw new InvalidOperationException("Account name is required for live trading"); } using var scope = _scopeFactory.CreateScope(); var logger = scope.ServiceProvider.GetRequiredService>(); var tradingBot = new TradingBotBase(logger, _scopeFactory, config); // Load state into the trading bot instance LoadStateIntoTradingBot(tradingBot); return tradingBot; } /// /// Loads grain state into a trading bot instance /// private void LoadStateIntoTradingBot(TradingBotBase tradingBot) { tradingBot.Signals = _state.State.Signals; tradingBot.Positions = _state.State.Positions; tradingBot.WalletBalances = _state.State.WalletBalances; tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount; tradingBot.ExecutionCount = _state.State.ExecutionCount; tradingBot.Identifier = _state.State.Identifier; tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime; tradingBot.LastCandle = _state.State.LastCandle; } /// /// Executes one cycle of the trading bot /// private async Task ExecuteBotCycle() { try { if (_tradingBot == null) { return; } // Execute the bot's Run method await _tradingBot.Run(); SyncStateFromBase(); await _state.WriteStateAsync(); // Save bot statistics to database await SaveBotAsync(BotStatus.Running); } catch (ObjectDisposedException) { // Gracefully handle disposed service provider during shutdown _logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); return; } catch (Exception ex) { // TODO : Turn off the bot if an error occurs _logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); } } public async Task OpenPositionManuallyAsync(TradeDirection direction) { try { if (_tradingBot == null) { throw new InvalidOperationException("Bot is not running"); } // Ensure LastCandle is available for manual position opening if (_tradingBot.LastCandle == null) { _logger.LogInformation("LastCandle is null, loading latest candle data for manual position opening"); await _tradingBot.LoadLastCandle(); // Sync the loaded candle to grain state SyncStateFromBase(); await _state.WriteStateAsync(); } return await _tradingBot.OpenPositionManually(direction); } catch (Exception ex) { _logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); throw; } } public Task GetBotDataAsync() { try { if (_tradingBot == null) { // For non-running bots, return data from grain state only return Task.FromResult(new LiveTradingBotModel { Identifier = _state.State.Identifier, Name = _state.State.Config?.Name ?? "Unknown", Config = _state.State.Config, Positions = _state.State.Positions ?? new Dictionary(), Signals = _state.State.Signals, WalletBalances = _state.State.WalletBalances ?? new Dictionary(), ProfitAndLoss = 0, // Calculate from persisted positions if needed WinRate = 0, // Calculate from persisted positions if needed ExecutionCount = _state.State.ExecutionCount, StartupTime = _state.State.StartupTime, CreateDate = _state.State.CreateDate }); } // For running bots, return live data return Task.FromResult(new LiveTradingBotModel { Identifier = _state.State.Identifier, Name = _state.State.Config?.Name ?? "Unknown", Config = _state.State.Config, Positions = _tradingBot.Positions, Signals = _tradingBot.Signals, WalletBalances = _tradingBot.WalletBalances, ProfitAndLoss = _tradingBot.GetProfitAndLoss(), WinRate = _tradingBot.GetWinRate(), ExecutionCount = _state.State.ExecutionCount, StartupTime = _state.State.StartupTime, CreateDate = _state.State.CreateDate }); } catch (Exception ex) { _logger.LogError(ex, "Failed to get bot data for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); throw; } } private void SyncStateFromBase() { if (_tradingBot == null) return; _state.State.Signals = _tradingBot.Signals; _state.State.Positions = _tradingBot.Positions; _state.State.WalletBalances = _tradingBot.WalletBalances; _state.State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount; _state.State.ExecutionCount = _tradingBot.ExecutionCount; _state.State.Identifier = _tradingBot.Identifier; _state.State.LastPositionClosingTime = _tradingBot.LastPositionClosingTime; _state.State.LastCandle = _tradingBot.LastCandle; _state.State.Config = _tradingBot.Config; } public async Task UpdateConfiguration(TradingBotConfig newConfig) { if (_tradingBot == null) { // For non-running bots, just update the configuration _state.State.Config = newConfig; await _state.WriteStateAsync(); var botRegistry = GrainFactory.GetGrain(0); var status = await botRegistry.GetBotStatus(this.GetPrimaryKey()); await SaveBotAsync(status); return true; } var result = await _tradingBot.UpdateConfiguration(newConfig); if (result) { var botRegistry = GrainFactory.GetGrain(0); var status = await botRegistry.GetBotStatus(this.GetPrimaryKey()); _state.State.Config = newConfig; await _state.WriteStateAsync(); await SaveBotAsync(status); } return result; } public Task GetAccount() { if (_tradingBot == null) { throw new InvalidOperationException("Bot is not running - cannot get account information"); } return Task.FromResult(_tradingBot.Account); } public Task GetConfiguration() { return Task.FromResult(_state.State.Config); } public async Task ClosePositionAsync(Guid positionId) { if (_tradingBot == null) { throw new InvalidOperationException("Bot is not running"); } if (!_tradingBot.Positions.TryGetValue(positionId, out var position)) { throw new InvalidOperationException($"Position with ID {positionId} not found"); } var signal = _tradingBot.Signals.TryGetValue(position.SignalIdentifier, out var foundSignal) ? foundSignal : null; if (signal == null) { throw new InvalidOperationException($"Signal with ID {position.SignalIdentifier} not found"); } await _tradingBot.CloseTrade(signal, position, position.Open, _tradingBot.LastCandle.Close, true); return position; } public async Task RestartAsync() { _logger.LogInformation("Restarting LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); try { await StopAsync(); // Add a small delay to ensure stop operations complete await Task.Delay(100); await StartAsync(); // Verify the restart was successful var botRegistry = GrainFactory.GetGrain(0); var finalStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey()); _logger.LogInformation("LiveTradingBotGrain {GrainId} restart completed with final status: {Status}", this.GetPrimaryKey(), finalStatus); } catch (Exception ex) { _logger.LogError(ex, "Failed to restart LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); throw; } } public async Task DeleteAsync() { try { // Stop the bot first if it's running await StopAsync(); // Unregister from the bot registry var botRegistry = GrainFactory.GetGrain(0); await botRegistry.UnregisterBot(_state.State.Identifier); // Unregister from the user's agent if (_state.State.User != null) { var agentGrain = GrainFactory.GetGrain(_state.State.User.Id); await agentGrain.UnregisterBotAsync(_state.State.Identifier); } // Clear the state _tradingBot = null; await _state.ClearStateAsync(); _logger.LogInformation("LiveTradingBotGrain {GrainId} deleted successfully", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); throw; } } /// /// Updates the bot status in the central BotRegistry with retry logic /// private async Task UpdateBotRegistryStatus(BotStatus status) { const int maxRetries = 3; var botId = this.GetPrimaryKey(); for (int attempt = 1; attempt <= maxRetries; attempt++) { try { var botRegistry = GrainFactory.GetGrain(0); await botRegistry.UpdateBotStatus(botId, status); // Verify the update was successful var actualStatus = await botRegistry.GetBotStatus(botId); if (actualStatus == status) { _logger.LogDebug("Bot {BotId} status successfully updated to {Status} in BotRegistry (attempt {Attempt})", botId, status, attempt); return; } else { _logger.LogWarning("Bot {BotId} status update verification failed. Expected: {Expected}, Actual: {Actual} (attempt {Attempt})", botId, status, actualStatus, attempt); } } catch (Exception ex) { _logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry (attempt {Attempt})", botId, status, attempt); if (attempt == maxRetries) { throw; } } // Wait before retry if (attempt < maxRetries) { await Task.Delay(TimeSpan.FromMilliseconds(100 * attempt)); } } } public async Task ReceiveReminder(string reminderName, TickStatus status) { _logger.LogInformation("Reminder '{ReminderName}' received for grain {GrainId}.", reminderName, this.GetPrimaryKey()); if (reminderName == _reminderName) { // Now a single, clean call to the method that handles all the logic await ResumeBotIfRequiredAsync(); } } /// /// Saves the current bot statistics to the database using BotService /// private async Task SaveBotAsync(BotStatus status) { try { Bot bot = null; if (_tradingBot == null || _state.State.User == null) { // Save bot statistics for saved bots bot = new Bot { Identifier = _state.State.Identifier, Name = _state.State.Config.Name, Ticker = _state.State.Config.Ticker, User = _state.State.User, Status = status, CreateDate = _state.State.CreateDate, StartupTime = _state.State.StartupTime, TradeWins = 0, TradeLosses = 0, Pnl = 0, Roi = 0, Volume = 0, Fees = 0 }; } else { // Calculate statistics using TradingBox helpers var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions); var pnl = _tradingBot.GetProfitAndLoss(); var fees = _tradingBot.GetTotalFees(); var volume = TradingBox.GetTotalVolumeTraded(_tradingBot.Positions); // Calculate ROI based on total investment var totalInvestment = _tradingBot.Positions.Values .Sum(p => p.Open.Quantity * p.Open.Price); var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0; // Calculate long and short position counts var longPositionCount = _tradingBot.Positions.Values .Count(p => p.OriginDirection == TradeDirection.Long); var shortPositionCount = _tradingBot.Positions.Values .Count(p => p.OriginDirection == TradeDirection.Short); // Create complete Bot object with all statistics bot = new Bot { Identifier = _state.State.Identifier, Name = _state.State.Config.Name, Ticker = _state.State.Config.Ticker, User = _state.State.User, Status = status, StartupTime = _state.State.StartupTime, CreateDate = _state.State.CreateDate, TradeWins = tradeWins, TradeLosses = tradeLosses, Pnl = pnl, Roi = roi, Volume = volume, Fees = fees, LongPositionCount = longPositionCount, ShortPositionCount = shortPositionCount }; } // Pass the complete Bot object to BotService for saving var success = await ServiceScopeHelpers.WithScopedService(_scopeFactory, async (botService) => { return await botService.SaveBotStatisticsAsync(bot); }); if (success) { _logger.LogDebug( "Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}, Long={LongPositions}, Short={ShortPositions}", _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees, bot.LongPositionCount, bot.ShortPositionCount); } else { _logger.LogWarning("Failed to save bot statistics for bot {BotId}", _state.State.Identifier); } } catch (Exception ex) { _logger.LogError(ex, "Failed to save bot statistics for bot {BotId}", _state.State.Identifier); } } }