Files
managing-apps/src/Managing.Application/Shared/MessengerService.cs

321 lines
13 KiB
C#

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 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 = $"🎯 Position {status}\n" +
$"Symbol: {position.Ticker}\n" +
$"Direction: {direction}\n" +
$"Identifier: {position.Identifier}\n" +
$"Initiator: {position.Initiator}\n" +
$"Date: {position.Date:yyyy-MM-dd HH:mm:ss}";
if (position.Open != null)
{
message += $"\nOpen Trade: {position.Open.Quantity} @ {position.Open.Price:F4}";
}
if (position.ProfitAndLoss != null)
{
var pnlEmoji = position.ProfitAndLoss.Realized >= 0 ? "✅" : "❌";
message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Realized:F2}";
}
if (!string.IsNullOrEmpty(position.SignalIdentifier))
{
message += $"\nSignal: {position.SignalIdentifier}";
}
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<Trader> traders)
{
await _discordService.SendBestTraders(traders);
}
public async Task SendBadTraders(List<Trader> 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 BuildBacktestConfigMessage(Backtest backtest)
{
var config = backtest.Config;
// 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.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";
return $"🚀 Excellent Backtest Results! 🚀\n\n" +
$"🔹 {config.Ticker} | ⏱️ {config.Timeframe}\n" +
$"💰 {config.BotTradingBalance:C}\n" +
$"🛡️ SL: {mmSl}% | 🎯 TP: {mmTp}% | 📈 Lev: {mmLev}x\n" +
$"🧩 {indicators}\n" +
$"📅 {backtest.StartDate:yyyy-MM-dd} to {backtest.EndDate:yyyy-MM-dd}";
}
private string BuildBacktestResultsMessage(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?.MaxDrawdownPc ?? 0;
var sharpeRatio = (backtest.Statistics?.SharpeRatio * 100) ?? 0;
return $"📈 Performance Metrics:\n" +
$"⭐ Score: {score:F1}/100 | 🏆 Win Rate: {winRate:F1}%\n" +
$"📊 Trades: {tradeCount} | 💰 PnL: ${finalPnl:F2}\n" +
$"📈 Growth: {growthPercentage:F1}% | 📉 DD: {maxDrawdown:F1}%\n" +
$"📊 Sharpe: {sharpeRatio:F2}\n\n" +
$"🆔 ID: {backtest.Id}";
}
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;
}
}