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 }