1 /** Wrapper for the IEX trading API.
2 
3     Documentation for the API is at https://iextrading.com/developer/docs/
4 */
5 module iex;
6 
7 import std.typecons : Flag;
8 public import std.typecons : Yes, No;
9 
10 import vibe.data.json;
11 
12 enum iexPrefix = "https://api.iextrading.com/1.0/";
13 
14 /** List of supported stock endpoints. */
15 enum EndpointType : string {
16     Book = "book",
17     Chart = "chart",
18     HistoricalPrices = EndpointType.Chart,
19     Company = "company",
20     DelayedQuote = "delayed-quote",
21     Dividends = "dividends",
22     Earnings = "earnings",
23     EffectiveSpread = "effective-spread",
24     Financials = "financials",
25     ThresholdSecuritiesList = "threshold-securities",
26     ShortInterestList = "short-interest",
27     KeyStats = "stats",
28     LargestTrades = "largest-trades",
29     List = "list",
30     Logo = "logo",
31     News = "news",
32     OHLC = "ohlc",
33     OpenClose = EndpointType.OHLC,
34     Peers = "peers",
35     Previous = "previous",
36     Price = "price",
37     Quote = "quote",
38     Relevant = "relevant",
39     Splits = "splits",
40     TimeSeries = "time-series",
41     VolumeByVenue = "volume-by-venue",
42 }
43 
44 struct Endpoint {
45     string urlString;
46     string[string] params;
47     string[string] options;
48 }
49 
50 /** Specify response formats for endpoints that support alternatives to JSON. */
51 enum ResponseFormat : string {
52     json = "json",
53     csv = "csv",
54     psv = "psv"
55 }
56 
57 /** Build a query on the Stock endpoint of the IEX API.
58 
59     The string created by calling toURL() is an IEX API-compatible URL.
60 */
61 // TODO: There is a maximum of 10 endpoints in a query.
62 // TODO: Clean up the URL generation code.
63 struct Stock {
64 
65     @disable this();
66 
67     this(string[] symbols...) in {
68         assert(symbols.length > 0);
69     } do {
70         this.symbols = symbols;
71     }
72 
73     /** Build an IEX API URL from this Stock object. */
74     @property
75     string toURL(string prefix = iexPrefix) {
76         string queryString = getSymbolURL(prefix);
77 
78         if (this.endpoints.length == 1) {
79             queryString ~= buildEndpoint(
80                     this.endpoints.keys()[0],
81                     this.endpoints[this.endpoints.keys()[0]],
82                     this.queriesMultipleSymbols());
83         } else {
84             // options are shared among endpoints. We only want to enter them
85             // once - duplicates are ignored, even if values differ.
86             bool[string] optionsFinished;
87 
88             foreach (type, endpoint; this.endpoints) {
89                 foreach (key, val; endpoint.params) {
90                     queryString ~= "&" ~ key ~ "=" ~ val;
91                 }
92                 foreach (key, val; endpoint.options) {
93                     if (key !in optionsFinished && val != "") {
94                         queryString ~= "&" ~ key ~ "=" ~ val;
95                         optionsFinished[key] = true;
96                     }
97                 }
98             }
99         }
100         return prefix ~ queryString;
101     }
102 
103 
104     private:
105 
106 
107     auto getSymbolURL(string prefix) {
108         if (this.queriesMultipleSymbols() || this.endpoints.length > 1) {
109             string queryString = "stock/market/batch?symbols=" ~ symbols[0];
110             for (int i = 1; i < symbols.length; ++i) {
111                 queryString ~= "," ~ symbols[i];
112             }
113 
114             queryString ~= "&types=";
115             foreach (type, params; this.endpoints) {
116                 queryString ~= type ~ ",";
117             }
118             return queryString[0..$-1];
119         } else {
120             return "stock/" ~ symbols[0] ~ "/";
121         }
122     }
123 
124     /** Add a query type to the Stock HTTP query.
125 
126         Params:
127             type =          The type of the endpoint to add.
128             params =        Any parameters to include.
129             urlAddition =   Any necessary text to append to the endpoint.
130     */
131     void addQueryType(
132             EndpointType type,
133             string[string] params = null,
134             string[string] options = null,
135             string urlAddition = "") {
136         Endpoint p = {
137             urlString: type ~ urlAddition,
138             params: params,
139             options: options
140         };
141         this.endpoints[type] = p;
142     }
143 
144     string buildEndpoint(
145             EndpointType type,
146             Endpoint endpoint,
147             bool isContinuing = false) {
148 
149         if (endpoint.params.length == 0) {
150             if (isContinuing) {
151                 string endpointString;
152                 foreach (option, value; endpoint.options) {
153                     endpointString ~= "&" ~ option ~ "=" ~ value;
154                 }
155                 return endpointString;
156             }
157             string endpointString = endpoint.urlString;
158 
159             foreach (option, value; endpoint.options) {
160                 endpointString ~= "/" ~ value;
161             }
162             return endpointString;
163         } else {
164             string endpointString;
165             if (! isContinuing) {
166                 endpointString = endpoint.urlString;
167                 foreach (option, value; endpoint.options) {
168                     endpointString ~= "/" ~ value;
169                 }
170                 endpointString ~= "?";
171             } else {
172                 endpointString = "&";
173             }
174 
175             foreach (param, value; endpoint.params) {
176                 endpointString ~= param ~ "=" ~ value ~ "&";
177             }
178             if (isContinuing) {
179                 foreach (option, value; endpoint.options) {
180                     endpointString ~= option ~ "=" ~ value ~ "&";
181                 }
182             }
183             return endpointString[0..$-1];
184         }
185     }
186 
187     @property bool queriesMultipleSymbols() { return symbols.length > 1; }
188 
189     string[] symbols;
190 
191     Endpoint[EndpointType] endpoints;
192 }
193 
194 
195 /** Send a Stock object to the IEX API and return the JSON results. */
196 Json query(Stock query) {
197     import std.conv : to;
198     import requests : getContent;
199     return getContent(query.toURL()).to!string().parseJsonString();
200 }
201 
202 
203 /** Make an arbitrary call to the IEX API.
204 
205     This is here to allow retrieving data from currently-unsupported endpoints.
206     This function is not (may not be) permanent.
207 */
208 Json query(string query, string prefix = iexPrefix) {
209     import std.conv : to;
210     import requests : getContent;
211     return getContent(prefix ~ query).to!string().parseJsonString();
212 }
213 
214 
215 /// ditto
216 Json query(string query, string[string] params, string prefix = iexPrefix) {
217     import std.conv : to;
218     import requests : getContent;
219     return getContent(prefix ~ query, params).to!string().parseJsonString();
220 }
221 
222 
223 /+ TODO: I'd like to do this but can't generate docs for it.
224     https://issues.dlang.org/show_bug.cgi?id=2420
225 
226 string GenerateSimpleEndpoint(string endpoint, string type) {
227     return
228         "/** Test comment */" ~
229         "Stock " ~ endpoint ~ "(Stock stock) {\n"
230       ~ "    stock.addQueryType(" ~ type ~ ");\n"
231       ~ "    return stock;\n"
232       ~ "}";
233 }
234 mixin(GenerateSimpleEndpoint("book", "EndpointType.Book"));
235 +/
236 
237 
238 /** Get the book for the specified stock(s).
239 
240     The data returned includes information from both the "deep" and the "quote"
241     endpoints.
242 
243     Notes:
244         It appears that "deep" is undocumented and forbidden; legacy endpoint?
245 */
246 Stock book(Stock stock) {
247     stock.addQueryType(EndpointType.Book);
248     return stock;
249 }
250 
251 
252 /** Values for the date range in a chart query. A custom date can be used
253     instead.
254 */
255 enum ChartRange : string {
256     FiveYears = "5y",
257     TwoYears = "2y",
258     OneYear = "1y",
259     YearToDate = "ytd",
260     YTD = ChartRange.YearToDate,
261     SixMonths = "6m",
262     ThreeMonths = "3m",
263     OneMonth = "1m",
264     OneDay = "1d",
265     Dynamic = "dynamic"
266 }
267 
268 /** Request historical prices for a stock.
269 
270     Params:
271         stock =             The Stock object to modify.
272         range =             The date range for which to retrieve prices. A custom
273                             date may be passed in the format "YYYYMMDD" within
274                             the last thirty days.
275         reset =             If Yes, the 1 day chart will reset at midnight
276                             instead of 9:30 AM ET.
277         simplify =          If Yes, runs a polyline simplification using the
278                             Douglas-Peucker algorithm.
279         changeFromClose =   If Yes, "changeOverTime" and "marketChangeOverTime"
280                             will be relative to the previous day close instead of
281                             the first value.
282         last =              Return the last n elements.
283 */
284 Stock chart(
285         Stock stock,
286         string range,
287         Flag!"resetAtMidnight" resetAtMidnight = No.resetAtMidnight,
288         Flag!"simplify" simplify = No.simplify,
289         int interval = -1,
290         Flag!"changeFromClose" changeFromClose = No.changeFromClose,
291         int last = -1) {
292     import std.conv : text;
293     import std.string : isNumeric;
294     // TODO: Enforce custom date is within last thirty days.
295     // TODO: I think last overrides range; test this, and if so, only take one
296     // or the other.
297 
298     string[string] params;
299     string[string] options;
300 
301     if (resetAtMidnight) params["chartReset"] = "true";
302     if (simplify) params["chartSimplify"] = "true";
303     if (interval > 0) params["chartInterval"] = interval.text;
304     if (changeFromClose) params["changeFromClose"] = "true";
305     if (last > 0) params["chartLast"] = last.text;
306 
307     options["range"] = range;
308 
309     if (range.isNumeric && range.length == 8)
310         stock.addQueryType(EndpointType.Chart, params, options, "/date");
311     else if (hasEnumMember!ChartRange(range))
312         stock.addQueryType(EndpointType.Chart, params, options);
313     else
314         throw new Exception("Invalid range for chart: " ~ range);
315 
316     return stock;
317 }
318 
319 /// ditto
320 Stock historicalPrices(
321         Stock stock,
322         string range,
323         Flag!"resetAtMidnight" resetAtMidnight = No.resetAtMidnight,
324         Flag!"simplify" simplify = No.simplify,
325         int interval = -1,
326         Flag!"changeFromClose" changeFromClose = No.changeFromClose,
327         int last = -1) {
328     return chart(stock, range,
329             resetAtMidnight, simplify, interval, changeFromClose, last);
330 }
331 
332 /// ditto
333 Stock timeSeries(
334         Stock stock,
335         string range,
336         Flag!"resetAtMidnight" resetAtMidnight = No.resetAtMidnight,
337         Flag!"simplify" simplify = No.simplify,
338         int interval = -1,
339         Flag!"changeFromClose" changeFromClose = No.changeFromClose,
340         int last = -1) {
341     return chart(stock, range,
342             resetAtMidnight, simplify, interval, changeFromClose, last);
343 }
344 
345 
346 /** Retrieve information about the specified company. */
347 Stock company(Stock stock) {
348     stock.addQueryType(EndpointType.Company);
349     return stock;
350 }
351 
352 
353 /** Retrieve the 15-minute delayed market quote. */
354 Stock delayedQuote(Stock stock) {
355     stock.addQueryType(EndpointType.DelayedQuote);
356     return stock;
357 }
358 
359 
360 /** Values for the date range in a dividend query. */
361 enum DividendRange : string {
362     FiveYears = "5y",
363     TwoYears = "2y",
364     OneYear = "1y",
365     YearToDate = "ytd",
366     YTD = ChartRange.YearToDate,
367     SixMonths = "6m",
368     ThreeMonths = "3m",
369     OneMonth = "1m"
370 }
371 
372 /** Request dividend distribution history.
373 
374     Params:
375         range = The range for which the list of distributions is desired.
376 */
377 Stock dividends(Stock stock, DividendRange range) {
378     string[string] options;
379     options["range"] = range;
380     stock.addQueryType(EndpointType.Dividends, null, options);
381     return stock;
382 }
383 
384 
385 /** Request earnings from the four most recent quarters. */
386 Stock earnings(Stock stock) {
387     stock.addQueryType(EndpointType.Earnings);
388     return stock;
389 }
390 
391 
392 /** Return the effective spread of a stock.
393 
394     Returns an array of effective spread, eligible volume, and price improvement.
395 */
396 Stock effectiveSpread(Stock stock) {
397     stock.addQueryType(EndpointType.EffectiveSpread);
398     return stock;
399 }
400 
401 
402 /** Request a company's financial data.
403 
404     Retrieves the company's income statement, balance sheet, and cash flow
405     statement from the four most recent quarters.
406 */
407 Stock financials(Stock stock) {
408     stock.addQueryType(EndpointType.Financials);
409     return stock;
410 }
411 
412 /** Request threshold securities for IEX-listed stocks.
413 
414     Params:
415         stock =     The stock object to modify.
416         date =      List data for the specified date in YYYYMMDD format, or
417                     "sample" for sample data.
418         format =    json, csv, or psv.
419         token =     Your IEX account token; if not specified, the CUSIP field
420                     will be excluded from the results.
421 */
422 Stock thresholdSecurities(
423         Stock stock,
424         string date = "",
425         ResponseFormat format = ResponseFormat.json,
426         string token = "") {
427     string[string] params;
428     string[string] options;
429 
430     options["range"] = date;
431     if (token.length > 0) params["token"] = token;
432 
433     if (stock.symbols[0] == "") {
434         stock.symbols[0] = "market";
435     }
436 
437     if (format != ResponseFormat.json) {
438         params["format"] = format;
439     }
440 
441     stock.addQueryType(EndpointType.ThresholdSecuritiesList, params, options);
442     return stock;
443 }
444 
445 
446 /** The IEX-listed short interest list. */
447 Stock shortInterest(
448         Stock stock,
449         string date = "",
450         ResponseFormat format = ResponseFormat.json,
451         string token = "") {
452     string[string] params;
453     string[string] options;
454 
455     if (stock.symbols[0] == "") stock.symbols[0] = "market";
456     if (token.length > 0) params["token"] = token;
457     options["range"] = date;
458 
459     if (format != ResponseFormat.json) {
460         params["format"] = format;
461     }
462 
463     stock.addQueryType(EndpointType.ShortInterestList, params, options);
464     return stock;
465 }
466 
467 
468 /** Retrieve current stock information. */
469 Stock keyStats(Stock stock) {
470     stock.addQueryType(EndpointType.KeyStats);
471     return stock;
472 }
473 
474 
475 /** Retrieve 15-minute delayed last sale eligible trades. */
476 Stock largestTrades(Stock stock) {
477     stock.addQueryType(EndpointType.LargestTrades);
478     return stock;
479 }
480 
481 
482 /** Filter for top ten lists. */
483 enum MarketList : string {
484     MostActive = "mostactive",
485     Gainers = "gainers",
486     Losers = "losers",
487     volume = "iexvolume",
488     percent = "iexpercent"
489 }
490 
491 /** Get a list of top ten stocks according to a filter.
492 
493     Params:
494         stock =             The Stock object to modify.
495         list =              The desired list.
496         displayPercent =    If true, percentage values are multiplied by 100.
497 */
498 Stock list(
499         Stock stock,
500         MarketList list,
501         Flag!"displayPercent" displayPercent = No.displayPercent) {
502     string[string] params;
503     string[string] options;
504 
505     stock.symbols[0] = "market";
506     options["list"] = list;
507     if (displayPercent) params["displayPercent"] = "true";
508 
509     stock.addQueryType(EndpointType.List, params, options);
510     return stock;
511 }
512 
513 
514 /** Retrieve the URL to the company's logo. */
515 Stock logo(Stock stock) {
516     stock.addQueryType(EndpointType.Logo);
517     return stock;
518 }
519 
520 
521 /** Retrieve news about the specified stocks or the market.
522 
523     Params:
524         stock = The Stock object to modify.
525         last =  The number of news results to return, between 1 and 50 inclusive.
526                 The default is 10.
527 */
528 Stock news(Stock stock, int last = 10) in {
529     assert(last > 0 && last < 51, "last must be between 1 and 50 inclusive.");
530 } do {
531     import std.conv : text;
532     string[string] params;
533 
534     if (stock.queriesMultipleSymbols() && last != 10)
535         params["last"] = last.text;
536 
537     if (last != 10)
538         stock.addQueryType(EndpointType.News, params, null, "/last/" ~ last.text);
539     else
540         stock.addQueryType(EndpointType.News, params);
541 
542     return stock;
543 }
544 
545 
546 /** Retrieve the open and close for the specified symbol(s) or the market. */
547 Stock ohlc(Stock stock) {
548     stock.addQueryType(EndpointType.OHLC);
549     return stock;
550 }
551 
552 // ditto
553 Stock openclose(Stock stock) { return ohlc(stock); }
554 
555 
556 /** Retrieve a list of IEX-defined peers for a stock. */
557 Stock peers(Stock stock) {
558     stock.addQueryType(EndpointType.Peers);
559     return stock;
560 }
561 
562 
563 /** Retrieve  the previous day adjusted price data for a stock or the market. */
564 Stock previous(Stock stock) {
565     stock.addQueryType(EndpointType.Previous);
566     return stock;
567 }
568 
569 
570 /** Retrieve the EIX real time price, the 15 minute delayed price, or previous
571     close price.
572 */
573 Stock price(Stock stock) {
574     stock.addQueryType(EndpointType.Price);
575     return stock;
576 }
577 
578 
579 /** Request a quote for the stock(s).
580 
581     Params:
582         stock =             The Stock object to manipulate.
583         displayPercent =    If Yes, percentage values are multiplied by 100.
584 
585     See_Also:
586         https://iextrading.com/developer/docs/#quote
587 */
588 Stock quote(Stock stock, Flag!"displayPercent" displayPercent = No.displayPercent) {
589     string[string] params;
590     if (displayPercent) params["displayPercent"] = "true";
591     stock.addQueryType(EndpointType.Quote, params);
592     return stock;
593 }
594 
595 
596 /** Retrieve a list of most active market symbols when peers are not available.
597 */
598 Stock relevant(Stock stock) {
599     stock.addQueryType(EndpointType.Relevant);
600     return stock;
601 }
602 
603 
604 alias SplitRange = DividendRange;
605 
606 /** Request a stock's split history.
607 
608     Params:
609         stock = The Stock object to modify.
610         range = The range for which the split history is desired.
611 */
612 Stock splits(Stock stock, SplitRange range) {
613     string[string] options;
614     options["range"] = range;
615     stock.addQueryType(EndpointType.Splits, null, options);
616     return stock;
617 }
618 
619 
620 /** Retrieve the 15 minute delayed and 30 day average consolidated volume
621     percentage of a stock by market.
622 */
623 Stock volumeByVenue(Stock stock) {
624     stock.addQueryType(EndpointType.VolumeByVenue);
625     return stock;
626 }
627 
628 
629 private:
630 
631 bool hasEnumMember(E, T)(T value) if (is(E == enum)) {
632     import std.traits : EnumMembers;
633     foreach (member; EnumMembers!E) {
634         if (member == value) return true;
635     }
636     return false;
637 }