336 lines
12 KiB
C#
336 lines
12 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 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) == true)
|
|
{
|
|
message += $"🎯 TP2 Hit: {position.TakeProfit2.Price} \n";
|
|
}
|
|
|
|
var pnl = position.ProfitAndLoss?.Net ?? 0;
|
|
var pnlEmoji = pnl >= 0 ? "✅" : "❌";
|
|
message += $"💰 PNL : {pnlEmoji} {pnl:F2} $";
|
|
|
|
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<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}");
|
|
}
|
|
}
|
|
|
|
public async Task SendDebugMessage(string message)
|
|
{
|
|
await _discordService.SendDebugMessage(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 netPnl = backtest.NetPnl;
|
|
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: {backtest.InitialBalance:C} -> {config.BotTradingBalance:C} ({netPnl: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" +
|
|
$"🏆 Win Rate: {winRate:F1}%\n" +
|
|
$"📊 Total Trades: {tradeCount}\n" +
|
|
$"📈 ROI: {growthPercentage:F1}%\n" +
|
|
$"📉 Max Drawdown: ${maxDrawdown:N}\n" +
|
|
$"📊 Sharpe Ratio: {sharpeRatio:F2}\n\n" +
|
|
$"⭐ Score: {score:F1}/100\n" +
|
|
$"🔍 Score Analysis: {backtest.ScoreMessage}\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;
|
|
}
|
|
} |