1 module unit.iex;
2 
3 import unit_threaded : HiddenTest, writelnUt;
4 
5 import iex;
6 
7 version(unittest) {
8     /**  Check that a string has the provided parameters in unittests.
9 
10         Parameters in a URL may be in any order, so a simple equality check is
11         insufficient.
12     */
13     bool hasParameters(string input, string[] parameters) {
14         import std.algorithm.searching : canFind;
15         foreach (parameter; parameters) {
16             if (! canFind(input, parameter)) return false;
17         }
18         return true;
19     }
20 
21     @("hasParameters() checks for substrings")
22     unittest {
23         assert("some-url?one=two&three=four".hasParameters(["one=two","three=four"]));
24         assert(! "some-url?one=two".hasParameters(["three=four"]));
25     }
26 }
27 
28 
29 @("book() builds an endpoint for a single stock symbol")
30 unittest {
31     auto stock = Stock("AAPL").book();
32     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/book", stock.toURL());
33 }
34 
35 @("book() builds an endpoint for multiple stock symbols")
36 unittest {
37     auto stock = Stock("AAPL", "BDC").book();
38     assert(stock.toURL() == iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=book", stock.toURL());
39 }
40 
41 
42 @("chart() builds an endpoint for pre-defined date ranges")
43 unittest {
44     auto stock = Stock("AAPL").chart(ChartRange.YearToDate);
45     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/chart/"  ~ ChartRange.YTD,
46             stock.toURL());
47 
48     stock = Stock("AAPL")
49             .chart(ChartRange.YearToDate, No.resetAtMidnight, Yes.simplify, 3);
50 
51     import std.string : split;
52     auto actual = stock.toURL().split('?');
53     assert(actual[0] == iexPrefix ~ "stock/AAPL/chart/" ~ ChartRange.YTD,
54             actual[0]);
55     assert(actual[1].hasParameters(["chartSimplify=true", "chartInterval=3"]),
56             actual[1]);
57 }
58 
59 @("chart() builds an endpoint for custom dates")
60 unittest {
61     auto stock = Stock("AAPL").chart("20180531");
62     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/chart/date/20180531",
63             stock.toURL());
64 }
65 
66 @("chart() builds an endpoint for multiple stock symbols")
67 unittest {
68     import std.string : split;
69     auto stock = Stock("AAPL", "BDC").chart("1y");
70     auto actual = stock.toURL().split('?');
71     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
72     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=chart", "range=1y"]),
73             actual[1]);
74 
75     stock = Stock("AAPL", "BDC").chart("20180531");
76     actual = stock.toURL().split('?');
77     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
78     assert(actual[1].hasParameters(
79             ["symbols=AAPL,BDC", "types=chart", "range=20180531"]),
80             actual[1]);
81 }
82 
83 @("historicalPrices() is equivalent to chart()")
84 unittest {
85     auto chart = Stock("AAPL").chart(ChartRange.OneYear);
86     auto history = Stock("AAPL").historicalPrices(ChartRange.OneYear);
87     assert(chart == history, history.toURL());
88 }
89 
90 @("timeSeries() is equivalent to chart()")
91 unittest {
92     auto chart = Stock("AAPL").chart(ChartRange.OneYear);
93     auto series = Stock("AAPL").timeSeries(ChartRange.OneYear);
94     assert(chart == series, series.toURL());
95 }
96 
97 
98 @("company() builds an endpoint for a single stock symbol")
99 unittest {
100     auto stock = Stock("AAPL").company();
101     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/company", stock.toURL());
102 }
103 
104 @("company() builds an endpoint for multiple stock symbols")
105 unittest {
106     auto stock = Stock("AAPL", "BDC").company();
107     assert(stock.toURL() == iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=company", stock.toURL());
108 }
109 
110 
111 @("delayedQuote() builds an endpoint for a single stock symbol")
112 unittest {
113     auto stock = Stock("AAPL").delayedQuote();
114     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/delayed-quote", stock.toURL());
115 }
116 
117 @("delayedQuote() builds an endpoint for multiple stock symbols")
118 unittest {
119     auto stock = Stock("AAPL", "BDC").delayedQuote();
120     assert(stock.toURL() ==
121             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=delayed-quote",
122             stock.toURL());
123 }
124 
125 
126 @("dividends() builds an endpoint for a single stock symbol")
127 unittest {
128     auto stock = Stock("AAPL").dividends(DividendRange.TwoYears);
129     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/dividends/2y", stock.toURL());
130 }
131 
132 @("dividends() builds an endpoint for multiple stock symbols")
133 unittest {
134     import std.string : split;
135     auto stock = Stock("AAPL", "BDC").dividends(DividendRange.TwoYears);
136     auto actual = stock.toURL().split('?');
137     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
138     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=dividends", "range=2y"]),
139             actual[1]);
140 }
141 
142 
143 @("earnings() builds an endpoint for a single stock symbol")
144 unittest {
145     auto stock = Stock("AAPL").earnings();
146     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/earnings", stock.toURL());
147 }
148 
149 @("earnings() builds an endpoint for multiple stock symbols")
150 unittest {
151     auto stock = Stock("AAPL", "BDC").earnings();
152     assert(stock.toURL() ==
153             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=earnings",
154             stock.toURL());
155 }
156 
157 
158 @("effectiveSpread() builds an endpoint for a single stock symbol")
159 unittest {
160     auto stock = Stock("AAPL").effectiveSpread();
161     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/effective-spread",
162             stock.toURL());
163 }
164 
165 @("effectiveSpread() builds an endpoint for multiple stock symbols")
166 unittest {
167     auto stock = Stock("AAPL", "BDC").effectiveSpread();
168     assert(stock.toURL() ==
169             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=effective-spread",
170             stock.toURL());
171 }
172 
173 
174 @("financials() builds an endpoint for a single stock symbol")
175 unittest {
176     auto stock = Stock("AAPL").financials();
177     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/financials",
178             stock.toURL());
179 }
180 
181 @("financials() builds an endpoint for multiple stock symbols")
182 unittest {
183     auto stock = Stock("AAPL", "BDC").financials();
184     assert(stock.toURL() ==
185             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=financials",
186             stock.toURL());
187 }
188 
189 
190 @("thresholdSecurities() builds an endpoint for a single stock symbol")
191 unittest {
192     import std.string : split;
193     auto stock = Stock("AAPL").thresholdSecurities();
194     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/threshold-securities/",
195             stock.toURL());
196 
197     stock = Stock("market").thresholdSecurities("20180531");
198     assert(stock.toURL() ==
199             iexPrefix ~ "stock/market/threshold-securities/" ~ "20180531",
200             stock.toURL());
201 }
202 
203 @("thresholdSecurities() builds an endpoint as part of a batch")
204 unittest {
205     import std.string : split;
206     auto stock = Stock("AAPL", "BDC")
207             .chart(ChartRange.YTD)
208             .thresholdSecurities();
209 
210     auto actual = stock.toURL().split('?');
211     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
212     assert(actual[1].hasParameters(
213             ["symbols=AAPL,BDC", "types=threshold-securities,chart", "range=ytd"]),
214             actual[1]);
215 }
216 
217 
218 @("shortInterest() builds an endpoint for a single stock symbol")
219 unittest {
220     import std.string : split;
221     auto stock = Stock("AAPL").shortInterest();
222     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/short-interest/",
223             stock.toURL());
224 
225     stock = Stock("AAPL").shortInterest("20180531", ResponseFormat.csv);
226     auto actual = stock.toURL().split('?');
227     assert(actual[0] == iexPrefix ~ "stock/AAPL/short-interest/20180531",
228             actual[0]);
229     assert(actual[1].hasParameters(["format=csv"]), actual[1]);
230 }
231 
232 @("shortInterest() builds an endpoint for the market")
233 unittest {
234     auto stock = Stock("").shortInterest();
235     assert(stock.toURL() == iexPrefix ~ "stock/market/short-interest/",
236             stock.toURL());
237 
238     stock = Stock("market").shortInterest();
239     assert(stock.toURL() == iexPrefix ~ "stock/market/short-interest/",
240             stock.toURL());
241 }
242 
243 
244 @("keyStats() builds an endpoint for a single stock symbol")
245 unittest {
246     auto stock = Stock("AAPL").keyStats();
247     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/stats",
248             stock.toURL());
249 }
250 
251 @("keyStats() builds an endpoint for multiple stock symbols")
252 unittest {
253     auto stock = Stock("AAPL", "BDC").keyStats();
254     assert(stock.toURL() ==
255             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=stats",
256             stock.toURL());
257 }
258 
259 
260 @("largestTrades() builds an endpoint for a single stock symbol")
261 unittest {
262     auto stock = Stock("AAPL").largestTrades();
263     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/largest-trades",
264             stock.toURL());
265 }
266 
267 @("largestTrades() builds an endpoint for multiple stock symbols")
268 unittest {
269     auto stock = Stock("AAPL", "BDC").largestTrades();
270     assert(stock.toURL() ==
271             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=largest-trades",
272             stock.toURL());
273 }
274 
275 
276 @("list() builds an endpoint for a single symbol")
277 unittest {
278     import std.string : split;
279     auto stock = Stock("").list(MarketList.MostActive);
280     assert(stock.toURL() == iexPrefix ~ "stock/market/list/mostactive",
281             stock.toURL());
282 
283     stock = Stock("").list(MarketList.MostActive, Yes.displayPercent);
284     auto actual = stock.toURL().split('?');
285     assert(actual[0] == iexPrefix ~ "stock/market/list/mostactive", actual[0]);
286     assert(actual[1].hasParameters(["displayPercent=true"]), actual[1]);
287 }
288 
289 @("list() ignores individual stock symbols passed to it")
290 unittest {
291     auto stock = Stock("AAPL").list(MarketList.MostActive);
292     assert(stock.toURL() == iexPrefix ~ "stock/market/list/mostactive",
293             stock.toURL());
294 }
295 
296 
297 @("logo() builds an endpoint for a single symbol")
298 unittest {
299     auto stock = Stock("AAPL").logo();
300     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/logo",
301             stock.toURL());
302 }
303 
304 @("logo() builds an endpoint for multiple symbols")
305 unittest {
306     auto stock = Stock("AAPL", "BDC").logo();
307     assert(stock.toURL() ==
308             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=logo",
309             stock.toURL());
310 }
311 
312 
313 @("news() builds an endpoint for a single symbol")
314 unittest {
315     auto stock = Stock("AAPL").news();
316     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/news", stock.toURL());
317 
318     stock = Stock("market").news();
319     assert(stock.toURL() == iexPrefix ~ "stock/market/news", stock.toURL());
320 
321     stock = Stock("AAPL").news(5);
322     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/news/last/5", stock.toURL());
323 }
324 
325 @("news() builds an endpoint for multiple symbols")
326 unittest {
327     import std.string : split;
328     auto stock = Stock("AAPL", "BDC").news();
329     auto actual = stock.toURL().split('?');
330     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
331     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=news"]), actual[1]);
332 
333     stock = Stock("AAPL", "BDC").news(5);
334     actual = stock.toURL().split('?');
335     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
336     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=news", "last=5"]),
337             actual[1]);
338 }
339 
340 
341 @("ohlc() builds an endpoint for a single stock symbol")
342 unittest {
343     auto stock = Stock("AAPL").ohlc();
344     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/ohlc", stock.toURL());
345 
346     stock = Stock("market").ohlc();
347     assert(stock.toURL() == iexPrefix ~ "stock/market/ohlc", stock.toURL());
348 }
349 
350 @("ohlc() builds an endpoint for multiple symbols")
351 unittest {
352     import std.string : split;
353     auto stock = Stock("AAPL", "BDC").ohlc();
354     auto actual = stock.toURL().split('?');
355     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
356     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=ohlc"]), actual[1]);
357 }
358 
359 @("openclose() is equivalent to ohlc()")
360 unittest {
361     auto ohlc = Stock("AAPL").ohlc();
362     auto openclose = Stock("AAPL").openclose();
363 }
364 
365 
366 @("peers() builds an endpoint for a single stock symbol")
367 unittest {
368     auto stock = Stock("AAPL").peers();
369     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/peers", stock.toURL());
370 }
371 
372 @("peers() builds an endpoint for multiple symbols")
373 unittest {
374     import std.string : split;
375     auto stock = Stock("AAPL", "BDC").peers();
376     auto actual = stock.toURL().split('?');
377     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
378     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=peers"]), actual[1]);
379 }
380 
381 
382 @("previous() builds an endpoint for a single stock symbol")
383 unittest {
384     auto stock = Stock("AAPL").previous();
385     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/previous", stock.toURL());
386 
387     stock = Stock("market").previous();
388     assert(stock.toURL() == iexPrefix ~ "stock/market/previous", stock.toURL());
389 }
390 
391 @("ohlc() builds an endpoint for multiple symbols")
392 unittest {
393     import std.string : split;
394     auto stock = Stock("AAPL", "BDC").previous();
395     auto actual = stock.toURL().split('?');
396     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
397     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=previous"]),
398             actual[1]);
399 }
400 
401 
402 @("price() builds an endpoint for a single stock symbol")
403 unittest {
404     auto stock = Stock("AAPL").price();
405     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/price", stock.toURL());
406 }
407 
408 @("price() builds an endpoint for multiple symbols")
409 unittest {
410     import std.string : split;
411     auto stock = Stock("AAPL", "BDC").price();
412     auto actual = stock.toURL().split('?');
413     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
414     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=price"]),
415             actual[1]);
416 }
417 
418 
419 @("quote() builds an endpoint for a single stock symbol")
420 unittest {
421     import std.string : split;
422     auto stock = Stock("AAPL").quote();
423     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/quote", stock.toURL());
424 
425     stock = Stock("AAPL").quote(Yes.displayPercent);
426     auto actual = stock.toURL().split('?');
427     assert(actual[0] == iexPrefix ~ "stock/AAPL/quote");
428     assert(actual[1].hasParameters(["displayPercent=true"]));
429 }
430 
431 @("quote() builds an endpoint for multiple stock symbols")
432 unittest {
433     import std.string : split;
434     auto stock = Stock("AAPL", "BDC").quote();
435     assert(stock.toURL() == iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=quote", stock.toURL());
436 
437     stock = Stock("AAPL", "BDC").quote(Yes.displayPercent);
438     auto actual = stock.toURL().split('?');
439     assert(actual[0] == iexPrefix ~ "stock/market/batch");
440     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=quote", "displayPercent=true"]));
441 }
442 
443 
444 @("relevant() builds an endpoint for a single stock symbol")
445 unittest {
446     auto stock = Stock("AAPL").relevant();
447     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/relevant", stock.toURL());
448 }
449 
450 @("relevant() builds an endpoint for multiple symbols")
451 unittest {
452     import std.string : split;
453     auto stock = Stock("AAPL", "BDC").relevant();
454     auto actual = stock.toURL().split('?');
455     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
456     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "types=relevant"]), actual[1]);
457 }
458 
459 
460 @("splits() builds an endpoint for a single stock symbol")
461 unittest {
462     auto stock = Stock("AAPL").splits(SplitRange.TwoYears);
463     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/splits/2y", stock.toURL());
464 }
465 
466 @("splits() builds an endpoint for multiple stock symbols")
467 unittest {
468     import std.string : split;
469     auto stock = Stock("AAPL", "BDC").splits(SplitRange.TwoYears);
470     auto actual = stock.toURL().split('?');
471     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
472     assert(actual[1].hasParameters(["symbols=AAPL,BDC", "range=2y", "types=splits"]),
473             actual[1]);
474 }
475 
476 
477 @("volumeByVenue() builds an endpoint for a single stock symbol")
478 unittest {
479     auto stock = Stock("AAPL").volumeByVenue();
480     assert(stock.toURL() == iexPrefix ~ "stock/AAPL/volume-by-venue", stock.toURL());
481 }
482 
483 @("volumeByVenue() builds an endpoint for multiple stock symbols")
484 unittest {
485     auto stock = Stock("AAPL", "BDC").volumeByVenue();
486     assert(stock.toURL() ==
487             iexPrefix ~ "stock/market/batch?symbols=AAPL,BDC&types=volume-by-venue",
488             stock.toURL());
489 }
490 
491 
492 @("Build single-symbol batch endpoints")
493 unittest {
494     import std.string : split;
495     auto stock = Stock("AAPL")
496             .company()
497             .dividends(DividendRange.FiveYears);
498 
499     // We don't have to use the /market endpoint but can, and it keeps the code
500     // simpler (e.g., we could do /stock/AAPL/batch instead).
501 
502     auto actual = stock.toURL.split('?');
503     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
504     assert(actual[1].hasParameters(
505             ["symbols=AAPL", "types=company,dividends", "range=5y"]),
506             actual[1]);
507 }
508 
509 @("Build multiple-symbol batch endpoints")
510 unittest {
511     import std.string : split;
512     auto stock = Stock("AAPL", "BDC")
513             .quote()
514             .news(5) // Last 5 items.
515             .chart(ChartRange.OneMonth);
516 
517     auto actual = stock.toURL().split('?');
518     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
519     assert(actual[1].hasParameters(
520             ["symbols=AAPL,BDC", "types=quote,chart,news", "range=1m", "last=5"]),
521             actual[1]);
522 }
523 
524 @("Only use the last-specified value of shared parameters.")
525 /+ TODO: I don't think I like this, but it's better than allowing inconsistency.
526 
527     auto stock = Stock("AAPL")
528                 .chart()
529                 .dividends()
530                     .range("1y");
531 +/
532 unittest {
533     import std.string : split;
534     auto stock = Stock("AAPL")
535                 .chart(ChartRange.TwoYears)
536                 .dividends(DividendRange.OneYear);
537 
538     auto actual = stock.toURL().split('?');
539     assert(actual[0] == iexPrefix ~ "stock/market/batch", actual[0]);
540     assert(actual[1].hasParameters(["types=dividends,chart", "range=1y"]),
541             actual[1]);
542 }