From 59e0311f9a6d3f8152027ace00dfbe0d3df62b7d Mon Sep 17 00:00:00 2001 From: ooples Date: Sat, 21 Jan 2023 17:03:11 -0500 Subject: [PATCH] Fixed issue with spark charts not retrieving proper info Added method to get real-time quotes Added options to get multiple symbols for real-time quotes and spark charts --- src/Helpers/DownloadHelper.cs | 62 +++++- src/Helpers/RealTimeQuoteHelper.cs | 17 ++ src/Helpers/SparkChartHelper.cs | 5 +- src/Helpers/UrlHelper.cs | 36 +++- src/Helpers/Usings.cs | 3 +- src/Models/RealTimeQuoteData.cs | 252 +++++++++++++++++++++++ src/OoplesFinance.YahooFinanceAPI.csproj | 1 + src/YahooClient.cs | 32 +++ tests/TestConsoleApp/Program.cs | 7 +- tests/UnitTests/YahooClientTests.cs | 26 +++ 10 files changed, 433 insertions(+), 8 deletions(-) create mode 100644 src/Helpers/RealTimeQuoteHelper.cs create mode 100644 src/Models/RealTimeQuoteData.cs diff --git a/src/Helpers/DownloadHelper.cs b/src/Helpers/DownloadHelper.cs index ffc4084..1d36fb7 100644 --- a/src/Helpers/DownloadHelper.cs +++ b/src/Helpers/DownloadHelper.cs @@ -161,7 +161,27 @@ internal static async Task DownloadSparkChartDataAsync(string symbol, Ti } else { - return await DownloadRawDataAsync(BuildYahooSparkChartUrl(symbol, timeRange, timeInterval)); + return await DownloadRawDataAsync(BuildYahooSparkChartUrl(new string[] { symbol }, timeRange, timeInterval)); + } + } + + /// + /// Downloads the spark chart json data using the chosen symbols + /// + /// + /// + /// + /// + /// + internal static async Task DownloadSparkChartDataAsync(IEnumerable symbols, TimeRange timeRange, TimeInterval timeInterval) + { + if (!symbols.Any()) + { + throw new ArgumentException("Symbols Parameter Must Contain At Least One Symbol"); + } + else + { + return await DownloadRawDataAsync(BuildYahooSparkChartUrl(symbols, timeRange, timeInterval)); } } @@ -186,6 +206,46 @@ internal static async Task DownloadStatsDataAsync(string symbol, Country } } + /// + /// Downloads the real-time quote json data using the chosen symbol + /// + /// + /// + /// + /// + /// + internal static async Task DownloadRealTimeQuoteDataAsync(string symbol, Country country, Language language) + { + if (string.IsNullOrWhiteSpace(symbol)) + { + throw new ArgumentException("Symbol Parameter Can't Be Empty Or Null"); + } + else + { + return await DownloadRawDataAsync(BuildYahooRealTimeQuoteUrl(new string[] { symbol }, country, language)); + } + } + + /// + /// Downloads the real-time quote json data using the chosen symbols + /// + /// + /// + /// + /// + /// + internal static async Task DownloadRealTimeQuoteDataAsync(IEnumerable symbols, Country country, Language language) + { + if (!symbols.Any()) + { + throw new ArgumentException("Symbols Parameter Must Contain At Least One Symbol"); + } + else + { + return await DownloadRawDataAsync(BuildYahooRealTimeQuoteUrl(symbols, country, language)); + } + } + /// /// Gets the base csv data that is used by all csv helper classes /// diff --git a/src/Helpers/RealTimeQuoteHelper.cs b/src/Helpers/RealTimeQuoteHelper.cs new file mode 100644 index 0000000..ce0e9f3 --- /dev/null +++ b/src/Helpers/RealTimeQuoteHelper.cs @@ -0,0 +1,17 @@ +namespace OoplesFinance.YahooFinanceAPI.Helpers; + +internal class RealTimeQuoteHelper : YahooJsonBase +{ + /// + /// Parses the raw json data for the Real-time Quote data + /// + /// + /// + /// + internal override IEnumerable ParseYahooJsonData(string jsonData) + { + var realTimeQuoteData = JsonSerializer.Deserialize(jsonData); + + return realTimeQuoteData != null ? (IEnumerable)realTimeQuoteData.QuoteResponse.Results : Enumerable.Empty(); + } +} diff --git a/src/Helpers/SparkChartHelper.cs b/src/Helpers/SparkChartHelper.cs index 21b036d..71a1518 100644 --- a/src/Helpers/SparkChartHelper.cs +++ b/src/Helpers/SparkChartHelper.cs @@ -10,8 +10,9 @@ internal class SparkChartHelper : YahooJsonBase /// internal override IEnumerable ParseYahooJsonData(string jsonData) { - var sparkChartData = JsonSerializer.Deserialize(jsonData); + var rootObjects = JsonDocument.Parse(jsonData).RootElement.EnumerateObject(); + var sparkChartData = rootObjects.Select(x => JsonSerializer.Deserialize(x.Value)); - return sparkChartData != null ? Enumerable.Cast(new SparkInfo[] { sparkChartData.Result }) : Enumerable.Empty(); + return sparkChartData != null ? (IEnumerable)sparkChartData : Enumerable.Empty(); } } diff --git a/src/Helpers/UrlHelper.cs b/src/Helpers/UrlHelper.cs index d5f5145..ae4dbdb 100644 --- a/src/Helpers/UrlHelper.cs +++ b/src/Helpers/UrlHelper.cs @@ -58,9 +58,9 @@ internal static Uri BuildYahooChartUrl(string symbol, TimeRange timeRange, TimeI /// /// /// - internal static Uri BuildYahooSparkChartUrl(string symbol, TimeRange timeRange, TimeInterval timeInterval) => + internal static Uri BuildYahooSparkChartUrl(IEnumerable symbols, TimeRange timeRange, TimeInterval timeInterval) => new(string.Format(CultureInfo.InvariantCulture, $"https://query2.finance.yahoo.com/v8/finance/spark?interval=" + - $"{GetTimeIntervalString(timeInterval)}&range={GetTimeRangeString(timeRange)}&symbols={symbol}?")); + $"{GetTimeIntervalString(timeInterval)}&range={GetTimeRangeString(timeRange)}&symbols={GetSymbolsString(symbols)}")); /// /// Creates a url that will be used to get stats for a selected symbol @@ -74,6 +74,38 @@ internal static Uri BuildYahooStatsUrl(string symbol, Country country, Language new(string.Format(CultureInfo.InvariantCulture, $"https://query1.finance.yahoo.com/v11/finance/quoteSummary/{symbol}?lang={GetLanguageString(language)}" + $"®ion={GetCountryString(country)}&modules={GetModuleString(module)}")); + /// + /// Creates a url that will be used to get real-time quotes for multiple symbols + /// + /// + /// + /// + /// + internal static Uri BuildYahooRealTimeQuoteUrl(IEnumerable symbols, Country country, Language language) => + new(string.Format(CultureInfo.InvariantCulture, $"https://query1.finance.yahoo.com/v6/finance/quote?region=" + + $"{GetCountryString(country)}&lang={GetLanguageString(language)}&symbols={GetSymbolsString(symbols)}")); + + /// + /// Returns a custom string for the symbols option + /// + /// + /// + private static string GetSymbolsString(IEnumerable symbolsList) + { + var result = string.Empty; + + var comma = Uri.EscapeDataString(","); + var count = symbolsList.Count(); + for (int i = 0; i < count; i++) + { + var symbol = symbolsList.ElementAt(i); + // if it isn't the first element then add the encoded comma before the symbol + result += i != 0 ? comma + symbol : symbol; + } + + return result; + } + /// /// Returns a custom string for the module option /// diff --git a/src/Helpers/Usings.cs b/src/Helpers/Usings.cs index 19687b6..9e70d0e 100644 --- a/src/Helpers/Usings.cs +++ b/src/Helpers/Usings.cs @@ -8,4 +8,5 @@ global using OoplesFinance.YahooFinanceAPI.Interfaces; global using System.Text.Json.Serialization; global using System.Net.Http.Json; -global using System.Text.Json; \ No newline at end of file +global using System.Text.Json; +global using System.Text.Encodings.Web; \ No newline at end of file diff --git a/src/Models/RealTimeQuoteData.cs b/src/Models/RealTimeQuoteData.cs new file mode 100644 index 0000000..8c89120 --- /dev/null +++ b/src/Models/RealTimeQuoteData.cs @@ -0,0 +1,252 @@ +namespace OoplesFinance.YahooFinanceAPI.Models; + +public class QuoteResponse +{ + [JsonPropertyName("result")] + public List Results { get; set; } = new(); + + [JsonPropertyName("error")] + public object Error { get; set; } = new(); +} + +public class RealTimeQuoteResult +{ + [JsonPropertyName("language")] + public string Language { get; set; } = string.Empty; + + [JsonPropertyName("region")] + public string Region { get; set; } = string.Empty; + + [JsonPropertyName("quoteType")] + public string QuoteType { get; set; } = string.Empty; + + [JsonPropertyName("typeDisp")] + public string TypeDisp { get; set; } = string.Empty; + + [JsonPropertyName("quoteSourceName")] + public string QuoteSourceName { get; set; } = string.Empty; + + [JsonPropertyName("triggerable")] + public bool? Triggerable { get; set; } + + [JsonPropertyName("customPriceAlertConfidence")] + public string CustomPriceAlertConfidence { get; set; } = string.Empty; + + [JsonPropertyName("currency")] + public string Currency { get; set; } = string.Empty; + + [JsonPropertyName("fiftyTwoWeekHighChange")] + public double? FiftyTwoWeekHighChange { get; set; } + + [JsonPropertyName("fiftyTwoWeekHighChangePercent")] + public double? FiftyTwoWeekHighChangePercent { get; set; } + + [JsonPropertyName("fiftyTwoWeekLow")] + public double? FiftyTwoWeekLow { get; set; } + + [JsonPropertyName("fiftyTwoWeekHigh")] + public double? FiftyTwoWeekHigh { get; set; } + + [JsonPropertyName("dividendDate")] + public int? DividendDate { get; set; } + + [JsonPropertyName("earningsTimestamp")] + public int? EarningsTimestamp { get; set; } + + [JsonPropertyName("earningsTimestampStart")] + public int? EarningsTimestampStart { get; set; } + + [JsonPropertyName("earningsTimestampEnd")] + public int? EarningsTimestampEnd { get; set; } + + [JsonPropertyName("trailingAnnualDividendRate")] + public double? TrailingAnnualDividendRate { get; set; } + + [JsonPropertyName("trailingPE")] + public double? TrailingPE { get; set; } + + [JsonPropertyName("trailingAnnualDividendYield")] + public double? TrailingAnnualDividendYield { get; set; } + + [JsonPropertyName("epsTrailingTwelveMonths")] + public double? EpsTrailingTwelveMonths { get; set; } + + [JsonPropertyName("epsForward")] + public double? EpsForward { get; set; } + + [JsonPropertyName("epsCurrentYear")] + public double? EpsCurrentYear { get; set; } + + [JsonPropertyName("priceEpsCurrentYear")] + public double? PriceEpsCurrentYear { get; set; } + + [JsonPropertyName("sharesOutstanding")] + public long? SharesOutstanding { get; set; } + + [JsonPropertyName("bookValue")] + public double? BookValue { get; set; } + + [JsonPropertyName("fiftyDayAverage")] + public double? FiftyDayAverage { get; set; } + + [JsonPropertyName("fiftyDayAverageChange")] + public double? FiftyDayAverageChange { get; set; } + + [JsonPropertyName("fiftyDayAverageChangePercent")] + public double? FiftyDayAverageChangePercent { get; set; } + + [JsonPropertyName("twoHundredDayAverage")] + public double? TwoHundredDayAverage { get; set; } + + [JsonPropertyName("twoHundredDayAverageChange")] + public double? TwoHundredDayAverageChange { get; set; } + + [JsonPropertyName("twoHundredDayAverageChangePercent")] + public double? TwoHundredDayAverageChangePercent { get; set; } + + [JsonPropertyName("marketCap")] + public long? MarketCap { get; set; } + + [JsonPropertyName("forwardPE")] + public double? ForwardPE { get; set; } + + [JsonPropertyName("priceToBook")] + public double? PriceToBook { get; set; } + + [JsonPropertyName("sourceInterval")] + public int? SourceInterval { get; set; } + + [JsonPropertyName("exchangeDataDelayedBy")] + public int? ExchangeDataDelayedBy { get; set; } + + [JsonPropertyName("averageAnalystRating")] public string AverageAnalystRating { get; set; } = string.Empty; + + [JsonPropertyName("tradeable")] + public bool? Tradeable { get; set; } + + [JsonPropertyName("cryptoTradeable")] + public bool? CryptoTradeable { get; set; } + + [JsonPropertyName("regularMarketChangePercent")] + public double? RegularMarketChangePercent { get; set; } + + [JsonPropertyName("regularMarketPrice")] + public double? RegularMarketPrice { get; set; } + + [JsonPropertyName("marketState")] + public string MarketState { get; set; } = string.Empty; + + [JsonPropertyName("exchange")] + public string Exchange { get; set; } = string.Empty; + + [JsonPropertyName("shortName")] + public string ShortName { get; set; } = string.Empty; + + [JsonPropertyName("longName")] + public string LongName { get; set; } = string.Empty; + + [JsonPropertyName("messageBoardId")] + public string MessageBoardId { get; set; } = string.Empty; + + [JsonPropertyName("exchangeTimezoneName")] + public string ExchangeTimezoneName { get; set; } = string.Empty; + + [JsonPropertyName("exchangeTimezoneShortName")] + public string ExchangeTimezoneShortName { get; set; } = string.Empty; + + [JsonPropertyName("gmtOffSetMilliseconds")] + public int? GmtOffSetMilliseconds { get; set; } + + [JsonPropertyName("market")] + public string Market { get; set; } = string.Empty; + + [JsonPropertyName("esgPopulated")] + public bool? EsgPopulated { get; set; } + + [JsonPropertyName("firstTradeDateMilliseconds")] + public object FirstTradeDateMilliseconds { get; set; } = new(); + + [JsonPropertyName("priceHint")] + public int? PriceHint { get; set; } + + [JsonPropertyName("postMarketChangePercent")] + public double? PostMarketChangePercent { get; set; } + + [JsonPropertyName("postMarketTime")] + public int? PostMarketTime { get; set; } + + [JsonPropertyName("postMarketPrice")] + public double? PostMarketPrice { get; set; } + + [JsonPropertyName("postMarketChange")] + public double? PostMarketChange { get; set; } + + [JsonPropertyName("regularMarketChange")] + public double? RegularMarketChange { get; set; } + + [JsonPropertyName("regularMarketTime")] + public int? RegularMarketTime { get; set; } + + [JsonPropertyName("regularMarketDayHigh")] + public double? RegularMarketDayHigh { get; set; } + + [JsonPropertyName("regularMarketDayRange")] + public string RegularMarketDayRange { get; set; } = string.Empty; + + [JsonPropertyName("regularMarketDayLow")] + public double? RegularMarketDayLow { get; set; } + + [JsonPropertyName("regularMarketVolume")] + public int? RegularMarketVolume { get; set; } + + [JsonPropertyName("regularMarketPreviousClose")] + public double? RegularMarketPreviousClose { get; set; } + + [JsonPropertyName("bid")] + public double? Bid { get; set; } + + [JsonPropertyName("ask")] + public double? Ask { get; set; } + + [JsonPropertyName("bidSize")] + public int? BidSize { get; set; } + + [JsonPropertyName("askSize")] + public int? AskSize { get; set; } + + [JsonPropertyName("fullExchangeName")] + public string FullExchangeName { get; set; } = string.Empty; + + [JsonPropertyName("financialCurrency")] + public string FinancialCurrency { get; set; } = string.Empty; + + [JsonPropertyName("regularMarketOpen")] + public double? RegularMarketOpen { get; set; } + + [JsonPropertyName("averageDailyVolume3Month")] + public int? AverageDailyVolume3Month { get; set; } + + [JsonPropertyName("averageDailyVolume10Day")] + public int? AverageDailyVolume10Day { get; set; } + + [JsonPropertyName("fiftyTwoWeekLowChange")] + public double? FiftyTwoWeekLowChange { get; set; } + + [JsonPropertyName("fiftyTwoWeekLowChangePercent")] + public double? FiftyTwoWeekLowChangePercent { get; set; } + + [JsonPropertyName("fiftyTwoWeekRange")] + public string FiftyTwoWeekRange { get; set; } = string.Empty; + + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + [JsonPropertyName("symbol")] + public string Symbol { get; set; } = string.Empty; +} + +public class RealTimeQuoteData +{ + [JsonPropertyName("quoteResponse")] + public QuoteResponse QuoteResponse { get; set; } = new(); +} \ No newline at end of file diff --git a/src/OoplesFinance.YahooFinanceAPI.csproj b/src/OoplesFinance.YahooFinanceAPI.csproj index 80eaceb..46b0f4a 100644 --- a/src/OoplesFinance.YahooFinanceAPI.csproj +++ b/src/OoplesFinance.YahooFinanceAPI.csproj @@ -141,6 +141,7 @@ + diff --git a/src/YahooClient.cs b/src/YahooClient.cs index d5d4a05..141d722 100644 --- a/src/YahooClient.cs +++ b/src/YahooClient.cs @@ -551,4 +551,36 @@ public async Task GetSparkChartInfoAsync(string symbol, TimeRange tim { return new SparkChartHelper().ParseYahooJsonData(await DownloadSparkChartDataAsync(symbol, timeRange, timeInterval)).First(); } + + /// + /// Gets spark chart info data for the selected stock symbols + /// + /// + /// + /// + /// + public async Task> GetSparkChartInfoAsync(IEnumerable symbols, TimeRange timeRange, TimeInterval timeInterval) + { + return new SparkChartHelper().ParseYahooJsonData(await DownloadSparkChartDataAsync(symbols, timeRange, timeInterval)); + } + + /// + /// Gets real-time quote data for the selected stock symbol + /// + /// + /// + public async Task GetRealTimeQuotesAsync(string symbol) + { + return new RealTimeQuoteHelper().ParseYahooJsonData(await DownloadRealTimeQuoteDataAsync(symbol, Country, Language)).First(); + } + + /// + /// Gets real-time quote data for the selected stock symbols + /// + /// + /// + public async Task> GetRealTimeQuotesAsync(IEnumerable symbols) + { + return new RealTimeQuoteHelper().ParseYahooJsonData(await DownloadRealTimeQuoteDataAsync(symbols, Country, Language)); + } } \ No newline at end of file diff --git a/tests/TestConsoleApp/Program.cs b/tests/TestConsoleApp/Program.cs index 8146d23..9403cf3 100644 --- a/tests/TestConsoleApp/Program.cs +++ b/tests/TestConsoleApp/Program.cs @@ -3,6 +3,8 @@ var startDate = DateTime.Now.AddYears(-1); var symbol = "AAPL"; +var fundSymbol = "VSMPX"; +var symbols = new string[] { symbol, "MSFT", "NFLX", "TSLA", "YHOO", "SPY", "A", "AA", "GOOG", "F", "UBER", "LYFT" }; var yahooClient = new YahooClient(); var historicalDataList = await yahooClient.GetHistoricalDataAsync(symbol, DataFrequency.Daily, startDate); @@ -29,7 +31,7 @@ var sectorTrendList = await yahooClient.GetSectorTrendAsync(symbol); var earningsTrendList = await yahooClient.GetEarningsTrendAsync(symbol); var assetProfileList = await yahooClient.GetAssetProfileAsync(symbol); -var fundProfileList = await yahooClient.GetFundProfileAsync("VSMPX"); +var fundProfileList = await yahooClient.GetFundProfileAsync(fundSymbol); var calendarEventsList = await yahooClient.GetCalendarEventsAsync(symbol); var earningsList = await yahooClient.GetEarningsAsync(symbol); var balanceSheetHistoryList = await yahooClient.GetBalanceSheetHistoryAsync(symbol); @@ -43,6 +45,7 @@ var cashflowStatementHistoryQuarterlyList = await yahooClient.GetCashflowStatementHistoryQuarterlyAsync(symbol); var balanceSheetHistoryQuarterlyList = await yahooClient.GetBalanceSheetHistoryQuarterlyAsync(symbol); var chartInfoList = await yahooClient.GetChartInfoAsync(symbol, TimeRange._1Day, TimeInterval._1Minute); -var sparkChartInfoList = await yahooClient.GetSparkChartInfoAsync(symbol, TimeRange._1Month, TimeInterval._1Day); +var sparkChartInfoList = await yahooClient.GetSparkChartInfoAsync(symbols, TimeRange._1Month, TimeInterval._1Day); +var realTimeQuoteList = await yahooClient.GetRealTimeQuotesAsync(symbol); Console.WriteLine(); \ No newline at end of file diff --git a/tests/UnitTests/YahooClientTests.cs b/tests/UnitTests/YahooClientTests.cs index 6674106..519d3d0 100644 --- a/tests/UnitTests/YahooClientTests.cs +++ b/tests/UnitTests/YahooClientTests.cs @@ -1017,4 +1017,30 @@ public async Task GetSparkChartInfo_ThrowsException_WhenEmptySymbolIsUsed() // Assert await result.Should().ThrowAsync().WithMessage("Symbol Parameter Can't Be Empty Or Null"); } + + [Fact] + public async Task GetRealTimeQuotes_ThrowsException_WhenNoSymbolIsFound() + { + // Arrange + var symbol = "OOPLES"; + + // Act + var result = async () => await _sut.GetRealTimeQuotesAsync(symbol); + + // Assert + await result.Should().ThrowAsync().WithMessage("Requested Information Not Available On Yahoo Finance"); + } + + [Fact] + public async Task GetRealTimeQuotes_ThrowsException_WhenEmptySymbolIsUsed() + { + // Arrange + var symbol = ""; + + // Act + var result = async () => await _sut.GetRealTimeQuotesAsync(symbol); + + // Assert + await result.Should().ThrowAsync().WithMessage("Symbol Parameter Can't Be Empty Or Null"); + } } \ No newline at end of file