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.Application.Bots.Models; using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Indicators; 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.Stopped; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier); throw; } } 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.Running) { return BotStatus.Running; } var botGrain = _grainFactory.GetGrain(identifier); // Check balances for EVM/GMX V2 bots before starting/restarting var botConfig = await botGrain.GetConfiguration(); var account = await ServiceScopeHelpers.WithScopedService( _scopeFactory, async accountService => await accountService.GetAccount(botConfig.AccountName, true, true)); if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2) { // Allocation guard: ensure this bot's configured balance fits in remaining allocation var availableAllocation = await GetAvailableAllocationUsdAsync(account, identifier); if (botConfig.BotTradingBalance > availableAllocation) { throw new InvalidOperationException( $"Insufficient available allocation. Requested: {botConfig.BotTradingBalance:F2} USDC, Available: {availableAllocation:F2} USDC."); } var balanceCheckResult = await CheckAccountBalancesAsync(account); if (!balanceCheckResult.IsSuccessful) { _tradingBotLogger.LogWarning( "Bot {Identifier} restart blocked due to insufficient balances: {Message}", identifier, balanceCheckResult.Message); throw new InvalidOperationException(balanceCheckResult.Message); } } var grainState = await botGrain.GetBotDataAsync(); if (previousStatus == BotStatus.Saved) { // First time startup await botGrain.StartAsync(); 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 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.Running; } catch (Exception e) { SentrySdk.CaptureException(e); throw; } } 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> GetBotConfigsByIdsAsync(IEnumerable botIds) { var configs = new List(); foreach (var botId in botIds) { try { var grain = _grainFactory.GetGrain(botId); var config = await grain.GetConfiguration(); configs.Add(config); } catch (Exception ex) { _tradingBotLogger.LogWarning(ex, "Failed to get configuration for bot {BotId}", botId); // Continue with other bots even if one fails } } return configs; } public async Task> GetActiveBotsNamesAsync() { var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Running); 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 CreateManualSignalAsync(Guid identifier, TradeDirection direction) { var grain = _grainFactory.GetGrain(identifier); return await grain.CreateManualSignalAsync(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 => p.CalculateTotalFees()); 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 netPnl = pnl - fees; var roi = totalInvestment > 0 ? (netPnl / 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; } var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier); // Check if bot already exists in database await ServiceScopeHelpers.WithScopedService( _scopeFactory, async repo => { if (existingBot == null) { _tradingBotLogger.LogInformation("Updating existing bot statistics for bot {BotId}", bot.Identifier); // Insert new bot await repo.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); } else if (existingBot.Status != bot.Status || existingBot.Pnl != Math.Round(bot.Pnl, 8) || existingBot.Roi != Math.Round(bot.Roi, 8) || existingBot.Volume != Math.Round(bot.Volume, 8) || existingBot.Fees != Math.Round(bot.Fees, 8) || existingBot.LongPositionCount != bot.LongPositionCount || existingBot.ShortPositionCount != bot.ShortPositionCount || !string.Equals(existingBot.Name, bot.Name, StringComparison.Ordinal) || existingBot.AccumulatedRunTimeSeconds != bot.AccumulatedRunTimeSeconds || existingBot.LastStartTime != bot.LastStartTime || existingBot.LastStopTime != bot.LastStopTime || existingBot.Ticker != bot.Ticker) { _tradingBotLogger.LogInformation("Update bot statistics for bot {BotId}", bot.Identifier); // Update existing bot await repo.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); } }); return true; } catch (Exception e) { _tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier); return false; } } public async Task GetAvailableAllocationUsdAsync(Account account, Guid excludeIdentifier = default) { try { return await ServiceScopeHelpers.WithScopedService( _scopeFactory, async repo => { // Get all bots for the account's user var botsForUser = (await repo.GetBotsByUserIdAsync(account.User.Id)).ToList(); // Sum allocations for bots using this account name, excluding the requested identifier var totalAllocatedForAccount = 0m; var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString()); Console.WriteLine($"Bots for user: {botsForUser.Count}"); Console.WriteLine($"Exclude identifier: {excludeIdentifier}"); foreach (var bot in botsForUser) { if (excludeIdentifier != default && bot.Identifier == excludeIdentifier) { continue; } if (bot.Status == BotStatus.Stopped || bot.Status == BotStatus.Saved) { continue; } var grain = _grainFactory.GetGrain(bot.Identifier); TradingBotConfig config; try { config = await grain.GetConfiguration(); Console.WriteLine($"Bot Balance: {config.BotTradingBalance}"); } catch { continue; } if (string.Equals(config.AccountName, account.Name, StringComparison.OrdinalIgnoreCase)) { totalAllocatedForAccount += config.BotTradingBalance; } } var usdcValue = usdcBalance?.Amount ?? 0m; var available = usdcValue - totalAllocatedForAccount; return available < 0m ? 0m : available; }); } catch { // On failure, be safe and return 0 to block over-allocation return 0m; } } public async Task<(IEnumerable Bots, int TotalCount)> GetBotsPaginatedAsync( int pageNumber, int pageSize, BotStatus? status = null, string? name = null, string? ticker = null, string? agentName = null, BotSortableColumn sortBy = BotSortableColumn.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); }); } /// /// Checks USDC and ETH balances for EVM/GMX V2 accounts /// /// The account to check balances for /// Balance check result public async Task CheckAccountBalancesAsync(Account account) { try { return await ServiceScopeHelpers .WithScopedServices( _scopeFactory, async (exchangeService, accountService) => { // Get current balances var balances = await exchangeService.GetBalances(account); var ethBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "ETH"); var usdcBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC"); var ethValueInUsd = ethBalance?.Amount * ethBalance?.Price ?? 0; var usdcValue = usdcBalance?.Value ?? 0; _tradingBotLogger.LogInformation( "Balance check for bot restart - Account: {AccountName}, ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD", account.Name, ethValueInUsd, usdcValue); // Check USDC minimum balance if (usdcValue < Constants.GMX.Config.MinimumPositionAmount) { return new BalanceCheckResult { IsSuccessful = false, FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum, Message = $"USDC balance ({usdcValue:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD). Please add more USDC to restart the bot.", ShouldStopBot = true }; } // Check ETH minimum balance for trading if (ethValueInUsd < Constants.GMX.Config.MinimumTradeEthBalanceUsd) { return new BalanceCheckResult { IsSuccessful = false, FailureReason = BalanceCheckFailureReason.InsufficientEthBelowMinimum, Message = $"ETH balance ({ethValueInUsd:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumTradeEthBalanceUsd} USD) for trading. Please add more ETH to restart the bot.", ShouldStopBot = true }; } return new BalanceCheckResult { IsSuccessful = true, FailureReason = BalanceCheckFailureReason.None, Message = "Balance check successful - Sufficient USDC and ETH balances", ShouldStopBot = false }; }); } catch (Exception ex) { _tradingBotLogger.LogError(ex, "Error checking balances for account {AccountName}", account.Name); return new BalanceCheckResult { IsSuccessful = false, FailureReason = BalanceCheckFailureReason.BalanceFetchError, Message = $"Failed to check balances: {ex.Message}", ShouldStopBot = false }; } } } }