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}"esCount={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";
+ }
+}