diff --git a/FinlyticCore/Dtos/Fundamentals/AssetFundamentalsDto.cs b/FinlyticCore/Dtos/Fundamentals/AssetFundamentalsDto.cs new file mode 100644 index 0000000..d92c0a3 --- /dev/null +++ b/FinlyticCore/Dtos/Fundamentals/AssetFundamentalsDto.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Fundamentals; + +/// +/// Data transfer object representing the complete fundamental analysis dataset of an asset. +/// +public record AssetFundamentalsDto +{ + [JsonPropertyName("isin")] + public string Isin { get; init; } = string.Empty; + [JsonPropertyName("primaryTicker")] + public string PrimaryTicker { get; init; } = string.Empty; + [JsonPropertyName("ticker")] + public string Ticker { get; init; } = string.Empty; + [JsonPropertyName("companyName")] + public string CompanyName { get; init; } = string.Empty; + [JsonPropertyName("exchange")] + public string? Exchange { get; init; } + [JsonPropertyName("tradingCurrency")] + public string? TradingCurrency { get; init; } + [JsonPropertyName("businessSummary")] + public string? BusinessSummary { get; init; } + [JsonPropertyName("sector")] + public string? Sector { get; init; } + [JsonPropertyName("industry")] + public string? Industry { get; init; } + [JsonPropertyName("country")] + public string? Country { get; init; } + [JsonPropertyName("employees")] + public int? Employees { get; init; } + + // Valuation Metrics (derived from Primary Ticker) + [JsonPropertyName("currentPrice")] + public decimal CurrentPrice { get; init; } + [JsonPropertyName("dayChangeAbsolute")] + public decimal DayChangeAbsolute { get; init; } + [JsonPropertyName("dayChangePercent")] + public decimal DayChangePercent { get; init; } + [JsonPropertyName("fiftyTwoWeekHigh")] + public decimal FiftyTwoWeekHigh { get; init; } + [JsonPropertyName("fiftyTwoWeekLow")] + public decimal FiftyTwoWeekLow { get; init; } + [JsonPropertyName("marketCapitalization")] + public decimal MarketCapitalization { get; init; } + [JsonPropertyName("enterpriseValue")] + public decimal EnterpriseValue { get; init; } + [JsonPropertyName("peRatioTrailing")] + public decimal? PeRatioTrailing { get; init; } + [JsonPropertyName("peRatioForward")] + public decimal? PeRatioForward { get; init; } + [JsonPropertyName("pegRatio")] + public decimal? PegRatio { get; init; } + [JsonPropertyName("pbRatio")] + public decimal? PbRatio { get; init; } + [JsonPropertyName("psRatio")] + public decimal? PsRatio { get; init; } + [JsonPropertyName("evToEbitda")] + public decimal? EvToEbitda { get; init; } + [JsonPropertyName("evToRevenue")] + public decimal? EvToRevenue { get; init; } + + // Financial Health & Leverage + [JsonPropertyName("grossMargin")] + public decimal? GrossMargin { get; init; } + [JsonPropertyName("operatingMargin")] + public decimal? OperatingMargin { get; init; } + [JsonPropertyName("netProfitMargin")] + public decimal? NetProfitMargin { get; init; } + [JsonPropertyName("returnOnEquity")] + public decimal? ReturnOnEquity { get; init; } + [JsonPropertyName("returnOnAssets")] + public decimal? ReturnOnAssets { get; init; } + [JsonPropertyName("returnOnInvestedCapital")] + public decimal? ReturnOnInvestedCapital { get; init; } + [JsonPropertyName("debtToEquity")] + public decimal? DebtToEquity { get; init; } + [JsonPropertyName("currentRatio")] + public decimal? CurrentRatio { get; init; } + [JsonPropertyName("quickRatio")] + public decimal? QuickRatio { get; init; } + [JsonPropertyName("interestCoverage")] + public decimal? InterestCoverage { get; init; } + + // Dividends & Ownership + [JsonPropertyName("dividendYield")] + public decimal? DividendYield { get; init; } + [JsonPropertyName("payoutRatio")] + public decimal? PayoutRatio { get; init; } + [JsonPropertyName("exDividendDate")] + public DateTime? ExDividendDate { get; init; } + [JsonPropertyName("nextEarningsDate")] + public DateTime? NextEarningsDate { get; init; } + [JsonPropertyName("percentHeldByInstitutions")] + public decimal? PercentHeldByInstitutions { get; init; } + [JsonPropertyName("percentHeldByInsiders")] + public decimal? PercentHeldByInsiders { get; init; } + [JsonPropertyName("shortRatio")] + public decimal? ShortRatio { get; init; } + [JsonPropertyName("shortPercentOfFloat")] + public decimal? ShortPercentOfFloat { get; init; } + + // Forecasts + [JsonPropertyName("consensusRating")] + public string? ConsensusRating { get; init; } + [JsonPropertyName("priceTargetLow")] + public decimal? PriceTargetLow { get; init; } + [JsonPropertyName("priceTargetHigh")] + public decimal? PriceTargetHigh { get; init; } + [JsonPropertyName("priceTargetMedian")] + public decimal? PriceTargetMedian { get; init; } + [JsonPropertyName("priceTargetMean")] + public decimal? PriceTargetMean { get; init; } + + // Timestamps + [JsonPropertyName("lastUpdatedAt")] + public DateTime LastUpdatedAt { get; init; } + + // Relational Collections + [JsonPropertyName("executives")] + public List Executives { get; init; } = []; + [JsonPropertyName("financialStatements")] + public List FinancialStatements { get; init; } = []; + [JsonPropertyName("estimates")] + public List Estimates { get; init; } = []; + [JsonPropertyName("availableTickers")] + public List AvailableTickers { get; init; } = []; +} + +public record CompanyExecutiveDto +{ + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + [JsonPropertyName("title")] + public string Title { get; init; } = string.Empty; + [JsonPropertyName("age")] + public int? Age { get; init; } + [JsonPropertyName("compensation")] + public decimal? Compensation { get; init; } +} + +public record FinancialStatementDto +{ + [JsonPropertyName("periodType")] + public string PeriodType { get; init; } = string.Empty; // "Annual" or "Quarterly" + [JsonPropertyName("endDate")] + public DateTime EndDate { get; init; } + + // Income Statement + [JsonPropertyName("totalRevenue")] + public decimal? TotalRevenue { get; init; } + [JsonPropertyName("costOfRevenue")] + public decimal? CostOfRevenue { get; init; } + [JsonPropertyName("grossProfit")] + public decimal? GrossProfit { get; init; } + [JsonPropertyName("operatingExpenses")] + public decimal? OperatingExpenses { get; init; } + [JsonPropertyName("operatingIncome")] + public decimal? OperatingIncome { get; init; } + [JsonPropertyName("ebitda")] + public decimal? Ebitda { get; init; } + [JsonPropertyName("netIncome")] + public decimal? NetIncome { get; init; } + [JsonPropertyName("epsBasic")] + public decimal? EpsBasic { get; init; } + [JsonPropertyName("epsDiluted")] + public decimal? EpsDiluted { get; init; } + + // Balance Sheet + [JsonPropertyName("cashAndCashEquivalents")] + public decimal? CashAndCashEquivalents { get; init; } + [JsonPropertyName("accountsReceivable")] + public decimal? AccountsReceivable { get; init; } + [JsonPropertyName("inventory")] + public decimal? Inventory { get; init; } + [JsonPropertyName("totalCurrentAssets")] + public decimal? TotalCurrentAssets { get; init; } + [JsonPropertyName("totalNonCurrentAssets")] + public decimal? TotalNonCurrentAssets { get; init; } + [JsonPropertyName("currentLiabilities")] + public decimal? CurrentLiabilities { get; init; } + [JsonPropertyName("longTermDebt")] + public decimal? LongTermDebt { get; init; } + [JsonPropertyName("totalLiabilities")] + public decimal? TotalLiabilities { get; init; } + [JsonPropertyName("totalStockholdersEquity")] + public decimal? TotalStockholdersEquity { get; init; } + + // Cash Flow Statement + [JsonPropertyName("operatingCashFlow")] + public decimal? OperatingCashFlow { get; init; } + [JsonPropertyName("investingCashFlow")] + public decimal? InvestingCashFlow { get; init; } + [JsonPropertyName("capitalExpenditures")] + public decimal? CapitalExpenditures { get; init; } + [JsonPropertyName("financingCashFlow")] + public decimal? FinancingCashFlow { get; init; } + [JsonPropertyName("freeCashFlow")] + public decimal? FreeCashFlow { get; init; } // OperatingCashFlow - CapEx +} + +public record ForwardEstimateDto +{ + [JsonPropertyName("period")] + public string Period { get; init; } = string.Empty; // "CurrentQuarter", "NextQuarter", "CurrentYear", "NextYear" + [JsonPropertyName("expectedRevenue")] + public decimal? ExpectedRevenue { get; init; } + [JsonPropertyName("expectedEps")] + public decimal? ExpectedEps { get; init; } + [JsonPropertyName("expectedGrowthRate")] + public decimal? ExpectedGrowthRate { get; init; } +} + +public record TickerDto +{ + [JsonPropertyName("ticker")] + public string Ticker { get; init; } = string.Empty; + [JsonPropertyName("exchange")] + public string? Exchange { get; init; } + [JsonPropertyName("tradingCurrency")] + public string? TradingCurrency { get; init; } + [JsonPropertyName("currentPrice")] + public decimal CurrentPrice { get; init; } + [JsonPropertyName("dayChangeAbsolute")] + public decimal DayChangeAbsolute { get; init; } + [JsonPropertyName("dayChangePercent")] + public decimal DayChangePercent { get; init; } + [JsonPropertyName("fiftyTwoWeekHigh")] + public decimal FiftyTwoWeekHigh { get; init; } + [JsonPropertyName("fiftyTwoWeekLow")] + public decimal FiftyTwoWeekLow { get; init; } + [JsonPropertyName("marketCapitalization")] + public decimal MarketCapitalization { get; init; } + [JsonPropertyName("enterpriseValue")] + public decimal EnterpriseValue { get; init; } + + [JsonPropertyName("peRatioTrailing")] + public decimal? PeRatioTrailing { get; init; } + [JsonPropertyName("peRatioForward")] + public decimal? PeRatioForward { get; init; } + [JsonPropertyName("pegRatio")] + public decimal? PegRatio { get; init; } + [JsonPropertyName("pbRatio")] + public decimal? PbRatio { get; init; } + [JsonPropertyName("psRatio")] + public decimal? PsRatio { get; init; } + [JsonPropertyName("evToEbitda")] + public decimal? EvToEbitda { get; init; } + [JsonPropertyName("evToRevenue")] + public decimal? EvToRevenue { get; init; } + + [JsonPropertyName("grossMargin")] + public decimal? GrossMargin { get; init; } + [JsonPropertyName("operatingMargin")] + public decimal? OperatingMargin { get; init; } + [JsonPropertyName("netProfitMargin")] + public decimal? NetProfitMargin { get; init; } + [JsonPropertyName("returnOnEquity")] + public decimal? ReturnOnEquity { get; init; } + [JsonPropertyName("returnOnAssets")] + public decimal? ReturnOnAssets { get; init; } + [JsonPropertyName("returnOnInvestedCapital")] + public decimal? ReturnOnInvestedCapital { get; init; } + [JsonPropertyName("debtToEquity")] + public decimal? DebtToEquity { get; init; } + [JsonPropertyName("currentRatio")] + public decimal? CurrentRatio { get; init; } + [JsonPropertyName("quickRatio")] + public decimal? QuickRatio { get; init; } + [JsonPropertyName("interestCoverage")] + public decimal? InterestCoverage { get; init; } + + [JsonPropertyName("dividendYield")] + public decimal? DividendYield { get; init; } + [JsonPropertyName("payoutRatio")] + public decimal? PayoutRatio { get; init; } + [JsonPropertyName("exDividendDate")] + public DateTime? ExDividendDate { get; init; } +} diff --git a/FinlyticCore/Dtos/Fundamentals/CorporateEventDto.cs b/FinlyticCore/Dtos/Fundamentals/CorporateEventDto.cs new file mode 100644 index 0000000..4af8f54 --- /dev/null +++ b/FinlyticCore/Dtos/Fundamentals/CorporateEventDto.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Fundamentals; + +/// +/// DTO representing a scheduled corporate event (e.g. Earnings, Ex-Dividend, Dividend Payout). +/// +public record CorporateEventDto +{ + [JsonPropertyName("isin")] + public string Isin { get; init; } = string.Empty; + + [JsonPropertyName("ticker")] + public string Ticker { get; init; } = string.Empty; + + [JsonPropertyName("companyName")] + public string CompanyName { get; init; } = string.Empty; + + [JsonPropertyName("eventType")] + public string EventType { get; init; } = string.Empty; // "Quartalsergebnis", "Ex-Dividendentag", "Dividenden-Zahltag" + + [JsonPropertyName("date")] + public DateTime Date { get; init; } +} diff --git a/FinlyticCore/Dtos/MqttRequestDtos.cs b/FinlyticCore/Dtos/MqttRequestDtos.cs new file mode 100644 index 0000000..70371d9 --- /dev/null +++ b/FinlyticCore/Dtos/MqttRequestDtos.cs @@ -0,0 +1,130 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos; + +/// +/// Generic request payload carrying only a limit parameter (e.g. news_GetPending). +/// +public record LimitRequest( + [property: JsonPropertyName("limit")] int Limit +); + +/// +/// Generic request payload for paginated queries with optional ISIN filter. +/// +public record PaginatedRequest( + [property: JsonPropertyName("limit")] int Limit, + [property: JsonPropertyName("offset")] int Offset, + [property: JsonPropertyName("isin")] string? Isin = null +); + +/// +/// Request payload for daily-news queries with optional filters. +/// +public record DailyNewsRequest( + [property: JsonPropertyName("limit")] int Limit, + [property: JsonPropertyName("offset")] int Offset, + [property: JsonPropertyName("isin")] string? Isin = null, + [property: JsonPropertyName("date")] string? Date = null, + [property: JsonPropertyName("status")] string? Status = null, + [property: JsonPropertyName("query")] string? Query = null, + [property: JsonPropertyName("hasSentiment")] bool? HasSentiment = null +); + + +/// +/// Request payload for fetching fundamentals or technical-analysis data by ISIN. +/// +public record IsinRequest( + [property: JsonPropertyName("isin")] string Isin, + [property: JsonPropertyName("ticker")] string? Ticker = "", + [property: JsonPropertyName("forceRefresh")] bool ForceRefresh = false +); + +/// +/// Request payload for fetching trades filtered by ISIN and/or status. +/// +public record GetTradesRequest( + [property: JsonPropertyName("isin")] string? Isin = null, + [property: JsonPropertyName("status")] string? Status = null, + [property: JsonPropertyName("userId")] string? UserId = null +); + +/// +/// Request payload for fetching sentiment by article ID. +/// +public record ArticleRequest( + [property: JsonPropertyName("articleId")] string ArticleId, + [property: JsonPropertyName("id")] string? Id = null +); + +/// +/// Request payload for triggering a manual sentiment analysis for an article or ISIN. +/// +public record AnalyzeSentimentRequest( + [property: JsonPropertyName("articleId")] string? ArticleId = null, + [property: JsonPropertyName("isin")] string? Isin = null, + [property: JsonPropertyName("forceReload")] bool ForceReload = false +); + +/// +/// Empty request payload for MQTT RPCs that require no parameters (e.g. events_GetAll). +/// +public record EmptyRequest; + +/// +/// Request payload for triggering a manual AI analysis. +/// +public record ManualAnalysisRpcRequest( + [property: JsonPropertyName("isin")] string Isin, + [property: JsonPropertyName("symbol")] string Symbol, + [property: JsonPropertyName("sector")] string Sector, + [property: JsonPropertyName("headline")] string Headline, + [property: JsonPropertyName("currentPrice")] decimal CurrentPrice, + [property: JsonPropertyName("riskScore")] int RiskScore, + [property: JsonPropertyName("minTimeframeValue")] int MinTimeframeValue, + [property: JsonPropertyName("maxTimeframeValue")] int MaxTimeframeValue, + [property: JsonPropertyName("timeframeUnit")] string TimeframeUnit, + [property: JsonPropertyName("instrumentType")] string InstrumentType, + [property: JsonPropertyName("userNotes")] string UserNotes, + [property: JsonPropertyName("taData")] FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? TaData, + [property: JsonPropertyName("fundamentalsData")] FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? FundamentalsData, + [property: JsonPropertyName("sentimentData")] FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? SentimentData +); + + +/// +/// Response payload returned by microservice health pings over MQTT. +/// +public record ServiceHealthResponse( + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("timestamp")] DateTime Timestamp, + [property: JsonPropertyName("dbStatus")] string DbStatus +); + +/// +/// Response payload for assets_FetchLogo RPC request. +/// +public record FetchLogoResponse( + [property: JsonPropertyName("isin")] string? Isin, + [property: JsonPropertyName("path")] string? Path, + [property: JsonPropertyName("success")] bool Success +); + +/// +/// Payload published to MQTT when the Admin Panel updates a microservice's configuration. +/// Replaces the anonymous type to be compatible with AOT/source-gen JSON serialization. +/// +public record ServiceConfigUpdatePayload( + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("timestamp")] DateTime Timestamp, + [property: JsonPropertyName("settings")] Dictionary Settings +); + +/// +/// Payload published to MQTT when a live market tick is received. +/// +public record TickMessageDto( + [property: JsonPropertyName("price")] decimal Price +); diff --git a/FinlyticCore/Dtos/News/DiscoveredArticle.cs b/FinlyticCore/Dtos/News/DiscoveredArticle.cs new file mode 100644 index 0000000..8d574f8 --- /dev/null +++ b/FinlyticCore/Dtos/News/DiscoveredArticle.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.News; + +/// +/// Represents a news article discovered by a feed/page scanner, carrying parsed metadata such as title, summary, publication date, language, and associated ISINs. +/// +public record DiscoveredArticle( + [property: JsonPropertyName("url")] + string Url, + [property: JsonPropertyName("isins")] + List? Isins = null, + [property: JsonPropertyName("title")] + string? Title = null, + [property: JsonPropertyName("summary")] + string? Summary = null, + [property: JsonPropertyName("publishedAt")] + DateTime? PublishedAt = null, + [property: JsonPropertyName("language")] + string? Language = null, + [property: JsonPropertyName("sourceName")] + string? SourceName = null +); diff --git a/FinlyticCore/Dtos/News/MatchedAssetDto.cs b/FinlyticCore/Dtos/News/MatchedAssetDto.cs new file mode 100644 index 0000000..bb9fad1 --- /dev/null +++ b/FinlyticCore/Dtos/News/MatchedAssetDto.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.News; + +/// +/// Data transfer object representing a financial asset matched inside an article. +/// +public record MatchedAssetDto +{ + /// + /// Gets or sets the name of the matched asset. + /// + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + /// + /// Gets or sets the ISIN (International Securities Identification Number) of the matched asset. + /// + [JsonPropertyName("isin")] + public string Isin { get; init; } = string.Empty; +} + diff --git a/FinlyticCore/Dtos/News/N8nPayloads.cs b/FinlyticCore/Dtos/News/N8nPayloads.cs new file mode 100644 index 0000000..397c2bd --- /dev/null +++ b/FinlyticCore/Dtos/News/N8nPayloads.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.News; + +/// +/// Payload representing the pre-filtered asset configuration dispatched to n8n. +/// +public record FilteredAssetPayload( + [property: JsonPropertyName("Name")] string Name, + [property: JsonPropertyName("Isin")] string Isin +); + +/// +/// Webhook payload structure dispatched to the n8n workflow. +/// +public record N8nRequestPayload( + [property: JsonPropertyName("article")] string Article, + [property: JsonPropertyName("filtered_assets")] List FilteredAssets +); + +/// +/// Matched asset returned by the n8n AI workflow classification. +/// +public record N8nMatchedAssetPayload( + [property: JsonPropertyName("ticker")] string? Ticker, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("confidence_score")] double ConfidenceScore +); + +/// +/// Enriched response payload returned by the n8n workflow webhook. +/// +public record N8nResponsePayload( + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("author")] string? Author, + [property: JsonPropertyName("published_at")] string? PublishedAt, + [property: JsonPropertyName("scraped_at")] string? ScrapedAt, + [property: JsonPropertyName("source_url")] string SourceUrl, + [property: JsonPropertyName("summary")] string? Summary, + [property: JsonPropertyName("content_raw")] string ContentRaw, + [property: JsonPropertyName("language")] string? Language, + [property: JsonPropertyName("matched_assets")] List MatchedAssets +); diff --git a/FinlyticCore/Dtos/News/NewsArticleDto.cs b/FinlyticCore/Dtos/News/NewsArticleDto.cs new file mode 100644 index 0000000..7f72a3e --- /dev/null +++ b/FinlyticCore/Dtos/News/NewsArticleDto.cs @@ -0,0 +1,101 @@ +using System.Text.Json.Serialization; +using FinlyticCore.Dtos.Sentiment; + +namespace FinlyticCore.Dtos.News; + +/// +/// Data transfer object representing a parsed and enriched news article with bundled sentiment metrics. +/// +public record NewsArticleDto +{ + /// + /// Gets or sets the unique article identifier. + /// + [JsonPropertyName("id")] + public Guid Id { get; init; } + + /// + /// Gets or sets the title of the article. + /// + [JsonPropertyName("title")] + public string Title { get; init; } = string.Empty; + + /// + /// Gets or sets the author of the article. + /// + [JsonPropertyName("author")] + public string? Author { get; init; } + + /// + /// Gets or sets a brief summary of the article content. + /// + [JsonPropertyName("summary")] + public string? Summary { get; init; } + + /// + /// Gets or sets the raw extracted text content of the article. + /// + [JsonPropertyName("contentRaw")] + public string ContentRaw { get; init; } = string.Empty; + + /// + /// Gets or sets the language of the article (e.g. "en", "de"). + /// + [JsonPropertyName("language")] + public string? Language { get; init; } + + /// + /// Gets or sets the unique source URL of the article. + /// + [JsonPropertyName("sourceUrl")] + public string SourceUrl { get; init; } = string.Empty; + + /// + /// Gets or sets the timestamp when the article was scraped. + /// + [JsonPropertyName("scrapedAt")] + public DateTime ScrapedAt { get; init; } + + /// + /// Gets or sets the publication timestamp of the article. + /// + [JsonPropertyName("publishedAt")] + public DateTime PublishedAt { get; init; } + + /// + /// Gets or sets the list of classified assets referenced in the article. + /// + [JsonPropertyName("matchedAssets")] + public List MatchedAssets { get; init; } = []; + + /// + /// Gets or sets the processing lifecycle state (e.g. "Pending", "Completed", "Analyzed"). + /// + [JsonPropertyName("status")] + public string Status { get; init; } = "Completed"; + + /// + /// Gets or sets the classified sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL"). + /// + [JsonPropertyName("sentiment")] + public string? Sentiment { get; init; } + + /// + /// Gets or sets the compound sentiment score (-1.0 to +1.0). + /// + [JsonPropertyName("sentimentScore")] + public double? SentimentScore { get; init; } + + /// + /// Gets or sets the FinBERT classification confidence score (0.0 to 1.0). + /// + [JsonPropertyName("confidence")] + public double? Confidence { get; init; } + + /// + /// Gets or sets the detailed FinBERT result breakdown. + /// + [JsonPropertyName("finbertResult")] + public FinBertResultDto? FinbertResult { get; init; } +} + diff --git a/FinlyticCore/Dtos/News/NewsRequests.cs b/FinlyticCore/Dtos/News/NewsRequests.cs new file mode 100644 index 0000000..98e15ae --- /dev/null +++ b/FinlyticCore/Dtos/News/NewsRequests.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.News; + +/// +/// Request payload sent by downstream services (e.g., FinlyticSentiment) to update the processing status of a news article. +/// +public record UpdateNewsStatusRequest( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("status")] string Status +); + +/// +/// Response payload returned to verify the success of the status update operation. +/// +public record UpdateNewsStatusResponse( + [property: JsonPropertyName("success")] bool Success, + [property: JsonPropertyName("message")] string? Message = null +); + diff --git a/FinlyticCore/Dtos/Sentiment/FinBertResultDto.cs b/FinlyticCore/Dtos/Sentiment/FinBertResultDto.cs new file mode 100644 index 0000000..84a8fcb --- /dev/null +++ b/FinlyticCore/Dtos/Sentiment/FinBertResultDto.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Sentiment; + +/// +/// Probabilities dictionary containing positive, negative, and neutral softmax scores. +/// +public record FinBertProbabilities +{ + /// + /// Gets or sets the positive probability score (0.0 to 1.0). + /// + [JsonPropertyName("positive")] + public double Positive { get; init; } + + /// + /// Gets or sets the negative probability score (0.0 to 1.0). + /// + [JsonPropertyName("negative")] + public double Negative { get; init; } + + /// + /// Gets or sets the neutral probability score (0.0 to 1.0). + /// + [JsonPropertyName("neutral")] + public double Neutral { get; init; } +} + +/// +/// Data transfer object holding the result of a FinBERT sentiment analysis. +/// +public record FinBertResultDto +{ + /// + /// Gets or sets the dominant sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL"). + /// + [JsonPropertyName("label")] + public string Label { get; init; } = "NEUTRAL"; + + /// + /// Gets or sets the compound score (-1.0 to +1.0). + /// + [JsonPropertyName("compoundScore")] + public double CompoundScore { get; init; } + + /// + /// Gets or sets the highest confidence score (0.0 to 1.0). + /// + [JsonPropertyName("confidence")] + public double Confidence { get; init; } + + /// + /// Gets or sets the probability breakdown. + /// + [JsonPropertyName("probabilities")] + public FinBertProbabilities Probabilities { get; init; } = new(); + + /// + /// Gets or sets the short summary snippet highlighting the impact of the article. + /// + [JsonPropertyName("summarySnippet")] + public string? SummarySnippet { get; init; } +} diff --git a/FinlyticCore/Dtos/Sentiment/IsinSentimentSummaryDto.cs b/FinlyticCore/Dtos/Sentiment/IsinSentimentSummaryDto.cs new file mode 100644 index 0000000..6c7f890 --- /dev/null +++ b/FinlyticCore/Dtos/Sentiment/IsinSentimentSummaryDto.cs @@ -0,0 +1,147 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Sentiment; + +/// +/// Article metadata referenced inside an ISIN analysis event. +/// +public record IsinAnalysisArticleRef +{ + /// + /// Gets or sets the article identifier. + /// + [JsonPropertyName("articleId")] + public string ArticleId { get; init; } = string.Empty; + + /// + /// Gets or sets the article title. + /// + [JsonPropertyName("title")] + public string Title { get; init; } = string.Empty; + + /// + /// Gets or sets the article source name. + /// + [JsonPropertyName("source")] + public string Source { get; init; } = string.Empty; + + /// + /// Gets or sets the publication timestamp in ISO-8601 format. + /// + [JsonPropertyName("publishedAt")] + public string PublishedAt { get; init; } = string.Empty; +} + +/// +/// Individual chronological analysis entry inside an ISIN summary file. +/// +public record IsinAnalysisEntry +{ + /// + /// Gets or sets the unique analysis ID (e.g. "sent_20260722_001"). + /// + [JsonPropertyName("analysisId")] + public string AnalysisId { get; init; } = string.Empty; + + /// + /// Gets or sets the analysis timestamp in ISO-8601 format. + /// + [JsonPropertyName("timestamp")] + public string Timestamp { get; init; } = string.Empty; + + /// + /// Gets or sets the referenced article details. + /// + [JsonPropertyName("article")] + public IsinAnalysisArticleRef Article { get; init; } = new(); + + /// + /// Gets or sets the FinBERT analysis result. + /// + [JsonPropertyName("finbertResult")] + public FinBertResultDto FinbertResult { get; init; } = new(); + + /// + /// Gets or sets the summary snippet. + /// + [JsonPropertyName("summarySnippet")] + public string SummarySnippet { get; init; } = string.Empty; +} + +/// +/// Current summary aggregate header inside an ISIN sentiment summary file. +/// +public record IsinCurrentSummary +{ + /// + /// Gets or sets the average compound score (-1.0 to +1.0). + /// + [JsonPropertyName("compoundScore")] + public double CompoundScore { get; init; } + + /// + /// Gets or sets the overall sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL"). + /// + [JsonPropertyName("sentimentLabel")] + public string SentimentLabel { get; init; } = "NEUTRAL"; + + /// + /// Gets or sets the average confidence across analyzed articles. + /// + [JsonPropertyName("avgConfidence")] + public double AvgConfidence { get; init; } + + /// + /// Gets or sets the total number of articles analyzed for this ISIN. + /// + [JsonPropertyName("totalArticlesAnalyzed")] + public int TotalArticlesAnalyzed { get; init; } + + /// + /// Gets or sets the overall synthesized sentiment text overview. + /// + [JsonPropertyName("text")] + public string Text { get; init; } = string.Empty; +} + +/// +/// Data transfer object for an ISIN sentiment summary file (stored in data/summaries/isin/ISIN.json). +/// +public record IsinSentimentSummaryDto +{ + /// + /// Gets or sets the ISIN code. + /// + [JsonPropertyName("isin")] + public string Isin { get; init; } = string.Empty; + + /// + /// Gets or sets the company name. + /// + [JsonPropertyName("companyName")] + public string CompanyName { get; init; } = string.Empty; + + /// + /// Gets or sets the sector name. + /// + [JsonPropertyName("sector")] + public string Sector { get; init; } = string.Empty; + + /// + /// Gets or sets the last updated timestamp in ISO-8601 format. + /// + [JsonPropertyName("lastUpdated")] + public string LastUpdated { get; init; } = string.Empty; + + /// + /// Gets or sets the current summary metrics and overview. + /// + [JsonPropertyName("currentSummary")] + public IsinCurrentSummary CurrentSummary { get; init; } = new(); + + /// + /// Gets or sets the list of historical analysis entries. + /// + [JsonPropertyName("analyses")] + public List Analyses { get; init; } = []; +} diff --git a/FinlyticCore/Dtos/Sentiment/SectorSentimentSummaryDto.cs b/FinlyticCore/Dtos/Sentiment/SectorSentimentSummaryDto.cs new file mode 100644 index 0000000..3d4ea96 --- /dev/null +++ b/FinlyticCore/Dtos/Sentiment/SectorSentimentSummaryDto.cs @@ -0,0 +1,99 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Sentiment; + +/// +/// Individual analysis entry inside a Sector sentiment summary file. +/// +public record SectorAnalysisEntry +{ + /// + /// Gets or sets the unique analysis ID. + /// + [JsonPropertyName("analysisId")] + public string AnalysisId { get; init; } = string.Empty; + + /// + /// Gets or sets the timestamp in ISO-8601 format. + /// + [JsonPropertyName("timestamp")] + public string Timestamp { get; init; } = string.Empty; + + /// + /// Gets or sets the related asset ISIN. + /// + [JsonPropertyName("relatedIsin")] + public string RelatedIsin { get; init; } = string.Empty; + + /// + /// Gets or sets the article ID. + /// + [JsonPropertyName("articleId")] + public string ArticleId { get; init; } = string.Empty; + + /// + /// Gets or sets the FinBERT analysis result. + /// + [JsonPropertyName("finbertResult")] + public FinBertResultDto FinbertResult { get; init; } = new(); +} + +/// +/// Current summary aggregate header inside a Sector sentiment summary file. +/// +public record SectorCurrentSummary +{ + /// + /// Gets or sets the sector compound score (-1.0 to +1.0). + /// + [JsonPropertyName("compoundScore")] + public double CompoundScore { get; init; } + + /// + /// Gets or sets the overall sector sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL"). + /// + [JsonPropertyName("sentimentLabel")] + public string SentimentLabel { get; init; } = "NEUTRAL"; + + /// + /// Gets or sets the list of active asset ISINs influencing the sector. + /// + [JsonPropertyName("activeIsins")] + public List ActiveIsins { get; init; } = []; + + /// + /// Gets or sets the overview text for the sector. + /// + [JsonPropertyName("text")] + public string Text { get; init; } = string.Empty; +} + +/// +/// Data transfer object for a Sector sentiment summary file (stored in data/summaries/sectors/SectorName.json). +/// +public record SectorSentimentSummaryDto +{ + /// + /// Gets or sets the sector name. + /// + [JsonPropertyName("sector")] + public string Sector { get; init; } = string.Empty; + + /// + /// Gets or sets the last updated timestamp in ISO-8601 format. + /// + [JsonPropertyName("lastUpdated")] + public string LastUpdated { get; init; } = string.Empty; + + /// + /// Gets or sets the current sector summary metrics and overview. + /// + [JsonPropertyName("currentSummary")] + public SectorCurrentSummary CurrentSummary { get; init; } = new(); + + /// + /// Gets or sets the list of historical sector analysis entries. + /// + [JsonPropertyName("analyses")] + public List Analyses { get; init; } = []; +} diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/CandleDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/CandleDto.cs new file mode 100644 index 0000000..fee98f1 --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/CandleDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record CandleDto( + [property: JsonPropertyName("timestamp")] DateTime Timestamp, + [property: JsonPropertyName("open")] decimal Open, + [property: JsonPropertyName("high")] decimal High, + [property: JsonPropertyName("low")] decimal Low, + [property: JsonPropertyName("close")] decimal Close, + [property: JsonPropertyName("volume")] long Volume, + [property: JsonPropertyName("bid")] decimal? Bid = null, + [property: JsonPropertyName("ask")] decimal? Ask = null +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/ChartPatternDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/ChartPatternDto.cs new file mode 100644 index 0000000..5b12254 --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/ChartPatternDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record PatternPointDto( + [property: JsonPropertyName("time")] DateTime Time, + [property: JsonPropertyName("price")] decimal Price +); + +public record BreakoutSignalDto( + [property: JsonPropertyName("time")] DateTime Time, + [property: JsonPropertyName("direction")] string Direction, // "BUY" or "SELL" + [property: JsonPropertyName("triggerPrice")] decimal TriggerPrice, + [property: JsonPropertyName("targetPrice")] decimal TargetPrice, + [property: JsonPropertyName("potentialPercent")] decimal? PotentialPercent = null +); + +public record ChartPatternDto( + [property: JsonPropertyName("type")] string Type, // "AscendingTriangle", "DescendingTriangle", "SymmetricalTriangle", "DoubleBottom", "DoubleTop", "HeadAndShoulders" + [property: JsonPropertyName("description")] string? Description, + [property: JsonPropertyName("upperLine")] List UpperLine, + [property: JsonPropertyName("lowerLine")] List LowerLine, + [property: JsonPropertyName("apexTime")] DateTime? ApexTime, + [property: JsonPropertyName("breakoutSignal")] BreakoutSignalDto? BreakoutSignal, + [property: JsonPropertyName("confidencePercent")] decimal? ConfidencePercent = null +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/IndicatorValuesDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/IndicatorValuesDto.cs new file mode 100644 index 0000000..85ddaeb --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/IndicatorValuesDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record IndicatorValuesDto( + [property: JsonPropertyName("timestamp")] DateTime Timestamp, + [property: JsonPropertyName("ema20")] decimal? Ema20, + [property: JsonPropertyName("sma50")] decimal? Sma50, + [property: JsonPropertyName("sma200")] decimal? Sma200, + [property: JsonPropertyName("rsi14")] decimal? Rsi14, + [property: JsonPropertyName("macdLine")] decimal? MacdLine, + [property: JsonPropertyName("macdSignal")] decimal? MacdSignal, + [property: JsonPropertyName("macdHistogram")] decimal? MacdHistogram, + [property: JsonPropertyName("atr14")] decimal? Atr14, + [property: JsonPropertyName("vwap")] decimal? Vwap, + [property: JsonPropertyName("supertrendUpper")] decimal? SupertrendUpper, + [property: JsonPropertyName("supertrendLower")] decimal? SupertrendLower, + [property: JsonPropertyName("supertrendDirection")] string? SupertrendDirection, + [property: JsonPropertyName("recommendedStopLoss")] decimal? RecommendedStopLoss +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/LivePriceDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/LivePriceDto.cs new file mode 100644 index 0000000..a5f35dd --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/LivePriceDto.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record LivePriceDto( + [property: JsonPropertyName("isin")] + string Isin, + [property: JsonPropertyName("currentPrice")] + decimal CurrentPrice, + [property: JsonPropertyName("dailyChangePercent")] + decimal DailyChangePercent, + [property: JsonPropertyName("bid")] + decimal? Bid, + [property: JsonPropertyName("ask")] + decimal? Ask +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/MarketRegimeDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/MarketRegimeDto.cs new file mode 100644 index 0000000..fa8e049 --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/MarketRegimeDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record MarketRegimeDto( + [property: JsonPropertyName("vixValue")] decimal VixValue, + [property: JsonPropertyName("vixRegime")] string VixRegime, // "LowVolatility", "Moderate", "HighVolatility" (>25) + [property: JsonPropertyName("marketTrend")] string MarketTrend, // "Bullish", "Bearish" + [property: JsonPropertyName("dxyValue")] decimal DxyValue, + [property: JsonPropertyName("dxyState")] string DxyState, // "DollarStrengthening", "DollarWeakening" + [property: JsonPropertyName("summaryText")] string SummaryText +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/StrategySignalDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/StrategySignalDto.cs new file mode 100644 index 0000000..a9bb128 --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/StrategySignalDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record StrategySignalDto( + [property: JsonPropertyName("type")] string Type, // "GoldenCross", "DeathCross", "RsiDivergenceBullish", "RsiDivergenceBearish", "Breakout" + [property: JsonPropertyName("timestamp")] DateTime Timestamp, + [property: JsonPropertyName("direction")] string Direction, // "BUY", "SELL", "NEUTRAL" + [property: JsonPropertyName("price")] decimal Price, + [property: JsonPropertyName("description")] string Description +); diff --git a/FinlyticCore/Dtos/TechnicalAnalysis/TechnicalAnalysisDto.cs b/FinlyticCore/Dtos/TechnicalAnalysis/TechnicalAnalysisDto.cs new file mode 100644 index 0000000..716862f --- /dev/null +++ b/FinlyticCore/Dtos/TechnicalAnalysis/TechnicalAnalysisDto.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.TechnicalAnalysis; + +public record TechnicalAnalysisDto( + [property: JsonPropertyName("isin")] string Isin, + [property: JsonPropertyName("ticker")] string Ticker, + [property: JsonPropertyName("companyName")] string CompanyName, + [property: JsonPropertyName("lastUpdated")] DateTime LastUpdated, + [property: JsonPropertyName("candles")] List Candles, + [property: JsonPropertyName("indicators")] List Indicators, + [property: JsonPropertyName("patterns")] List Patterns, + [property: JsonPropertyName("signals")] List Signals, + [property: JsonPropertyName("marketRegime")] MarketRegimeDto MarketRegime, + [property: JsonPropertyName("currency")] string Currency = "EUR" +); diff --git a/FinlyticCore/Dtos/Yahoo/YahooChartDto.cs b/FinlyticCore/Dtos/Yahoo/YahooChartDto.cs new file mode 100644 index 0000000..1cca842 --- /dev/null +++ b/FinlyticCore/Dtos/Yahoo/YahooChartDto.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Yahoo; + +/// +/// Response object for Yahoo Finance chart API (/v8/finance/chart/{symbol}). +/// +public record YahooChartResponseDto( + [property: JsonPropertyName("chart")] YahooChartResultWrapperDto? Chart +); + +public record YahooChartResultWrapperDto( + [property: JsonPropertyName("result")] List? Result, + [property: JsonPropertyName("error")] object? Error +); + +public record YahooChartResultDto( + [property: JsonPropertyName("meta")] YahooChartMetaDto? Meta, + [property: JsonPropertyName("timestamp")] List? Timestamp, + [property: JsonPropertyName("indicators")] YahooChartIndicatorsDto? Indicators +); + +public record YahooChartMetaDto( + [property: JsonPropertyName("currency")] string? Currency, + [property: JsonPropertyName("symbol")] string? Symbol, + [property: JsonPropertyName("exchangeName")] string? ExchangeName, + [property: JsonPropertyName("fullExchangeName")] string? FullExchangeName, + [property: JsonPropertyName("instrumentType")] string? InstrumentType, + [property: JsonPropertyName("firstTradeDate")] long? FirstTradeDate, + [property: JsonPropertyName("regularMarketTime")] long? RegularMarketTime, + [property: JsonPropertyName("hasPrePostMarketData")] bool? HasPrePostMarketData, + [property: JsonPropertyName("gmtoffset")] int? GmtOffset, + [property: JsonPropertyName("timezone")] string? Timezone, + [property: JsonPropertyName("exchangeTimezoneName")] string? ExchangeTimezoneName, + [property: JsonPropertyName("regularMarketPrice")] double? RegularMarketPrice, + [property: JsonPropertyName("fiftyTwoWeekHigh")] double? FiftyTwoWeekHigh, + [property: JsonPropertyName("fiftyTwoWeekLow")] double? FiftyTwoWeekLow, + [property: JsonPropertyName("regularMarketDayHigh")] double? RegularMarketDayHigh, + [property: JsonPropertyName("regularMarketDayLow")] double? RegularMarketDayLow, + [property: JsonPropertyName("regularMarketVolume")] long? RegularMarketVolume, + [property: JsonPropertyName("chartPreviousClose")] double? ChartPreviousClose, + [property: JsonPropertyName("previousClose")] double? PreviousClose, + [property: JsonPropertyName("scale")] int? Scale, + [property: JsonPropertyName("priceHint")] int? PriceHint, + [property: JsonPropertyName("dataGranularity")] string? DataGranularity, + [property: JsonPropertyName("range")] string? Range, + [property: JsonPropertyName("validRanges")] List? ValidRanges +); + +public record YahooChartIndicatorsDto( + [property: JsonPropertyName("quote")] List? Quote, + [property: JsonPropertyName("adjclose")] List? AdjClose +); + +public record YahooChartQuoteDto( + [property: JsonPropertyName("open")] List? Open, + [property: JsonPropertyName("high")] List? High, + [property: JsonPropertyName("low")] List? Low, + [property: JsonPropertyName("close")] List? Close, + [property: JsonPropertyName("volume")] List? Volume +); + +public record YahooChartAdjCloseDto( + [property: JsonPropertyName("adjclose")] List? AdjClose +); diff --git a/FinlyticCore/Dtos/Yahoo/YahooQuoteDto.cs b/FinlyticCore/Dtos/Yahoo/YahooQuoteDto.cs new file mode 100644 index 0000000..bb3a2f8 --- /dev/null +++ b/FinlyticCore/Dtos/Yahoo/YahooQuoteDto.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Yahoo; + +/// +/// Response object for Yahoo Finance quick quotes API (/v7/finance/quote?symbols=...). +/// +public record YahooQuoteResponseDto( + [property: JsonPropertyName("quoteResponse")] YahooQuoteResultWrapperDto? QuoteResponse +); + +public record YahooQuoteResultWrapperDto( + [property: JsonPropertyName("result")] List? Result, + [property: JsonPropertyName("error")] object? Error +); + +public record YahooQuoteItemDto( + [property: JsonPropertyName("language")] string? Language, + [property: JsonPropertyName("region")] string? Region, + [property: JsonPropertyName("quoteType")] string? QuoteType, + [property: JsonPropertyName("typeDisp")] string? TypeDisp, + [property: JsonPropertyName("quoteSourceName")] string? QuoteSourceName, + [property: JsonPropertyName("triggerable")] bool? Triggerable, + [property: JsonPropertyName("customPriceAlertConfidence")] string? CustomPriceAlertConfidence, + [property: JsonPropertyName("currency")] string? Currency, + [property: JsonPropertyName("marketState")] string? MarketState, + [property: JsonPropertyName("exchange")] string? Exchange, + [property: JsonPropertyName("shortName")] string? ShortName, + [property: JsonPropertyName("longName")] string? LongName, + [property: JsonPropertyName("messageBoardId")] string? MessageBoardId, + [property: JsonPropertyName("exchangeTimezoneName")] string? ExchangeTimezoneName, + [property: JsonPropertyName("exchangeTimezoneShortName")] string? ExchangeTimezoneShortName, + [property: JsonPropertyName("gmtOffSetMilliseconds")] long? GmtOffSetMilliseconds, + [property: JsonPropertyName("market")] string? Market, + [property: JsonPropertyName("esgPopulated")] bool? EsgPopulated, + [property: JsonPropertyName("regularMarketChangePercent")] double? RegularMarketChangePercent, + [property: JsonPropertyName("regularMarketPrice")] double? RegularMarketPrice, + [property: JsonPropertyName("regularMarketChange")] double? RegularMarketChange, + [property: JsonPropertyName("regularMarketTime")] long? RegularMarketTime, + [property: JsonPropertyName("regularMarketDayHigh")] double? RegularMarketDayHigh, + [property: JsonPropertyName("regularMarketDayRange")] string? RegularMarketDayRange, + [property: JsonPropertyName("regularMarketDayLow")] double? RegularMarketDayLow, + [property: JsonPropertyName("regularMarketVolume")] long? RegularMarketVolume, + [property: JsonPropertyName("regularMarketPreviousClose")] double? RegularMarketPreviousClose, + [property: JsonPropertyName("bid")] double? Bid, + [property: JsonPropertyName("ask")] double? Ask, + [property: JsonPropertyName("bidSize")] long? BidSize, + [property: JsonPropertyName("askSize")] long? AskSize, + [property: JsonPropertyName("fullExchangeName")] string? FullExchangeName, + [property: JsonPropertyName("financialCurrency")] string? FinancialCurrency, + [property: JsonPropertyName("regularMarketOpen")] double? RegularMarketOpen, + [property: JsonPropertyName("averageDailyVolume3Month")] long? AverageDailyVolume3Month, + [property: JsonPropertyName("averageDailyVolume10Day")] long? AverageDailyVolume10Day, + [property: JsonPropertyName("fiftyTwoWeekLowChange")] double? FiftyTwoWeekLowChange, + [property: JsonPropertyName("fiftyTwoWeekLowChangePercent")] double? FiftyTwoWeekLowChangePercent, + [property: JsonPropertyName("fiftyTwoWeekRange")] string? FiftyTwoWeekRange, + [property: JsonPropertyName("fiftyTwoWeekHighChange")] double? FiftyTwoWeekHighChange, + [property: JsonPropertyName("fiftyTwoWeekHighChangePercent")] double? FiftyTwoWeekHighChangePercent, + [property: JsonPropertyName("fiftyTwoWeekLow")] double? FiftyTwoWeekLow, + [property: JsonPropertyName("fiftyTwoWeekHigh")] double? FiftyTwoWeekHigh, + [property: JsonPropertyName("marketCap")] double? MarketCap, + [property: JsonPropertyName("symbol")] string Symbol = "" +); diff --git a/FinlyticCore/Dtos/Yahoo/YahooQuoteSummaryDto.cs b/FinlyticCore/Dtos/Yahoo/YahooQuoteSummaryDto.cs new file mode 100644 index 0000000..6927c56 --- /dev/null +++ b/FinlyticCore/Dtos/Yahoo/YahooQuoteSummaryDto.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Yahoo; + +/// +/// Root response object for Yahoo Finance quoteSummary API (/v10/finance/quoteSummary/{symbol}). +/// +public record YahooQuoteSummaryResponseDto( + [property: JsonPropertyName("quoteSummary")] YahooQuoteSummaryResultDto? QuoteSummary +); + +public record YahooQuoteSummaryResultDto( + [property: JsonPropertyName("result")] List? Result, + [property: JsonPropertyName("error")] object? Error +); + +/// +/// Contains module blocks requested via the modules query parameter. +/// +public record YahooQuoteSummaryModulesDto( + [property: JsonPropertyName("assetProfile")] YahooAssetProfileDto? AssetProfile, + [property: JsonPropertyName("financialData")] YahooFinancialDataDto? FinancialData, + [property: JsonPropertyName("defaultKeyStatistics")] YahooDefaultKeyStatisticsDto? DefaultKeyStatistics, + [property: JsonPropertyName("summaryDetail")] YahooSummaryDetailDto? SummaryDetail, + [property: JsonPropertyName("incomeStatementHistory")] YahooFinancialStatementHistoryDto? IncomeStatementHistory, + [property: JsonPropertyName("incomeStatementHistoryQuarterly")] YahooFinancialStatementHistoryDto? IncomeStatementHistoryQuarterly, + [property: JsonPropertyName("balanceSheetHistory")] YahooFinancialStatementHistoryDto? BalanceSheetHistory, + [property: JsonPropertyName("balanceSheetHistoryQuarterly")] YahooFinancialStatementHistoryDto? BalanceSheetHistoryQuarterly, + [property: JsonPropertyName("cashflowStatementHistory")] YahooFinancialStatementHistoryDto? CashflowStatementHistory, + [property: JsonPropertyName("cashflowStatementHistoryQuarterly")] YahooFinancialStatementHistoryDto? CashflowStatementHistoryQuarterly, + [property: JsonPropertyName("calendarEvents")] YahooCalendarEventsDto? CalendarEvents +); + +#region Module DTOs + +/// +/// Asset profile details including address, industry, sector, officers, and corporate governance risks. +/// +public record YahooAssetProfileDto( + [property: JsonPropertyName("address1")] string? Address1, + [property: JsonPropertyName("address2")] string? Address2, + [property: JsonPropertyName("city")] string? City, + [property: JsonPropertyName("state")] string? State, + [property: JsonPropertyName("zip")] string? Zip, + [property: JsonPropertyName("country")] string? Country, + [property: JsonPropertyName("phone")] string? Phone, + [property: JsonPropertyName("website")] string? Website, + [property: JsonPropertyName("industry")] string? Industry, + [property: JsonPropertyName("industryKey")] string? IndustryKey, + [property: JsonPropertyName("industryDisp")] string? IndustryDisp, + [property: JsonPropertyName("sector")] string? Sector, + [property: JsonPropertyName("sectorKey")] string? SectorKey, + [property: JsonPropertyName("sectorDisp")] string? SectorDisp, + [property: JsonPropertyName("longBusinessSummary")] string? LongBusinessSummary, + [property: JsonPropertyName("fullTimeEmployees")] int? FullTimeEmployees, + [property: JsonPropertyName("companyOfficers")] List? CompanyOfficers, + [property: JsonPropertyName("auditRisk")] int? AuditRisk, + [property: JsonPropertyName("boardRisk")] int? BoardRisk, + [property: JsonPropertyName("compensationRisk")] int? CompensationRisk, + [property: JsonPropertyName("shareHolderRightsRisk")] int? ShareHolderRightsRisk, + [property: JsonPropertyName("overallRisk")] int? OverallRisk, + [property: JsonPropertyName("governanceEpochDate")] long? GovernanceEpochDate, + [property: JsonPropertyName("compensationAsOfEpochDate")] long? CompensationAsOfEpochDate +); + +public record YahooCompanyOfficerDto( + [property: JsonPropertyName("name")] string? Name, + [property: JsonPropertyName("age")] int? Age, + [property: JsonPropertyName("title")] string? Title, + [property: JsonPropertyName("yearBorn")] int? YearBorn, + [property: JsonPropertyName("fiscalYear")] int? FiscalYear, + [property: JsonPropertyName("totalPay")] YahooValueDto? TotalPay, + [property: JsonPropertyName("exercisedValue")] YahooValueDto? ExercisedValue, + [property: JsonPropertyName("unexercisedValue")] YahooValueDto? UnexercisedValue +); + +/// +/// Financial metrics including target prices, debt to equity, margins, and cash flow indicators. +/// +public record YahooFinancialDataDto( + [property: JsonPropertyName("currentPrice")] YahooValueDto? CurrentPrice, + [property: JsonPropertyName("targetHighPrice")] YahooValueDto? TargetHighPrice, + [property: JsonPropertyName("targetLowPrice")] YahooValueDto? TargetLowPrice, + [property: JsonPropertyName("targetMeanPrice")] YahooValueDto? TargetMeanPrice, + [property: JsonPropertyName("targetMedianPrice")] YahooValueDto? TargetMedianPrice, + [property: JsonPropertyName("recommendationMean")] YahooValueDto? RecommendationMean, + [property: JsonPropertyName("recommendationKey")] string? RecommendationKey, + [property: JsonPropertyName("numberOfAnalystOpinions")] YahooValueDto? NumberOfAnalystOpinions, + [property: JsonPropertyName("totalCash")] YahooValueDto? TotalCash, + [property: JsonPropertyName("totalCashPerShare")] YahooValueDto? TotalCashPerShare, + [property: JsonPropertyName("ebitda")] YahooValueDto? Ebitda, + [property: JsonPropertyName("totalDebt")] YahooValueDto? TotalDebt, + [property: JsonPropertyName("quickRatio")] YahooValueDto? QuickRatio, + [property: JsonPropertyName("currentRatio")] YahooValueDto? CurrentRatio, + [property: JsonPropertyName("totalRevenue")] YahooValueDto? TotalRevenue, + [property: JsonPropertyName("debtToEquity")] YahooValueDto? DebtToEquity, + [property: JsonPropertyName("revenuePerShare")] YahooValueDto? RevenuePerShare, + [property: JsonPropertyName("returnOnAssets")] YahooValueDto? ReturnOnAssets, + [property: JsonPropertyName("returnOnEquity")] YahooValueDto? ReturnOnEquity, + [property: JsonPropertyName("grossProfits")] YahooValueDto? GrossProfits, + [property: JsonPropertyName("freeCashflow")] YahooValueDto? FreeCashflow, + [property: JsonPropertyName("operatingCashflow")] YahooValueDto? OperatingCashflow, + [property: JsonPropertyName("revenueGrowth")] YahooValueDto? RevenueGrowth, + [property: JsonPropertyName("grossMargins")] YahooValueDto? GrossMargins, + [property: JsonPropertyName("ebitdaMargins")] YahooValueDto? EbitdaMargins, + [property: JsonPropertyName("operatingMargins")] YahooValueDto? OperatingMargins, + [property: JsonPropertyName("profitMargins")] YahooValueDto? ProfitMargins, + [property: JsonPropertyName("financialCurrency")] string? FinancialCurrency +); + +/// +/// Key statistics including valuation ratios (P/E, Enterprise Value, Short Ratio, Shares Outstanding). +/// +public record YahooDefaultKeyStatisticsDto( + [property: JsonPropertyName("priceToBook")] YahooValueDto? PriceToBook, + [property: JsonPropertyName("enterpriseValue")] YahooValueDto? EnterpriseValue, + [property: JsonPropertyName("forwardPE")] YahooValueDto? ForwardPE, + [property: JsonPropertyName("profitMargins")] YahooValueDto? ProfitMargins, + [property: JsonPropertyName("floatShares")] YahooValueDto? FloatShares, + [property: JsonPropertyName("sharesOutstanding")] YahooValueDto? SharesOutstanding, + [property: JsonPropertyName("sharesShort")] YahooValueDto? SharesShort, + [property: JsonPropertyName("sharesShortPriorMonth")] YahooValueDto? SharesShortPriorMonth, + [property: JsonPropertyName("sharesShortPreviousMonthDate")] YahooValueDto? SharesShortPreviousMonthDate, + [property: JsonPropertyName("dateShortInterest")] YahooValueDto? DateShortInterest, + [property: JsonPropertyName("sharesPercentSharesOut")] YahooValueDto? SharesPercentSharesOut, + [property: JsonPropertyName("heldPercentInsiders")] YahooValueDto? HeldPercentInsiders, + [property: JsonPropertyName("heldPercentInstitutions")] YahooValueDto? HeldPercentInstitutions, + [property: JsonPropertyName("shortRatio")] YahooValueDto? ShortRatio, + [property: JsonPropertyName("shortPercentOfFloat")] YahooValueDto? ShortPercentOfFloat, + [property: JsonPropertyName("beta")] YahooValueDto? Beta, + [property: JsonPropertyName("category")] string? Category, + [property: JsonPropertyName("bookValue")] YahooValueDto? BookValue, + [property: JsonPropertyName("priceToSalesTrailing12Months")] YahooValueDto? PriceToSalesTrailing12Months, + [property: JsonPropertyName("lastFiscalYearEnd")] YahooValueDto? LastFiscalYearEnd, + [property: JsonPropertyName("nextFiscalYearEnd")] YahooValueDto? NextFiscalYearEnd, + [property: JsonPropertyName("mostRecentQuarter")] YahooValueDto? MostRecentQuarter, + [property: JsonPropertyName("earningsQuarterlyGrowth")] YahooValueDto? EarningsQuarterlyGrowth, + [property: JsonPropertyName("netIncomeToCommon")] YahooValueDto? NetIncomeToCommon, + [property: JsonPropertyName("trailingEps")] YahooValueDto? TrailingEps, + [property: JsonPropertyName("forwardEps")] YahooValueDto? ForwardEps, + [property: JsonPropertyName("pegRatio")] YahooValueDto? PegRatio, + [property: JsonPropertyName("enterpriseToRevenue")] YahooValueDto? EnterpriseToRevenue, + [property: JsonPropertyName("enterpriseToEbitda")] YahooValueDto? EnterpriseToEbitda, + [property: JsonPropertyName("52WeekChange")] YahooValueDto? FiftyTwoWeekChange, + [property: JsonPropertyName("SandP52WeekChange")] YahooValueDto? SandP52WeekChange +); + +/// +/// Summary details including dividends, 52-week ranges, and market capitalization. +/// +public record YahooSummaryDetailDto( + [property: JsonPropertyName("maxAge")] long? MaxAge, + [property: JsonPropertyName("priceHint")] YahooValueDto? PriceHint, + [property: JsonPropertyName("previousClose")] YahooValueDto? PreviousClose, + [property: JsonPropertyName("open")] YahooValueDto? Open, + [property: JsonPropertyName("dayLow")] YahooValueDto? DayLow, + [property: JsonPropertyName("dayHigh")] YahooValueDto? DayHigh, + [property: JsonPropertyName("regularMarketPreviousClose")] YahooValueDto? RegularMarketPreviousClose, + [property: JsonPropertyName("regularMarketOpen")] YahooValueDto? RegularMarketOpen, + [property: JsonPropertyName("regularMarketDayLow")] YahooValueDto? RegularMarketDayLow, + [property: JsonPropertyName("regularMarketDayHigh")] YahooValueDto? RegularMarketDayHigh, + [property: JsonPropertyName("dividendRate")] YahooValueDto? DividendRate, + [property: JsonPropertyName("dividendYield")] YahooValueDto? DividendYield, + [property: JsonPropertyName("exDividendDate")] YahooValueDto? ExDividendDate, + [property: JsonPropertyName("payoutRatio")] YahooValueDto? PayoutRatio, + [property: JsonPropertyName("fiveYearAvgDividendYield")] YahooValueDto? FiveYearAvgDividendYield, + [property: JsonPropertyName("beta")] YahooValueDto? Beta, + [property: JsonPropertyName("trailingPE")] YahooValueDto? TrailingPE, + [property: JsonPropertyName("forwardPE")] YahooValueDto? ForwardPE, + [property: JsonPropertyName("volume")] YahooValueDto? Volume, + [property: JsonPropertyName("regularMarketVolume")] YahooValueDto? RegularMarketVolume, + [property: JsonPropertyName("averageVolume")] YahooValueDto? AverageVolume, + [property: JsonPropertyName("averageVolume10days")] YahooValueDto? AverageVolume10days, + [property: JsonPropertyName("averageDailyVolume10Day")] YahooValueDto? AverageDailyVolume10Day, + [property: JsonPropertyName("bid")] YahooValueDto? Bid, + [property: JsonPropertyName("ask")] YahooValueDto? Ask, + [property: JsonPropertyName("bidSize")] YahooValueDto? BidSize, + [property: JsonPropertyName("askSize")] YahooValueDto? AskSize, + [property: JsonPropertyName("marketCap")] YahooValueDto? MarketCap, + [property: JsonPropertyName("fiftyTwoWeekLow")] YahooValueDto? FiftyTwoWeekLow, + [property: JsonPropertyName("fiftyTwoWeekHigh")] YahooValueDto? FiftyTwoWeekHigh, + [property: JsonPropertyName("priceToSalesTrailing12Months")] YahooValueDto? PriceToSalesTrailing12Months, + [property: JsonPropertyName("currency")] string? Currency +); + +/// +/// Historical financial statements container. +/// +public record YahooFinancialStatementHistoryDto( + [property: JsonPropertyName("incomeStatementHistory")] List? IncomeStatementHistory, + [property: JsonPropertyName("balanceSheetStatements")] List? BalanceSheetStatements, + [property: JsonPropertyName("cashflowStatements")] List? CashflowStatements +); + +public record YahooIncomeStatementDto( + [property: JsonPropertyName("endDate")] YahooValueDto? EndDate, + [property: JsonPropertyName("totalRevenue")] YahooValueDto? TotalRevenue, + [property: JsonPropertyName("costOfRevenue")] YahooValueDto? CostOfRevenue, + [property: JsonPropertyName("grossProfit")] YahooValueDto? GrossProfit, + [property: JsonPropertyName("researchDevelopment")] YahooValueDto? ResearchDevelopment, + [property: JsonPropertyName("sellingGeneralAdministrative")] YahooValueDto? SellingGeneralAdministrative, + [property: JsonPropertyName("totalOperatingExpenses")] YahooValueDto? TotalOperatingExpenses, + [property: JsonPropertyName("operatingIncome")] YahooValueDto? OperatingIncome, + [property: JsonPropertyName("totalOtherIncomeExpenseNet")] YahooValueDto? TotalOtherIncomeExpenseNet, + [property: JsonPropertyName("ebit")] YahooValueDto? Ebit, + [property: JsonPropertyName("interestExpense")] YahooValueDto? InterestExpense, + [property: JsonPropertyName("incomeBeforeTax")] YahooValueDto? IncomeBeforeTax, + [property: JsonPropertyName("incomeTaxExpense")] YahooValueDto? IncomeTaxExpense, + [property: JsonPropertyName("netIncome")] YahooValueDto? NetIncome, + [property: JsonPropertyName("netIncomeApplicableToCommonShares")] YahooValueDto? NetIncomeApplicableToCommonShares +); + +public record YahooBalanceSheetStatementDto( + [property: JsonPropertyName("endDate")] YahooValueDto? EndDate, + [property: JsonPropertyName("cash")] YahooValueDto? Cash, + [property: JsonPropertyName("shortTermInvestments")] YahooValueDto? ShortTermInvestments, + [property: JsonPropertyName("netReceivables")] YahooValueDto? NetReceivables, + [property: JsonPropertyName("inventory")] YahooValueDto? Inventory, + [property: JsonPropertyName("otherCurrentAssets")] YahooValueDto? OtherCurrentAssets, + [property: JsonPropertyName("totalCurrentAssets")] YahooValueDto? TotalCurrentAssets, + [property: JsonPropertyName("longTermInvestments")] YahooValueDto? LongTermInvestments, + [property: JsonPropertyName("propertyPlantEquipment")] YahooValueDto? PropertyPlantEquipment, + [property: JsonPropertyName("goodWill")] YahooValueDto? GoodWill, + [property: JsonPropertyName("intangibleAssets")] YahooValueDto? IntangibleAssets, + [property: JsonPropertyName("otherAssets")] YahooValueDto? OtherAssets, + [property: JsonPropertyName("totalAssets")] YahooValueDto? TotalAssets, + [property: JsonPropertyName("accountsPayable")] YahooValueDto? AccountsPayable, + [property: JsonPropertyName("shortLongTermDebt")] YahooValueDto? ShortLongTermDebt, + [property: JsonPropertyName("otherCurrentLiabilities")] YahooValueDto? OtherCurrentLiabilities, + [property: JsonPropertyName("totalCurrentLiabilities")] YahooValueDto? TotalCurrentLiabilities, + [property: JsonPropertyName("longTermDebt")] YahooValueDto? LongTermDebt, + [property: JsonPropertyName("otherLiabilities")] YahooValueDto? OtherLiabilities, + [property: JsonPropertyName("totalLiab")] YahooValueDto? TotalLiab, + [property: JsonPropertyName("commonStock")] YahooValueDto? CommonStock, + [property: JsonPropertyName("retainedEarnings")] YahooValueDto? RetainedEarnings, + [property: JsonPropertyName("treasuryStock")] YahooValueDto? TreasuryStock, + [property: JsonPropertyName("otherStockholderEquity")] YahooValueDto? OtherStockholderEquity, + [property: JsonPropertyName("totalStockholderEquity")] YahooValueDto? TotalStockholderEquity +); + +public record YahooCashflowStatementDto( + [property: JsonPropertyName("endDate")] YahooValueDto? EndDate, + [property: JsonPropertyName("netIncome")] YahooValueDto? NetIncome, + [property: JsonPropertyName("depreciation")] YahooValueDto? Depreciation, + [property: JsonPropertyName("changeToNetincome")] YahooValueDto? ChangeToNetincome, + [property: JsonPropertyName("changeToAccountReceivables")] YahooValueDto? ChangeToAccountReceivables, + [property: JsonPropertyName("changeToLiabilities")] YahooValueDto? ChangeToLiabilities, + [property: JsonPropertyName("changeToInventory")] YahooValueDto? ChangeToInventory, + [property: JsonPropertyName("changeToOperatingActivities")] YahooValueDto? ChangeToOperatingActivities, + [property: JsonPropertyName("totalCashFromOperatingActivities")] YahooValueDto? TotalCashFromOperatingActivities, + [property: JsonPropertyName("capitalExpenditures")] YahooValueDto? CapitalExpenditures, + [property: JsonPropertyName("investments")] YahooValueDto? Investments, + [property: JsonPropertyName("otherCashflowsFromInvestingActivities")] YahooValueDto? OtherCashflowsFromInvestingActivities, + [property: JsonPropertyName("totalCashflowsFromInvestingActivities")] YahooValueDto? TotalCashflowsFromInvestingActivities, + [property: JsonPropertyName("dividendsPaid")] YahooValueDto? DividendsPaid, + [property: JsonPropertyName("netBorrowings")] YahooValueDto? NetBorrowings, + [property: JsonPropertyName("otherCashflowsFromFinancingActivities")] YahooValueDto? OtherCashflowsFromFinancingActivities, + [property: JsonPropertyName("totalCashFromFinancingActivities")] YahooValueDto? TotalCashFromFinancingActivities, + [property: JsonPropertyName("changeInCashAndCashEquivalents")] YahooValueDto? ChangeInCashAndCashEquivalents +); + +/// +/// Corporate calendar dates including upcoming earnings calls and ex-dividend dates. +/// +public record YahooCalendarEventsDto( + [property: JsonPropertyName("earnings")] YahooEarningsCalendarDto? Earnings, + [property: JsonPropertyName("exDividendDate")] YahooValueDto? ExDividendDate, + [property: JsonPropertyName("dividendDate")] YahooValueDto? DividendDate +); + +public record YahooEarningsCalendarDto( + [property: JsonPropertyName("earningsDate")] List? EarningsDate, + [property: JsonPropertyName("earningsAverage")] YahooValueDto? EarningsAverage, + [property: JsonPropertyName("earningsLow")] YahooValueDto? EarningsLow, + [property: JsonPropertyName("earningsHigh")] YahooValueDto? EarningsHigh, + [property: JsonPropertyName("revenueAverage")] YahooValueDto? RevenueAverage, + [property: JsonPropertyName("revenueLow")] YahooValueDto? RevenueLow, + [property: JsonPropertyName("revenueHigh")] YahooValueDto? RevenueHigh +); + +#endregion diff --git a/FinlyticCore/Dtos/Yahoo/YahooSearchResponseDto.cs b/FinlyticCore/Dtos/Yahoo/YahooSearchResponseDto.cs new file mode 100644 index 0000000..030dd2c --- /dev/null +++ b/FinlyticCore/Dtos/Yahoo/YahooSearchResponseDto.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Yahoo; + +/// +/// Root DTO response returned by https://query2.finance.yahoo.com/v1/finance/search +/// +public class YahooSearchResponseDto +{ + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("quotes")] + public List Quotes { get; set; } = new(); + + [JsonPropertyName("totalTime")] + public int TotalTime { get; set; } + + [JsonPropertyName("timeTakenForQuotes")] + public int TimeTakenForQuotes { get; set; } +} + +/// +/// Represents an individual quote item returned within the Yahoo Finance search results. +/// +public class YahooSearchQuoteDto +{ + [JsonPropertyName("symbol")] + public string Symbol { get; set; } = string.Empty; + + [JsonPropertyName("shortname")] + public string? ShortName { get; set; } + + [JsonPropertyName("longname")] + public string? LongName { get; set; } + + [JsonPropertyName("exchange")] + public string? Exchange { get; set; } + + [JsonPropertyName("exchDisp")] + public string? ExchDisp { get; set; } + + [JsonPropertyName("quoteType")] + public string? QuoteType { get; set; } + + [JsonPropertyName("typeDisp")] + public string? TypeDisp { get; set; } + + [JsonPropertyName("index")] + public string? Index { get; set; } + + [JsonPropertyName("score")] + public double Score { get; set; } + + [JsonPropertyName("sector")] + public string? Sector { get; set; } + + [JsonPropertyName("sectorDisp")] + public string? SectorDisp { get; set; } + + [JsonPropertyName("industry")] + public string? Industry { get; set; } + + [JsonPropertyName("industryDisp")] + public string? IndustryDisp { get; set; } + + [JsonPropertyName("dispSecIndFlag")] + public bool DispSecIndFlag { get; set; } + + [JsonPropertyName("isYahooFinance")] + public bool IsYahooFinance { get; set; } +} \ No newline at end of file diff --git a/FinlyticCore/Dtos/Yahoo/YahooValueDto.cs b/FinlyticCore/Dtos/Yahoo/YahooValueDto.cs new file mode 100644 index 0000000..35f4af6 --- /dev/null +++ b/FinlyticCore/Dtos/Yahoo/YahooValueDto.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Dtos.Yahoo; + +/// +/// Represents a Yahoo Finance value wrapper containing raw numerical data alongside formatted strings. +/// +public record YahooValueDto +{ + [JsonPropertyName("raw")] + public double? Raw { get; init; } + + [JsonPropertyName("fmt")] + public string? Fmt { get; init; } + + [JsonPropertyName("longFmt")] + public string? LongFmt { get; init; } + + /// + /// Helper property to retrieve Raw as double (or fallback 0.0). + /// + [JsonIgnore] + public double DoubleValue => Raw ?? 0.0; + + /// + /// Helper property to retrieve Raw as decimal (or fallback 0m). + /// + [JsonIgnore] + public decimal DecimalValue => Raw.HasValue ? (decimal)Raw.Value : 0m; + + /// + /// Helper property to retrieve Raw as long (or fallback 0L). + /// + [JsonIgnore] + public long LongValue => Raw.HasValue ? (long)Raw.Value : 0L; +} diff --git a/FinlyticCore/FinlyticCore.csproj b/FinlyticCore/FinlyticCore.csproj index 858b750..8dd0ef6 100644 --- a/FinlyticCore/FinlyticCore.csproj +++ b/FinlyticCore/FinlyticCore.csproj @@ -1,4 +1,4 @@ - + net10.0 diff --git a/FinlyticCore/Models/Analyzer/AssetRecommendationDto.cs b/FinlyticCore/Models/Analyzer/AssetRecommendationDto.cs new file mode 100644 index 0000000..5c771d7 --- /dev/null +++ b/FinlyticCore/Models/Analyzer/AssetRecommendationDto.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Models.Analyzer; + +public class AssetRecommendationDto +{ + [JsonPropertyName("mode")] + public string Mode { get; set; } = "AUTO_SCREENER"; + + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("recommended_asset")] + public RecommendedAssetInfo RecommendedAsset { get; set; } = new(); + + [JsonPropertyName("rationale")] + public RecommendationRationaleInfo Rationale { get; set; } = new(); + + [JsonPropertyName("action_required")] + public string ActionRequired { get; set; } = "PROMPT_USER_FOR_MANUAL_TRADE"; // "PROMPT_USER_FOR_MANUAL_TRADE" | "NO_ACTION" +} + +public class RecommendedAssetInfo +{ + [JsonPropertyName("symbol")] + public string Symbol { get; set; } = string.Empty; + + [JsonPropertyName("company_name")] + public string CompanyName { get; set; } = string.Empty; + + [JsonPropertyName("isin")] + public string Isin { get; set; } = string.Empty; + + [JsonPropertyName("market")] + public string Market { get; set; } = "US_EQUITIES"; + + [JsonPropertyName("bias")] + public string Bias { get; set; } = "BULLISH"; // "BULLISH" | "BEARISH" | "NEUTRAL" + + [JsonPropertyName("confidence_score")] + public double ConfidenceScore { get; set; } + + [JsonPropertyName("timeframe")] + public string Timeframe { get; set; } = "1D"; +} + +public class RecommendationRationaleInfo +{ + [JsonPropertyName("pattern_detected")] + public string PatternDetected { get; set; } = string.Empty; + + [JsonPropertyName("vix_context")] + public string VixContext { get; set; } = string.Empty; + + [JsonPropertyName("key_technical_levels")] + public KeyTechnicalLevelsInfo KeyTechnicalLevels { get; set; } = new(); + + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; +} + +public class KeyTechnicalLevelsInfo +{ + [JsonPropertyName("support")] + public List Support { get; set; } = new(); + + [JsonPropertyName("resistance")] + public List Resistance { get; set; } = new(); +} diff --git a/FinlyticCore/Models/Analyzer/ManualAnalysisResponseDto.cs b/FinlyticCore/Models/Analyzer/ManualAnalysisResponseDto.cs new file mode 100644 index 0000000..65096a3 --- /dev/null +++ b/FinlyticCore/Models/Analyzer/ManualAnalysisResponseDto.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using FinlyticCore.Models.Trades; + +namespace FinlyticCore.Models.Analyzer; + +/// +/// Response payload for manual AI analysis trigger RPC. +/// +public class ManualAnalysisResponseDto +{ + [JsonPropertyName("analysisId")] + public string AnalysisId { get; set; } = string.Empty; + + [JsonPropertyName("isTradeProposed")] + public bool IsTradeProposed { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } = "Success"; + + [JsonPropertyName("recommendation")] + public string Recommendation { get; set; } = "RECOMMENDED"; + + [JsonPropertyName("n8nResponse")] + public N8nAnalysisResponseDto? N8nResponse { get; set; } + + [JsonPropertyName("proposal")] + public TradeProposalDto? Proposal { get; set; } +} diff --git a/FinlyticCore/Models/Analyzer/N8nAnalysisRequestDto.cs b/FinlyticCore/Models/Analyzer/N8nAnalysisRequestDto.cs new file mode 100644 index 0000000..6f9e4e3 --- /dev/null +++ b/FinlyticCore/Models/Analyzer/N8nAnalysisRequestDto.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; + +namespace FinlyticCore.Models.Analyzer; + +public class TargetAssetInfo +{ + public string Symbol { get; set; } = string.Empty; // e.g. "AAPL" + public string Name { get; set; } = string.Empty; // e.g. "Apple Inc." + public string Isin { get; set; } = string.Empty; + public string Sector { get; set; } = string.Empty; +} + +public class MarketContextInfo +{ + public decimal Vix { get; set; } + public string MarketRegime { get; set; } = string.Empty; +} + +public class FilterContextInfo +{ + public double ImpactScore { get; set; } + public string RawNewsHeadline { get; set; } = string.Empty; +} + +public class UserPreferencesInfo +{ + public int RiskScore { get; set; } = 50; // 0 to 100 + public string RiskTolerance { get; set; } = "Balanced"; + public int MinTimeframeValue { get; set; } = 1; + public int MaxTimeframeValue { get; set; } = 7; + public string TimeframeUnit { get; set; } = "Tage"; // "Stunden", "Tage", "Wochen", "Monate" + public string TimeframeFormatted { get; set; } = "1-7 Tage"; + public string InstrumentType { get; set; } = "Stock"; // "Stock", "KnockOut", "Option", "CFD", "Future" + public string UserNotes { get; set; } = string.Empty; +} + +public class TradeFeedbackInfo +{ + public int TotalAssetTrades { get; set; } + public double AssetWinRate { get; set; } + public double AvgReturnPercent { get; set; } + public string LastTradeResult { get; set; } = "NONE"; // "WIN", "LOSS", "NONE" +} + +public class PatternContextInfo +{ + public string PatternName { get; set; } = string.Empty; + public string? BreakoutDirection { get; set; } + public double? TargetPrice { get; set; } + public double? PotentialPercent { get; set; } +} + +public class TechnicalContextInfo +{ + public string Rsi { get; set; } = "N/A"; + public string SupertrendStatus { get; set; } = "N/A"; + public string Atr { get; set; } = "N/A"; + public double? Sma50 { get; set; } + public double? Sma200 { get; set; } + public List DetectedPatterns { get; set; } = new(); +} + +public class SentimentContextInfo +{ + public double AssetSentimentScore { get; set; } + public double SectorSentimentScore { get; set; } + public string NewsSentimentSummary { get; set; } = "Neutral"; +} + +public class FundamentalContextInfo +{ + public double? PeRatio { get; set; } + public double? ForwardPeRatio { get; set; } + public double? PegRatio { get; set; } + public double? MarketCap { get; set; } + public double? DebtToEquity { get; set; } + public double? GrossMargin { get; set; } + public double? NetProfitMargin { get; set; } + public double? ReturnOnEquity { get; set; } + public double? DividendYield { get; set; } + public double? ShortPercentOfFloat { get; set; } + public double? AnalystTargetMedian { get; set; } + public double? EvToEbitda { get; set; } +} + +public class N8nAnalysisRequestDto +{ + public string RequestId { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public string TriggerType { get; set; } = "AutomatedNews"; // "Manual" | "AutomatedNews" + + public TargetAssetInfo TargetAsset { get; set; } = new(); + public MarketContextInfo MarketContext { get; set; } = new(); + public FilterContextInfo FilterContext { get; set; } = new(); + public UserPreferencesInfo UserPreferences { get; set; } = new(); + public TradeFeedbackInfo TradeFeedback { get; set; } = new(); + public TechnicalContextInfo TechnicalContext { get; set; } = new(); + public SentimentContextInfo SentimentContext { get; set; } = new(); + public FundamentalContextInfo FundamentalContext { get; set; } = new(); +} diff --git a/FinlyticCore/Models/Analyzer/N8nAnalysisResponseDto.cs b/FinlyticCore/Models/Analyzer/N8nAnalysisResponseDto.cs new file mode 100644 index 0000000..9dcb266 --- /dev/null +++ b/FinlyticCore/Models/Analyzer/N8nAnalysisResponseDto.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace FinlyticCore.Models.Analyzer; + +public class N8nAnalysisResponseDto +{ + public string RequestId { get; set; } = string.Empty; + public double EvalScore { get; set; } // 0.00 to 1.00 + public string AiDecision { get; set; } = "Proceed"; // "Proceed" | "Reject" | "Hold" + public string SuggestedDirection { get; set; } = "Long"; // "Long" | "Short" + public string AiReasoning { get; set; } = string.Empty; + public string SuggestedTimeframe { get; set; } = "Intraday"; // "Scalp" | "Intraday" | "Swing" + public string SuggestedRisk { get; set; } = "Medium"; // "Low" | "Medium" | "High" + + public ExecutionPlanInfo? ExecutionPlan { get; set; } + public DetailedAnalysisInfo? DetailedAnalysis { get; set; } +} + +public class ExecutionPlanInfo +{ + public EntryZoneInfo? EntryZone { get; set; } + public decimal StopLoss { get; set; } + public List? TakeProfitTargets { get; set; } + public decimal RiskRewardRatio { get; set; } + public decimal MaxLeverage { get; set; } +} + +public class EntryZoneInfo +{ + public decimal Min { get; set; } + public decimal Max { get; set; } +} + +public class DetailedAnalysisInfo +{ + public string TechnicalRationale { get; set; } = string.Empty; + public string FundamentalRationale { get; set; } = string.Empty; + public string RiskWarning { get; set; } = string.Empty; +} diff --git a/FinlyticCore/Models/Analyzer/VixMarketRegime.cs b/FinlyticCore/Models/Analyzer/VixMarketRegime.cs new file mode 100644 index 0000000..0e06022 --- /dev/null +++ b/FinlyticCore/Models/Analyzer/VixMarketRegime.cs @@ -0,0 +1,12 @@ +namespace FinlyticCore.Models.Analyzer; + +/// +/// Market volatility regime derived from VIX / VDAX index level. +/// +public enum VixMarketRegime +{ + LowVol = 0, // VIX < 15 + Normal = 1, // VIX 15 - 20 + HighVol = 2, // VIX 20 - 30 + Panic = 3 // VIX > 30 +} diff --git a/FinlyticCore/Models/Auth/AuthResponseDto.cs b/FinlyticCore/Models/Auth/AuthResponseDto.cs new file mode 100644 index 0000000..8507d10 --- /dev/null +++ b/FinlyticCore/Models/Auth/AuthResponseDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace FinlyticCore.Models.Auth; + +public class AuthResponseDto +{ + public string Token { get; set; } = string.Empty; + public Guid UserId { get; set; } + public string Email { get; set; } = string.Empty; + public string FullName { get; set; } = string.Empty; + public string Role { get; set; } = "User"; + public List FcmTokens { get; set; } = new(); + public DateTime ExpiresAt { get; set; } + public bool RequiresPasswordChange { get; set; } +} diff --git a/FinlyticCore/Models/Auth/CreateUserRequestDto.cs b/FinlyticCore/Models/Auth/CreateUserRequestDto.cs new file mode 100644 index 0000000..74e0481 --- /dev/null +++ b/FinlyticCore/Models/Auth/CreateUserRequestDto.cs @@ -0,0 +1,9 @@ +namespace FinlyticCore.Models.Auth; + +public class CreateUserRequestDto +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string FullName { get; set; } = string.Empty; + public string Role { get; set; } = "User"; // "User" | "Admin" +} diff --git a/FinlyticCore/Models/Auth/ITradeClient.cs b/FinlyticCore/Models/Auth/ITradeClient.cs new file mode 100644 index 0000000..b1a3527 --- /dev/null +++ b/FinlyticCore/Models/Auth/ITradeClient.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using FinlyticCore.Models.Trades; + +namespace FinlyticCore.Models.Auth; + +/// +/// Strongly typed SignalR client interface for real-time WebSocket/SSE streaming. +/// +public interface ITradeClient +{ + Task OnTradeProposed(TradeProposalDto proposal); + Task OnTradeUpdated(TradeHourlyUpdateDto update); + Task OnTradeClosed(string tradeId, decimal exitPrice, string reason); + Task OnNewsReceived(object newsItem); +} diff --git a/FinlyticCore/Models/Auth/LoginRequestDto.cs b/FinlyticCore/Models/Auth/LoginRequestDto.cs new file mode 100644 index 0000000..4928f7e --- /dev/null +++ b/FinlyticCore/Models/Auth/LoginRequestDto.cs @@ -0,0 +1,7 @@ +namespace FinlyticCore.Models.Auth; + +public class LoginRequestDto +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/FinlyticCore/Models/Auth/RegisterRequestDto.cs b/FinlyticCore/Models/Auth/RegisterRequestDto.cs new file mode 100644 index 0000000..ce1049a --- /dev/null +++ b/FinlyticCore/Models/Auth/RegisterRequestDto.cs @@ -0,0 +1,22 @@ +namespace FinlyticCore.Models.Auth; + +/// +/// DTO representing a request for self-registration by a new user. +/// +public class RegisterRequestDto +{ + /// + /// User email address. + /// + public string Email { get; set; } = string.Empty; + + /// + /// User plain-text password. + /// + public string Password { get; set; } = string.Empty; + + /// + /// User full name. + /// + public string FullName { get; set; } = string.Empty; +} diff --git a/FinlyticCore/Models/Auth/UpdateFcmTokenRequestDto.cs b/FinlyticCore/Models/Auth/UpdateFcmTokenRequestDto.cs new file mode 100644 index 0000000..8f4a3d1 --- /dev/null +++ b/FinlyticCore/Models/Auth/UpdateFcmTokenRequestDto.cs @@ -0,0 +1,7 @@ +namespace FinlyticCore.Models.Auth; + +public class UpdateFcmTokenRequestDto +{ + public string FcmToken { get; set; } = string.Empty; + public string DeviceName { get; set; } = "MobileDevice"; +} diff --git a/FinlyticCore/Models/Auth/UpdateUserRequestDto.cs b/FinlyticCore/Models/Auth/UpdateUserRequestDto.cs new file mode 100644 index 0000000..be58df4 --- /dev/null +++ b/FinlyticCore/Models/Auth/UpdateUserRequestDto.cs @@ -0,0 +1,22 @@ +namespace FinlyticCore.Models.Auth; + +/// +/// DTO for updating user role or active status by an admin. +/// +public class UpdateUserRequestDto +{ + /// + /// Updated user role (User, Premium, Admin). + /// + public string? Role { get; set; } + + /// + /// Updated active state of user. + /// + public bool? IsActive { get; set; } + + /// + /// Updated full name. + /// + public string? FullName { get; set; } +} diff --git a/FinlyticCore/Models/Auth/UserDto.cs b/FinlyticCore/Models/Auth/UserDto.cs new file mode 100644 index 0000000..e6f3040 --- /dev/null +++ b/FinlyticCore/Models/Auth/UserDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace FinlyticCore.Models.Auth; + +public class UserDto +{ + public Guid Id { get; set; } + public string Email { get; set; } = string.Empty; + public string FullName { get; set; } = string.Empty; + public string Role { get; set; } = "User"; + public bool IsActive { get; set; } = true; + public List FcmTokens { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public DateTime? LastLoginAt { get; set; } +} diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicAssetResponse.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicAssetResponse.cs new file mode 100644 index 0000000..7ddc0b6 --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicAssetResponse.cs @@ -0,0 +1,139 @@ +namespace FinlyticCore.Models.TradeRepublic; + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +// 1. Der Response-Wrapper +public record TradeRepublicAssetResponse( + [property: JsonPropertyName("correlationId")] string CorrelationId, + [property: JsonPropertyName("resultCount")] int ResultCount, + [property: JsonPropertyName("results")] IList Results +); + +// 2. Das Tag-Objekt +public record TradeRepublicTag +{ + [JsonPropertyName("id")] public string Id { get; init; } = ""; + [JsonPropertyName("name")] public string Name { get; init; } = ""; + [JsonPropertyName("type")] public string Type { get; init; } = ""; +} + +// 3. Die Basisklasse MIT UNSEREM CUSTOM CONVERTER (Kein [JsonPolymorphic] mehr!) +[JsonConverter(typeof(TradeRepublicAssetConverter))] +public record TradeRepublicAsset +{ + [JsonPropertyName("isin")] public string Isin { get; init; } = ""; + [JsonPropertyName("name")] public string Name { get; init; } = ""; + [JsonPropertyName("type")] public string Type { get; init; } = ""; + [JsonPropertyName("instrumentCategory")] public string InstrumentCategory { get; init; } = ""; + [JsonPropertyName("hasCfd")] public bool HasCfd { get; init; } + [JsonPropertyName("imageId")] public string? ImageId { get; init; } + + [JsonPropertyName("tags")] + public IReadOnlyList Tags { get; init; } = Array.Empty(); +} + +// 4. Die spezifischen Klassen (inklusive Bond und Derivative aus deinem JSON!) + +public record TradeRepublicStock : TradeRepublicAsset +{ + [JsonPropertyName("derivativeProductCategories")] + public IReadOnlyList DerivativeProductCategories { get; init; } = Array.Empty(); +} + +public record TradeRepublicCrypto : TradeRepublicAsset +{ + [JsonPropertyName("subtitle")] public string Subtitle { get; init; } = ""; + [JsonPropertyName("searchSubtitle")] public string SearchSubtitle { get; init; } = ""; +} + +public record TradeRepublicEtf : TradeRepublicAsset +{ + [JsonPropertyName("derivativeProductCategories")] + public IReadOnlyList DerivativeProductCategories { get; init; } = Array.Empty(); + [JsonPropertyName("etfDescription")] public string EtfDescription { get; init; } = ""; + [JsonPropertyName("mappedEtfIndexName")] public string MappedEtfIndexName { get; init; } = ""; + [JsonPropertyName("subtitle")] public string Subtitle { get; init; } = ""; + [JsonPropertyName("searchSubtitle")] public string SearchSubtitle { get; init; } = ""; +} + +public record TradeRepublicSynthetic : TradeRepublicAsset +{ + [JsonPropertyName("derivativeProductCategories")] + public IReadOnlyList DerivativeProductCategories { get; init; } = Array.Empty(); +} + +// NEU: Anleihen +public record TradeRepublicBond : TradeRepublicAsset +{ + [JsonPropertyName("bondIssuerName")] public string BondIssuerName { get; init; } = ""; + [JsonPropertyName("searchSubtitle")] public string SearchSubtitle { get; init; } = ""; +} + +// NEU: Derivate (Hebeleffekte etc.) +public record TradeRepublicDerivative : TradeRepublicAsset +{ + [JsonPropertyName("derivativeProductCategories")] + public IReadOnlyList DerivativeProductCategories { get; init; } = Array.Empty(); + + [JsonIgnore] + public string? UnderlyingIsin + { + get + { + // Wenn die ImageId z.B. "logos/US0378331005/v2" ist... + if (!string.IsNullOrEmpty(ImageId) && ImageId.StartsWith("logos/")) + { + var parts = ImageId.Split('/'); + if (parts.Length >= 2) + { + return parts[1]; // Gibt "US0378331005" zurück + } + } + return null; // Falls das Format mal anders ist + } + } +} + +// 5. Der Custom Converter - Die Maschine, die das JSON scannt und verteilt +public class TradeRepublicAssetConverter : JsonConverter +{ + public override TradeRepublicAsset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + // Wir scannen nach instrumentType, egal wo im JSON es steht! + string? instrumentType = null; + if (root.TryGetProperty("instrumentType", out var typeElement)) + { + instrumentType = typeElement.GetString(); + } + + // Wir werfen das JSON gezielt in die richtige Klasse + TradeRepublicAsset? result = instrumentType switch + { + "stock" => JsonSerializer.Deserialize(root.GetRawText(), options), + "crypto" => JsonSerializer.Deserialize(root.GetRawText(), options), + "fund" => JsonSerializer.Deserialize(root.GetRawText(), options), + "synthetic" => JsonSerializer.Deserialize(root.GetRawText(), options), + "bond" => JsonSerializer.Deserialize(root.GetRawText(), options), + "derivative" => JsonSerializer.Deserialize(root.GetRawText(), options), + + // Wenn TR einen Typ schickt, den wir noch nicht kennen: Fallback nutzen! + _ => JsonSerializer.Deserialize(root.GetRawText(), options) + }; + + return result ?? new TradeRepublicAssetFallback(); + } + + public override void Write(Utf8JsonWriter writer, TradeRepublicAsset value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} + +// Ein reiner Fallback-Record, der nur intern vom Converter genutzt wird +file record TradeRepublicAssetFallback : TradeRepublicAsset; \ No newline at end of file diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicConnectRequest.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicConnectRequest.cs new file mode 100644 index 0000000..a31f427 --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicConnectRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Models.TradeRepublic; + +public record TradeRepublicConnectRequest( + [property: JsonPropertyName("clientId")] string ClientId = "app.traderepublic.com", + [property: JsonPropertyName("clientVersion")] string ClientVersion = "15.65.6", + [property: JsonPropertyName("locale")] string Locale = "en", + [property: JsonPropertyName("platformId")] string PlatformId = "webtrading", + [property: JsonPropertyName("platformVersion")] string PlatformVersion = "chrome - 149.0.0", + TradeRepublicHeaders? Headers = null +) +{ + [JsonPropertyName("__headers")] + public TradeRepublicHeaders Headers { get; init; } = Headers ?? new TradeRepublicHeaders(); +} diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicHeaders.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicHeaders.cs new file mode 100644 index 0000000..979f2fc --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicHeaders.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using FinlyticCore.Util; + +namespace FinlyticCore.Models.TradeRepublic; + +public record TradeRepublicHeaders( + [property: JsonPropertyName("traceparent")] string Traceparent +) +{ + public TradeRepublicHeaders() : this(StringCodeGenerator.GenerateTraceparent()) + {} +} diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicSearchRequest.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicSearchRequest.cs new file mode 100644 index 0000000..c5ea13a --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicSearchRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Models.TradeRepublic; + +public record TradeRepublicFilter( + [property: JsonPropertyName("key")] string Key, + [property: JsonPropertyName("value")] string Value +); + +public record TradeRepublicSearchData( + [property: JsonPropertyName("q")] string Query = "", + [property: JsonPropertyName("page")] int Page = 1, + [property: JsonPropertyName("pageSize")] int PageSize = 50, + IReadOnlyList? Filter = null +) +{ + [JsonPropertyName("filter")] + public IReadOnlyList Filter { get; init; } = Filter ?? Array.Empty(); +} + +public record TradeRepublicSearchRequest( + [property: JsonPropertyName("data")] TradeRepublicSearchData Data, + [property: JsonPropertyName("type")] string Type = "neonSearch", + TradeRepublicHeaders? Headers = null +) +{ + [JsonPropertyName("__headers")] + public TradeRepublicHeaders Headers { get; init; } = Headers ?? new TradeRepublicHeaders(); +} diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerRequest.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerRequest.cs new file mode 100644 index 0000000..83b57d9 --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerRequest.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace FinlyticCore.Models.TradeRepublic; + +public record TradeRepublicTickerRequest( + [property: JsonPropertyName("id")] string Id, // e.g. "US5398301094.TIB" + [property: JsonPropertyName("type")] string Type = "ticker", + TradeRepublicHeaders? Headers = null +) +{ + [JsonPropertyName("__headers")] + public TradeRepublicHeaders Headers { get; init; } = Headers ?? new TradeRepublicHeaders(); +} diff --git a/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerResponse.cs b/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerResponse.cs new file mode 100644 index 0000000..bb2ed11 --- /dev/null +++ b/FinlyticCore/Models/TradeRepublic/TradeRepublicTickerResponse.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Text.Json.Serialization; + +namespace FinlyticCore.Models.TradeRepublic; + +public record TradeRepublicPriceTick( + [property: JsonPropertyName("time")] long Time, + [property: JsonPropertyName("price")] string Price, + [property: JsonPropertyName("size")] decimal Size +) +{ + public decimal PriceValue => decimal.TryParse(Price, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : 0m; + public DateTime DateTimeUtc => DateTimeOffset.FromUnixTimeMilliseconds(Time).UtcDateTime; +} + +public record TradeRepublicTickerResponse( + [property: JsonPropertyName("bid")] TradeRepublicPriceTick? Bid, + [property: JsonPropertyName("ask")] TradeRepublicPriceTick? Ask, + [property: JsonPropertyName("last")] TradeRepublicPriceTick? Last, + [property: JsonPropertyName("pre")] TradeRepublicPriceTick? Pre, + [property: JsonPropertyName("open")] TradeRepublicPriceTick? Open, + [property: JsonPropertyName("qualityId")] string? QualityId, + [property: JsonPropertyName("leverage")] decimal? Leverage, + [property: JsonPropertyName("delta")] decimal? Delta +); diff --git a/FinlyticCore/Models/Trades/CloseTradeRequest.cs b/FinlyticCore/Models/Trades/CloseTradeRequest.cs new file mode 100644 index 0000000..6e717de --- /dev/null +++ b/FinlyticCore/Models/Trades/CloseTradeRequest.cs @@ -0,0 +1,13 @@ +using System; + +namespace FinlyticCore.Models.Trades; + +/// +/// Request payload for manually closing an active trade via REST API. +/// +public class CloseTradeRequest +{ + public decimal UserExitPrice { get; set; } + public DateTime? UserExitTimestamp { get; set; } + public string CloseReason { get; set; } = "ManualClosure"; // "TakeProfitHit", "StopLossHit", "ManualClosure", "TimeExpired" +} diff --git a/FinlyticCore/Models/Trades/TradeAcceptanceDto.cs b/FinlyticCore/Models/Trades/TradeAcceptanceDto.cs new file mode 100644 index 0000000..1ad5652 --- /dev/null +++ b/FinlyticCore/Models/Trades/TradeAcceptanceDto.cs @@ -0,0 +1,31 @@ +using System; + +namespace FinlyticCore.Models.Trades; + +public class TradeAcceptanceDto +{ + public string TradeId { get; set; } = string.Empty; + public string AnalysisId { get; set; } = string.Empty; + public string Isin { get; set; } = string.Empty; + public string? UserId { get; set; } = "default_user"; + + public decimal? ActualEntryPrice { get; set; } + public decimal? PositionSize { get; set; } + public decimal? LeverageUsed { get; set; } = 1; + public decimal? EntryFee { get; set; } = 0; + public decimal? ExitFee { get; set; } = 0; + + public string? Symbol { get; set; } + public string? SignalType { get; set; } + public decimal? EntryPrice { get; set; } + public decimal? StopLoss { get; set; } + public decimal? TakeProfit { get; set; } + public string? InstrumentType { get; set; } + public string? Timeframe { get; set; } + public string? Reasoning { get; set; } + + public DateTime? ExecutionTimestamp { get; set; } + public decimal? Quantity { get; set; } + public decimal? KnockoutThreshold { get; set; } + public bool IsRecurring { get; set; } = false; +} diff --git a/FinlyticCore/Models/Trades/TradeFeedbackRecord.cs b/FinlyticCore/Models/Trades/TradeFeedbackRecord.cs new file mode 100644 index 0000000..580d25f --- /dev/null +++ b/FinlyticCore/Models/Trades/TradeFeedbackRecord.cs @@ -0,0 +1,35 @@ +using System; +using FinlyticCore.Models.Analyzer; + +namespace FinlyticCore.Models.Trades; + +/// +/// Structured closed trade record exported to JSON/Parquet for AI win-rate calibration feedback loops. +/// +public class TradeFeedbackRecord +{ + public string TradeId { get; set; } = string.Empty; + public string AnalysisId { get; set; } = string.Empty; + public string Sector { get; set; } = string.Empty; + public string Symbol { get; set; } = string.Empty; + public string Isin { get; set; } = string.Empty; + + public decimal EntryPrice { get; set; } + public decimal StopLoss { get; set; } + public decimal TakeProfit { get; set; } + public decimal UserExitPrice { get; set; } + + public decimal PnlAbsolute { get; set; } + public decimal PnlPercent { get; set; } + public bool IsWin { get; set; } + + public string CloseReason { get; set; } = string.Empty; + public VixMarketRegime VixRegime { get; set; } + public decimal VixValue { get; set; } + + public double ReactionDelayMinutes { get; set; } + public decimal SlippagePercent { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime ClosedAt { get; set; } +} diff --git a/FinlyticCore/Models/Trades/TradeHourlyUpdateDto.cs b/FinlyticCore/Models/Trades/TradeHourlyUpdateDto.cs new file mode 100644 index 0000000..9a0b04d --- /dev/null +++ b/FinlyticCore/Models/Trades/TradeHourlyUpdateDto.cs @@ -0,0 +1,20 @@ +using System; + +namespace FinlyticCore.Models.Trades; + +/// +/// Hourly AI recommendation update for an active trade. +/// +public class TradeHourlyUpdateDto +{ + public string TradeId { get; set; } = string.Empty; + public string Recommendation { get; set; } = "Hold"; // "Hold", "AdjustSL", "AdjustTP", "Close" + + public decimal CurrentPrice { get; set; } + public decimal? SuggestedStopLoss { get; set; } + public decimal? SuggestedTakeProfit { get; set; } + public decimal VixValue { get; set; } + + public string Reasoning { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} diff --git a/FinlyticCore/Models/Trades/TradeProposalDto.cs b/FinlyticCore/Models/Trades/TradeProposalDto.cs new file mode 100644 index 0000000..35a5363 --- /dev/null +++ b/FinlyticCore/Models/Trades/TradeProposalDto.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using FinlyticCore.Models.Analyzer; + +namespace FinlyticCore.Models.Trades; + +/// +/// Trade proposal generated by FinlyticAnalyzer and dispatched via MQTT QoS 2. +/// +public class TradeProposalDto +{ + public string TradeId { get; set; } = string.Empty; + public string? UserId { get; set; } + public bool IsGlobalProposal { get; set; } = true; + public string Status { get; set; } = "Proposed"; + + public string AnalysisId { get; set; } = string.Empty; + public string EventId { get; set; } = string.Empty; + public string Sector { get; set; } = string.Empty; + public string Symbol { get; set; } = string.Empty; + public string Isin { get; set; } = string.Empty; + public string CompanyName { get; set; } = string.Empty; + + public decimal EntryPrice { get; set; } + public decimal StopLoss { get; set; } + public decimal TakeProfit { get; set; } + + public string SignalType { get; set; } = "BUY"; // "BUY", "SELL" + public string RiskTolerance { get; set; } = "Moderate"; // "Conservative", "Moderate", "Aggressive" + public string Timeframe { get; set; } = "1D"; // "1H", "4H", "1D", "1W" + public string InstrumentType { get; set; } = "Stock"; // "Stock", "Option", "CFD", "Crypto" + + public double WinRate { get; set; } + public VixMarketRegime VixRegime { get; set; } + public decimal VixValue { get; set; } + + public int TtlMinutes { get; set; } = 60; + public string Reasoning { get; set; } = string.Empty; + + // --- New Fields for Detailed Execution & Rationale --- + public decimal? EntryZoneMin { get; set; } + public decimal? EntryZoneMax { get; set; } + public List? TakeProfitTargets { get; set; } + public decimal? RiskRewardRatio { get; set; } + public decimal? MaxLeverage { get; set; } + + public string TechnicalRationale { get; set; } = string.Empty; + public string FundamentalRationale { get; set; } = string.Empty; + public string RiskWarning { get; set; } = string.Empty; + + // --- Real Trade Execution Data --- + public decimal? ActualEntryPrice { get; set; } + public decimal? PositionSize { get; set; } + public decimal? LeverageUsed { get; set; } + public decimal? EntryFee { get; set; } + public decimal? ExitFee { get; set; } + public DateTime? ExecutionTimestamp { get; set; } + public decimal? Quantity { get; set; } + public decimal? KnockoutThreshold { get; set; } + public bool IsRecurring { get; set; } = false; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/FinlyticCore/Models/Trades/TradeStatus.cs b/FinlyticCore/Models/Trades/TradeStatus.cs new file mode 100644 index 0000000..c429b35 --- /dev/null +++ b/FinlyticCore/Models/Trades/TradeStatus.cs @@ -0,0 +1,14 @@ +namespace FinlyticCore.Models.Trades; + +/// +/// Status of a proposed/active trade lifecycle. +/// +public enum TradeStatus +{ + Proposed = 0, + Active = 1, + Closed = 2, + Expired = 3, + Rejected = 4, + Invalidated = 5 +} diff --git a/FinlyticCore/Project.md b/FinlyticCore/Project.md new file mode 100644 index 0000000..26d96d9 --- /dev/null +++ b/FinlyticCore/Project.md @@ -0,0 +1,32 @@ +# FinlyticCore Library + +`FinlyticCore` is the central shared class library for the Finlytic microservice architecture. It provides standardized data transfer objects (DTOs), domain models, MQTT communication primitives (`ManagedMqttClient`), and .NET 8 JSON Source Generators. + +--- + +## Key Modules & Components + +1. **`ManagedMqttClient`**: + - Resilient MQTT wrapper handling auto-reconnect, structured JSON publishing, topic subscription management, and synchronous Request-Reply (RPC) execution over MQTT. + +2. **`FinlyticJsonSerializerContext`**: + - .NET 8 Source Generator context (`[JsonSourceGenerationOptions]`, `[JsonSerializable]`) for reflection-free, zero-allocation UTF-8 JSON serialization across MQTT messages. + +3. **Domain Models & DTOs**: + - `Dtos/News`: `NewsArticleDto`, `DiscoveredArticle`, `MatchedAssetDto`, `FinBertResultDto`. + - `Dtos/Fundamentals`: `AssetFundamentalsDto`, `CorporateEventDto`. + - `Dtos/TechnicalAnalysis`: `CandleDto`, `ChartPatternDto`, `IndicatorValuesDto`, `MarketRegimeDto`, `StrategySignalDto`, `TechnicalAnalysisDto`. + - `Dtos/Sentiment`: `IsinSentimentSummaryDto`, `SectorSentimentSummaryDto`. + - `Models/Trades`: `TradeProposalDto`, `CloseTradeRequest`, `TradeHourlyUpdateDto`, `TradeFeedbackRecord`, `TradeStatus`. + +--- + +## Feature Status + +### Implemented Features +- [x] Centralized DTO definitions shared across all C# microservices. +- [x] Zero-allocation .NET 8 JSON Source Generation for all MQTT payloads. +- [x] Resilient MQTT RPC engine (`ExecuteRpcAsync`). + +### Planned Features +- [ ] Binary Protocol Buffers (protobuf) serialization option for ultra-low latency internal MQTT streaming. diff --git a/FinlyticCore/Services/TradeRepublic/TradeRepublicClient.cs b/FinlyticCore/Services/TradeRepublic/TradeRepublicClient.cs new file mode 100644 index 0000000..da1bc5e --- /dev/null +++ b/FinlyticCore/Services/TradeRepublic/TradeRepublicClient.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Concurrent; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FinlyticCore.Models.TradeRepublic; +using FinlyticCore.Util; +using Microsoft.Extensions.Logging; + +namespace FinlyticCore.Services.TradeRepublic; + +/// +/// A managed, thread-safe WebSocket client designed to communicate with the Trade Republic API. +/// Supports both single RPC requests and real-time live ticker subscriptions (e.g. {isin}.TIB). +/// +public class TradeRepublicClient : ManagedWebSocket +{ + private readonly ILogger _logger; + private int _currentSub; + private readonly ConcurrentDictionary> _pendingRequests = new(); + private readonly ConcurrentDictionary> _tickerSubscriptions = new(); + + public event Action? UnhandledMessageReceived; + public event Action? SystemMessageReceived; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + public TradeRepublicClient(ILogger logger) + { + _logger = logger; + } + + /// + /// Connects to the Trade Republic WebSocket API. + /// + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the connection was successful. + public async Task InitAsync(CancellationToken cancellationToken = default) + { + if (IsConnected) return true; + + await ConnectAsync("wss://api.traderepublic.com/", TimeSpan.FromSeconds(10)); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _pendingRequests.TryAdd(-1, tcs); + + try + { + var json = JsonSerializer.Serialize(new TradeRepublicConnectRequest(), typeof(TradeRepublicConnectRequest), FinlyticJsonSerializerContext.Default); + await SendAsync($"connect 34 {json}"); + + var res = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); + if (string.IsNullOrWhiteSpace(res.Type)) return false; + + var isConnected = res.Type == "connected"; + if (isConnected) + { + _logger.LogInformation("[{Channel}] WebSocket connection to Trade Republic established.", "TradeRepublicChannel"); + } + + return isConnected; + } + catch (Exception ex) + { + _pendingRequests.TryRemove(-1, out _); + _logger.LogWarning(ex, "[{Channel}] Failed or timed out establishing Trade Republic WebSocket connection.", "TradeRepublicChannel"); + return false; + } + } + + /// + /// Sends a JSON request to the Trade Republic WebSocket API and waits for the response. + /// + /// The expected response type. + /// The request type. + /// The request to send. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the deserialized response, or null if the request failed or timed out. + public async Task SendRequestAsync(TRequest request, CancellationToken cancellationToken = default) + where TResponse : class where TRequest : class + { + var tempSub = Interlocked.Increment(ref _currentSub); + var msg = $"sub {tempSub} {JsonSerializer.Serialize(request, typeof(TRequest), FinlyticJsonSerializerContext.Default)}"; + + _logger.LogDebug("[{Channel}] TR WS Sent (Request): {Message}", "TradeRepublicChannel", msg); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _pendingRequests.TryAdd(tempSub, tcs); + await SendAsync(msg); + + try + { + var res = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(8), cancellationToken); + if (string.IsNullOrWhiteSpace(res.Data) || !res.Type.Contains('A')) return null; + + return (TResponse?)JsonSerializer.Deserialize(res.Data, typeof(TResponse), FinlyticJsonSerializerContext.Default); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[{Channel}] Error waiting for Trade Republic response ID {SubId}", "TradeRepublicChannel", tempSub); + return null; + } + finally + { + _pendingRequests.TryRemove(tempSub, out _); + try { await SendAsync($"unsub {tempSub}"); } catch { } + } + } + + /// + /// Subscribes to the real-time ticker stream for a specific ISIN (e.g., US5398301094.TIB). + /// + public async Task SubscribeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(isin)) return null; + + var cleanIsin = isin.Trim().ToUpperInvariant(); + var tickerId = cleanIsin.EndsWith(".TIB") ? cleanIsin : $"{cleanIsin}.TIB"; + + var tempSub = Interlocked.Increment(ref _currentSub); + var req = new TradeRepublicTickerRequest(tickerId); + var msg = $"sub {tempSub} {JsonSerializer.Serialize(req, typeof(TradeRepublicTickerRequest), FinlyticJsonSerializerContext.Default)}"; + + _tickerSubscriptions[tempSub] = jsonPayload => + { + // Skip empty or non-JSON payloads (e.g. TR protocol ack messages) + if (string.IsNullOrWhiteSpace(jsonPayload) || (!jsonPayload.TrimStart().StartsWith('{') && !jsonPayload.TrimStart().StartsWith('['))) + return; + + try + { + var tickerRes = (TradeRepublicTickerResponse?)JsonSerializer.Deserialize(jsonPayload, typeof(TradeRepublicTickerResponse), FinlyticJsonSerializerContext.Default); + if (tickerRes != null) + { + onTick(tickerRes); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[{Channel}] Failed to parse real-time ticker payload for {TickerId}", "TradeRepublicChannel", tickerId); + } + }; + + _logger.LogInformation("[{Channel}] Subscribing to Trade Republic real-time ticker {TickerId} (Sub ID: {SubId})", "TradeRepublicChannel", tickerId, tempSub); + _logger.LogDebug("[{Channel}] TR WS Sent: {Message}", "TradeRepublicChannel", msg); + await SendAsync(msg); + return tempSub; + } + + /// + /// Unsubscribes from a real-time ticker stream. + /// + /// The subscription ID to unsubscribe. + /// A task representing the async operation. + public async Task UnsubscribeTickerAsync(int subId) + { + _tickerSubscriptions.TryRemove(subId, out _); + try + { + await SendAsync($"unsub {subId}"); + } + catch { } + } + + /// + protected override void OnMessageReceived(string message) + { + if (string.IsNullOrWhiteSpace(message)) return; + + _logger.LogDebug("[{Channel}] TR WS Recv: {Message}", "TradeRepublicChannel", message); + + // Trade Republic message formats: + // "34 connected" -> subId = 34, type = "connected", payload = "connected" + // "22A {...}" or "22A{...}" -> subId = 22, type = "A", payload = "{...}" + var digitLen = 0; + while (digitLen < message.Length && char.IsDigit(message[digitLen])) + { + digitLen++; + } + + if (digitLen == 0) + { + SystemMessageReceived?.Invoke(message); + return; + } + + if (!int.TryParse(message.Substring(0, digitLen), out var subId)) + { + SystemMessageReceived?.Invoke(message); + return; + } + + var remainder = message.Substring(digitLen).TrimStart(); + string type; + string payload; + + if (remainder.StartsWith("connected")) + { + type = "connected"; + payload = remainder; + } + else if (remainder.Length > 0) + { + // Type is usually a single character like 'A' or 'E' + // The JSON payload (or ack) starts immediately after or after a space + type = remainder[0].ToString(); + payload = remainder.Substring(1).TrimStart(); + } + else + { + type = "ack"; + payload = string.Empty; + } + + var received = new ReceivedMessage(subId, type, payload); + + if (_pendingRequests.TryGetValue(subId, out var tcs)) + { + tcs.TrySetResult(received); + } + + if (_tickerSubscriptions.TryGetValue(subId, out var handler)) + { + handler(payload); + } + + UnhandledMessageReceived?.Invoke(received); + } +} + +/// +/// Represents a received message from the Trade Republic WebSocket. +/// +/// The subscription ID. +/// The message type. +/// The payload data. +public record ReceivedMessage(int SubId, string Type, string Data); diff --git a/FinlyticCore/Services/TradeRepublic/TradeRepublicService.cs b/FinlyticCore/Services/TradeRepublic/TradeRepublicService.cs new file mode 100644 index 0000000..e93bdca --- /dev/null +++ b/FinlyticCore/Services/TradeRepublic/TradeRepublicService.cs @@ -0,0 +1,201 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using FinlyticCore.Models.TradeRepublic; +using FinlyticCore.Models.Assets; +using Microsoft.Extensions.Logging; + +namespace FinlyticCore.Services.TradeRepublic; + +/// +/// Service for interacting with the Trade Republic API. +/// +public interface ITradeRepublicService +{ + /// + /// Fetches asset metadata from Trade Republic by ISIN. + /// + /// The ISIN to search for. + /// A cancellation token. + /// The Trade Republic search response, or null if not found/failed. + Task GetAsset(string isin, CancellationToken cancellationToken = default); + + /// + /// Retrieves the total count of available assets grouped by their types. + /// + /// A token to monitor for cancellation requests. + /// An object containing the metrics. + Task GetAssetsCount(CancellationToken cancellationToken = default); + + /// + /// Retrieves a paginated chunk of assets filtered by a specific type. + /// + /// The type of assets to retrieve. + /// The zero-based page index. + /// The number of elements per page. + /// A token to monitor for cancellation requests. + /// A containing the elements, or null if the request fails. + Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default); + + /// + /// Subscribes to the real-time ticker stream for a specific ISIN. + /// + /// The ISIN. + /// The callback action when a tick is received. + /// A cancellation token. + /// The subscription ID, or null if failed. + Task SubscribeRealtimeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default); + + /// + /// Unsubscribes from a real-time ticker stream. + /// + /// The subscription ID to unsubscribe. + /// A task representing the async operation. + Task UnsubscribeRealtimeTickerAsync(int subId); +} + +public class TradeRepublicService : ITradeRepublicService, IDisposable +{ + private readonly TradeRepublicClient _client; + private readonly ILogger _logger; + private readonly System.Timers.Timer _inactivityTimer; + private readonly SemaphoreSlim _lock = new(1, 1); + + public TradeRepublicService(TradeRepublicClient client, ILogger logger) + { + _client = client; + _logger = logger; + + _inactivityTimer = new System.Timers.Timer(TimeSpan.FromSeconds(461).TotalMilliseconds); + _inactivityTimer.AutoReset = false; + _inactivityTimer.Elapsed += OnInactivityTimeout; + } + + private async Task EnsureConnectedAsync() + { + await _lock.WaitAsync(); + try + { + _inactivityTimer.Stop(); + if (!_client.IsConnected) + { + _logger.LogInformation("[{Channel}] Connecting to Trade Republic API WebSocket...", "TradeRepublicChannel"); + bool connected = await _client.InitAsync(); + if (!connected) + { + _logger.LogWarning("[{Channel}] Trade Republic WebSocket connection failed or timed out.", "TradeRepublicChannel"); + throw new InvalidOperationException("Trade Republic WebSocket is not connected."); + } + _logger.LogInformation("[{Channel}] Successfully connected to Trade Republic API.", "TradeRepublicChannel"); + } + _inactivityTimer.Start(); + } + finally + { + _lock.Release(); + } + } + + /// + public async Task GetAsset(string isin, CancellationToken cancellationToken = default) + { + try + { + await EnsureConnectedAsync(); + var reqData = new TradeRepublicSearchData + { + Query = isin, + Page = 1, + PageSize = 1, + Filter = new[] { new TradeRepublicFilter("jurisdiction", "DE") } + }; + var request = new TradeRepublicSearchRequest(Data: reqData); + return await _client.SendRequestAsync(request, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "[{Channel}] Error while fetching asset metadata for ISIN {Isin}", "TradeRepublicChannel", isin); + return null; + } + } + + /// + public async Task GetAssetsCount(CancellationToken cancellationToken = default) + { + await EnsureConnectedAsync(); + var counts = new AssetsCount(); + foreach (var type in Enum.GetValues()) + { + var reqData = new TradeRepublicSearchData + { + Query = "", + Page = 1, + PageSize = 1, + Filter = new[] + { + new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), + new TradeRepublicFilter("jurisdiction", "DE") + } + }; + var request = new TradeRepublicSearchRequest(Data: reqData); + var response = await _client.SendRequestAsync(request, cancellationToken); + var count = response?.ResultCount ?? 0; + counts.SetCountOfType(type, count); + await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken); + } + return counts; + } + + /// + public async Task GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default) + { + await EnsureConnectedAsync(); + var reqData = new TradeRepublicSearchData + { + Query = "", + Page = page, + PageSize = pageSize, + Filter = new[] + { + new TradeRepublicFilter("type", type.ToString().ToLowerInvariant()), + new TradeRepublicFilter("jurisdiction", "DE") + } + }; + var request = new TradeRepublicSearchRequest(Data: reqData); + return await _client.SendRequestAsync(request, cancellationToken); + } + + /// + public async Task SubscribeRealtimeTickerAsync(string isin, Action onTick, CancellationToken cancellationToken = default) + { + await EnsureConnectedAsync(); + return await _client.SubscribeTickerAsync(isin, onTick, cancellationToken); + } + + /// + public async Task UnsubscribeRealtimeTickerAsync(int subId) + { + await _client.UnsubscribeTickerAsync(subId); + } + + private async void OnInactivityTimeout(object? sender, ElapsedEventArgs e) + { + try + { + await _lock.WaitAsync(); + if (!_client.IsConnected) return; + _logger.LogInformation("[{Channel}] Inactivity timer expired. Auto-disconnecting Trade Republic WebSocket.", "TradeRepublicChannel"); + await _client.DisconnectAsync(); + } + catch { } + finally { _lock.Release(); } + } + + public void Dispose() + { + _inactivityTimer.Dispose(); + _lock.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/FinlyticCore/Services/Yahoo/YahooFinanceClient.cs b/FinlyticCore/Services/Yahoo/YahooFinanceClient.cs new file mode 100644 index 0000000..5d32092 --- /dev/null +++ b/FinlyticCore/Services/Yahoo/YahooFinanceClient.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FinlyticCore.Dtos.Yahoo; +using Microsoft.Extensions.Logging; + +namespace FinlyticCore.Services.Yahoo; + +/// +/// Managed thread-safe HTTP client for Yahoo Finance APIs. +/// Implements the two-step Cookie (A3) & Crumb token authentication flow. +/// +public class YahooFinanceClient +{ + private const string DefaultUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; + + private readonly HttpClient _httpClient; + private readonly CookieContainer _cookieContainer; + private readonly ILogger? _logger; + private readonly SemaphoreSlim _authLock = new(1, 1); + + private string? _crumb; + private DateTime _lastAuthTime = DateTime.MinValue; + + /// + /// Standard modules available for the quoteSummary endpoint. + /// + public static readonly string[] StandardQuoteSummaryModules = new[] + { + "assetProfile", + "financialData", + "defaultKeyStatistics", + "summaryDetail", + "incomeStatementHistory", + "incomeStatementHistoryQuarterly", + "balanceSheetHistory", + "balanceSheetHistoryQuarterly", + "cashflowStatementHistory", + "cashflowStatementHistoryQuarterly", + "calendarEvents" + }; + + public YahooFinanceClient(ILogger? logger = null, HttpClient? httpClient = null) + { + _logger = logger; + _cookieContainer = new CookieContainer(); + + if (httpClient != null) + { + _httpClient = httpClient; + } + else + { + var handler = new HttpClientHandler + { + CookieContainer = _cookieContainer, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + _httpClient = new HttpClient(handler); + } + + if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent")) + { + _httpClient.DefaultRequestHeaders.Add("User-Agent", DefaultUserAgent); + } + } + + /// + /// Executes the Cookie (A3) & Crumb token authentication flow. + /// 1. GET https://fc.yahoo.com (sets session A3 cookie) + /// 2. GET https://query1.finance.yahoo.com/v1/test/getcrumb (returns crumb string) + /// + public async Task EnsureAuthenticatedAsync(bool forceRefresh = false, + CancellationToken cancellationToken = default) + { + if (!forceRefresh && !string.IsNullOrWhiteSpace(_crumb) && (DateTime.UtcNow - _lastAuthTime).TotalHours < 12) + { + return _crumb; + } + + await _authLock.WaitAsync(cancellationToken); + try + { + if (!forceRefresh && !string.IsNullOrWhiteSpace(_crumb) && + (DateTime.UtcNow - _lastAuthTime).TotalHours < 12) + { + return _crumb; + } + + _logger?.LogInformation("[YahooFinanceClient] Authenticating session (Cookie + Crumb)..."); + + // 1. Send GET request to fc.yahoo.com to obtain session cookie A3 + using (var initRequest = new HttpRequestMessage(HttpMethod.Get, "https://fc.yahoo.com")) + { + using var initResponse = await _httpClient.SendAsync(initRequest, cancellationToken); + // CookieContainer automatically intercepts and stores 'A3' cookie + } + + // 2. Send GET request to getcrumb to obtain the dynamic crumb token + using (var crumbRequest = + new HttpRequestMessage(HttpMethod.Get, "https://query1.finance.yahoo.com/v1/test/getcrumb")) + { + using var crumbResponse = await _httpClient.SendAsync(crumbRequest, cancellationToken); + if (!crumbResponse.IsSuccessStatusCode) + { + _logger?.LogWarning("[YahooFinanceClient] Failed to fetch crumb token. Status: {Status}", + crumbResponse.StatusCode); + return null; + } + + var crumbText = await crumbResponse.Content.ReadAsStringAsync(cancellationToken); + _crumb = crumbText.Trim('"', ' ', '\t', '\r', '\n'); + _lastAuthTime = DateTime.UtcNow; + + _logger?.LogInformation("[YahooFinanceClient] Acquired Crumb token successfully: {Crumb}", _crumb); + return _crumb; + } + } + catch (Exception ex) + { + _logger?.LogError(ex, "[YahooFinanceClient] Exception during Cookie & Crumb authentication."); + return null; + } + finally + { + _authLock.Release(); + } + } + + /// + /// Searches for tickers, names, ISINs, or companies via the Yahoo Finance search API. + /// URL: https://query2.finance.yahoo.com/v1/finance/search?q={query}&quotesCount={quotesCount}&newsCount={newsCount} + /// Note: Does not require Cookie/Crumb authentication. + /// + public async Task SearchAsync( + string query, + int quotesCount = 10, + int newsCount = 0, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(query)) return null; + + try + { + var url = + $"https://query2.finance.yahoo.com/v1/finance/search?q={Uri.EscapeDataString(query)}"esCount={quotesCount}&newsCount={newsCount}"; + using var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger?.LogWarning("[YahooFinanceClient] Search for '{Query}' failed with status {Status}", query, + response.StatusCode); + return null; + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + return JsonSerializer.Deserialize(json, GetJsonOptions()); + } + catch (Exception ex) + { + _logger?.LogError(ex, "[YahooFinanceClient] Exception during Search for query '{Query}'", query); + return null; + } + } + + /// + /// Retrieves fundamentals and company metadata using the quoteSummary endpoint. + /// URL: https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?crumb={crumb}&modules={modules} + /// + public async Task GetQuoteSummaryAsync( + string symbol, + IEnumerable modules, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(symbol)) return null; + + var moduleList = string.Join(",", modules); + return await ExecuteWithRetryAsync(async (crumb) => + { + var url = + $"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{Uri.EscapeDataString(symbol)}?crumb={Uri.EscapeDataString(crumb)}&modules={Uri.EscapeDataString(moduleList)}"; + using var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger?.LogWarning("[YahooFinanceClient] GetQuoteSummary for '{Symbol}' failed with status {Status}", + symbol, response.StatusCode); + return ( + response.StatusCode == HttpStatusCode.Unauthorized || + response.StatusCode == HttpStatusCode.Forbidden, null); + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var dto = JsonSerializer.Deserialize(json, GetJsonOptions()); + return (false, dto); + }, cancellationToken); + } + + /// + /// Convenience method to fetch all standard quoteSummary modules for a given symbol. + /// + public Task GetFullQuoteSummaryAsync(string symbol, + CancellationToken cancellationToken = default) + { + return GetQuoteSummaryAsync(symbol, StandardQuoteSummaryModules, cancellationToken); + } + + /// + /// Retrieves historical OHLCV chart data for a given symbol. + /// URL: https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range={range}&interval={interval}&crumb={crumb} + /// + public async Task GetChartAsync( + string symbol, + string range = "1y", + string interval = "1d", + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(symbol)) return null; + + return await ExecuteWithRetryAsync(async (crumb) => + { + var url = + $"https://query1.finance.yahoo.com/v8/finance/chart/{Uri.EscapeDataString(symbol)}?range={Uri.EscapeDataString(range)}&interval={Uri.EscapeDataString(interval)}&crumb={Uri.EscapeDataString(crumb)}"; + using var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger?.LogWarning("[YahooFinanceClient] GetChart for '{Symbol}' failed with status {Status}", symbol, + response.StatusCode); + return ( + response.StatusCode == HttpStatusCode.Unauthorized || + response.StatusCode == HttpStatusCode.Forbidden, null); + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var dto = JsonSerializer.Deserialize(json, GetJsonOptions()); + return (false, dto); + }, cancellationToken); + } + + /// + /// Retrieves quick real-time price quotes for one or more symbols. + /// URL: https://query1.finance.yahoo.com/v7/finance/quote?symbols={symbols}&crumb={crumb} + /// + public async Task GetQuotesAsync( + IEnumerable symbols, + CancellationToken cancellationToken = default) + { + var symbolList = symbols.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); + if (symbolList.Count == 0) return null; + + var symbolsParam = string.Join(",", symbolList); + return await ExecuteWithRetryAsync(async (crumb) => + { + var url = + $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={Uri.EscapeDataString(symbolsParam)}&crumb={Uri.EscapeDataString(crumb)}"; + using var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger?.LogWarning("[YahooFinanceClient] GetQuotes failed with status {Status}", response.StatusCode); + return ( + response.StatusCode == HttpStatusCode.Unauthorized || + response.StatusCode == HttpStatusCode.Forbidden, null); + } + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var dto = JsonSerializer.Deserialize(json, GetJsonOptions()); + return (false, dto); + }, cancellationToken); + } + + /// + /// Convenient helper method to fetch the current live price for a single symbol (e.g., "^VIX"). + /// + public async Task GetLivePriceAsync(string symbol, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(symbol)) return null; + + var quotes = await GetQuotesAsync(new[] { symbol }, cancellationToken); + var item = quotes?.QuoteResponse?.Result?.FirstOrDefault(); + + if (item?.RegularMarketPrice.HasValue == true && item.RegularMarketPrice.Value > 0) + { + return Convert.ToDecimal(item.RegularMarketPrice.Value); + } + + return null; + } + + private async Task ExecuteWithRetryAsync( + Func> action, + CancellationToken cancellationToken) where T : class + { + var crumb = await EnsureAuthenticatedAsync(false, cancellationToken); + if (string.IsNullOrEmpty(crumb)) return null; + + var (isAuthError, result) = await action(crumb); + if (!isAuthError && result != null) + { + return result; + } + + if (isAuthError) + { + _logger?.LogInformation( + "[YahooFinanceClient] Authentication error encountered (401/403). Re-authenticating..."); + crumb = await EnsureAuthenticatedAsync(true, cancellationToken); + if (string.IsNullOrEmpty(crumb)) return null; + + var (_, retryResult) = await action(crumb); + return retryResult; + } + + return result; + } + + private static JsonSerializerOptions GetJsonOptions() + { + return new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + }; + } +} \ No newline at end of file diff --git a/FinlyticCore/Util/AssetMapper.cs b/FinlyticCore/Util/AssetMapper.cs index 2a68d77..3ce6a08 100644 --- a/FinlyticCore/Util/AssetMapper.cs +++ b/FinlyticCore/Util/AssetMapper.cs @@ -1,16 +1,13 @@ -using FinlyticCore.Dtos.Assets; +using FinlyticCore.Dtos.Assets; using FinlyticCore.Entities.Assets; namespace FinlyticCore.Util; public static class AssetMapper { - /// - /// Mappt eine AssetEntity (Datenbank) sicher auf ein zyklusfreies AssetDto (MQTT Payload). - /// + public static AssetDto ToDto(this AssetEntity entity) { - // 1. Tags zyklusfrei mappen var dtoTags = entity.Tags.Select(t => new TagDto { Id = t.Id, @@ -18,46 +15,91 @@ public static class AssetMapper Type = t.Type }).ToList(); - // 2. Polymorphes Mapping basierend auf dem Laufzeittyp return entity switch { StockEntity stock => new StockDto { - Isin = stock.Isin, Name = stock.Name, Type = stock.Type, InstrumentCategory = stock.InstrumentCategory, HasCfd = stock.HasCfd, ImageId = stock.ImageId, UpdateAt = stock.UpdateAt, LastUpdatedAt = stock.LastUpdatedAt, Tags = dtoTags, + Isin = stock.Isin, + Name = stock.Name, + Type = stock.Type, + InstrumentCategory = stock.InstrumentCategory, + HasCfd = stock.HasCfd, + ImageId = stock.ImageId, + LastUpdatedAt = stock.LastUpdatedAt, + Tags = dtoTags, DerivativeProductCategories = stock.DerivativeProductCategories }, EtfEntity etf => new EtfDto { - Isin = etf.Isin, Name = etf.Name, Type = etf.Type, InstrumentCategory = etf.InstrumentCategory, HasCfd = etf.HasCfd, ImageId = etf.ImageId, UpdateAt = etf.UpdateAt, LastUpdatedAt = etf.LastUpdatedAt, Tags = dtoTags, - DerivativeProductCategories = etf.DerivativeProductCategories, EtfDescription = etf.EtfDescription, MappedEtfIndexName = etf.MappedEtfIndexName, Subtitle = etf.Subtitle, SearchSubtitle = etf.SearchSubtitle + Isin = etf.Isin, + Name = etf.Name, + Type = etf.Type, + InstrumentCategory = etf.InstrumentCategory, + HasCfd = etf.HasCfd, + ImageId = etf.ImageId, + LastUpdatedAt = etf.LastUpdatedAt, + Tags = dtoTags, + DerivativeProductCategories = etf.DerivativeProductCategories, + EtfDescription = etf.EtfDescription, + MappedEtfIndexName = etf.MappedEtfIndexName, + Subtitle = etf.Subtitle, + SearchSubtitle = etf.SearchSubtitle }, CryptoEntity crypto => new CryptoDto { - Isin = crypto.Isin, Name = crypto.Name, Type = crypto.Type, InstrumentCategory = crypto.InstrumentCategory, HasCfd = crypto.HasCfd, ImageId = crypto.ImageId, UpdateAt = crypto.UpdateAt, LastUpdatedAt = crypto.LastUpdatedAt, Tags = dtoTags, - Subtitle = crypto.Subtitle, SearchSubtitle = crypto.SearchSubtitle + Isin = crypto.Isin, + Name = crypto.Name, + Type = crypto.Type, + InstrumentCategory = crypto.InstrumentCategory, + HasCfd = crypto.HasCfd, + ImageId = crypto.ImageId, + LastUpdatedAt = crypto.LastUpdatedAt, + Tags = dtoTags, + Subtitle = crypto.Subtitle, + SearchSubtitle = crypto.SearchSubtitle }, BondEntity bond => new BondDto { - Isin = bond.Isin, Name = bond.Name, Type = bond.Type, InstrumentCategory = bond.InstrumentCategory, HasCfd = bond.HasCfd, ImageId = bond.ImageId, UpdateAt = bond.UpdateAt, LastUpdatedAt = bond.LastUpdatedAt, Tags = dtoTags, - BondIssuerName = bond.BondIssuerName, SearchSubtitle = bond.SearchSubtitle + Isin = bond.Isin, + Name = bond.Name, + Type = bond.Type, + InstrumentCategory = bond.InstrumentCategory, + HasCfd = bond.HasCfd, + ImageId = bond.ImageId, + LastUpdatedAt = bond.LastUpdatedAt, + Tags = dtoTags, + BondIssuerName = bond.BondIssuerName, + SearchSubtitle = bond.SearchSubtitle }, DerivativeEntity deriv => new DerivativeDto { - Isin = deriv.Isin, Name = deriv.Name, Type = deriv.Type, InstrumentCategory = deriv.InstrumentCategory, HasCfd = deriv.HasCfd, ImageId = deriv.ImageId, UpdateAt = deriv.UpdateAt, LastUpdatedAt = deriv.LastUpdatedAt, Tags = dtoTags, - DerivativeProductCategories = deriv.DerivativeProductCategories, UnderlyingIsin = deriv.UnderlyingIsin + Isin = deriv.Isin, + Name = deriv.Name, + Type = deriv.Type, + InstrumentCategory = deriv.InstrumentCategory, + HasCfd = deriv.HasCfd, + ImageId = deriv.ImageId, + LastUpdatedAt = deriv.LastUpdatedAt, + Tags = dtoTags, + DerivativeProductCategories = deriv.DerivativeProductCategories, + UnderlyingIsin = deriv.UnderlyingIsin }, SyntheticEntity synth => new SyntheticDto { - Isin = synth.Isin, Name = synth.Name, Type = synth.Type, InstrumentCategory = synth.InstrumentCategory, HasCfd = synth.HasCfd, ImageId = synth.ImageId, UpdateAt = synth.UpdateAt, LastUpdatedAt = synth.LastUpdatedAt, Tags = dtoTags, + Isin = synth.Isin, + Name = synth.Name, + Type = synth.Type, + InstrumentCategory = synth.InstrumentCategory, + HasCfd = synth.HasCfd, + ImageId = synth.ImageId, + LastUpdatedAt = synth.LastUpdatedAt, + Tags = dtoTags, DerivativeProductCategories = synth.DerivativeProductCategories }, _ => throw new NotSupportedException($"Mapping for type {entity.GetType().Name} is not supported.") }; } - - /// - /// Mappt direkt eine ganze Liste von AssetEntities. - /// + public static List ToDtoList(this IEnumerable entities) { return entities.Select(e => e.ToDto()).ToList(); diff --git a/FinlyticCore/Util/FinlyticJsonSerializerContext.cs b/FinlyticCore/Util/FinlyticJsonSerializerContext.cs new file mode 100644 index 0000000..73e035c --- /dev/null +++ b/FinlyticCore/Util/FinlyticJsonSerializerContext.cs @@ -0,0 +1,129 @@ +using System.Text.Json.Serialization; +using FinlyticCore.Dtos; +using FinlyticCore.Dtos.Fundamentals; +using FinlyticCore.Dtos.News; +using FinlyticCore.Dtos.Sentiment; +using FinlyticCore.Dtos.TechnicalAnalysis; +using FinlyticCore.Dtos.Yahoo; +using FinlyticCore.Models.Trades; +using FinlyticCore.Models.Analyzer; +using System.Collections.Generic; +using FinlyticAssets.Models; + +namespace FinlyticCore.Util; + +[JsonSourceGenerationOptions( + WriteIndented = false, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(TradeProposalDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(TradeAcceptanceDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(CloseTradeRequest))] +[JsonSerializable(typeof(ManualAnalysisResponseDto))] +[JsonSerializable(typeof(N8nAnalysisResponseDto))] +[JsonSerializable(typeof(TradeHourlyUpdateDto))] +[JsonSerializable(typeof(TradeFeedbackRecord))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(NewsArticleDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(DiscoveredArticle))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(MatchedAssetDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(FinBertResultDto))] +[JsonSerializable(typeof(UpdateNewsStatusRequest))] +[JsonSerializable(typeof(UpdateNewsStatusResponse))] +[JsonSerializable(typeof(N8nRequestPayload))] +[JsonSerializable(typeof(N8nResponsePayload))] +[JsonSerializable(typeof(N8nMatchedAssetPayload))] +[JsonSerializable(typeof(FilteredAssetPayload))] +[JsonSerializable(typeof(AssetFundamentalsDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(CorporateEventDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IsinSentimentSummaryDto))] +[JsonSerializable(typeof(IsinAnalysisEntry))] +[JsonSerializable(typeof(SectorSentimentSummaryDto))] + +[JsonSerializable(typeof(CandleDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ChartPatternDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IndicatorValuesDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(MarketRegimeDto))] +[JsonSerializable(typeof(StrategySignalDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(TechnicalAnalysisDto))] +[JsonSerializable(typeof(LivePriceDto))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(object))] +// Named MQTT request DTOs (replaces anonymous types, required for source-gen serialization) +[JsonSerializable(typeof(LimitRequest))] +[JsonSerializable(typeof(PaginatedRequest))] +[JsonSerializable(typeof(DailyNewsRequest))] +[JsonSerializable(typeof(IsinRequest))] +[JsonSerializable(typeof(GetTradesRequest))] +[JsonSerializable(typeof(ArticleRequest))] +[JsonSerializable(typeof(AnalyzeSentimentRequest))] +[JsonSerializable(typeof(EmptyRequest))] +[JsonSerializable(typeof(ManualAnalysisRpcRequest))] + +[JsonSerializable(typeof(ServiceHealthResponse))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(FetchLogoResponse))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(ServiceConfigUpdatePayload))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.AssetDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.StockDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.EtfDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.CryptoDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.BondDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.DerivativeDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.SyntheticDto))] +[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.TagDto))] +[JsonSerializable(typeof(FinlyticCore.Models.Assets.GetValidAssetRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.Assets.SearchAssetsRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.Assets.GetDiscoveryAssetsRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.TradeRepublic.TradeRepublicTickerResponse))] +[JsonSerializable(typeof(FinlyticCore.Models.TradeRepublic.TradeRepublicTickerRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.TradeRepublic.TradeRepublicConnectRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.TradeRepublic.TradeRepublicSearchRequest))] +[JsonSerializable(typeof(FinlyticCore.Models.TradeRepublic.TradeRepublicAssetResponse))] +[JsonSerializable(typeof(YahooValueDto))] +[JsonSerializable(typeof(YahooQuoteSummaryResponseDto))] +[JsonSerializable(typeof(YahooQuoteSummaryResultDto))] +[JsonSerializable(typeof(YahooQuoteSummaryModulesDto))] +[JsonSerializable(typeof(YahooAssetProfileDto))] +[JsonSerializable(typeof(YahooCompanyOfficerDto))] +[JsonSerializable(typeof(YahooFinancialDataDto))] +[JsonSerializable(typeof(YahooDefaultKeyStatisticsDto))] +[JsonSerializable(typeof(YahooSummaryDetailDto))] +[JsonSerializable(typeof(YahooFinancialStatementHistoryDto))] +[JsonSerializable(typeof(YahooIncomeStatementDto))] +[JsonSerializable(typeof(YahooBalanceSheetStatementDto))] +[JsonSerializable(typeof(YahooCashflowStatementDto))] +[JsonSerializable(typeof(YahooCalendarEventsDto))] +[JsonSerializable(typeof(YahooEarningsCalendarDto))] +[JsonSerializable(typeof(YahooChartResponseDto))] +[JsonSerializable(typeof(YahooChartResultWrapperDto))] +[JsonSerializable(typeof(YahooChartResultDto))] +[JsonSerializable(typeof(YahooChartMetaDto))] +[JsonSerializable(typeof(YahooChartIndicatorsDto))] +[JsonSerializable(typeof(YahooChartQuoteDto))] +[JsonSerializable(typeof(YahooChartAdjCloseDto))] +[JsonSerializable(typeof(YahooQuoteResponseDto))] +[JsonSerializable(typeof(YahooQuoteResultWrapperDto))] +[JsonSerializable(typeof(YahooQuoteItemDto))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(N8nAnalysisRequestDto))] +[JsonSerializable(typeof(TickMessageDto))] +public partial class FinlyticJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/FinlyticCore/Util/ManagedMqttClient.cs b/FinlyticCore/Util/ManagedMqttClient.cs index 1f71c95..bacaa48 100644 --- a/FinlyticCore/Util/ManagedMqttClient.cs +++ b/FinlyticCore/Util/ManagedMqttClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Text; using System.Text.Json; @@ -150,15 +150,34 @@ public abstract class ManagedMqttClient : IDisposable /// /// Serializes a generic object into a structured JSON string and publishes it to the specified topic. + /// Utilizes .NET 8 JSON Source Generators for zero-reflection overhead, with reflection fallback for unregistered types. /// public Task PublishAsync(string topic, T data, bool retain = false) { - var jsonOptions = new JsonSerializerOptions + byte[] jsonBytes; + var typeInfo = FinlyticJsonSerializerContext.Default.GetTypeInfo(typeof(T)) + ?? (data != null ? FinlyticJsonSerializerContext.Default.GetTypeInfo(data.GetType()) : null); + + if (typeInfo != null) { - ReferenceHandler = ReferenceHandler.IgnoreCycles - }; - var json = JsonSerializer.Serialize(data, jsonOptions); - return PublishAsync(topic, json, retain); + jsonBytes = JsonSerializer.SerializeToUtf8Bytes(data, typeInfo); + } + else + { + jsonBytes = JsonSerializer.SerializeToUtf8Bytes(data); + } + + if (!IsConnected) + throw new InvalidOperationException("Cannot publish message: MQTT client is offline."); + + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(jsonBytes) + .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(retain) + .Build(); + + return _mqttClient.PublishAsync(message, CancellationToken.None); } /// @@ -190,12 +209,12 @@ public abstract class ManagedMqttClient : IDisposable // 2. Serialize and dispatch via the existing JSON helper await PublishAsync(requestTopic, requestData); - _logger.LogDebug("RPC request published to '{Topic}' [CorrelationId: {Id}]", requestTopic, correlationId); + _logger.LogInformation("RPC request published to '{Topic}' [CorrelationId: {Id}]", requestTopic, correlationId); try { // 3. Block asynchronously until the response loop resolves the token - var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(10); + var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(25); var rawJsonResult = await tcs.Task.WaitAsync(effectiveTimeout); if (typeof(TResponse) == typeof(string)) @@ -203,6 +222,12 @@ public abstract class ManagedMqttClient : IDisposable return rawJsonResult as TResponse; } + var respTypeInfo = FinlyticJsonSerializerContext.Default.GetTypeInfo(typeof(TResponse)); + if (respTypeInfo != null) + { + return JsonSerializer.Deserialize(rawJsonResult, respTypeInfo) as TResponse; + } + return JsonSerializer.Deserialize(rawJsonResult); } catch (TimeoutException) @@ -223,6 +248,7 @@ public abstract class ManagedMqttClient : IDisposable { var topic = e.ApplicationMessage.Topic; var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + _logger.LogInformation("MQTT message received on topic '{Topic}', length={Length}", topic, payload?.Length ?? 0); // Intercept message if it belongs to the RPC response convention if (topic.StartsWith("services/response/")) diff --git a/FinlyticCore/Util/ManagedWebSocket.cs b/FinlyticCore/Util/ManagedWebSocket.cs index 19c7537..7feac6d 100644 --- a/FinlyticCore/Util/ManagedWebSocket.cs +++ b/FinlyticCore/Util/ManagedWebSocket.cs @@ -1,4 +1,4 @@ -namespace FinlyticAssets.Util; +namespace FinlyticCore.Util; using System; using System.IO; diff --git a/FinlyticCore/Util/StringCodeGenerator.cs b/FinlyticCore/Util/StringCodeGenerator.cs new file mode 100644 index 0000000..c54383d --- /dev/null +++ b/FinlyticCore/Util/StringCodeGenerator.cs @@ -0,0 +1,11 @@ +namespace FinlyticCore.Util; + +public static class StringCodeGenerator +{ + public static string GenerateTraceparent() + { + var traceId = Guid.NewGuid().ToString("N"); + var spanId = Guid.NewGuid().ToString("N").Substring(0, 16); + return $"00-{traceId}-{spanId}-01"; + } +}