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.None); _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.Up && _tradingBot == null) { // Now, we can proceed with resuming the bot. await ResumeBotInternalAsync(); } } private async Task ResumeBotInternalAsync() { // The core of this method remains idempotent thanks to the _tradingBot null check if (_tradingBot != null) { return; } try { // Load state from persisted grain state _tradingBot = CreateTradingBotInstance(_state.State.Config); LoadStateIntoBase(); await _tradingBot.Start(); // Start the in-memory timer and persistent reminder RegisterAndStartTimer(); await RegisterReminder(); await SaveBotAsync(BotStatus.Up); _logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey()); } catch (Exception ex) { _logger.LogError(ex, "Failed to resume bot {GrainId}", this.GetPrimaryKey()); // If resume fails, update the status to Down via the registry and stop await UpdateBotRegistryStatus(BotStatus.Down); throw; } } public async Task StartAsync() { var botRegistry = GrainFactory.GetGrain(0); var botId = this.GetPrimaryKey(); var status = await botRegistry.GetBotStatus(botId); // This is the new idempotency check, using the registry as the source of truth if (status == BotStatus.Up && _tradingBot != null) { await RegisterReminder(); _logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey()); return; } try { // Resume the bot using the internal logic await ResumeBotInternalAsync(); // Update registry status (if it was previously 'Down') await UpdateBotRegistryStatus(BotStatus.Up); _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 even on failure await UpdateBotRegistryStatus(BotStatus.Down); 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.Down) { _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.Down); _tradingBot = null; await UpdateBotRegistryStatus(BotStatus.Down); _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); // Restore state from grain state 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; return tradingBot; } /// /// 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.Up); } 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) { throw new InvalidOperationException("Bot is not running"); } return Task.FromResult(new LiveTradingBotModel { Identifier = _state.State.Identifier, Name = _state.State.Name, 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 LoadStateIntoBase() { if (_tradingBot == null) _tradingBot = CreateTradingBotInstance(_state.State.Config); if (_tradingBot == null) throw new InvalidOperationException("TradingBotBase instance could not be created"); _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; } 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) LoadStateIntoBase(); var result = await _tradingBot!.UpdateConfiguration(newConfig); if (result) { var botRegistry = GrainFactory.GetGrain(0); var botId = this.GetPrimaryKey(); var status = await botRegistry.GetBotStatus(botId); _state.State.Config = newConfig; await _state.WriteStateAsync(); await SaveBotAsync(status); } return result; } public Task GetAccount() { 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() { await StopAsync(); await StartAsync(); } 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 /// private async Task UpdateBotRegistryStatus(BotStatus status) { try { var botRegistry = GrainFactory.GetGrain(0); var botId = this.GetPrimaryKey(); await botRegistry.UpdateBotStatus(botId, status); _logger.LogDebug("Bot {BotId} status updated to {Status} in BotRegistry", botId, status); } catch (Exception ex) { _logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry", this.GetPrimaryKey(), status); } } 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; // 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 }; } // 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}", _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees); } 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); } } }