feat(Core): update DTOs and shared models

This commit is contained in:
2026-08-09 21:01:38 +02:00
parent 6337e63a77
commit 5475c3ac51
58 changed files with 3418 additions and 30 deletions
@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Fundamentals;
/// <summary>
/// Data transfer object representing the complete fundamental analysis dataset of an asset.
/// </summary>
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<CompanyExecutiveDto> Executives { get; init; } = [];
[JsonPropertyName("financialStatements")]
public List<FinancialStatementDto> FinancialStatements { get; init; } = [];
[JsonPropertyName("estimates")]
public List<ForwardEstimateDto> Estimates { get; init; } = [];
[JsonPropertyName("availableTickers")]
public List<TickerDto> 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; }
}
@@ -0,0 +1,25 @@
using System;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Fundamentals;
/// <summary>
/// DTO representing a scheduled corporate event (e.g. Earnings, Ex-Dividend, Dividend Payout).
/// </summary>
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; }
}
+130
View File
@@ -0,0 +1,130 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos;
/// <summary>
/// Generic request payload carrying only a limit parameter (e.g. news_GetPending).
/// </summary>
public record LimitRequest(
[property: JsonPropertyName("limit")] int Limit
);
/// <summary>
/// Generic request payload for paginated queries with optional ISIN filter.
/// </summary>
public record PaginatedRequest(
[property: JsonPropertyName("limit")] int Limit,
[property: JsonPropertyName("offset")] int Offset,
[property: JsonPropertyName("isin")] string? Isin = null
);
/// <summary>
/// Request payload for daily-news queries with optional filters.
/// </summary>
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
);
/// <summary>
/// Request payload for fetching fundamentals or technical-analysis data by ISIN.
/// </summary>
public record IsinRequest(
[property: JsonPropertyName("isin")] string Isin,
[property: JsonPropertyName("ticker")] string? Ticker = "",
[property: JsonPropertyName("forceRefresh")] bool ForceRefresh = false
);
/// <summary>
/// Request payload for fetching trades filtered by ISIN and/or status.
/// </summary>
public record GetTradesRequest(
[property: JsonPropertyName("isin")] string? Isin = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("userId")] string? UserId = null
);
/// <summary>
/// Request payload for fetching sentiment by article ID.
/// </summary>
public record ArticleRequest(
[property: JsonPropertyName("articleId")] string ArticleId,
[property: JsonPropertyName("id")] string? Id = null
);
/// <summary>
/// Request payload for triggering a manual sentiment analysis for an article or ISIN.
/// </summary>
public record AnalyzeSentimentRequest(
[property: JsonPropertyName("articleId")] string? ArticleId = null,
[property: JsonPropertyName("isin")] string? Isin = null,
[property: JsonPropertyName("forceReload")] bool ForceReload = false
);
/// <summary>
/// Empty request payload for MQTT RPCs that require no parameters (e.g. events_GetAll).
/// </summary>
public record EmptyRequest;
/// <summary>
/// Request payload for triggering a manual AI analysis.
/// </summary>
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
);
/// <summary>
/// Response payload returned by microservice health pings over MQTT.
/// </summary>
public record ServiceHealthResponse(
[property: JsonPropertyName("serviceName")] string ServiceName,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("timestamp")] DateTime Timestamp,
[property: JsonPropertyName("dbStatus")] string DbStatus
);
/// <summary>
/// Response payload for assets_FetchLogo RPC request.
/// </summary>
public record FetchLogoResponse(
[property: JsonPropertyName("isin")] string? Isin,
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("success")] bool Success
);
/// <summary>
/// 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.
/// </summary>
public record ServiceConfigUpdatePayload(
[property: JsonPropertyName("serviceName")] string ServiceName,
[property: JsonPropertyName("timestamp")] DateTime Timestamp,
[property: JsonPropertyName("settings")] Dictionary<string, string> Settings
);
/// <summary>
/// Payload published to MQTT when a live market tick is received.
/// </summary>
public record TickMessageDto(
[property: JsonPropertyName("price")] decimal Price
);
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.News;
/// <summary>
/// Represents a news article discovered by a feed/page scanner, carrying parsed metadata such as title, summary, publication date, language, and associated ISINs.
/// </summary>
public record DiscoveredArticle(
[property: JsonPropertyName("url")]
string Url,
[property: JsonPropertyName("isins")]
List<string>? 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
);
+22
View File
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.News;
/// <summary>
/// Data transfer object representing a financial asset matched inside an article.
/// </summary>
public record MatchedAssetDto
{
/// <summary>
/// Gets or sets the name of the matched asset.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the ISIN (International Securities Identification Number) of the matched asset.
/// </summary>
[JsonPropertyName("isin")]
public string Isin { get; init; } = string.Empty;
}
+43
View File
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.News;
/// <summary>
/// Payload representing the pre-filtered asset configuration dispatched to n8n.
/// </summary>
public record FilteredAssetPayload(
[property: JsonPropertyName("Name")] string Name,
[property: JsonPropertyName("Isin")] string Isin
);
/// <summary>
/// Webhook payload structure dispatched to the n8n workflow.
/// </summary>
public record N8nRequestPayload(
[property: JsonPropertyName("article")] string Article,
[property: JsonPropertyName("filtered_assets")] List<FilteredAssetPayload> FilteredAssets
);
/// <summary>
/// Matched asset returned by the n8n AI workflow classification.
/// </summary>
public record N8nMatchedAssetPayload(
[property: JsonPropertyName("ticker")] string? Ticker,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("confidence_score")] double ConfidenceScore
);
/// <summary>
/// Enriched response payload returned by the n8n workflow webhook.
/// </summary>
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<N8nMatchedAssetPayload> MatchedAssets
);
+101
View File
@@ -0,0 +1,101 @@
using System.Text.Json.Serialization;
using FinlyticCore.Dtos.Sentiment;
namespace FinlyticCore.Dtos.News;
/// <summary>
/// Data transfer object representing a parsed and enriched news article with bundled sentiment metrics.
/// </summary>
public record NewsArticleDto
{
/// <summary>
/// Gets or sets the unique article identifier.
/// </summary>
[JsonPropertyName("id")]
public Guid Id { get; init; }
/// <summary>
/// Gets or sets the title of the article.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the author of the article.
/// </summary>
[JsonPropertyName("author")]
public string? Author { get; init; }
/// <summary>
/// Gets or sets a brief summary of the article content.
/// </summary>
[JsonPropertyName("summary")]
public string? Summary { get; init; }
/// <summary>
/// Gets or sets the raw extracted text content of the article.
/// </summary>
[JsonPropertyName("contentRaw")]
public string ContentRaw { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the language of the article (e.g. "en", "de").
/// </summary>
[JsonPropertyName("language")]
public string? Language { get; init; }
/// <summary>
/// Gets or sets the unique source URL of the article.
/// </summary>
[JsonPropertyName("sourceUrl")]
public string SourceUrl { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the timestamp when the article was scraped.
/// </summary>
[JsonPropertyName("scrapedAt")]
public DateTime ScrapedAt { get; init; }
/// <summary>
/// Gets or sets the publication timestamp of the article.
/// </summary>
[JsonPropertyName("publishedAt")]
public DateTime PublishedAt { get; init; }
/// <summary>
/// Gets or sets the list of classified assets referenced in the article.
/// </summary>
[JsonPropertyName("matchedAssets")]
public List<MatchedAssetDto> MatchedAssets { get; init; } = [];
/// <summary>
/// Gets or sets the processing lifecycle state (e.g. "Pending", "Completed", "Analyzed").
/// </summary>
[JsonPropertyName("status")]
public string Status { get; init; } = "Completed";
/// <summary>
/// Gets or sets the classified sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL").
/// </summary>
[JsonPropertyName("sentiment")]
public string? Sentiment { get; init; }
/// <summary>
/// Gets or sets the compound sentiment score (-1.0 to +1.0).
/// </summary>
[JsonPropertyName("sentimentScore")]
public double? SentimentScore { get; init; }
/// <summary>
/// Gets or sets the FinBERT classification confidence score (0.0 to 1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double? Confidence { get; init; }
/// <summary>
/// Gets or sets the detailed FinBERT result breakdown.
/// </summary>
[JsonPropertyName("finbertResult")]
public FinBertResultDto? FinbertResult { get; init; }
}
+20
View File
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.News;
/// <summary>
/// Request payload sent by downstream services (e.g., FinlyticSentiment) to update the processing status of a news article.
/// </summary>
public record UpdateNewsStatusRequest(
[property: JsonPropertyName("id")] Guid Id,
[property: JsonPropertyName("status")] string Status
);
/// <summary>
/// Response payload returned to verify the success of the status update operation.
/// </summary>
public record UpdateNewsStatusResponse(
[property: JsonPropertyName("success")] bool Success,
[property: JsonPropertyName("message")] string? Message = null
);
@@ -0,0 +1,63 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Sentiment;
/// <summary>
/// Probabilities dictionary containing positive, negative, and neutral softmax scores.
/// </summary>
public record FinBertProbabilities
{
/// <summary>
/// Gets or sets the positive probability score (0.0 to 1.0).
/// </summary>
[JsonPropertyName("positive")]
public double Positive { get; init; }
/// <summary>
/// Gets or sets the negative probability score (0.0 to 1.0).
/// </summary>
[JsonPropertyName("negative")]
public double Negative { get; init; }
/// <summary>
/// Gets or sets the neutral probability score (0.0 to 1.0).
/// </summary>
[JsonPropertyName("neutral")]
public double Neutral { get; init; }
}
/// <summary>
/// Data transfer object holding the result of a FinBERT sentiment analysis.
/// </summary>
public record FinBertResultDto
{
/// <summary>
/// Gets or sets the dominant sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL").
/// </summary>
[JsonPropertyName("label")]
public string Label { get; init; } = "NEUTRAL";
/// <summary>
/// Gets or sets the compound score (-1.0 to +1.0).
/// </summary>
[JsonPropertyName("compoundScore")]
public double CompoundScore { get; init; }
/// <summary>
/// Gets or sets the highest confidence score (0.0 to 1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; }
/// <summary>
/// Gets or sets the probability breakdown.
/// </summary>
[JsonPropertyName("probabilities")]
public FinBertProbabilities Probabilities { get; init; } = new();
/// <summary>
/// Gets or sets the short summary snippet highlighting the impact of the article.
/// </summary>
[JsonPropertyName("summarySnippet")]
public string? SummarySnippet { get; init; }
}
@@ -0,0 +1,147 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Sentiment;
/// <summary>
/// Article metadata referenced inside an ISIN analysis event.
/// </summary>
public record IsinAnalysisArticleRef
{
/// <summary>
/// Gets or sets the article identifier.
/// </summary>
[JsonPropertyName("articleId")]
public string ArticleId { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the article title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the article source name.
/// </summary>
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the publication timestamp in ISO-8601 format.
/// </summary>
[JsonPropertyName("publishedAt")]
public string PublishedAt { get; init; } = string.Empty;
}
/// <summary>
/// Individual chronological analysis entry inside an ISIN summary file.
/// </summary>
public record IsinAnalysisEntry
{
/// <summary>
/// Gets or sets the unique analysis ID (e.g. "sent_20260722_001").
/// </summary>
[JsonPropertyName("analysisId")]
public string AnalysisId { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the analysis timestamp in ISO-8601 format.
/// </summary>
[JsonPropertyName("timestamp")]
public string Timestamp { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the referenced article details.
/// </summary>
[JsonPropertyName("article")]
public IsinAnalysisArticleRef Article { get; init; } = new();
/// <summary>
/// Gets or sets the FinBERT analysis result.
/// </summary>
[JsonPropertyName("finbertResult")]
public FinBertResultDto FinbertResult { get; init; } = new();
/// <summary>
/// Gets or sets the summary snippet.
/// </summary>
[JsonPropertyName("summarySnippet")]
public string SummarySnippet { get; init; } = string.Empty;
}
/// <summary>
/// Current summary aggregate header inside an ISIN sentiment summary file.
/// </summary>
public record IsinCurrentSummary
{
/// <summary>
/// Gets or sets the average compound score (-1.0 to +1.0).
/// </summary>
[JsonPropertyName("compoundScore")]
public double CompoundScore { get; init; }
/// <summary>
/// Gets or sets the overall sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL").
/// </summary>
[JsonPropertyName("sentimentLabel")]
public string SentimentLabel { get; init; } = "NEUTRAL";
/// <summary>
/// Gets or sets the average confidence across analyzed articles.
/// </summary>
[JsonPropertyName("avgConfidence")]
public double AvgConfidence { get; init; }
/// <summary>
/// Gets or sets the total number of articles analyzed for this ISIN.
/// </summary>
[JsonPropertyName("totalArticlesAnalyzed")]
public int TotalArticlesAnalyzed { get; init; }
/// <summary>
/// Gets or sets the overall synthesized sentiment text overview.
/// </summary>
[JsonPropertyName("text")]
public string Text { get; init; } = string.Empty;
}
/// <summary>
/// Data transfer object for an ISIN sentiment summary file (stored in data/summaries/isin/ISIN.json).
/// </summary>
public record IsinSentimentSummaryDto
{
/// <summary>
/// Gets or sets the ISIN code.
/// </summary>
[JsonPropertyName("isin")]
public string Isin { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the company name.
/// </summary>
[JsonPropertyName("companyName")]
public string CompanyName { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the sector name.
/// </summary>
[JsonPropertyName("sector")]
public string Sector { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the last updated timestamp in ISO-8601 format.
/// </summary>
[JsonPropertyName("lastUpdated")]
public string LastUpdated { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the current summary metrics and overview.
/// </summary>
[JsonPropertyName("currentSummary")]
public IsinCurrentSummary CurrentSummary { get; init; } = new();
/// <summary>
/// Gets or sets the list of historical analysis entries.
/// </summary>
[JsonPropertyName("analyses")]
public List<IsinAnalysisEntry> Analyses { get; init; } = [];
}
@@ -0,0 +1,99 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Sentiment;
/// <summary>
/// Individual analysis entry inside a Sector sentiment summary file.
/// </summary>
public record SectorAnalysisEntry
{
/// <summary>
/// Gets or sets the unique analysis ID.
/// </summary>
[JsonPropertyName("analysisId")]
public string AnalysisId { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the timestamp in ISO-8601 format.
/// </summary>
[JsonPropertyName("timestamp")]
public string Timestamp { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the related asset ISIN.
/// </summary>
[JsonPropertyName("relatedIsin")]
public string RelatedIsin { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the article ID.
/// </summary>
[JsonPropertyName("articleId")]
public string ArticleId { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the FinBERT analysis result.
/// </summary>
[JsonPropertyName("finbertResult")]
public FinBertResultDto FinbertResult { get; init; } = new();
}
/// <summary>
/// Current summary aggregate header inside a Sector sentiment summary file.
/// </summary>
public record SectorCurrentSummary
{
/// <summary>
/// Gets or sets the sector compound score (-1.0 to +1.0).
/// </summary>
[JsonPropertyName("compoundScore")]
public double CompoundScore { get; init; }
/// <summary>
/// Gets or sets the overall sector sentiment label ("POSITIVE", "NEGATIVE", "NEUTRAL").
/// </summary>
[JsonPropertyName("sentimentLabel")]
public string SentimentLabel { get; init; } = "NEUTRAL";
/// <summary>
/// Gets or sets the list of active asset ISINs influencing the sector.
/// </summary>
[JsonPropertyName("activeIsins")]
public List<string> ActiveIsins { get; init; } = [];
/// <summary>
/// Gets or sets the overview text for the sector.
/// </summary>
[JsonPropertyName("text")]
public string Text { get; init; } = string.Empty;
}
/// <summary>
/// Data transfer object for a Sector sentiment summary file (stored in data/summaries/sectors/SectorName.json).
/// </summary>
public record SectorSentimentSummaryDto
{
/// <summary>
/// Gets or sets the sector name.
/// </summary>
[JsonPropertyName("sector")]
public string Sector { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the last updated timestamp in ISO-8601 format.
/// </summary>
[JsonPropertyName("lastUpdated")]
public string LastUpdated { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the current sector summary metrics and overview.
/// </summary>
[JsonPropertyName("currentSummary")]
public SectorCurrentSummary CurrentSummary { get; init; } = new();
/// <summary>
/// Gets or sets the list of historical sector analysis entries.
/// </summary>
[JsonPropertyName("analyses")]
public List<SectorAnalysisEntry> Analyses { get; init; } = [];
}
@@ -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
);
@@ -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<PatternPointDto> UpperLine,
[property: JsonPropertyName("lowerLine")] List<PatternPointDto> LowerLine,
[property: JsonPropertyName("apexTime")] DateTime? ApexTime,
[property: JsonPropertyName("breakoutSignal")] BreakoutSignalDto? BreakoutSignal,
[property: JsonPropertyName("confidencePercent")] decimal? ConfidencePercent = null
);
@@ -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
);
@@ -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
);
@@ -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
);
@@ -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
);
@@ -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<CandleDto> Candles,
[property: JsonPropertyName("indicators")] List<IndicatorValuesDto> Indicators,
[property: JsonPropertyName("patterns")] List<ChartPatternDto> Patterns,
[property: JsonPropertyName("signals")] List<StrategySignalDto> Signals,
[property: JsonPropertyName("marketRegime")] MarketRegimeDto MarketRegime,
[property: JsonPropertyName("currency")] string Currency = "EUR"
);
+66
View File
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Yahoo;
/// <summary>
/// Response object for Yahoo Finance chart API (/v8/finance/chart/{symbol}).
/// </summary>
public record YahooChartResponseDto(
[property: JsonPropertyName("chart")] YahooChartResultWrapperDto? Chart
);
public record YahooChartResultWrapperDto(
[property: JsonPropertyName("result")] List<YahooChartResultDto>? Result,
[property: JsonPropertyName("error")] object? Error
);
public record YahooChartResultDto(
[property: JsonPropertyName("meta")] YahooChartMetaDto? Meta,
[property: JsonPropertyName("timestamp")] List<long>? 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<string>? ValidRanges
);
public record YahooChartIndicatorsDto(
[property: JsonPropertyName("quote")] List<YahooChartQuoteDto>? Quote,
[property: JsonPropertyName("adjclose")] List<YahooChartAdjCloseDto>? AdjClose
);
public record YahooChartQuoteDto(
[property: JsonPropertyName("open")] List<double?>? Open,
[property: JsonPropertyName("high")] List<double?>? High,
[property: JsonPropertyName("low")] List<double?>? Low,
[property: JsonPropertyName("close")] List<double?>? Close,
[property: JsonPropertyName("volume")] List<long?>? Volume
);
public record YahooChartAdjCloseDto(
[property: JsonPropertyName("adjclose")] List<double?>? AdjClose
);
+64
View File
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Yahoo;
/// <summary>
/// Response object for Yahoo Finance quick quotes API (/v7/finance/quote?symbols=...).
/// </summary>
public record YahooQuoteResponseDto(
[property: JsonPropertyName("quoteResponse")] YahooQuoteResultWrapperDto? QuoteResponse
);
public record YahooQuoteResultWrapperDto(
[property: JsonPropertyName("result")] List<YahooQuoteItemDto>? 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 = ""
);
@@ -0,0 +1,282 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Yahoo;
/// <summary>
/// Root response object for Yahoo Finance quoteSummary API (/v10/finance/quoteSummary/{symbol}).
/// </summary>
public record YahooQuoteSummaryResponseDto(
[property: JsonPropertyName("quoteSummary")] YahooQuoteSummaryResultDto? QuoteSummary
);
public record YahooQuoteSummaryResultDto(
[property: JsonPropertyName("result")] List<YahooQuoteSummaryModulesDto>? Result,
[property: JsonPropertyName("error")] object? Error
);
/// <summary>
/// Contains module blocks requested via the modules query parameter.
/// </summary>
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
/// <summary>
/// Asset profile details including address, industry, sector, officers, and corporate governance risks.
/// </summary>
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<YahooCompanyOfficerDto>? 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
);
/// <summary>
/// Financial metrics including target prices, debt to equity, margins, and cash flow indicators.
/// </summary>
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
);
/// <summary>
/// Key statistics including valuation ratios (P/E, Enterprise Value, Short Ratio, Shares Outstanding).
/// </summary>
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>
/// Summary details including dividends, 52-week ranges, and market capitalization.
/// </summary>
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
);
/// <summary>
/// Historical financial statements container.
/// </summary>
public record YahooFinancialStatementHistoryDto(
[property: JsonPropertyName("incomeStatementHistory")] List<YahooIncomeStatementDto>? IncomeStatementHistory,
[property: JsonPropertyName("balanceSheetStatements")] List<YahooBalanceSheetStatementDto>? BalanceSheetStatements,
[property: JsonPropertyName("cashflowStatements")] List<YahooCashflowStatementDto>? 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
);
/// <summary>
/// Corporate calendar dates including upcoming earnings calls and ex-dividend dates.
/// </summary>
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<YahooValueDto>? 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
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Yahoo;
/// <summary>
/// Root DTO response returned by https://query2.finance.yahoo.com/v1/finance/search
/// </summary>
public class YahooSearchResponseDto
{
[JsonPropertyName("count")]
public int Count { get; set; }
[JsonPropertyName("quotes")]
public List<YahooSearchQuoteDto> Quotes { get; set; } = new();
[JsonPropertyName("totalTime")]
public int TotalTime { get; set; }
[JsonPropertyName("timeTakenForQuotes")]
public int TimeTakenForQuotes { get; set; }
}
/// <summary>
/// Represents an individual quote item returned within the Yahoo Finance search results.
/// </summary>
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; }
}
+36
View File
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.Yahoo;
/// <summary>
/// Represents a Yahoo Finance value wrapper containing raw numerical data alongside formatted strings.
/// </summary>
public record YahooValueDto
{
[JsonPropertyName("raw")]
public double? Raw { get; init; }
[JsonPropertyName("fmt")]
public string? Fmt { get; init; }
[JsonPropertyName("longFmt")]
public string? LongFmt { get; init; }
/// <summary>
/// Helper property to retrieve Raw as double (or fallback 0.0).
/// </summary>
[JsonIgnore]
public double DoubleValue => Raw ?? 0.0;
/// <summary>
/// Helper property to retrieve Raw as decimal (or fallback 0m).
/// </summary>
[JsonIgnore]
public decimal DecimalValue => Raw.HasValue ? (decimal)Raw.Value : 0m;
/// <summary>
/// Helper property to retrieve Raw as long (or fallback 0L).
/// </summary>
[JsonIgnore]
public long LongValue => Raw.HasValue ? (long)Raw.Value : 0L;
}
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -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<double> Support { get; set; } = new();
[JsonPropertyName("resistance")]
public List<double> Resistance { get; set; } = new();
}
@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
using FinlyticCore.Models.Trades;
namespace FinlyticCore.Models.Analyzer;
/// <summary>
/// Response payload for manual AI analysis trigger RPC.
/// </summary>
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; }
}
@@ -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<PatternContextInfo> 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();
}
@@ -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<decimal>? 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;
}
@@ -0,0 +1,12 @@
namespace FinlyticCore.Models.Analyzer;
/// <summary>
/// Market volatility regime derived from VIX / VDAX index level.
/// </summary>
public enum VixMarketRegime
{
LowVol = 0, // VIX < 15
Normal = 1, // VIX 15 - 20
HighVol = 2, // VIX 20 - 30
Panic = 3 // VIX > 30
}
@@ -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<string> FcmTokens { get; set; } = new();
public DateTime ExpiresAt { get; set; }
public bool RequiresPasswordChange { get; set; }
}
@@ -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"
}
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using FinlyticCore.Models.Trades;
namespace FinlyticCore.Models.Auth;
/// <summary>
/// Strongly typed SignalR client interface for real-time WebSocket/SSE streaming.
/// </summary>
public interface ITradeClient
{
Task OnTradeProposed(TradeProposalDto proposal);
Task OnTradeUpdated(TradeHourlyUpdateDto update);
Task OnTradeClosed(string tradeId, decimal exitPrice, string reason);
Task OnNewsReceived(object newsItem);
}
@@ -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;
}
@@ -0,0 +1,22 @@
namespace FinlyticCore.Models.Auth;
/// <summary>
/// DTO representing a request for self-registration by a new user.
/// </summary>
public class RegisterRequestDto
{
/// <summary>
/// User email address.
/// </summary>
public string Email { get; set; } = string.Empty;
/// <summary>
/// User plain-text password.
/// </summary>
public string Password { get; set; } = string.Empty;
/// <summary>
/// User full name.
/// </summary>
public string FullName { get; set; } = string.Empty;
}
@@ -0,0 +1,7 @@
namespace FinlyticCore.Models.Auth;
public class UpdateFcmTokenRequestDto
{
public string FcmToken { get; set; } = string.Empty;
public string DeviceName { get; set; } = "MobileDevice";
}
@@ -0,0 +1,22 @@
namespace FinlyticCore.Models.Auth;
/// <summary>
/// DTO for updating user role or active status by an admin.
/// </summary>
public class UpdateUserRequestDto
{
/// <summary>
/// Updated user role (User, Premium, Admin).
/// </summary>
public string? Role { get; set; }
/// <summary>
/// Updated active state of user.
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// Updated full name.
/// </summary>
public string? FullName { get; set; }
}
+16
View File
@@ -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<string> FcmTokens { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
}
@@ -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<TradeRepublicAsset> 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<TradeRepublicTag> Tags { get; init; } = Array.Empty<TradeRepublicTag>();
}
// 4. Die spezifischen Klassen (inklusive Bond und Derivative aus deinem JSON!)
public record TradeRepublicStock : TradeRepublicAsset
{
[JsonPropertyName("derivativeProductCategories")]
public IReadOnlyList<string> DerivativeProductCategories { get; init; } = Array.Empty<string>();
}
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<string> DerivativeProductCategories { get; init; } = Array.Empty<string>();
[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<string> DerivativeProductCategories { get; init; } = Array.Empty<string>();
}
// 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<string> DerivativeProductCategories { get; init; } = Array.Empty<string>();
[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<TradeRepublicAsset>
{
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<TradeRepublicStock>(root.GetRawText(), options),
"crypto" => JsonSerializer.Deserialize<TradeRepublicCrypto>(root.GetRawText(), options),
"fund" => JsonSerializer.Deserialize<TradeRepublicEtf>(root.GetRawText(), options),
"synthetic" => JsonSerializer.Deserialize<TradeRepublicSynthetic>(root.GetRawText(), options),
"bond" => JsonSerializer.Deserialize<TradeRepublicBond>(root.GetRawText(), options),
"derivative" => JsonSerializer.Deserialize<TradeRepublicDerivative>(root.GetRawText(), options),
// Wenn TR einen Typ schickt, den wir noch nicht kennen: Fallback nutzen!
_ => JsonSerializer.Deserialize<TradeRepublicAssetFallback>(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;
@@ -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();
}
@@ -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())
{}
}
@@ -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<TradeRepublicFilter>? Filter = null
)
{
[JsonPropertyName("filter")]
public IReadOnlyList<TradeRepublicFilter> Filter { get; init; } = Filter ?? Array.Empty<TradeRepublicFilter>();
}
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();
}
@@ -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();
}
@@ -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
);
@@ -0,0 +1,13 @@
using System;
namespace FinlyticCore.Models.Trades;
/// <summary>
/// Request payload for manually closing an active trade via REST API.
/// </summary>
public class CloseTradeRequest
{
public decimal UserExitPrice { get; set; }
public DateTime? UserExitTimestamp { get; set; }
public string CloseReason { get; set; } = "ManualClosure"; // "TakeProfitHit", "StopLossHit", "ManualClosure", "TimeExpired"
}
@@ -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;
}
@@ -0,0 +1,35 @@
using System;
using FinlyticCore.Models.Analyzer;
namespace FinlyticCore.Models.Trades;
/// <summary>
/// Structured closed trade record exported to JSON/Parquet for AI win-rate calibration feedback loops.
/// </summary>
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; }
}
@@ -0,0 +1,20 @@
using System;
namespace FinlyticCore.Models.Trades;
/// <summary>
/// Hourly AI recommendation update for an active trade.
/// </summary>
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;
}
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using FinlyticCore.Models.Analyzer;
namespace FinlyticCore.Models.Trades;
/// <summary>
/// Trade proposal generated by FinlyticAnalyzer and dispatched via MQTT QoS 2.
/// </summary>
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<decimal>? 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;
}
+14
View File
@@ -0,0 +1,14 @@
namespace FinlyticCore.Models.Trades;
/// <summary>
/// Status of a proposed/active trade lifecycle.
/// </summary>
public enum TradeStatus
{
Proposed = 0,
Active = 1,
Closed = 2,
Expired = 3,
Rejected = 4,
Invalidated = 5
}
+32
View File
@@ -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.
@@ -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;
/// <summary>
/// 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).
/// </summary>
public class TradeRepublicClient : ManagedWebSocket
{
private readonly ILogger<TradeRepublicClient> _logger;
private int _currentSub;
private readonly ConcurrentDictionary<int, TaskCompletionSource<ReceivedMessage>> _pendingRequests = new();
private readonly ConcurrentDictionary<int, Action<string>> _tickerSubscriptions = new();
public event Action<ReceivedMessage>? UnhandledMessageReceived;
public event Action<string>? SystemMessageReceived;
/// <summary>
/// Initializes a new instance of the <see cref="TradeRepublicClient"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public TradeRepublicClient(ILogger<TradeRepublicClient> logger)
{
_logger = logger;
}
/// <summary>
/// Connects to the Trade Republic WebSocket API.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the connection was successful.</returns>
public async Task<bool> InitAsync(CancellationToken cancellationToken = default)
{
if (IsConnected) return true;
await ConnectAsync("wss://api.traderepublic.com/", TimeSpan.FromSeconds(10));
var tcs = new TaskCompletionSource<ReceivedMessage>(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;
}
}
/// <summary>
/// Sends a JSON request to the Trade Republic WebSocket API and waits for the response.
/// </summary>
/// <typeparam name="TResponse">The expected response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="request">The request to send.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized response, or null if the request failed or timed out.</returns>
public async Task<TResponse?> SendRequestAsync<TResponse, TRequest>(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<ReceivedMessage>(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 { }
}
}
/// <summary>
/// Subscribes to the real-time ticker stream for a specific ISIN (e.g., US5398301094.TIB).
/// </summary>
public async Task<int?> SubscribeTickerAsync(string isin, Action<TradeRepublicTickerResponse> 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;
}
/// <summary>
/// Unsubscribes from a real-time ticker stream.
/// </summary>
/// <param name="subId">The subscription ID to unsubscribe.</param>
/// <returns>A task representing the async operation.</returns>
public async Task UnsubscribeTickerAsync(int subId)
{
_tickerSubscriptions.TryRemove(subId, out _);
try
{
await SendAsync($"unsub {subId}");
}
catch { }
}
/// <inheritdoc />
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);
}
}
/// <summary>
/// Represents a received message from the Trade Republic WebSocket.
/// </summary>
/// <param name="SubId">The subscription ID.</param>
/// <param name="Type">The message type.</param>
/// <param name="Data">The payload data.</param>
public record ReceivedMessage(int SubId, string Type, string Data);
@@ -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;
/// <summary>
/// Service for interacting with the Trade Republic API.
/// </summary>
public interface ITradeRepublicService
{
/// <summary>
/// Fetches asset metadata from Trade Republic by ISIN.
/// </summary>
/// <param name="isin">The ISIN to search for.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The Trade Republic search response, or null if not found/failed.</returns>
Task<TradeRepublicAssetResponse?> GetAsset(string isin, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the total count of available assets grouped by their types.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>An <see cref="AssetsCount"/> object containing the metrics.</returns>
Task<AssetsCount> GetAssetsCount(CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a paginated chunk of assets filtered by a specific type.
/// </summary>
/// <param name="type">The type of assets to retrieve.</param>
/// <param name="page">The zero-based page index.</param>
/// <param name="pageSize">The number of elements per page.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A <see cref="TradeRepublicAssetResponse"/> containing the elements, or null if the request fails.</returns>
Task<TradeRepublicAssetResponse?> GetAssets(AssetType type, int page, int pageSize, CancellationToken cancellationToken = default);
/// <summary>
/// Subscribes to the real-time ticker stream for a specific ISIN.
/// </summary>
/// <param name="isin">The ISIN.</param>
/// <param name="onTick">The callback action when a tick is received.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The subscription ID, or null if failed.</returns>
Task<int?> SubscribeRealtimeTickerAsync(string isin, Action<TradeRepublicTickerResponse> onTick, CancellationToken cancellationToken = default);
/// <summary>
/// Unsubscribes from a real-time ticker stream.
/// </summary>
/// <param name="subId">The subscription ID to unsubscribe.</param>
/// <returns>A task representing the async operation.</returns>
Task UnsubscribeRealtimeTickerAsync(int subId);
}
public class TradeRepublicService : ITradeRepublicService, IDisposable
{
private readonly TradeRepublicClient _client;
private readonly ILogger<TradeRepublicService> _logger;
private readonly System.Timers.Timer _inactivityTimer;
private readonly SemaphoreSlim _lock = new(1, 1);
public TradeRepublicService(TradeRepublicClient client, ILogger<TradeRepublicService> 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();
}
}
/// <inheritdoc />
public async Task<TradeRepublicAssetResponse?> 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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error while fetching asset metadata for ISIN {Isin}", "TradeRepublicChannel", isin);
return null;
}
}
/// <inheritdoc />
public async Task<AssetsCount> GetAssetsCount(CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync();
var counts = new AssetsCount();
foreach (var type in Enum.GetValues<AssetType>())
{
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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
var count = response?.ResultCount ?? 0;
counts.SetCountOfType(type, count);
await Task.Delay(TimeSpan.FromMilliseconds(320), cancellationToken);
}
return counts;
}
/// <inheritdoc />
public async Task<TradeRepublicAssetResponse?> 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<TradeRepublicAssetResponse, TradeRepublicSearchRequest>(request, cancellationToken);
}
/// <inheritdoc />
public async Task<int?> SubscribeRealtimeTickerAsync(string isin, Action<TradeRepublicTickerResponse> onTick, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync();
return await _client.SubscribeTickerAsync(isin, onTick, cancellationToken);
}
/// <inheritdoc />
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);
}
}
@@ -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;
/// <summary>
/// Managed thread-safe HTTP client for Yahoo Finance APIs.
/// Implements the two-step Cookie (A3) & Crumb token authentication flow.
/// </summary>
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<YahooFinanceClient>? _logger;
private readonly SemaphoreSlim _authLock = new(1, 1);
private string? _crumb;
private DateTime _lastAuthTime = DateTime.MinValue;
/// <summary>
/// Standard modules available for the quoteSummary endpoint.
/// </summary>
public static readonly string[] StandardQuoteSummaryModules = new[]
{
"assetProfile",
"financialData",
"defaultKeyStatistics",
"summaryDetail",
"incomeStatementHistory",
"incomeStatementHistoryQuarterly",
"balanceSheetHistory",
"balanceSheetHistoryQuarterly",
"cashflowStatementHistory",
"cashflowStatementHistoryQuarterly",
"calendarEvents"
};
public YahooFinanceClient(ILogger<YahooFinanceClient>? 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);
}
}
/// <summary>
/// Executes the Cookie (A3) &amp; 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)
/// </summary>
public async Task<string?> 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();
}
}
/// <summary>
/// Searches for tickers, names, ISINs, or companies via the Yahoo Finance search API.
/// URL: https://query2.finance.yahoo.com/v1/finance/search?q={query}&amp;quotesCount={quotesCount}&amp;newsCount={newsCount}
/// Note: Does not require Cookie/Crumb authentication.
/// </summary>
public async Task<YahooSearchResponseDto?> 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)}&quotesCount={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<YahooSearchResponseDto>(json, GetJsonOptions());
}
catch (Exception ex)
{
_logger?.LogError(ex, "[YahooFinanceClient] Exception during Search for query '{Query}'", query);
return null;
}
}
/// <summary>
/// Retrieves fundamentals and company metadata using the quoteSummary endpoint.
/// URL: https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?crumb={crumb}&amp;modules={modules}
/// </summary>
public async Task<YahooQuoteSummaryResponseDto?> GetQuoteSummaryAsync(
string symbol,
IEnumerable<string> 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<YahooQuoteSummaryResponseDto>(json, GetJsonOptions());
return (false, dto);
}, cancellationToken);
}
/// <summary>
/// Convenience method to fetch all standard quoteSummary modules for a given symbol.
/// </summary>
public Task<YahooQuoteSummaryResponseDto?> GetFullQuoteSummaryAsync(string symbol,
CancellationToken cancellationToken = default)
{
return GetQuoteSummaryAsync(symbol, StandardQuoteSummaryModules, cancellationToken);
}
/// <summary>
/// Retrieves historical OHLCV chart data for a given symbol.
/// URL: https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range={range}&amp;interval={interval}&amp;crumb={crumb}
/// </summary>
public async Task<YahooChartResponseDto?> 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<YahooChartResponseDto>(json, GetJsonOptions());
return (false, dto);
}, cancellationToken);
}
/// <summary>
/// Retrieves quick real-time price quotes for one or more symbols.
/// URL: https://query1.finance.yahoo.com/v7/finance/quote?symbols={symbols}&amp;crumb={crumb}
/// </summary>
public async Task<YahooQuoteResponseDto?> GetQuotesAsync(
IEnumerable<string> 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<YahooQuoteResponseDto>(json, GetJsonOptions());
return (false, dto);
}, cancellationToken);
}
/// <summary>
/// Convenient helper method to fetch the current live price for a single symbol (e.g., "^VIX").
/// </summary>
public async Task<decimal?> 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<T?> ExecuteWithRetryAsync<T>(
Func<string, Task<(bool isAuthError, T? result)>> 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
};
}
}
+62 -20
View File
@@ -1,16 +1,13 @@
using FinlyticCore.Dtos.Assets;
using FinlyticCore.Dtos.Assets;
using FinlyticCore.Entities.Assets;
namespace FinlyticCore.Util;
public static class AssetMapper
{
/// <summary>
/// Mappt eine AssetEntity (Datenbank) sicher auf ein zyklusfreies AssetDto (MQTT Payload).
/// </summary>
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.")
};
}
/// <summary>
/// Mappt direkt eine ganze Liste von AssetEntities.
/// </summary>
public static List<AssetDto> ToDtoList(this IEnumerable<AssetEntity> entities)
{
return entities.Select(e => e.ToDto()).ToList();
@@ -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<TradeProposalDto>))]
[JsonSerializable(typeof(TradeAcceptanceDto))]
[JsonSerializable(typeof(List<TradeAcceptanceDto>))]
[JsonSerializable(typeof(CloseTradeRequest))]
[JsonSerializable(typeof(ManualAnalysisResponseDto))]
[JsonSerializable(typeof(N8nAnalysisResponseDto))]
[JsonSerializable(typeof(TradeHourlyUpdateDto))]
[JsonSerializable(typeof(TradeFeedbackRecord))]
[JsonSerializable(typeof(List<TradeFeedbackRecord>))]
[JsonSerializable(typeof(NewsArticleDto))]
[JsonSerializable(typeof(List<NewsArticleDto>))]
[JsonSerializable(typeof(DiscoveredArticle))]
[JsonSerializable(typeof(List<DiscoveredArticle>))]
[JsonSerializable(typeof(MatchedAssetDto))]
[JsonSerializable(typeof(List<MatchedAssetDto>))]
[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<AssetFundamentalsDto>))]
[JsonSerializable(typeof(CorporateEventDto))]
[JsonSerializable(typeof(List<CorporateEventDto>))]
[JsonSerializable(typeof(IsinSentimentSummaryDto))]
[JsonSerializable(typeof(IsinAnalysisEntry))]
[JsonSerializable(typeof(SectorSentimentSummaryDto))]
[JsonSerializable(typeof(CandleDto))]
[JsonSerializable(typeof(List<CandleDto>))]
[JsonSerializable(typeof(ChartPatternDto))]
[JsonSerializable(typeof(List<ChartPatternDto>))]
[JsonSerializable(typeof(IndicatorValuesDto))]
[JsonSerializable(typeof(List<IndicatorValuesDto>))]
[JsonSerializable(typeof(MarketRegimeDto))]
[JsonSerializable(typeof(StrategySignalDto))]
[JsonSerializable(typeof(List<StrategySignalDto>))]
[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<ServiceHealthResponse>))]
[JsonSerializable(typeof(FetchLogoResponse))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(ServiceConfigUpdatePayload))]
[JsonSerializable(typeof(FinlyticCore.Dtos.Assets.AssetDto))]
[JsonSerializable(typeof(List<FinlyticCore.Dtos.Assets.AssetDto>))]
[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<AssetIndex>))]
[JsonSerializable(typeof(N8nAnalysisRequestDto))]
[JsonSerializable(typeof(TickMessageDto))]
public partial class FinlyticJsonSerializerContext : JsonSerializerContext
{
}
+34 -8
View File
@@ -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
/// <summary>
/// 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.
/// </summary>
public Task PublishAsync<T>(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);
}
/// <summary>
@@ -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<TResponse>(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/"))
+1 -1
View File
@@ -1,4 +1,4 @@
namespace FinlyticAssets.Util;
namespace FinlyticCore.Util;
using System;
using System.IO;
+11
View File
@@ -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";
}
}