using Managing.Application.Abstractions.Services; using Managing.Common; using Managing.Domain.Backtests; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Managing.Domain.Users; namespace Managing.Application.Shared; public class MessengerService : IMessengerService { private readonly IDiscordService _discordService; private readonly IWebhookService _webhookService; private readonly IUserService _userService; public MessengerService(IDiscordService discordService, IWebhookService webhookService, IUserService userService) { _discordService = discordService; _webhookService = webhookService; _userService = userService; } public async Task SendClosedPosition(string address, Trade oldTrade) { await _discordService.SendClosedPosition(address, oldTrade); } public void SendClosingPosition(Position position) { // Fire-and-forget: Send closing position notification without blocking the thread _ = Task.Run(async () => { try { await _discordService.SendClosingPosition(position); } catch (Exception ex) { // Log the exception but don't let it affect the main thread Console.WriteLine($"Failed to send closing position notification: {ex.Message}"); } }); } public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null) { await _discordService.SendIncreasePosition(address, trade, copyAccountName, oldTrade); } public async Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount) { await _discordService.SendDecreasePosition(address, newTrade, decreaseAmount); } public async Task SendMessage(string message) { await _discordService.SendMessage(message); } public async Task SendPosition(Position position) { // Send to Discord with try-catch to not block try { await _discordService.SendPosition(position); } catch (Exception e) { Console.WriteLine(e); } // Send to webhook (n8n/telegram) try { var message = BuildPositionMessage(position); await _webhookService.SendMessage(message, position.User?.TelegramChannel); } catch (Exception e) { Console.WriteLine(e); } } public async Task SendClosedPosition(Position position, User user) { var message = BuildClosePositionMessage(position); try { await _discordService.SendMessage(message); } catch (Exception e) { SentrySdk.CaptureException(e); } try { await _webhookService.SendMessage(message, user.TelegramChannel); } catch (Exception e) { SentrySdk.CaptureException(e); } } private string BuildClosePositionMessage(Position position) { var message = $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + $"Open Price : {position.Open.Price} \n"; if (position.StopLoss.Status.Equals(Enums.TradeStatus.Filled)) { message += $"SL Hit: {position.StopLoss.Price} \n"; } if (position.TakeProfit1.Status.Equals(Enums.TradeStatus.Filled)) { message += $"TP1 Hit: {position.TakeProfit1.Price} \n"; } if (position.TakeProfit2.Status.Equals(Enums.TradeStatus.Filled)) { message += $"TP2 Hit: {position.TakeProfit2.Price} \n"; } message += $"PNL : {position.ProfitAndLoss.Net} $"; return message; } public async Task SendMessage(string message, string telegramChannel) { await _webhookService.SendMessage(message, telegramChannel); } private string BuildPositionMessage(Position position) { var direction = position.OriginDirection.ToString(); var status = position.Status.ToString(); var message = $"🎯 {status} Position \n" + $"Symbol: {position.Ticker}\n" + $"Direction: {direction}\n" + $"Date: {position.Date:yyyy-MM-dd HH:mm:ss}"; if (position.Open != null) { message += $"\nOpen Trade: {position.Open.Price:F5}"; } if (position.StopLoss != null) { message += $"\nStop Loss: {position.StopLoss.Price:F5}"; } if (position.TakeProfit1 != null) { message += $"\nTake Profit 1: {position.TakeProfit1.Price:F5}"; } if (position.ProfitAndLoss != null) { var pnlEmoji = position.ProfitAndLoss.Net >= 0 ? "✅" : "❌"; message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Net:F5}"; } return message; } public async Task SendSignal(string message, Enums.TradingExchanges exchange, Enums.Ticker ticker, Enums.TradeDirection direction, Enums.Timeframe timeframe) { await _discordService.SendSignal(message, exchange, ticker, direction, timeframe); } public async Task SendTradeMessage(string message, bool isBadBehavior = false, User user = null) { // Always send to Discord try { await _discordService.SendTradeMessage(message, isBadBehavior); } catch (Exception e) { Console.WriteLine(e); } // If user is provided, also send to webhook if (user != null) { await _webhookService.SendTradeNotification(user, message, isBadBehavior); } } public async Task SendBestTraders(List traders) { await _discordService.SendBestTraders(traders); } public async Task SendBadTraders(List traders) { await _discordService.SendBadTraders(traders); } public async Task SendDowngradedFundingRate(FundingRate oldRate) { await _discordService.SendDowngradedFundingRate(oldRate); } public async Task SendNewTopFundingRate(FundingRate newRate) { await _discordService.SendNewTopFundingRate(newRate); } public async Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate) { await _discordService.SendFundingRateUpdate(oldRate, newRate); } public async Task SendBacktestNotification(Backtest backtest) { try { var message = BuildBacktestMessage(backtest); await _webhookService.SendMessage(message, "2775292276"); } catch (Exception e) { Console.WriteLine($"Failed to send backtest notification: {e.Message}"); } } public async Task SendGeneticAlgorithmNotification(GeneticRequest request, double bestFitness, object? bestChromosome) { try { var message = BuildGeneticAlgorithmMessage(request, bestFitness, bestChromosome); await _webhookService.SendMessage(message, "2775292276"); } catch (Exception e) { Console.WriteLine($"Failed to send genetic algorithm notification: {e.Message}"); } } private string BuildBacktestMessage(Backtest backtest) { var config = backtest.Config; var score = backtest.Score; var winRate = backtest.WinRate; var tradeCount = backtest.Positions?.Count ?? 0; var finalPnl = backtest.FinalPnl; var growthPercentage = backtest.GrowthPercentage; var maxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0; var sharpeRatio = (backtest.Statistics?.SharpeRatio * 100) ?? 0; // Get indicators list as comma-separated string var indicators = config.Scenario?.Indicators != null && config.Scenario.Indicators.Any() ? string.Join(", ", config.Scenario.Indicators.Select(i => i.Name ?? i.Type.ToString())) : "N/A"; // MoneyManagement summary var mmSl = config.MoneyManagement != null ? (config.MoneyManagement.StopLoss * 100).ToString("F2") : "N/A"; var mmTp = config.MoneyManagement != null ? (config.MoneyManagement.TakeProfit * 100).ToString("F2") : "N/A"; var mmLev = config.MoneyManagement != null ? config.MoneyManagement.Leverage.ToString("F2") : "N/A"; var message = $"🚀 Excellent Backtest Results! 🚀\n\n" + $"🔹 Symbol: {config.Ticker} | " + $"⏱️ Timeframe: {config.Timeframe}\n" + $"👤 Account: {config.AccountName}\n" + $"💼 MM: 🛡️ SL: {mmSl}% | 🎯 TP: {mmTp}% | 📈 Lev: {mmLev}x\n" + $"💰 Balance: {config.BotTradingBalance:C}\n" + $"🧩 Indicators: {indicators}\n" + $"📅 Period: {backtest.StartDate:yyyy-MM-dd} to {backtest.EndDate:yyyy-MM-dd}\n" + $"⏳ Cooldown: {config.CooldownPeriod} | 🔥 Max Loss Streak: {config.MaxLossStreak}\n" + $"🔄 Flipping: {(config.FlipPosition ? "Yes" : "No")} | 🔒 Flip Only When In Profit: {(config.FlipOnlyWhenInProfit ? "Yes" : "No")}\n" + $"{(config.MaxPositionTimeHours.HasValue && config.MaxPositionTimeHours.Value > 0 ? $"⏰ Max Position Time (hrs): {config.MaxPositionTimeHours.Value} | " : "")}🏁 Close Early When Profitable: {(config.CloseEarlyWhenProfitable ? "Yes" : "No")}\n" + $"\n📈 Performance Metrics:\n" + $"⭐ Score: {score:F1}/100\n" + $"🔍 Score Analysis: {backtest.ScoreMessage}\n" + $"🏆 Win Rate: {winRate:F1}%\n" + $"📊 Total Trades: {tradeCount}\n" + $"💰 Final PnL: ${finalPnl:F2}\n" + $"📈 Growth: {growthPercentage:F1}%\n" + $"📉 Max Drawdown: ${maxDrawdown:N}\n" + $"📊 Sharpe Ratio: {sharpeRatio:F2}\n\n" + $"🆔 Backtest ID: {backtest.Id}"; return message; } private string BuildGeneticAlgorithmMessage(GeneticRequest request, double bestFitness, object? bestChromosome) { var duration = request.CompletedAt.HasValue ? request.CompletedAt.Value - request.CreatedAt : TimeSpan.Zero; var indicators = request.EligibleIndicators.Any() ? string.Join(", ", request.EligibleIndicators.Select(i => i.ToString())) : "N/A"; if (request.EligibleIndicators.Count > 5) { indicators += $" (+{request.EligibleIndicators.Count - 5} more)"; } var message = $"🧬 Genetic Algorithm Completed! 🧬\n\n" + $"🔹 Symbol: {request.Ticker}\n" + $"⏱️ Timeframe: {request.Timeframe}\n" + $"👥 Population: {request.PopulationSize}\n" + $"🔄 Generations: {request.Generations}\n" + $"🎯 Selection: {request.SelectionMethod}\n" + $"🔀 Crossover: {request.CrossoverMethod}\n" + $"🧬 Mutation: {request.MutationMethod}\n" + $"🧩 Indicators: {indicators}\n" + $"📅 Period: {request.StartDate:yyyy-MM-dd} to {request.EndDate:yyyy-MM-dd}\n" + $"📈 Results:\n" + $"⭐ Best Fitness: {bestFitness:F4}\n" + $"⏱️ Duration: {duration.TotalMinutes:F1} minutes\n" + $"🆔 Request ID: {request.RequestId[..8]}..."; if (request.BestFitnessSoFar.HasValue && request.BestFitnessSoFar.Value > 0) { message += $"\n🏆 Best Fitness So Far: {request.BestFitnessSoFar.Value:F4}"; } return message; } }