mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	More restructuring
This commit is contained in:
		
							
								
								
									
										196
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/CryptoCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/CryptoCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Searches.Services;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public partial class Searches
 | 
			
		||||
{
 | 
			
		||||
    public partial class FinanceCommands : NadekoModule<CryptoService>
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IStockDataService _stocksService;
 | 
			
		||||
        private readonly IStockChartDrawingService _stockDrawingService;
 | 
			
		||||
 | 
			
		||||
        public FinanceCommands(IStockDataService stocksService, IStockChartDrawingService stockDrawingService)
 | 
			
		||||
        {
 | 
			
		||||
            _stocksService = stocksService;
 | 
			
		||||
            _stockDrawingService = stockDrawingService;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task Stock([Leftover]string query)
 | 
			
		||||
        {
 | 
			
		||||
            using var typing = ctx.Channel.EnterTypingState();
 | 
			
		||||
            
 | 
			
		||||
            var stock = await _stocksService.GetStockDataAsync(query);
 | 
			
		||||
 | 
			
		||||
            if (stock is null)
 | 
			
		||||
            {
 | 
			
		||||
                var symbols = await _stocksService.SearchSymbolAsync(query);
 | 
			
		||||
 | 
			
		||||
                if (symbols.Count == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var symbol = symbols.First();
 | 
			
		||||
                var promptEmbed = _eb.Create()
 | 
			
		||||
                                     .WithDescription(symbol.Description)
 | 
			
		||||
                                     .WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
 | 
			
		||||
                
 | 
			
		||||
                if (!await PromptUserConfirmAsync(promptEmbed))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                query = symbol.Symbol;
 | 
			
		||||
                stock = await _stocksService.GetStockDataAsync(query);
 | 
			
		||||
 | 
			
		||||
                if (stock is null)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var candles = await _stocksService.GetCandleDataAsync(query);
 | 
			
		||||
            var stockImageTask = _stockDrawingService.GenerateCombinedChartAsync(candles);
 | 
			
		||||
            
 | 
			
		||||
            var localCulture = (CultureInfo)Culture.Clone();
 | 
			
		||||
            localCulture.NumberFormat.CurrencySymbol = "$";
 | 
			
		||||
 | 
			
		||||
            var sign = stock.Price >= stock.Close
 | 
			
		||||
                ? "\\🔼"
 | 
			
		||||
                : "\\🔻";
 | 
			
		||||
 | 
			
		||||
            var change = (stock.Price - stock.Close).ToString("N2", Culture);
 | 
			
		||||
            var changePercent = (1 - (stock.Close / stock.Price)).ToString("P1", Culture);
 | 
			
		||||
            
 | 
			
		||||
            var sign50 = stock.Change50d >= 0
 | 
			
		||||
                ? "\\🔼"
 | 
			
		||||
                : "\\🔻";
 | 
			
		||||
 | 
			
		||||
            var change50 = (stock.Change50d).ToString("P1", Culture);
 | 
			
		||||
            
 | 
			
		||||
            var sign200 = stock.Change200d >= 0
 | 
			
		||||
                ? "\\🔼"
 | 
			
		||||
                : "\\🔻";
 | 
			
		||||
            
 | 
			
		||||
            var change200 = (stock.Change200d).ToString("P1", Culture);
 | 
			
		||||
            
 | 
			
		||||
            var price = stock.Price.ToString("C2", localCulture);
 | 
			
		||||
 | 
			
		||||
            var eb = _eb.Create()
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithAuthor(stock.Symbol)
 | 
			
		||||
                        .WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
 | 
			
		||||
                        .WithTitle(stock.Name)
 | 
			
		||||
                        .AddField(GetText(strs.price), $"{sign} **{price}**", true)
 | 
			
		||||
                        .AddField(GetText(strs.market_cap), stock.MarketCap.ToString("C0", localCulture), true)
 | 
			
		||||
                        .AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true)
 | 
			
		||||
                        .AddField("Change", $"{change} ({changePercent})", true)
 | 
			
		||||
                        .AddField("Change 50d", $"{sign50}{change50}", true)
 | 
			
		||||
                        .AddField("Change 200d", $"{sign200}{change200}", true)
 | 
			
		||||
                        .WithFooter(stock.Exchange);
 | 
			
		||||
            
 | 
			
		||||
            var message = await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
            await using var imageData = await stockImageTask;
 | 
			
		||||
            if (imageData is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var fileName = $"{query}-sparkline.{imageData.Extension}";
 | 
			
		||||
            using var attachment = new FileAttachment(
 | 
			
		||||
                imageData.FileData,
 | 
			
		||||
                fileName
 | 
			
		||||
            );
 | 
			
		||||
            await message.ModifyAsync(mp =>
 | 
			
		||||
            {
 | 
			
		||||
                mp.Attachments =
 | 
			
		||||
                    new(new[]
 | 
			
		||||
                    {
 | 
			
		||||
                        attachment
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async Task Crypto(string name)
 | 
			
		||||
        {
 | 
			
		||||
            name = name?.ToUpperInvariant();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var (crypto, nearest) = await _service.GetCryptoData(name);
 | 
			
		||||
 | 
			
		||||
            if (nearest is not null)
 | 
			
		||||
            {
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithTitle(GetText(strs.crypto_not_found))
 | 
			
		||||
                               .WithDescription(
 | 
			
		||||
                                   GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
 | 
			
		||||
 | 
			
		||||
                if (await PromptUserConfirmAsync(embed))
 | 
			
		||||
                    crypto = nearest;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (crypto is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.crypto_not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var usd = crypto.Quote["USD"];
 | 
			
		||||
 | 
			
		||||
            var localCulture = (CultureInfo)Culture.Clone();
 | 
			
		||||
            localCulture.NumberFormat.CurrencySymbol = "$";
 | 
			
		||||
            
 | 
			
		||||
            var sevenDay = (usd.PercentChange7d / 100).ToString("P2", localCulture);
 | 
			
		||||
            var lastDay = (usd.PercentChange24h / 100).ToString("P2", localCulture);
 | 
			
		||||
            var price = usd.Price < 0.01
 | 
			
		||||
                ? usd.Price.ToString(localCulture)
 | 
			
		||||
                : usd.Price.ToString("C2", localCulture);
 | 
			
		||||
 | 
			
		||||
            var volume = usd.Volume24h.ToString("C0", localCulture);
 | 
			
		||||
            var marketCap = usd.MarketCap.ToString("C0", localCulture);
 | 
			
		||||
            var dominance = (usd.MarketCapDominance / 100).ToString("P2", localCulture);
 | 
			
		||||
 | 
			
		||||
            await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
 | 
			
		||||
            var fileName = $"{crypto.Slug}_7d.png";
 | 
			
		||||
            
 | 
			
		||||
            var toSend = _eb.Create()
 | 
			
		||||
                            .WithOkColor()
 | 
			
		||||
                            .WithAuthor($"#{crypto.CmcRank}")
 | 
			
		||||
                            .WithTitle($"{crypto.Name} ({crypto.Symbol})")
 | 
			
		||||
                            .WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
 | 
			
		||||
                            .WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
 | 
			
		||||
                            .AddField(GetText(strs.market_cap), marketCap, true)
 | 
			
		||||
                            .AddField(GetText(strs.price), price, true)
 | 
			
		||||
                            .AddField(GetText(strs.volume_24h), volume, true)
 | 
			
		||||
                            .AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
 | 
			
		||||
                            .AddField(GetText(strs.market_cap_dominance), dominance, true)
 | 
			
		||||
                            .WithImageUrl($"attachment://{fileName}");
 | 
			
		||||
 | 
			
		||||
            if (crypto.CirculatingSupply is double cs)
 | 
			
		||||
            {
 | 
			
		||||
                var csStr = cs.ToString("N0", localCulture);
 | 
			
		||||
                
 | 
			
		||||
                if (crypto.MaxSupply is double ms)
 | 
			
		||||
                {
 | 
			
		||||
                    var perc = (cs / ms).ToString("P1", localCulture);
 | 
			
		||||
                    
 | 
			
		||||
                    toSend.AddField(GetText(strs.circulating_supply), $"{csStr} ({perc})", true);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    toSend.AddField(GetText(strs.circulating_supply), csStr, true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										216
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/CryptoService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/CryptoService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,216 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Xml;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
			
		||||
using StringExtensions = NadekoBot.Extensions.StringExtensions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Services;
 | 
			
		||||
 | 
			
		||||
public class CryptoService : INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IBotCache _cache;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
 | 
			
		||||
    private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
 | 
			
		||||
 | 
			
		||||
    public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
 | 
			
		||||
    {
 | 
			
		||||
        _cache = cache;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PointF[] GetSparklinePointsFromSvgText(string svgText)
 | 
			
		||||
    {
 | 
			
		||||
        var xml = new XmlDocument();
 | 
			
		||||
        xml.LoadXml(svgText);
 | 
			
		||||
 | 
			
		||||
        var gElement = xml["svg"]?["g"];
 | 
			
		||||
        if (gElement is null)
 | 
			
		||||
            return Array.Empty<PointF>();
 | 
			
		||||
 | 
			
		||||
        Span<PointF> points = new PointF[gElement.ChildNodes.Count];
 | 
			
		||||
        var cnt = 0;
 | 
			
		||||
 | 
			
		||||
        bool GetValuesFromAttributes(
 | 
			
		||||
            XmlAttributeCollection attrs,
 | 
			
		||||
            out float x1,
 | 
			
		||||
            out float y1,
 | 
			
		||||
            out float x2,
 | 
			
		||||
            out float y2)
 | 
			
		||||
        {
 | 
			
		||||
            (x1, y1, x2, y2) = (0, 0, 0, 0);
 | 
			
		||||
            return attrs["x1"]?.Value is string x1Str
 | 
			
		||||
                   && float.TryParse(x1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x1)
 | 
			
		||||
                   && attrs["y1"]?.Value is string y1Str
 | 
			
		||||
                   && float.TryParse(y1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y1)
 | 
			
		||||
                   && attrs["x2"]?.Value is string x2Str
 | 
			
		||||
                   && float.TryParse(x2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x2)
 | 
			
		||||
                   && attrs["y2"]?.Value is string y2Str
 | 
			
		||||
                   && float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (XmlElement x in gElement.ChildNodes)
 | 
			
		||||
        {
 | 
			
		||||
            if (x.Name != "line")
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            if (GetValuesFromAttributes(x.Attributes, out var x1, out var y1, out var x2, out var y2))
 | 
			
		||||
            {
 | 
			
		||||
                points[cnt++] = new(x1, y1);
 | 
			
		||||
                // this point will be set twice to the same value
 | 
			
		||||
                // on all points except the last one
 | 
			
		||||
                if (cnt + 1 < points.Length)
 | 
			
		||||
                    points[cnt + 1] = new(x2, y2);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cnt == 0)
 | 
			
		||||
            return Array.Empty<PointF>();
 | 
			
		||||
 | 
			
		||||
        return points.Slice(0, cnt).ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private SixLabors.ImageSharp.Image<Rgba32> GenerateSparklineChart(PointF[] points, bool up)
 | 
			
		||||
    {
 | 
			
		||||
        const int width = 164;
 | 
			
		||||
        const int height = 48;
 | 
			
		||||
 | 
			
		||||
        var img = new Image<Rgba32>(width, height, Color.Transparent);
 | 
			
		||||
        var color = up
 | 
			
		||||
            ? Color.Green
 | 
			
		||||
            : Color.FromRgb(220, 0, 0);
 | 
			
		||||
 | 
			
		||||
        img.Mutate(x =>
 | 
			
		||||
        {
 | 
			
		||||
            x.DrawLines(color, 2, points);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return img;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
            return (null, null);
 | 
			
		||||
 | 
			
		||||
        name = name.ToUpperInvariant();
 | 
			
		||||
        var cryptos = await GetCryptoDataInternal();
 | 
			
		||||
 | 
			
		||||
        if (cryptos is null or { Count: 0 })
 | 
			
		||||
            return (null, null);
 | 
			
		||||
 | 
			
		||||
        var crypto = cryptos.FirstOrDefault(x
 | 
			
		||||
            => x.Slug.ToUpperInvariant() == name
 | 
			
		||||
               || x.Name.ToUpperInvariant() == name
 | 
			
		||||
               || x.Symbol.ToUpperInvariant() == name);
 | 
			
		||||
 | 
			
		||||
        if (crypto is not null)
 | 
			
		||||
            return (crypto, null);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var nearest = cryptos
 | 
			
		||||
                      .Select(elem => (Elem: elem,
 | 
			
		||||
                          Distance: StringExtensions.LevenshteinDistance(elem.Name.ToUpperInvariant(), name)))
 | 
			
		||||
                      .OrderBy(x => x.Distance)
 | 
			
		||||
                      .FirstOrDefault(x => x.Distance <= 2);
 | 
			
		||||
 | 
			
		||||
        return (null, nearest.Elem);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<CmcResponseData>?> GetCryptoDataInternal()
 | 
			
		||||
    {
 | 
			
		||||
        await _getCryptoLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"),
 | 
			
		||||
                async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        using var http = _httpFactory.CreateClient();
 | 
			
		||||
                        var data = await http.GetFromJsonAsync<CryptoResponse>(
 | 
			
		||||
                            "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?"
 | 
			
		||||
                            + $"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}"
 | 
			
		||||
                            + "&start=1"
 | 
			
		||||
                            + "&limit=5000"
 | 
			
		||||
                            + "&convert=USD");
 | 
			
		||||
 | 
			
		||||
                        return data;
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Error(ex, "Error getting crypto data: {Message}", ex.Message);
 | 
			
		||||
                        return default;
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                TimeSpan.FromHours(2));
 | 
			
		||||
 | 
			
		||||
            if (data is null)
 | 
			
		||||
                return default;
 | 
			
		||||
 | 
			
		||||
            return data.Data;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error retreiving crypto data: {Message}", ex.Message);
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _getCryptoLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private TypedKey<byte[]> GetSparklineKey(int id)
 | 
			
		||||
        => new($"crypto:sparkline:{id}");
 | 
			
		||||
 | 
			
		||||
    public async Task<Stream?> GetSparklineAsync(int id, bool up)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = await _cache.GetOrAddAsync(GetSparklineKey(id),
 | 
			
		||||
                async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    // if it fails, generate a new one
 | 
			
		||||
                    var points = await DownloadSparklinePointsAsync(id);
 | 
			
		||||
                    var sparkline = GenerateSparklineChart(points, up);
 | 
			
		||||
 | 
			
		||||
                    using var stream = await sparkline.ToStreamAsync();
 | 
			
		||||
                    return stream.ToArray();
 | 
			
		||||
                },
 | 
			
		||||
                TimeSpan.FromHours(1));
 | 
			
		||||
 | 
			
		||||
            if (bytes is { Length: > 0 })
 | 
			
		||||
            {
 | 
			
		||||
                return bytes.ToStream();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(ex,
 | 
			
		||||
                "Exception occurred while downloading sparkline points: {ErrorMessage}",
 | 
			
		||||
                ex.Message);
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<PointF[]> DownloadSparklinePointsAsync(int id)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        var str = await http.GetStringAsync(
 | 
			
		||||
            $"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg");
 | 
			
		||||
        var points = GetSparklinePointsFromSvgText(str);
 | 
			
		||||
        return points;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,103 @@
 | 
			
		||||
using CsvHelper;
 | 
			
		||||
using CsvHelper.Configuration;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
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<StockData?> GetStockDataAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (!query.IsAlphaNumeric())
 | 
			
		||||
                return default;
 | 
			
		||||
 | 
			
		||||
            using var http = _httpClientFactory.CreateClient();
 | 
			
		||||
            var data = await http.GetFromJsonAsync<YahooQueryModel>(
 | 
			
		||||
                $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={query}");
 | 
			
		||||
 | 
			
		||||
            if (data is null)
 | 
			
		||||
                return default; 
 | 
			
		||||
            
 | 
			
		||||
            var symbol = data.QuoteResponse.Result.FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
            if (symbol is null)
 | 
			
		||||
                return default;
 | 
			
		||||
            
 | 
			
		||||
            return new()
 | 
			
		||||
            {
 | 
			
		||||
                Name = symbol.LongName,
 | 
			
		||||
                Symbol = symbol.Symbol,
 | 
			
		||||
                Price = symbol.RegularMarketPrice,
 | 
			
		||||
                Close = symbol.RegularMarketPreviousClose,
 | 
			
		||||
                MarketCap = symbol.MarketCap,
 | 
			
		||||
                Change50d = symbol.FiftyDayAverageChangePercent,
 | 
			
		||||
                Change200d = symbol.TwoHundredDayAverageChangePercent,
 | 
			
		||||
                DailyVolume = symbol.AverageDailyVolume10Day,
 | 
			
		||||
                Exchange = symbol.FullExchangeName
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception)
 | 
			
		||||
        {
 | 
			
		||||
            // Log.Warning(ex, "Error getting stock data: {ErrorMessage}", ex.Message);
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyCollection<SymbolData>> 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<YahooFinanceSearchResponse>(res);
 | 
			
		||||
 | 
			
		||||
        if (data is null or { Items: null })
 | 
			
		||||
            return Array.Empty<SymbolData>();
 | 
			
		||||
        
 | 
			
		||||
        return data.Items
 | 
			
		||||
                  .Where(x => x.Type == "S")
 | 
			
		||||
                  .Select(x => new SymbolData(x.Symbol, x.Name))
 | 
			
		||||
                  .ToList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static CsvConfiguration _csvConfig = new(CultureInfo.InvariantCulture)
 | 
			
		||||
    {
 | 
			
		||||
        PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // todo replace .ToTimestamp() and remove google protobuf dependency
 | 
			
		||||
    // todo this needs testing
 | 
			
		||||
    public async Task<IReadOnlyCollection<CandleData>> 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<YahooFinanceCandleData>().ToArray();
 | 
			
		||||
 | 
			
		||||
        return records
 | 
			
		||||
            .Map(static x => new CandleData(x.Open, x.Close, x.High, x.Low, x.Volume));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// All data required to draw a candle
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <param name="IsGreen">Whether the candle is green</param>
 | 
			
		||||
/// <param name="BodyRect">Rectangle for the body</param>
 | 
			
		||||
/// <param name="High">High line point</param>
 | 
			
		||||
/// <param name="Low">Low line point</param>
 | 
			
		||||
public record CandleDrawingData(bool IsGreen, RectangleF BodyRect, PointF High, PointF Low);
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface IStockChartDrawingService
 | 
			
		||||
{
 | 
			
		||||
    Task<ImageData?> GenerateSparklineAsync(IReadOnlyCollection<CandleData> series);
 | 
			
		||||
    Task<ImageData?> GenerateCombinedChartAsync(IReadOnlyCollection<CandleData> series);
 | 
			
		||||
    Task<ImageData?> GenerateCandleChartAsync(IReadOnlyCollection<CandleData> series);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,202 @@
 | 
			
		||||
using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class ImagesharpStockChartDrawingService : IStockChartDrawingService, INService
 | 
			
		||||
{
 | 
			
		||||
    private const int WIDTH = 300;
 | 
			
		||||
    private const int HEIGHT = 100;
 | 
			
		||||
    private const decimal MAX_HEIGHT = HEIGHT * 0.8m;
 | 
			
		||||
 | 
			
		||||
    private static readonly Rgba32 _backgroundColor = Rgba32.ParseHex("17181E");
 | 
			
		||||
    private static readonly Rgba32 _lineGuideColor = Rgba32.ParseHex("212125");
 | 
			
		||||
    private static readonly Rgba32 _sparklineColor = Rgba32.ParseHex("2961FC");
 | 
			
		||||
    private static readonly Rgba32 _greenBrush = Rgba32.ParseHex("26A69A");
 | 
			
		||||
    private static readonly Rgba32 _redBrush = Rgba32.ParseHex("EF5350");
 | 
			
		||||
 | 
			
		||||
    private static float GetNormalizedPoint(decimal max, decimal point, decimal range)
 | 
			
		||||
        => (float)((MAX_HEIGHT * ((max - point) / range)) + HeightOffset());
 | 
			
		||||
        
 | 
			
		||||
    private PointF[] GetSparklinePointsInternal(IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        var candleStep = WIDTH / (series.Count + 1);
 | 
			
		||||
        var max = series.Max(static x => x.High);
 | 
			
		||||
        var min = series.Min(static x => x.Low);
 | 
			
		||||
    
 | 
			
		||||
        var range = max - min;
 | 
			
		||||
    
 | 
			
		||||
        var points = new PointF[series.Count];
 | 
			
		||||
    
 | 
			
		||||
        var i = 0;
 | 
			
		||||
        foreach (var candle in series)
 | 
			
		||||
        {
 | 
			
		||||
            var x = candleStep * (i + 1);
 | 
			
		||||
 | 
			
		||||
            var y = GetNormalizedPoint(max, candle.Close, range);
 | 
			
		||||
            points[i++] = new(x, y);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return points;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private static decimal HeightOffset()
 | 
			
		||||
        => (HEIGHT - MAX_HEIGHT) / 2m;
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    private static Image<Rgba32> CreateCanvasInternal()
 | 
			
		||||
        => new Image<Rgba32>(WIDTH, HEIGHT, _backgroundColor);
 | 
			
		||||
 | 
			
		||||
    private CandleDrawingData[] GetChartDrawingDataInternal(IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        var candleMargin = 2;
 | 
			
		||||
        var candleStep = (WIDTH - (candleMargin * series.Count)) / (series.Count + 1);
 | 
			
		||||
        var max = series.Max(static x => x.High);
 | 
			
		||||
        var min = series.Min(static x => x.Low);
 | 
			
		||||
 | 
			
		||||
        var range = max - min;
 | 
			
		||||
 | 
			
		||||
        var drawData = new CandleDrawingData[series.Count];
 | 
			
		||||
 | 
			
		||||
        var candleWidth = candleStep;
 | 
			
		||||
        
 | 
			
		||||
        var i = 0;
 | 
			
		||||
        foreach (var candle in series)
 | 
			
		||||
        {
 | 
			
		||||
            var offsetX = (i - 1) * candleMargin; 
 | 
			
		||||
            var x = (candleStep * (i + 1)) + offsetX;
 | 
			
		||||
            var yOpen = GetNormalizedPoint(max, candle.Open, range);
 | 
			
		||||
            var yClose = GetNormalizedPoint(max, candle.Close, range);
 | 
			
		||||
            var y = candle.Open > candle.Close
 | 
			
		||||
                ? yOpen
 | 
			
		||||
                : yClose;
 | 
			
		||||
 | 
			
		||||
            var sizeH = Math.Abs(yOpen - yClose);
 | 
			
		||||
 | 
			
		||||
            var high = GetNormalizedPoint(max, candle.High, range);
 | 
			
		||||
            var low = GetNormalizedPoint(max, candle.Low, range);
 | 
			
		||||
            drawData[i] = new(candle.Open < candle.Close,
 | 
			
		||||
                new(x, y, candleWidth, sizeH),
 | 
			
		||||
                new(x + (candleStep / 2), high),
 | 
			
		||||
                new(x + (candleStep / 2), low));
 | 
			
		||||
            ++i;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return drawData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void DrawChartData(Image<Rgba32> image, CandleDrawingData[] drawData)
 | 
			
		||||
        => image.Mutate(ctx =>
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var data in drawData)
 | 
			
		||||
                DrawLineExtensions.DrawLines(ctx,
 | 
			
		||||
                    data.IsGreen
 | 
			
		||||
                        ? _greenBrush
 | 
			
		||||
                        : _redBrush,
 | 
			
		||||
                    1,
 | 
			
		||||
                    data.High,
 | 
			
		||||
                    data.Low);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            foreach (var data in drawData)
 | 
			
		||||
                FillRectangleExtensions.Fill(ctx,
 | 
			
		||||
                    data.IsGreen
 | 
			
		||||
                        ? _greenBrush
 | 
			
		||||
                        : _redBrush,
 | 
			
		||||
                    data.BodyRect);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    private void DrawLineGuides(Image<Rgba32> image, IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        var max = series.Max(x => x.High);
 | 
			
		||||
        var min = series.Min(x => x.Low);
 | 
			
		||||
 | 
			
		||||
        var step = (max - min) / 5;
 | 
			
		||||
 | 
			
		||||
        var lines = new float[6];
 | 
			
		||||
        
 | 
			
		||||
        for (var i = 0; i < 6; i++)
 | 
			
		||||
        {
 | 
			
		||||
            var y = GetNormalizedPoint(max, min + (step * i), max - min);
 | 
			
		||||
            lines[i] = y;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        image.Mutate(ctx =>
 | 
			
		||||
        {
 | 
			
		||||
            // draw guides
 | 
			
		||||
            foreach (var y in lines)
 | 
			
		||||
                ctx.DrawLines(_lineGuideColor, 1, new PointF(0, y), new PointF(WIDTH, y));
 | 
			
		||||
            
 | 
			
		||||
            // // draw min and max price on the chart
 | 
			
		||||
            // ctx.DrawText(min.ToString(CultureInfo.InvariantCulture),
 | 
			
		||||
            //     SystemFonts.CreateFont("Arial", 5),
 | 
			
		||||
            //     Color.White,
 | 
			
		||||
            //     new PointF(0, (float)HeightOffset() - 5)
 | 
			
		||||
            // );
 | 
			
		||||
            //
 | 
			
		||||
            // ctx.DrawText(max.ToString("N1", CultureInfo.InvariantCulture),
 | 
			
		||||
            //     SystemFonts.CreateFont("Arial", 5),
 | 
			
		||||
            //     Color.White,
 | 
			
		||||
            //     new PointF(0,  HEIGHT - (float)HeightOffset())
 | 
			
		||||
            // );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public Task<ImageData?> GenerateSparklineAsync(IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        if (series.Count == 0)
 | 
			
		||||
            return Task.FromResult<ImageData?>(default);
 | 
			
		||||
 | 
			
		||||
        using var image = CreateCanvasInternal();
 | 
			
		||||
 | 
			
		||||
        var points = GetSparklinePointsInternal(series);
 | 
			
		||||
        
 | 
			
		||||
        image.Mutate(ctx =>
 | 
			
		||||
        {
 | 
			
		||||
            ctx.DrawLines(_sparklineColor, 2, points);
 | 
			
		||||
        });
 | 
			
		||||
    
 | 
			
		||||
        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<ImageData?> GenerateCombinedChartAsync(IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        if (series.Count == 0)
 | 
			
		||||
            return Task.FromResult<ImageData?>(default);
 | 
			
		||||
 | 
			
		||||
        using var image = CreateCanvasInternal();
 | 
			
		||||
        
 | 
			
		||||
        DrawLineGuides(image, series);
 | 
			
		||||
        
 | 
			
		||||
        var chartData = GetChartDrawingDataInternal(series);
 | 
			
		||||
        DrawChartData(image, chartData);
 | 
			
		||||
 | 
			
		||||
        var points = GetSparklinePointsInternal(series);
 | 
			
		||||
        image.Mutate(ctx =>
 | 
			
		||||
        {
 | 
			
		||||
            ctx.DrawLines(Color.ParseHex("00FFFFAA"), 1, points);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public Task<ImageData?> GenerateCandleChartAsync(IReadOnlyCollection<CandleData> series)
 | 
			
		||||
    {
 | 
			
		||||
        if (series.Count == 0)
 | 
			
		||||
            return Task.FromResult<ImageData?>(default);
 | 
			
		||||
 | 
			
		||||
        using var image = CreateCanvasInternal();
 | 
			
		||||
 | 
			
		||||
        DrawLineGuides(image, series);
 | 
			
		||||
        
 | 
			
		||||
        var drawData = GetChartDrawingDataInternal(series);
 | 
			
		||||
        DrawChartData(image, drawData);
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface IStockDataService
 | 
			
		||||
{
 | 
			
		||||
    public Task<StockData?> GetStockDataAsync(string symbol);
 | 
			
		||||
    Task<IReadOnlyCollection<SymbolData>> SearchSymbolAsync(string query);
 | 
			
		||||
    Task<IReadOnlyCollection<CandleData>> GetCandleDataAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class FinnHubSearchResponse
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("count")]
 | 
			
		||||
    public int Count { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("result")]
 | 
			
		||||
    public List<FinnHubSearchResult> Result { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class FinnHubSearchResult
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("description")]
 | 
			
		||||
    public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("displaySymbol")]
 | 
			
		||||
    public string DisplaySymbol { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("symbol")]
 | 
			
		||||
    public string Symbol { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("type")]
 | 
			
		||||
    public string Type { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
// using System.Net.Http.Json;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Searches;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class PolygonApiClient : IDisposable
 | 
			
		||||
// {
 | 
			
		||||
//     private const string BASE_URL = "https://api.polygon.io/v3";
 | 
			
		||||
//     
 | 
			
		||||
//     private readonly HttpClient _httpClient;
 | 
			
		||||
//     private readonly string _apiKey;
 | 
			
		||||
//
 | 
			
		||||
//     public PolygonApiClient(HttpClient httpClient, string apiKey)
 | 
			
		||||
//     {
 | 
			
		||||
//         _httpClient = httpClient;
 | 
			
		||||
//         _apiKey = apiKey;
 | 
			
		||||
//     }
 | 
			
		||||
//     
 | 
			
		||||
//     public async Task<IReadOnlyCollection<PolygonTickerData>> TickersAsync(string? ticker = null, string? query = null)
 | 
			
		||||
//     {
 | 
			
		||||
//         if (string.IsNullOrWhiteSpace(query))
 | 
			
		||||
//             query = null; 
 | 
			
		||||
//         
 | 
			
		||||
//         if(query is not null)
 | 
			
		||||
//             query = Uri.EscapeDataString(query);
 | 
			
		||||
//         
 | 
			
		||||
//         var requestString = $"{BASE_URL}/reference/tickers"
 | 
			
		||||
//                             + "?type=CS"
 | 
			
		||||
//                             + "&active=true"
 | 
			
		||||
//                             + "&order=asc"
 | 
			
		||||
//                             + "&limit=1000"
 | 
			
		||||
//                             + $"&apiKey={_apiKey}";
 | 
			
		||||
//
 | 
			
		||||
//         if (!string.IsNullOrWhiteSpace(ticker))
 | 
			
		||||
//             requestString += $"&ticker={ticker}";
 | 
			
		||||
//         
 | 
			
		||||
//         if (!string.IsNullOrWhiteSpace(query))
 | 
			
		||||
//             requestString += $"&search={query}";
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
//         var response = await _httpClient.GetFromJsonAsync<PolygonTickerResponse>(requestString);
 | 
			
		||||
//
 | 
			
		||||
//         if (response is null)
 | 
			
		||||
//             return Array.Empty<PolygonTickerData>();
 | 
			
		||||
//
 | 
			
		||||
//         return response.Results;
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     // public async Task<PolygonTickerDetailsV3> TickerDetailsV3Async(string ticker)
 | 
			
		||||
//     // {
 | 
			
		||||
//     //     return new();
 | 
			
		||||
//     // }
 | 
			
		||||
//
 | 
			
		||||
//     public void Dispose()
 | 
			
		||||
//         => _httpClient.Dispose();
 | 
			
		||||
// }
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
// namespace NadekoBot.Modules.Searches;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class PolygonStockDataService : IStockDataService
 | 
			
		||||
// {
 | 
			
		||||
//     private readonly IHttpClientFactory _httpClientFactory;
 | 
			
		||||
//     private readonly IBotCredsProvider _credsProvider;
 | 
			
		||||
//
 | 
			
		||||
//     public PolygonStockDataService(IHttpClientFactory httpClientFactory, IBotCredsProvider credsProvider)
 | 
			
		||||
//     {
 | 
			
		||||
//         _httpClientFactory = httpClientFactory;
 | 
			
		||||
//         _credsProvider = credsProvider;
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     public async Task<IReadOnlyCollection<StockData>> GetStockDataAsync(string? query = null)
 | 
			
		||||
//     {
 | 
			
		||||
//         using var httpClient = _httpClientFactory.CreateClient();
 | 
			
		||||
//         using var client = new PolygonApiClient(httpClient, string.Empty);
 | 
			
		||||
//         var data = await client.TickersAsync(query: query);
 | 
			
		||||
//
 | 
			
		||||
//         return data.Map(static x => new StockData()
 | 
			
		||||
//         {
 | 
			
		||||
//             Name = x.Name,
 | 
			
		||||
//             Ticker = x.Ticker,
 | 
			
		||||
//         });
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class PolygonTickerData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("ticker")]
 | 
			
		||||
    public string Ticker { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("name")]
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("market")]
 | 
			
		||||
    public string Market { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("locale")]
 | 
			
		||||
    public string Locale { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("primary_exchange")]
 | 
			
		||||
    public string PrimaryExchange { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("type")]
 | 
			
		||||
    public string Type { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("active")]
 | 
			
		||||
    public bool Active { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("currency_name")]
 | 
			
		||||
    public string CurrencyName { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("cik")]
 | 
			
		||||
    public string Cik { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("composite_figi")]
 | 
			
		||||
    public string CompositeFigi { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("share_class_figi")]
 | 
			
		||||
    public string ShareClassFigi { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("last_updated_utc")]
 | 
			
		||||
    public DateTime LastUpdatedUtc { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class PolygonTickerResponse
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("status")]
 | 
			
		||||
    public string Status { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    [JsonPropertyName("results")]
 | 
			
		||||
    public List<PolygonTickerData> Results { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public record CandleData(
 | 
			
		||||
    decimal Open,
 | 
			
		||||
    decimal Close,
 | 
			
		||||
    decimal High,
 | 
			
		||||
    decimal Low,
 | 
			
		||||
    long Volume);
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public record ImageData(string Extension, Stream FileData) : IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    public ValueTask DisposeAsync()
 | 
			
		||||
        => FileData.DisposeAsync();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class QuoteResponse
 | 
			
		||||
{
 | 
			
		||||
    public class ResultModel
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("longName")]
 | 
			
		||||
        public string LongName { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("regularMarketPrice")]
 | 
			
		||||
        public double RegularMarketPrice { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("regularMarketPreviousClose")]
 | 
			
		||||
        public double RegularMarketPreviousClose { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("fullExchangeName")]
 | 
			
		||||
        public string FullExchangeName { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("averageDailyVolume10Day")]
 | 
			
		||||
        public int AverageDailyVolume10Day { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("fiftyDayAverageChangePercent")]
 | 
			
		||||
        public double FiftyDayAverageChangePercent { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("twoHundredDayAverageChangePercent")]
 | 
			
		||||
        public double TwoHundredDayAverageChangePercent { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("marketCap")]
 | 
			
		||||
        public long MarketCap { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("symbol")]
 | 
			
		||||
        public string Symbol { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [JsonPropertyName("result")]
 | 
			
		||||
    public List<ResultModel> Result { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("error")]
 | 
			
		||||
    public object Error { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/_Common/StockData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/Nadeko.Bot.Modules.Searches/Crypto/_Common/StockData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class StockData
 | 
			
		||||
{
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
    public string Symbol { get; set; }
 | 
			
		||||
    public double Price { get; set; }
 | 
			
		||||
    public long MarketCap { get; set; }
 | 
			
		||||
    public double Close { get; set; }
 | 
			
		||||
    public double Change50d { get; set; }
 | 
			
		||||
    public double Change200d { get; set; }
 | 
			
		||||
    public long DailyVolume { get; set; }
 | 
			
		||||
    public string Exchange { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public record SymbolData(string Symbol, string Description);
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class YahooFinanceCandleData
 | 
			
		||||
{
 | 
			
		||||
    public DateTime Date { get; set; }
 | 
			
		||||
    public decimal Open { get; set; }
 | 
			
		||||
    public decimal High { get; set; }
 | 
			
		||||
    public decimal Low { get; set; }
 | 
			
		||||
    public decimal Close { get; set; }
 | 
			
		||||
    public decimal AdjClose { get; set; }
 | 
			
		||||
    public long Volume { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class YahooFinanceSearchResponse
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("suggestionTitleAccessor")]
 | 
			
		||||
    public string SuggestionTitleAccessor { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("suggestionMeta")]
 | 
			
		||||
    public List<string> SuggestionMeta { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("hiConf")]
 | 
			
		||||
    public bool HiConf { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("items")]
 | 
			
		||||
    public List<YahooFinanceSearchResponseItem> Items { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class YahooFinanceSearchResponseItem
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("symbol")]
 | 
			
		||||
    public string Symbol { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("name")]
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("exch")]
 | 
			
		||||
    public string Exch { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("type")]
 | 
			
		||||
    public string Type { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("exchDisp")]
 | 
			
		||||
    public string ExchDisp { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("typeDisp")]
 | 
			
		||||
    public string TypeDisp { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class YahooQueryModel
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("quoteResponse")]
 | 
			
		||||
    public QuoteResponse QuoteResponse { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user