Added a .stock command

This commit is contained in:
Kwoth
2022-02-06 17:43:19 +01:00
parent 73555ff70e
commit 3d1f9b8b75
16 changed files with 446 additions and 46 deletions

View File

@@ -8,6 +8,110 @@ public partial class Searches
{
public partial class CryptoCommands : NadekoSubmodule<CryptoService>
{
private readonly IStockDataService _stocksService;
public CryptoCommands(IStockDataService stocksService)
{
_stocksService = stocksService;
}
[Cmd]
public async partial Task Stock([Leftover]string query)
{
if (!query.IsAlphaNumeric())
return;
using var typing = ctx.Channel.EnterTypingState();
var stocks = await _stocksService.GetStockDataAsync(query);
if (stocks.Count == 0)
{
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;
stocks = await _stocksService.GetStockDataAsync(query);
if (stocks.Count == 0)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
}
// try to find a ticker match
var stock = stocks.Count == 1
? stocks.FirstOrDefault()
: stocks.FirstOrDefault(x => x.Ticker == query.ToUpperInvariant());
if (stock is null)
{
var ebImprecise = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.stocks_multiple_results))
.WithDescription(stocks.Take(20)
.Select(s => $"{Format.Code(s.Ticker)} {s.Name.TrimTo(50)}")
.Join('\n'));
await ctx.Channel.EmbedAsync(ebImprecise);
return;
}
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 / 100).ToString("P1", Culture);
var sign200 = stock.Change200d >= 0
? "\\🔼"
: "\\🔻";
var change200 = (stock.Change200d / 100).ToString("P1", Culture);
var price = stock.Price.ToString("C2", localCulture);
var eb = _eb.Create()
.WithOkColor()
.WithAuthor(stock.Ticker)
.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);
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
public async partial Task Crypto(string name)
{

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Searches;
public interface IStockDataService
{
public Task<IReadOnlyCollection<StockData>> GetStockDataAsync(string query);
Task<IReadOnlyCollection<SymbolData>> SearchSymbolAsync(string query);
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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();
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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,
// });
// }
// }

View File

@@ -0,0 +1,15 @@
#nullable disable
namespace NadekoBot.Modules.Searches;
public class StockData
{
public string Name { get; set; }
public string Ticker { 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; }
}

View File

@@ -0,0 +1,83 @@
using System.Net.Http.Json;
using YahooFinanceApi;
namespace NadekoBot.Modules.Searches;
public sealed class DefaultStockDataService : IStockDataService, INService
{
private readonly IHttpClientFactory _httpClientFactory;
public DefaultStockDataService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<IReadOnlyCollection<StockData>> GetStockDataAsync(string query)
{
try
{
var symbols = await Yahoo.Symbols(query)
.Fields(Field.LongName,
Field.Symbol,
Field.RegularMarketPrice,
Field.RegularMarketPreviousClose,
Field.MarketCap,
Field.FiftyDayAverageChangePercent,
Field.TwoHundredDayAverageChangePercent,
Field.AverageDailyVolume10Day,
Field.FullExchangeName)
.QueryAsync();
return symbols
.Select(static x => x.Value)
.Select(static x => new StockData()
{
Name = x.LongName,
Ticker = x.Symbol,
Price = x.RegularMarketPrice,
Close = x.RegularMarketPreviousClose,
MarketCap = x.MarketCap,
Change50d = x.FiftyDayAverageChangePercent,
Change200d = x.TwoHundredDayAverageChangePercent,
DailyVolume = x.AverageDailyVolume10Day,
Exchange = x.FullExchangeName
})
.ToList();
}
catch (Exception ex)
{
// what the hell is this api exception
Log.Warning(ex, "Error getting stock data: {ErrorMessage}", ex.Message);
return Array.Empty<StockData>();
}
}
public Task<IReadOnlyCollection<SymbolData>> SearchSymbolAsync(string query)
{
return Task.FromResult<IReadOnlyCollection<SymbolData>>(Array.Empty<SymbolData>());
// try
// {
// query = Uri.EscapeDataString(query);
// using var http = _httpClientFactory.CreateClient();
// var response = await http.GetFromJsonAsync<FinnHubSearchResponse>($"https://finnhub.io/api/v1/search"
// + $"?q={query}"
// + $"&token=");
//
// if (response is null)
// return Array.Empty<SymbolData>();
//
// return response.Result
// .Where(x => x.Type == "Common Stock")
// .Select(static x => new SymbolData(x.Symbol, x.Description))
// .ToList();
// }
// catch (Exception ex)
// {
// Log.Warning(ex, "Error searching stock symbol: {ErrorMessage}", ex.Message);
// return Array.Empty<SymbolData>();
// }
}
}
public record SymbolData(string Symbol, string Description);