using System.Globalization; using System.Linq; using System.Text; using FinlyticCore.Dtos.Bot; using FinlyticCore.Dtos.News; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticCore.Dtos.Trading; namespace FinlyticNotify.Services; /// /// Service interface for transforming trading and news events into structured ntfy push notifications. /// public interface INotificationFormatter { /// /// Formats a new trade proposal into a high-priority opportunity notification. /// NtfyNotification FormatProposalNotification(TradeProposalDto proposal, string targetTopic); /// /// Formats an active trade lifecycle change into a user-specific status notification. /// NtfyNotification FormatTradeStatusNotification(ActiveTradeDto trade, string targetTopic); /// /// Formats an automated paper-trading bot execution event into a notification. /// NtfyNotification FormatBotTradeNotification(BotTradeOrderDto botTrade, string targetTopic); /// /// Formats an analyzed news article with sentiment evaluation into a push notification. /// NtfyNotification FormatNewsNotification(NewsArticleDto article, string targetTopic); } /// /// Implementation of that creates emoji-rich Markdown messages /// formatted for the ntfy mobile/web applications. /// public class NotificationFormatter : INotificationFormatter { /// public NtfyNotification FormatProposalNotification(TradeProposalDto proposal, string targetTopic) { bool isBuy = proposal.Direction == SignalDirection.Buy; string dirEmoji = isBuy ? "🟢" : "🔴"; string dirText = isBuy ? "Long" : "Short"; string title = $"{dirEmoji} Neuer Trade-Vorschlag: {proposal.Symbol} ({dirText})"; var tags = new List { isBuy ? "chart_with_upwards_trend" : "chart_with_downwards_trend", "moneybag", "dart" }; int priority = proposal.CompositeScore >= 80m ? 4 : 3; var sb = new StringBuilder(); sb.AppendLine($"**Strategie:** {proposal.StrategyKey} | **Score:** {proposal.CompositeScore:F1}/100"); sb.AppendLine($"**Einstieg:** {proposal.EntryPrice:F2} €"); sb.AppendLine($"**Stop-Loss:** {proposal.InvalidationPrice:F2} €"); var tp1 = proposal.ExitPlan?.TakeProfitStages?.FirstOrDefault(); if (tp1 != null) { sb.AppendLine($"**Ziel (TP1):** {tp1.TargetPrice:F2} € ({tp1.Description})"); } if (proposal.SelectedDerivative != null) { sb.AppendLine($"**Knock-Out:** {proposal.SelectedDerivative.Issuer} ({proposal.SelectedDerivative.OptionType}, Hebel: {proposal.SelectedDerivative.Leverage:F1}x)"); } if (!string.IsNullOrWhiteSpace(proposal.AiValidation?.ThesisSummary)) { sb.AppendLine(); sb.AppendLine($"**KI-These:** {proposal.AiValidation.ThesisSummary}"); } return new NtfyNotification( Topic: targetTopic, Title: title, Message: sb.ToString().TrimEnd(), Priority: priority, Tags: tags ); } /// public NtfyNotification FormatTradeStatusNotification(ActiveTradeDto trade, string targetTopic) { string dirText = trade.Direction == SignalDirection.Buy ? "Long" : "Short"; return trade.Status switch { TradeStatus.Active or TradeStatus.Proposed => new NtfyNotification( Topic: targetTopic, Title: $"⚡ Trade aktiv: {trade.Symbol} ({dirText})", Message: $"**Buy-In:** {trade.AverageBuyIn:F2} € | **Menge:** {trade.TotalQuantity:F2}\n" + $"**Initialer Stop-Loss:** {trade.InitialStopLoss:F2} €\n" + $"**Aktueller Kurs:** {trade.CurrentPrice:F2} €", Priority: 3, Tags: ["zap", "white_check_mark"] ), TradeStatus.Tp1Hit => new NtfyNotification( Topic: targetTopic, Title: $"🎯 Teilgewinn erreicht (TP1): {trade.Symbol} (+{trade.UnrealizedPnlPercent:F1}%)", Message: $"**Gewinn:** +{trade.UnrealizedPnlEur:F2} € (+{trade.UnrealizedPnlPercent:F1}%)\n" + $"**Aktueller Kurs:** {trade.CurrentPrice:F2} € (Buy-In: {trade.AverageBuyIn:F2} €)\n" + $"**Neuer Stop-Loss:** {trade.CurrentStopLoss:F2} € (Break-Even gesichert)", Priority: 4, Tags: ["tada", "dart", "chart_with_upwards_trend"] ), TradeStatus.Tp2Hit => new NtfyNotification( Topic: targetTopic, Title: $"🏆 Vollziel erreicht (TP2): {trade.Symbol} (+{trade.RealizedPnlEur:F2} €)", Message: $"**Realisierter Gewinn:** +{trade.RealizedPnlEur:F2} €\n" + $"**Schlusskurs:** {trade.CurrentPrice:F2} € (Buy-In: {trade.AverageBuyIn:F2} €)\n" + $"**Status:** Trade erfolgreich mit Maximalziel abgeschlossen!", Priority: 4, Tags: ["trophy", "money_with_wings", "star2"] ), TradeStatus.StoppedOut => new NtfyNotification( Topic: targetTopic, Title: $"🛑 Stop-Loss ausgelöst: {trade.Symbol} ({trade.RealizedPnlEur:F2} €)", Message: $"**Verlust:** {trade.RealizedPnlEur:F2} €\n" + $"**Ausstiegskurs:** {trade.CurrentPrice:F2} € (Stop war bei {trade.CurrentStopLoss:F2} €)\n" + $"**Status:** Position durch Stop-Loss risikokontrolliert geschlossen.", Priority: 4, Tags: ["octagonal_sign", "warning", "shield"] ), TradeStatus.Closed => new NtfyNotification( Topic: targetTopic, Title: $"🏁 Trade geschlossen: {trade.Symbol} (G/V: {trade.RealizedPnlEur:F2} €)", Message: $"**Realisierter G/V:** {trade.RealizedPnlEur:F2} €\n" + $"**Schlusskurs:** {trade.CurrentPrice:F2} € (Buy-In: {trade.AverageBuyIn:F2} €)", Priority: 3, Tags: ["checkered_flag", "information_source"] ), _ => new NtfyNotification( Topic: targetTopic, Title: $"🛡️ Trade Update: {trade.Symbol} ({trade.Status})", Message: $"**Aktueller Stop-Loss:** {trade.CurrentStopLoss:F2} €\n" + $"**Aktueller Kurs:** {trade.CurrentPrice:F2} € (Buy-In: {trade.AverageBuyIn:F2} €)\n" + $"**Unrealisierter G/V:** {trade.UnrealizedPnlEur:F2} € ({trade.UnrealizedPnlPercent:F1}%)", Priority: 2, Tags: ["shield", "chart"] ) }; } /// public NtfyNotification FormatBotTradeNotification(BotTradeOrderDto botTrade, string targetTopic) { string dirText = botTrade.Direction == SignalDirection.Buy ? "Long" : "Short"; string title = $"🤖 Bot Trade [{botTrade.Status}]: {botTrade.Symbol} ({dirText})"; var tags = new List { "robot", "chart" }; if (botTrade.Status == BotPositionStatus.Tp1Hit || botTrade.Status == BotPositionStatus.Tp2Hit) tags.Add("dart"); if (botTrade.Status == BotPositionStatus.StoppedOut) tags.Add("warning"); var sb = new StringBuilder(); sb.AppendLine($"**Venue:** {botTrade.Venue} | **Status:** {botTrade.Status}"); sb.AppendLine($"**Buy-In:** {botTrade.AverageBuyIn:F2} € | **Menge:** {botTrade.FilledQuantity:F2}"); sb.AppendLine($"**Stop-Loss:** {botTrade.CurrentStopLoss:F2} €"); sb.AppendLine($"**Aktueller Kurs:** {botTrade.CurrentPrice:F2} €"); if (botTrade.Status == BotPositionStatus.Closed || botTrade.Status == BotPositionStatus.StoppedOut || botTrade.Status == BotPositionStatus.Tp2Hit) { sb.AppendLine($"**Realisierter G/V:** {botTrade.RealizedPnlEur:F2} €"); } else { sb.AppendLine($"**Unrealisierter G/V:** {botTrade.UnrealizedPnlEur:F2} €"); } return new NtfyNotification( Topic: targetTopic, Title: title, Message: sb.ToString().TrimEnd(), Priority: 3, Tags: tags ); } /// public NtfyNotification FormatNewsNotification(NewsArticleDto article, string targetTopic) { string sentimentLabel = (article.Sentiment ?? "NEUTRAL").ToUpperInvariant(); double score = article.SentimentScore ?? 0.0; double confidence = article.Confidence ?? 0.0; string sentimentEmoji = sentimentLabel switch { "POSITIVE" => "🟢", "NEGATIVE" => "🔴", _ => "⚪" }; string primaryAsset = article.MatchedAssets?.FirstOrDefault()?.Name ?? article.MatchedAssets?.FirstOrDefault()?.Isin ?? "Markt"; string title = $"{sentimentEmoji} News ({sentimentLabel}): {primaryAsset}"; var tags = new List { "newspaper" }; if (sentimentLabel == "POSITIVE") { tags.Add("chart_with_upwards_trend"); tags.Add("tada"); } else if (sentimentLabel == "NEGATIVE") { tags.Add("chart_with_downwards_trend"); tags.Add("warning"); } else { tags.Add("information_source"); } int priority = (confidence >= 0.8 && Math.Abs(score) >= 0.6) ? 4 : 3; var sb = new StringBuilder(); sb.AppendLine($"**{article.Title}**"); sb.AppendLine(); sb.AppendLine($"**Sentiment:** {sentimentLabel} (Score: {score:+0.00;-0.00;0.00} | Konfidenz: {confidence:P0})"); if (article.MatchedAssets != null && article.MatchedAssets.Count > 0) { var assetList = string.Join(", ", article.MatchedAssets.Select(a => $"{a.Name} ({a.Isin})")); sb.AppendLine($"**Assets:** {assetList}"); } if (!string.IsNullOrWhiteSpace(article.Summary)) { sb.AppendLine(); sb.AppendLine($"_{article.Summary}_"); } sb.AppendLine(); sb.AppendLine($"**Veröffentlicht:** {article.PublishedAt:dd.MM.yyyy HH:mm} UTC"); return new NtfyNotification( Topic: targetTopic, Title: title, Message: sb.ToString().TrimEnd(), Priority: priority, Tags: tags, ClickUrl: !string.IsNullOrWhiteSpace(article.SourceUrl) ? article.SourceUrl : null ); } }