using AngleSharp; using CsvHelper; using CsvHelper.Configuration; using System.Diagnostics; using System.Globalization; using System.Text.Json; namespace NadekoBot.Modules.Searches; public sealed class DefaultStockDataService : IStockDataService, INService { private readonly IHttpClientFactory _httpClientFactory; public DefaultStockDataService(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; public async Task GetStockDataAsync(string query) { try { if (!query.IsAlphaNumeric()) return default; using var http = _httpClientFactory.CreateClient(); var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}"; var config = Configuration.Default.WithDefaultLoader(); using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage); var tickerName = document.QuerySelector("div.top > .left > .container > h1") ?.TextContent; if (tickerName is null) return default; var marketcap = document .QuerySelector("li > span > fin-streamer[data-field='marketCap']") ?.TextContent; var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']") ?.TextContent; var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']") ?.TextContent ?? "0"; var price = document.QuerySelector("fin-streamer.livePrice > span") ?.TextContent ?? "0"; return new() { Name = tickerName, Symbol = query, Price = double.Parse(price, NumberStyles.Any, CultureInfo.InvariantCulture), Close = double.Parse(close, NumberStyles.Any, CultureInfo.InvariantCulture), MarketCap = marketcap, DailyVolume = (long)double.Parse(volume ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture), }; } catch (Exception ex) { Log.Warning(ex, "Error getting stock data: {ErrorMessage}", ex.ToString()); return default; } } public async Task> SearchSymbolAsync(string query) { if (string.IsNullOrWhiteSpace(query)) throw new ArgumentNullException(nameof(query)); query = Uri.EscapeDataString(query); using var http = _httpClientFactory.CreateClient(); var res = await http.GetStringAsync( "https://finance.yahoo.com/_finance_doubledown/api/resource/searchassist" + $";searchTerm={query}" + "?device=console"); var data = JsonSerializer.Deserialize(res); if (data is null or { Items: null }) return Array.Empty(); return data.Items .Where(x => x.Type == "S") .Select(x => new SymbolData(x.Symbol, x.Name)) .ToList(); } private static CsvConfiguration _csvConfig = new(CultureInfo.InvariantCulture); public async Task> GetCandleDataAsync(string query) { using var http = _httpClientFactory.CreateClient(); await using var resStream = await http.GetStreamAsync( $"https://query1.finance.yahoo.com/v7/finance/download/{query}" + $"?period1={DateTime.UtcNow.Subtract(30.Days()).ToTimestamp()}" + $"&period2={DateTime.UtcNow.ToTimestamp()}" + "&interval=1d"); using var textReader = new StreamReader(resStream); using var csv = new CsvReader(textReader, _csvConfig); var records = csv.GetRecords().ToArray(); return records .Map(static x => new CandleData(x.Open, x.Close, x.High, x.Low, x.Volume)); } }