Repository navigation
Expand file tree
/
Copy pathStocksCommand.java
More file actions
511 lines (468 loc) · 18.1 KB
/
Copy pathStocksCommand.java
File metadata and controls
511 lines (468 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
package Command.Commands;
import Command.Structure.*;
import Network.NetworkRequest;
import Network.NetworkResponse;
import Network.Secret;
import Stock.Symbol;
import Stock.MarketQuote;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.json.JSONArray;
import org.json.JSONObject;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.*;
import static Stock.MarketQuote.*;
/**
* View info on some stocks/crypto
*/
public class StocksCommand extends OnReadyDiscordCommand {
private final String
finnhubBaseUrl = "https://finnhub.io/api/v1/",
thumbnail = "https://i.imgur.com/HaSlhp2.png";
private static final String
ALF_SAD_GIF = "https://media.giphy.com/media/axJZCNO2pnpakVzkOV/giphy.gif",
ALF_HAPPY_GIF = "https://media.giphy.com/media/8ibiL6qNFUIuOLxVIG/giphy.gif",
STOCK = "stock",
CRYPTO = "crypto";
private final ArrayList<Symbol> marketSymbols;
private boolean stockSymbols = false;
private String cryptoEmote;
private enum SEARCH_TYPE {
ALL,
STOCK_ONLY,
CRYPTO_ONLY;
/**
* Check if the given symbol matches the search type
*
* @param symbol Symbol to check
* @return Symbol matches search type
*/
public boolean matches(Symbol symbol) {
switch(this) {
case CRYPTO_ONLY:
return symbol.isCrypto();
case STOCK_ONLY:
return !symbol.isCrypto();
default:
return true;
}
}
}
public StocksCommand() {
super(
"$",
"Check out some stocks & crypto!",
"$[" + STOCK + "/" + CRYPTO + "] [stock/crypto symbol or search term]"
);
this.marketSymbols = getMarketSymbols();
}
/**
* Get a list of all supported crypto/stock symbols.
*
* @return List of crypto/stock symbols
*/
private ArrayList<Symbol> getMarketSymbols() {
ArrayList<Symbol> marketSymbols = new ArrayList<>(getCryptoSymbols());
this.stockSymbols = addStockSymbols(marketSymbols);
return marketSymbols;
}
/**
* Add the stock symbols to the given list of market symbols.
* Return the success of fetching the symbols
*
* @param marketSymbols List of market symbols to add stock symbols to
* @return Success of adding stock symbols
*/
private boolean addStockSymbols(ArrayList<Symbol> marketSymbols) {
ArrayList<Symbol> stockSymbols = getStockSymbols();
marketSymbols.addAll(stockSymbols);
return !stockSymbols.isEmpty();
}
/**
* Parse the list of all crypto currency asset symbols from the Messari API
*
* @return List of all supported crypto currency asset symbols from Messari
*/
private ArrayList<Symbol> getCryptoSymbols() {
ArrayList<Symbol> cryptoSymbols = new ArrayList<>();
String endpoint = "v2/assets?limit=500";
int page = 1;
NetworkResponse response;
while((response = messariRequest(endpoint + "&page=" + page)).code != 404) {
System.out.println("Parsing crypto symbols - Page " + page + "...");
JSONArray data = new JSONObject(response.body).getJSONArray("data");
for(int i = 0; i < data.length(); i++) {
JSONObject symbolData = data.getJSONObject(i);
if(symbolData.isNull("symbol") || symbolData.isNull("name")) {
continue;
}
cryptoSymbols.add(
new Symbol(
symbolData.getString("symbol"),
symbolData.getString("name"),
symbolData.getString("id"),
true
)
);
}
page++;
}
return cryptoSymbols;
}
/**
* Make a request to the Messari API, authenticate with the API key for increased rate limit.
*
* @param endpoint Endpoint to append to base URL
* @return API response
*/
private NetworkResponse messariRequest(String endpoint) {
HashMap<String, String> headers = new HashMap<>();
headers.put("x-messari-api-key", Secret.MESSARI_KEY);
return new NetworkRequest("https://data.messari.io/api/" + endpoint, false).get(headers);
}
/**
* Parse the list of all stock symbols from the Finnhub API
*
* @return List of all supported stock symbols from Finnhub
*/
private ArrayList<Symbol> getStockSymbols() {
ArrayList<Symbol> stockSymbols = new ArrayList<>();
NetworkResponse response = new NetworkRequest(
finnhubBaseUrl + "stock/symbol/?exchange=US&token=" + Secret.FINNHUB_KEY,
false
).get();
if(response.code != 200) {
return stockSymbols;
}
JSONArray data = new JSONArray(response.body);
for(int i = 0; i < data.length(); i++) {
JSONObject symbolData = data.getJSONObject(i);
stockSymbols.add(
new Symbol(
symbolData.getString("symbol"),
symbolData.getString("description"),
symbolData.getString("figi"),
false
)
);
}
return stockSymbols;
}
@Override
public void execute(CommandContext context) {
MessageChannel channel = context.getMessageChannel();
if(!stockSymbols) {
stockSymbols = addStockSymbols(marketSymbols);
if(!stockSymbols) {
channel.sendMessage("Something went wrong fetching the stock symbols!").queue();
return;
}
}
String query = context.getMessageContent().replace(getTrigger(), "").trim();
Member member = context.getMember();
if(query.isEmpty()) {
channel.sendMessage(getHelpNameCoded()).queue();
return;
}
SEARCH_TYPE searchType = SEARCH_TYPE.ALL;
if(query.startsWith(CRYPTO)) {
query = query.replaceFirst(CRYPTO, "").trim();
searchType = SEARCH_TYPE.CRYPTO_ONLY;
}
else if(query.startsWith(STOCK)) {
query = query.replaceFirst(STOCK, "").trim();
searchType = SEARCH_TYPE.STOCK_ONLY;
}
channel.sendTyping().queue();
Symbol[] searchResults = getSymbolsByQuery(query.toLowerCase(), searchType);
if(searchResults.length == 1) {
Symbol symbol = searchResults[0];
MarketQuote quote = symbol.isCrypto() ? getCryptoMarketQuote(symbol) : getStockMarketQuote(symbol);
if(quote == null) {
channel.sendMessage(
member.getAsMention()
+ " I wasn't able to parse the market data for: **" + symbol.getSymbol() + "**"
).queue();
return;
}
channel.sendMessage(buildMarketQuoteEmbed(quote)).queue();
return;
}
buildSearchResultsEmbed(context, query, searchResults);
}
/**
* Build a message embed showing the symbol search results for the given query
*
* @param context Command context
* @param query Search query
* @param symbols Symbol search results
*/
private void buildSearchResultsEmbed(CommandContext context, String query, Symbol[] symbols) {
String desc = "__" + symbols.length + " results found for **" + query + "**__\n";
Symbol[] cryptoResults = Arrays.stream(symbols).filter(Symbol::isCrypto).toArray(Symbol[]::new);
if(cryptoResults.length > 0) {
desc += "\n" + cryptoEmote + " = Crypto currency";
}
new PageableTableEmbed<Symbol>(
context,
Arrays.asList(symbols),
thumbnail,
"Market Search Results",
desc,
"Try: " + getHelpName(),
new String[]{"Symbol", "Name", "ID"},
5
) {
@Override
public String getNoItemsDescription() {
return "No symbols found for: **" + query + "**";
}
@Override
public String[] getRowValues(int index, Symbol symbol, boolean defaultSort) {
String identifier = getTrigger() + symbol.getSymbol();
if(symbol.isCrypto()) {
identifier = cryptoEmote + " " + identifier;
}
return new String[]{identifier, symbol.getName(), getTrigger() + symbol.getId()};
}
@Override
public void sortItems(List<Symbol> items, boolean defaultSort) {
items.sort((o1, o2) -> defaultSort
? o1.getSymbol().compareTo(o2.getSymbol())
: o2.getSymbol().compareTo(o1.getSymbol()));
}
}.showMessage();
}
/**
* Build a message embed detailing the given market quote
*
* @param quote Market quote to display
* @return Message embed displaying market quote
*/
private MessageEmbed buildMarketQuoteEmbed(MarketQuote quote) {
DecimalFormat decimalFormat = new DecimalFormat("0.00");
NumberFormat currFormat = NumberFormat.getCurrencyInstance();
Symbol symbol = quote.getSymbol();
boolean down = quote.getDiff() < 0;
EmbedBuilder builder = new EmbedBuilder()
.setTitle(symbol.getName() + " (" + symbol.getSymbol() + ")")
.setFooter("Try: " + getHelpName())
.setThumbnail(symbol.hasLogoUrl() ? symbol.getLogoUrl() : thumbnail)
.setImage(down ? ALF_SAD_GIF : ALF_HAPPY_GIF)
.setColor(down ? EmbedHelper.RED : EmbedHelper.GREEN)
.addField(
"Current Price\n(C)",
currFormat.format(quote.getCurrentPrice()),
true
);
if(quote.hasOHLCV()) {
builder.addField(
"Difference\n" + (symbol.isCrypto() ? "(O -> C)" : "(PC -> C)"),
(down ? "-" : "+") + currFormat.format(Math.abs(quote.getDiff()))
+ " (" + decimalFormat.format(quote.getDiffPercent()) + "%)",
true
)
.addBlankField(true)
.addField("Opening Price\n(O)", currFormat.format(quote.getOpenPrice()), true)
.addField(
"Previous Close Price\n(PC)",
currFormat.format(quote.getPreviousClosePrice()),
true
)
.addBlankField(true)
.addField(
"Day Range",
currFormat.format(quote.getLowPrice())
+ " - "
+ currFormat.format(quote.getHighPrice()),
true
);
}
return builder.build();
}
/**
* Get the market quote for the given stock symbol
*
* @param symbol Stock symbol to get market quote for
* @return Market quote for symbol
*/
private MarketQuote getStockMarketQuote(Symbol symbol) {
try {
JSONObject data = new JSONObject(
new NetworkRequest(
finnhubBaseUrl + "quote?symbol=" + symbol.getSymbol() + "&token=" + Secret.FINNHUB_KEY,
false
).get().body
);
return new MarketQuoteBuilder(symbol)
.setHighPrice(data.getDouble("h"))
.setLowPrice(data.getDouble("l"))
.setOpenPrice(data.getDouble("o"))
.setPreviousClosePrice(data.getDouble("pc"))
.setCurrentPrice(data.getDouble("c"))
.build();
}
catch(Exception e) {
return null;
}
}
/**
* Get the market quote for the given crypto symbol
*
* @param symbol Crypto symbol to get market quote for
* @return Market quote for symbol
*/
private MarketQuote getCryptoMarketQuote(Symbol symbol) {
try {
JSONObject data = new JSONObject(
messariRequest("v1/assets/" + symbol.getId() + "/metrics/market-data").body
).getJSONObject("data").getJSONObject("market_data");
MarketQuoteBuilder builder = new MarketQuoteBuilder(symbol)
.setCurrentPrice(data.getDouble("price_usd"));
String marketDataKey = "ohlcv_last_24_hour";
if(data.isNull(marketDataKey)) {
return builder.build();
}
JSONObject ohlcv = data.getJSONObject(marketDataKey); // open, high, low, close, volume
return builder
.setHighPrice(ohlcv.getDouble("high"))
.setLowPrice(ohlcv.getDouble("low"))
.setOpenPrice(ohlcv.getDouble("open"))
.setPreviousClosePrice(ohlcv.getDouble("close"))
.build();
}
catch(Exception e) {
return null;
}
}
/**
* Attempt to get stock/crypto symbols from the given query.
* Check for symbols with an exact match to the query, otherwise perform a fuzzy search.
* Get the symbol logo if it has not been seen before and there is a singular result
*
* @param query Query - either symbol or search query
* @param searchType Search type
* @return Array of search results
*/
private Symbol[] getSymbolsByQuery(String query, SEARCH_TYPE searchType) {
Symbol[] symbols = getMatchingSymbolsById(query, searchType);
if(symbols.length == 1) {
return new Symbol[]{completeSymbol(symbols[0])};
}
symbols = getMatchingSymbolsBySymbol(query, searchType);
if(symbols.length == 0) {
return getFuzzyMatchingSymbols(query, searchType);
}
if(symbols.length == 1) {
return new Symbol[]{completeSymbol(symbols[0])};
}
return symbols;
}
/**
* Add the symbol logo
*
* @param symbol Symbol to add logo to
* @return Symbol with logo
*/
private Symbol completeSymbol(Symbol symbol) {
if(!symbol.hasLogoUrl()) {
String logo = symbol.isCrypto() ? getCryptoLogoUrl(symbol) : getStockLogoUrl(symbol);
if(logo != null) {
symbol.setLogoUrl(logo);
}
}
return symbol;
}
/**
* Get an array of symbols where the symbol matches the given query
*
* @param query Query to match to symbol
* @param searchType Search type
* @return Symbols matching query
*/
private Symbol[] getMatchingSymbolsBySymbol(String query, SEARCH_TYPE searchType) {
return marketSymbols
.stream()
.filter(symbol -> symbol.getSymbol().equalsIgnoreCase(query) && searchType.matches(symbol))
.toArray(Symbol[]::new);
}
/**
* Get an array of symbols where the name matches the given query
*
* @param query Query to match to name
* @param searchType Search type
* @return Symbols with name matching query
*/
private Symbol[] getMatchingSymbolsById(String query, SEARCH_TYPE searchType) {
return marketSymbols
.stream()
.filter(symbol -> symbol.getId().equalsIgnoreCase(query) && searchType.matches(symbol))
.toArray(Symbol[]::new);
}
/**
* Get an array of symbols where the name or symbol contain the given query
*
* @param query Query to fuzzy search
* @param searchType Search type
* @return Symbols where the name or symbol contain the given query
*/
private Symbol[] getFuzzyMatchingSymbols(String query, SEARCH_TYPE searchType) {
return marketSymbols
.stream()
.filter(symbol -> {
boolean fuzzyMatch = symbol.getSymbol().toLowerCase().contains(query)
|| symbol.getName().toLowerCase().contains(query);
return fuzzyMatch && searchType.matches(symbol);
})
.toArray(Symbol[]::new);
}
/**
* Attempt to get the logo URL for the given stock symbol
*
* @param stockSymbol Stock symbol to get URL for
* @return Symbol logo URL
*/
private String getStockLogoUrl(Symbol stockSymbol) {
JSONObject info = new JSONObject(
new NetworkRequest(
finnhubBaseUrl
+ "stock/profile2?symbol=" + stockSymbol.getSymbol()
+ "&token=" + Secret.FINNHUB_KEY,
false
).get().body
);
if(info.has("logo")) {
String logoURL = info.getString("logo");
if(!logoURL.isEmpty()) {
return logoURL;
}
}
return null;
}
/**
* Attempt to get the logo URL for the given crypto symbol
*
* @param cryptoSymbol Crypto symbol to get URL for
* @return Crypto logo URL
*/
private String getCryptoLogoUrl(Symbol cryptoSymbol) {
String url = "https://cryptoicons.org/api/icon/" + cryptoSymbol.getSymbol().toLowerCase() + "/200";
if(new NetworkRequest(url, false).get().code == 200) {
return url;
}
return null;
}
@Override
public boolean matches(String query, Message message) {
return query.startsWith(getTrigger());
}
@Override
public void onReady(JDA jda, EmoteHelper emoteHelper) {
cryptoEmote = emoteHelper.getCrypto().getAsMention();
}
}