feat(notify): add FinlyticNotify push notification microservice and test suite
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user