using System; using System.Collections.Generic; using FinlyticCore.Dtos.Bot; using FinlyticCore.Dtos.News; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticCore.Dtos.Trading; using FinlyticNotify.Services; using Xunit; namespace FinlyticNotify.Tests.Services; public class NotificationFormatterTests { private readonly NotificationFormatter _formatter = new(); [Fact] public void FormatProposalNotification_LongBuy_FormatsCorrectly() { // Arrange var proposal = new TradeProposalDto( ProposalId: Guid.NewGuid(), UnderlyingIsin: "US67066G1040", Symbol: "NVDA", StrategyKey: "TrendPullbackFvg", Direction: SignalDirection.Buy, QualityScore: 88m, CompositeScore: 84.5m, CurrentPrice: 125.50m, EntryPrice: 125.00m, InvalidationPrice: 120.00m, ExitPlan: new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: 120.00m, TakeProfitStages: [ new TakeProfitStage(1, 135.00m, 1.0m, 2.0m, "TP1: 100% Exit at 135.00") ] ), SelectedDerivative: new DerivativeSelectionDto( DerivativeIsin: "DE000TEST123", DerivativeWkn: null, Issuer: "HSBC", OptionType: "LONG", Strike: 110.0m, Barrier: 110.0m, Leverage: 8.2m, SafetyBufferPercent: 0.08m, SpreadPercentage: 0.005m, Size: 100m ), AiValidation: new AiValidationResultDto( IsApproved: true, Confidence: 0.85m, Source: ValidationSource.Ai, ThesisSummary: "Bruch des lokalen Abwärtstrends mit starkem Volumen und positiver Halbleiter-Sektor-Dynamik.", InvalidationReason: "", KeyCatalysts: ["Earnings Momentum"], IdentifiedRisks: ["Allgemeine Marktvolatilität"] ), CreatedAtUtc: DateTime.UtcNow, ExpiresAtUtc: DateTime.UtcNow.AddHours(4) ); // Act var notification = _formatter.FormatProposalNotification(proposal, "finlytic_broadcast"); // Assert Assert.Equal("finlytic_broadcast", notification.Topic); Assert.Contains("🟢 Neuer Trade-Vorschlag: NVDA (Long)", notification.Title); Assert.Equal(4, notification.Priority); // Score >= 80 -> Priority 4 Assert.Contains("chart_with_upwards_trend", notification.Tags!); Assert.Contains("moneybag", notification.Tags!); Assert.Contains("**Strategie:** TrendPullbackFvg", notification.Message); Assert.Contains("125,00", notification.Message.Replace('.', ',')); Assert.Contains("HSBC", notification.Message); Assert.Contains("Bruch des lokalen Abwärtstrends", notification.Message); } [Fact] public void FormatProposalNotification_ShortSell_FormatsCorrectly() { // Arrange var proposal = new TradeProposalDto( ProposalId: Guid.NewGuid(), UnderlyingIsin: "US88160R1014", Symbol: "TSLA", StrategyKey: "SmcLiquiditySweep", Direction: SignalDirection.Sell, QualityScore: 74m, CompositeScore: 72.0m, CurrentPrice: 210.00m, EntryPrice: 209.50m, InvalidationPrice: 216.00m, ExitPlan: new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: 216.00m, TakeProfitStages: [new TakeProfitStage(1, 196.50m, 1.0m, 2.0m, "TP1")] ), SelectedDerivative: null, AiValidation: new AiValidationResultDto( IsApproved: true, Confidence: null, Source: ValidationSource.RuleBased, ThesisSummary: "Bärischer Liquidity Sweep über dem Vortagshoch mit starker Ablehnung.", InvalidationReason: "", KeyCatalysts: [], IdentifiedRisks: [] ), CreatedAtUtc: DateTime.UtcNow, ExpiresAtUtc: DateTime.UtcNow.AddHours(4) ); // Act var notification = _formatter.FormatProposalNotification(proposal, "finlytic_broadcast"); // Assert Assert.Equal("finlytic_broadcast", notification.Topic); Assert.Contains("🔴 Neuer Trade-Vorschlag: TSLA (Short)", notification.Title); Assert.Equal(3, notification.Priority); // Score < 80 -> Priority 3 Assert.Contains("chart_with_downwards_trend", notification.Tags!); } [Fact] public void FormatTradeStatusNotification_Tp1Hit_FormatsCorrectlyForUser() { // Arrange var trade = new ActiveTradeDto( TradeId: Guid.NewGuid(), ProposalId: Guid.NewGuid(), UserId: Guid.NewGuid(), UnderlyingIsin: "US0378331005", Symbol: "AAPL", DerivativeIsin: null, DerivativeWkn: null, ExecutionMode: ExecutionMode.ManualTradeRepublic, InstrumentType: InstrumentCategoryType.Stock, Direction: SignalDirection.Buy, Status: TradeStatus.Tp1Hit, AverageBuyIn: 150.00m, TotalQuantity: 10m, InitialStopLoss: 145.00m, CurrentStopLoss: 151.00m, CurrentPrice: 160.00m, UnrealizedPnlEur: 100.00m, UnrealizedPnlPercent: 6.67m, RealizedPnlEur: 0m, ExitPlan: new ExitPlan(ExitStrategyType.StagedScaleOutWithBreakEven, 145.00m, []), Fills: [], OpenedAtUtc: DateTime.UtcNow.AddDays(-1), ClosedAtUtc: null ); // Act var notification = _formatter.FormatTradeStatusNotification(trade, "finlytic_lars"); // Assert Assert.Equal("finlytic_lars", notification.Topic); Assert.Contains("🎯 Teilgewinn erreicht (TP1): AAPL", notification.Title); Assert.Equal(4, notification.Priority); Assert.Contains("tada", notification.Tags!); Assert.Contains("Break-Even gesichert", notification.Message); } [Fact] public void FormatTradeStatusNotification_StoppedOut_FormatsCorrectlyForUser() { // Arrange var trade = new ActiveTradeDto( TradeId: Guid.NewGuid(), ProposalId: Guid.NewGuid(), UserId: Guid.NewGuid(), UnderlyingIsin: "US0378331005", Symbol: "AAPL", DerivativeIsin: null, DerivativeWkn: null, ExecutionMode: ExecutionMode.ManualTradeRepublic, InstrumentType: InstrumentCategoryType.Stock, Direction: SignalDirection.Buy, Status: TradeStatus.StoppedOut, AverageBuyIn: 150.00m, TotalQuantity: 10m, InitialStopLoss: 145.00m, CurrentStopLoss: 145.00m, CurrentPrice: 144.50m, UnrealizedPnlEur: 0m, UnrealizedPnlPercent: 0m, RealizedPnlEur: -55.00m, ExitPlan: new ExitPlan(ExitStrategyType.FixedSingleTarget, 145.00m, []), Fills: [], OpenedAtUtc: DateTime.UtcNow.AddDays(-1), ClosedAtUtc: DateTime.UtcNow ); // Act var notification = _formatter.FormatTradeStatusNotification(trade, "finlytic_john"); // Assert Assert.Equal("finlytic_john", notification.Topic); Assert.Contains("🛑 Stop-Loss ausgelöst: AAPL", notification.Title); Assert.Equal(4, notification.Priority); Assert.Contains("warning", notification.Tags!); Assert.Contains("risikokontrolliert geschlossen", notification.Message); } [Fact] public void FormatBotTradeNotification_Active_FormatsCorrectly() { // Arrange var botTrade = new BotTradeOrderDto( OrderId: Guid.NewGuid(), ProposalId: Guid.NewGuid(), Isin: "US5949181045", Symbol: "MSFT", Venue: BotExecutionVenue.AlpacaPaperTrading, AlpacaOrderId: "alpaca_123", ClientOrderId: "client_123", Direction: SignalDirection.Buy, RequestedQuantity: 5m, FilledQuantity: 5m, EntryPrice: 420.00m, AverageBuyIn: 420.50m, InitialStopLoss: 410.00m, CurrentStopLoss: 410.00m, TakeProfit1: 440.00m, TakeProfit2: 460.00m, CurrentPrice: 425.00m, UnrealizedPnlEur: 22.50m, RealizedPnlEur: 0m, Status: BotPositionStatus.Active, ExitPlan: new ExitPlan(ExitStrategyType.FixedSingleTarget, 410.00m, []), CreatedAtUtc: DateTime.UtcNow, FilledAtUtc: DateTime.UtcNow, ClosedAtUtc: null ); // Act var notification = _formatter.FormatBotTradeNotification(botTrade, "finlytic_bot"); // Assert Assert.Equal("finlytic_bot", notification.Topic); Assert.Contains("🤖 Bot Trade [Active]: MSFT (Long)", notification.Title); Assert.Contains("robot", notification.Tags!); } [Fact] public void FormatNewsNotification_PositiveSentiment_FormatsCorrectly() { // Arrange var article = new NewsArticleDto { Id = Guid.NewGuid(), Title = "NVIDIA Reports Record Q4 Revenue Driven by AI Chip Demand", Summary = "NVIDIA exceeded analyst expectations across data center and gaming segments.", PublishedAt = DateTime.UtcNow, SourceUrl = "https://example.com/news/nvda-q4", Sentiment = "POSITIVE", SentimentScore = 0.88, Confidence = 0.92, MatchedAssets = [ new MatchedAssetDto { Name = "NVIDIA Corp.", Isin = "US67066G1040" } ] }; // Act var notification = _formatter.FormatNewsNotification(article, "finlytic_news"); // Assert Assert.Equal("finlytic_news", notification.Topic); Assert.Contains("🟢 News (POSITIVE): NVIDIA Corp.", notification.Title); Assert.Equal(4, notification.Priority); // High confidence + high positive score -> Priority 4 Assert.Contains("newspaper", notification.Tags!); Assert.Contains("chart_with_upwards_trend", notification.Tags!); Assert.Contains("NVIDIA Reports Record Q4 Revenue", notification.Message); Assert.Contains("0.88", notification.Message); Assert.Equal("https://example.com/news/nvda-q4", notification.ClickUrl); } [Fact] public void FormatNewsNotification_NegativeSentiment_FormatsCorrectly() { // Arrange var article = new NewsArticleDto { Id = Guid.NewGuid(), Title = "Tesla Faces Supply Chain Delays and Reduced Delivery Targets", Summary = "Tesla lowers annual guidance following factory shutdowns.", PublishedAt = DateTime.UtcNow, SourceUrl = "https://example.com/news/tsla-delays", Sentiment = "NEGATIVE", SentimentScore = -0.75, Confidence = 0.85, MatchedAssets = [ new MatchedAssetDto { Name = "Tesla Inc.", Isin = "US88160R1014" } ] }; // Act var notification = _formatter.FormatNewsNotification(article, "finlytic_news"); // Assert Assert.Equal("finlytic_news", notification.Topic); Assert.Contains("🔴 News (NEGATIVE): Tesla Inc.", notification.Title); Assert.Equal(4, notification.Priority); Assert.Contains("newspaper", notification.Tags!); Assert.Contains("chart_with_downwards_trend", notification.Tags!); Assert.Contains("Tesla Faces Supply Chain Delays", notification.Message); Assert.Contains("-0.75", notification.Message); Assert.Equal("https://example.com/news/tsla-delays", notification.ClickUrl); } }