feat(Core): update DTOs and shared models
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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,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"
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) & 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}&quotesCount={quotesCount}&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)}"esCount={quotesCount}&newsCount={newsCount}";
|
||||
using var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger?.LogWarning("[YahooFinanceClient] Search for '{Query}' failed with status {Status}", query,
|
||||
response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonSerializer.Deserialize<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}&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}&interval={interval}&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}&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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
namespace FinlyticAssets.Util;
|
||||
namespace FinlyticCore.Util;
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user