using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; using Managing.Core; using Managing.Domain.Bots; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.ManageBot { public class BotService : IBotService { private readonly IBotRepository _botRepository; private readonly IMessengerService _messengerService; private readonly ILogger _tradingBotLogger; private readonly ITradingService _tradingService; private readonly IGrainFactory _grainFactory; private readonly IServiceScopeFactory _scopeFactory; public BotService(IBotRepository botRepository, IMessengerService messengerService, ILogger tradingBotLogger, ITradingService tradingService, IGrainFactory grainFactory, IServiceScopeFactory scopeFactory) { _botRepository = botRepository; _messengerService = messengerService; _tradingBotLogger = tradingBotLogger; _tradingService = tradingService; _grainFactory = grainFactory; _scopeFactory = scopeFactory; } public async Task> GetBotsAsync() { return await _botRepository.GetBotsAsync(); } public async Task> GetBotsByStatusAsync(BotStatus status) { return await _botRepository.GetBotsByStatusAsync(status); } public async Task StopBot(Guid identifier) { try { var grain = _grainFactory.GetGrain(identifier); await grain.StopAsync(); return BotStatus.Down; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier); return BotStatus.Down; } } public async Task DeleteBot(Guid identifier) { var grain = _grainFactory.GetGrain(identifier); try { var config = await grain.GetConfiguration(); var account = await grain.GetAccount(); await grain.StopAsync(); await _botRepository.DeleteBot(identifier); await grain.DeleteAsync(); var deleteMessage = $"🗑️ **Bot Deleted**\n\n" + $"🎯 **Agent:** {account.User.AgentName}\n" + $"🤖 **Bot Name:** {config.Name}\n" + $"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"⚠️ **Bot has been permanently deleted and all data removed.**"; await _messengerService.SendTradeMessage(deleteMessage, false, account.User); return true; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error deleting bot {Identifier}", identifier); return false; } } public async Task RestartBot(Guid identifier) { try { var registryGrain = _grainFactory.GetGrain(0); var previousStatus = await registryGrain.GetBotStatus(identifier); // If bot is already up, return the status directly if (previousStatus == BotStatus.Up) { return BotStatus.Up; } var botGrain = _grainFactory.GetGrain(identifier); if (previousStatus == BotStatus.None) { // First time startup await botGrain.StartAsync(); var grainState = await botGrain.GetBotDataAsync(); var account = await botGrain.GetAccount(); var startupMessage = $"🚀 **Bot Started**\n\n" + $"🎯 **Agent:** {account.User.AgentName}\n" + $"🤖 **Bot Name:** {grainState.Config.Name}\n" + $"⏰ **Started At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" + $"🕐 **Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"✅ **Bot has been successfully started and is now active.**"; await _messengerService.SendTradeMessage(startupMessage, false, account.User); } else { // Restart (bot was previously down) await botGrain.RestartAsync(); var grainState = await botGrain.GetBotDataAsync(); var account = await botGrain.GetAccount(); var restartMessage = $"🔄 **Bot Restarted**\n\n" + $"🎯 **Agent:** {account.User.AgentName}\n" + $"🤖 **Bot Name:** {grainState.Config.Name}\n" + $"⏰ **Restarted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" + $"🕐 **New Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" + $"🚀 **Bot has been successfully restarted and is now active.**"; await _messengerService.SendTradeMessage(restartMessage, false, account.User); } return BotStatus.Up; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier); return BotStatus.Down; } } private async Task GetBot(Guid identifier) { var bot = await _botRepository.GetBotByIdentifierAsync(identifier); return bot; } /// /// Updates the configuration of an existing bot without stopping and restarting it. /// /// The bot identifier /// The new configuration to apply /// True if the configuration was successfully updated, false otherwise public async Task UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig) { var grain = _grainFactory.GetGrain(identifier); // Ensure the scenario is properly loaded from database if needed if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName)) { var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName); if (scenario != null) { newConfig.Scenario = LightScenario.FromScenario(scenario); } else { throw new ArgumentException( $"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration"); } } if (newConfig.Scenario == null) { throw new ArgumentException( "Scenario object must be provided or ScenarioName must be valid when updating configuration"); } return await grain.UpdateConfiguration(newConfig); } public async Task GetBotConfig(Guid identifier) { var grain = _grainFactory.GetGrain(identifier); return await grain.GetConfiguration(); } public async Task> GetActiveBotsNamesAsync() { var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Up); return bots.Select(b => b.Name); } public async Task> GetBotsByUser(int id) { return await _botRepository.GetBotsByUserIdAsync(id); } public async Task> GetBotsByIdsAsync(IEnumerable botIds) { return await _botRepository.GetBotsByIdsAsync(botIds); } public async Task GetBotByName(string name) { return await _botRepository.GetBotByNameAsync(name); } public async Task GetBotByIdentifier(Guid identifier) { return await _botRepository.GetBotByIdentifierAsync(identifier); } public async Task OpenPositionManuallyAsync(Guid identifier, TradeDirection direction) { var grain = _grainFactory.GetGrain(identifier); return await grain.OpenPositionManuallyAsync(direction); } public async Task ClosePositionAsync(Guid identifier, Guid positionId) { var grain = _grainFactory.GetGrain(identifier); return await grain.ClosePositionAsync(positionId); } public async Task UpdateBotStatisticsAsync(Guid identifier) { try { var grain = _grainFactory.GetGrain(identifier); var botData = await grain.GetBotDataAsync(); // Get the current bot from database var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier); if (existingBot == null) { _tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update", identifier); return false; } // Calculate statistics using TradingBox helpers var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions); var pnl = botData.ProfitAndLoss; var fees = botData.Positions.Values.Sum(p => { if (p.Open.Price > 0 && p.Open.Quantity > 0) { var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage; var uiFeeRate = 0.001m; // 0.1% var uiFeeOpen = positionSizeUsd * uiFeeRate; var networkFeeForOpening = 0.50m; return uiFeeOpen + networkFeeForOpening; } return 0; }); var volume = TradingBox.GetTotalVolumeTraded(botData.Positions); // Calculate ROI based on total investment var totalInvestment = botData.Positions.Values .Where(p => p.IsFinished()) .Sum(p => p.Open.Quantity * p.Open.Price); var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0; // Update bot statistics existingBot.TradeWins = tradeWins; existingBot.TradeLosses = tradeLosses; existingBot.Pnl = pnl; existingBot.Roi = roi; existingBot.Volume = volume; existingBot.Fees = fees; // Use the new SaveBotStatisticsAsync method return await SaveBotStatisticsAsync(existingBot); } catch (Exception e) { _tradingBotLogger.LogError(e, "Error updating bot statistics for {Identifier}", identifier); return false; } } public async Task SaveBotStatisticsAsync(Bot bot) { try { if (bot == null) { _tradingBotLogger.LogWarning("Cannot save bot statistics: bot object is null"); return false; } // Check if bot already exists in database var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier); if (existingBot != null) { // Update existing bot await _botRepository.UpdateBot(bot); _tradingBotLogger.LogDebug( "Updated bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}", bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees); } else { // Insert new bot await _botRepository.InsertBotAsync(bot); _tradingBotLogger.LogInformation( "Created new bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}", bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees); } return true; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier); return false; } } public async Task<(IEnumerable Bots, int TotalCount)> GetBotsPaginatedAsync( int pageNumber, int pageSize, BotStatus? status = null, string? name = null, string? ticker = null, string? agentName = null, string sortBy = "CreateDate", string sortDirection = "Desc") { return await ServiceScopeHelpers.WithScopedService Bots, int TotalCount)>( _scopeFactory, async repo => { return await repo.GetBotsPaginatedAsync( pageNumber, pageSize, status, name, ticker, agentName, sortBy, sortDirection); }); } } }