├── docs └── API DOCS.pdf ├── requirements.txt ├── README.md ├── LICENSE.md ├── tdapi_test.py └── tdapi.py /docs/API DOCS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tacodog1/tdameritrade/HEAD/docs/API DOCS.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.14.1 2 | pandas==0.22.0 3 | python-dateutil==2.6.1 4 | pytz==2018.3 5 | six==1.11.0 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tdameritrade 2 | Basic python client implementation of TDAmeritrade API 3 | 4 | TD Ameritrade provides a developer API which supports a variety of features including historical pricing data, snapshot and streaming quotes, option chains, historical volatility, etc. This is a basic client implementation in python along with a simple test example for snapshot quotes and option chains. 5 | 6 | This is pre-Alpha level code and only implements a small portion of the TDA API. However, it is useful for my needs and I didn't see another python implementation on github, so I'm making it available. Please let me know if you are interested in using it. 7 | 8 | Note that in order to access the TDA API you will need to have an API key provided by TDAmeritrade. If you don't have one please contact TDAmeritrade. 9 | 10 | To use the tdapi_test.py example, provide your API key, username, and a ticker: 11 | 12 | ```sh 13 | python tdapi_test.py YOUR_API_KEY someuser AMZN 14 | ``` 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tdapi_test.py: -------------------------------------------------------------------------------- 1 | from tdapi import TDAmeritradeAPI 2 | import getpass 3 | import argparse 4 | import pandas as pd 5 | import datetime 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('sourceid', help="API key provided by TD Ameritrade") 10 | parser.add_argument('userid') 11 | parser.add_argument('ticker', default='AMZN') 12 | args = parser.parse_args() 13 | 14 | #print args.sourceid 15 | #print args.userid 16 | 17 | if not (args.sourceid and args.userid): 18 | parser.print_help() 19 | 20 | pwd = getpass.getpass() 21 | td = TDAmeritradeAPI(args.sourceid) 22 | td.login(args.userid, pwd) 23 | print('Getting snapshot quote for %s' % args.ticker) 24 | quoteList = td.getSnapshotQuote([args.ticker],'stock',True) 25 | print(quoteList[args.ticker]) 26 | 27 | print('Getting price history') 28 | dt = datetime.datetime.now() 29 | df = td.getPriceHistory(args.ticker, intervalType='DAILY', intervalDuration='1', periodType='MONTH', 30 | period='6', startdate=None, enddate=dt.strftime('%Y%m%d'), extended=None) 31 | print(df) 32 | 33 | print('Getting binary option chain for %s' % args.ticker) 34 | chain = td.getBinaryOptionChain(args.ticker) 35 | print('Returned %d contracts. First 10:' % len(chain)) 36 | for option in chain[:10]: 37 | print(option) 38 | 39 | td.logout() 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /tdapi.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import datetime 3 | import base64 4 | import http.client 5 | import urllib.request, urllib.parse, urllib.error 6 | import getpass 7 | import binascii 8 | import time 9 | import array 10 | import string 11 | import math, types 12 | from struct import pack, unpack 13 | from xml.etree import ElementTree 14 | import logging 15 | import pandas 16 | 17 | class StockQuote(): 18 | symbol = None 19 | description = None 20 | bid = None 21 | ask = None 22 | bidAskSize = None 23 | last = None 24 | lastTradeSize = None 25 | lastTradeDate = None 26 | openPrice = None 27 | highPrice = None 28 | lowPrice = None 29 | closePrice = None 30 | volume = None 31 | yearHigh = None 32 | yearLow = None 33 | realTime = None 34 | exchange = None 35 | assetType = None 36 | change = None 37 | changePercent = None 38 | 39 | def __init__(self, elementTree): 40 | i = elementTree 41 | self.symbol = i.findall('symbol')[0].text 42 | self.description = i.findall('description')[0].text 43 | self.bid = float(i.findall('bid')[0].text) 44 | self.ask = float(i.findall('ask')[0].text) 45 | self.bidAskSize = i.findall('bid-ask-size')[0].text 46 | self.last = float(i.findall('last')[0].text) 47 | self.lastTradeSize = i.findall('last-trade-size')[0].text 48 | self.lastTradeDate = i.findall('last-trade-date')[0].text 49 | self.openPrice = float(i.findall('open')[0].text) 50 | self.highPrice = float(i.findall('high')[0].text) 51 | self.lowPrice = float(i.findall('low')[0].text) 52 | self.closePrice = float(i.findall('close')[0].text) 53 | self.volume = float(i.findall('volume')[0].text) 54 | self.yearHigh = float(i.findall('year-high')[0].text) 55 | self.yearLow = float(i.findall('year-low')[0].text) 56 | self.realTime = i.findall('real-time')[0].text 57 | self.exchange = i.findall('exchange')[0].text 58 | self.assetType = i.findall('asset-type')[0].text 59 | self.change = float(i.findall('change')[0].text) 60 | self.changePercent = i.findall('change-percent')[0].text 61 | 62 | 63 | def __str__(self): 64 | s = '' 65 | for key in dir(self): 66 | if key[0] != '_': 67 | s += '%s: %s\n' % (key, getattr(self, key)) 68 | return s 69 | 70 | class OptionChainElement(): 71 | 72 | quoteDateTime = None 73 | 74 | # Option Date Length - Short - 2 75 | # Option Date - String - Variable 76 | optionDate = None 77 | 78 | # Expiration Type Length - Short - 2 79 | # Expiration Type - String - Variable (R for Regular, L for LEAP) 80 | expirationType = None 81 | # Strike Price - Double - 8 82 | strike = None 83 | 84 | # Standard Option Flag - Byte - 1 (1 = true, 0 = false) 85 | standardOptionFlag = None 86 | 87 | # Put/Call Indicator - Char - 2 (P or C in unicode) 88 | pcIndicator = None 89 | 90 | # Option Symbol Length - Short - 2 91 | # Option Symbol - String - Variable 92 | optionSymbol = None 93 | 94 | # Option Description Length - Short - 2 95 | # Option Description - String - Variable 96 | optionDescription = None 97 | 98 | # Bid - Double - 8 99 | bid = None 100 | 101 | # Ask - Double - 8 102 | ask = None 103 | 104 | # Bid/Ask Size Length - Short - 2 105 | # Bid/Ask Size - String - Variable 106 | baSize = None 107 | 108 | # Last - Double - 8 109 | last = None 110 | 111 | # Last Trade Size Length - Short - 2 112 | # Last Trade Size - String - Variable 113 | lastTradeSize = None 114 | 115 | # Last Trade Date Length - short - 2 116 | # Last Trade Date - String - Variable 117 | lastTradeDate = None 118 | 119 | # Volume - Long - 8 120 | volume = None 121 | 122 | # Open Interest - Integer - 4 123 | openInterest = None 124 | 125 | # RT Quote Flag - Byte - 1 (1=true, 0=false) 126 | rtQuoteFlag = None 127 | 128 | # Underlying Symbol length - Short 2 129 | # Underlying Symbol - String - Variable 130 | underlyingSymbol = None 131 | 132 | # Delta - Double- 8 133 | # Gamma - Double - 8 134 | # Theta - Double - 8 135 | # Vega - Double - 8 136 | # Rho - Double - 8 137 | delta = None 138 | gamma = None 139 | theta = None 140 | vega = None 141 | rho = None 142 | 143 | # Implied Volatility - Double - 8 144 | impliedVolatility = None 145 | 146 | # Time Value Index - Double - 8 147 | timeValueIndex = None 148 | 149 | # Multiplier - Double - 8 150 | multiplier = None 151 | 152 | # Change - Double - 8 153 | change = None 154 | 155 | # Change Percentage - Double - 8 156 | changePercentage = None 157 | 158 | # ITM Flag - Byte - 1 (1 = true, 0 = false) 159 | itmFlag = None 160 | 161 | # NTM Flag - Byte - 1 (1 = true, 0 = false) 162 | ntmFlag = None 163 | 164 | # Theoretical value - Double - 8 165 | theoreticalValue = None 166 | 167 | # Deliverable Note Length - Short - 2 168 | # Deliverable Note - String - Variable 169 | deliverableNote = None 170 | 171 | # CIL Dollar Amount - Double - 8 172 | cilDollarAmoubt = None 173 | 174 | # OP Cash Dollar Amount - Double - 8 175 | opCashDollarAmount = None 176 | 177 | # Index Option Flag - Byte - 1 (1 = true, 0 = false) 178 | indexOptionFlag = None 179 | 180 | # Number of Deliverables - Integer - 4 181 | deliverables = [] # (symbol, shares) 182 | # REPEATING block for each Deliverable 183 | # Deliverable Symbol Length - Short - 2 184 | # Deliverable Symbol - String - Variable 185 | # Deliverable Shares - Integer - 4 186 | # END 187 | 188 | def setQuoteDateTime(self, quoteTime): 189 | #self.quoteDateTime = datetime.datetime.now() 190 | self.quoteDateTime = quoteTime 191 | 192 | def __str__(self): 193 | s = self.optionDescription.decode() 194 | 195 | if self.last != None: 196 | s = '%s Last: $%.2f' % (s, self.last) 197 | else: 198 | s = '%s Last: N/A' % s 199 | if not (self.delta is None or self.gamma is None or self.theta is None or \ 200 | self.vega is None or self.rho is None): 201 | s = "%s (d: %f g: %f t: %f v: %f r: %f)" % \ 202 | (s, self.delta, self.gamma, self.theta, self.vega, self.rho) 203 | # date 204 | # expirationType 205 | # strike 206 | # standardOptionFlag 207 | # pcIndicator 208 | # optionSymbol 209 | # optionDescription 210 | # bid 211 | # ask 212 | # baSize 213 | # last 214 | # lastTradeSize 215 | # lastTradeDate 216 | # volume 217 | # openInterest 218 | # rtQuoteFlag 219 | # underlyingSymbol 220 | # delta 221 | # gamma 222 | # theta 223 | # vega 224 | # rho 225 | # impliedVolatility 226 | # timeValueIndex 227 | # multiplier 228 | # change 229 | # changePercentage 230 | # itmFlag 231 | # ntmFlag 232 | # theoreticalValue 233 | # deliverableNote 234 | # cilDollarAmoubt 235 | # opCashDollarAmount 236 | # indexOptionFlag 237 | # deliverables = [] # (symbol, shares) 238 | # REPEATING block for each Deliverable 239 | # Deliverable Symbol Length - Short - 2 240 | # Deliverable Symbol - String - Variable 241 | # Deliverable Shares - Integer - 4 242 | return s 243 | 244 | # Python3 245 | __repr__ = __str__ 246 | 247 | class HistoricalPriceBar(): 248 | close = None 249 | high = None 250 | low = None 251 | open = None 252 | volume = None 253 | timestamp = None 254 | 255 | def __init__(self): 256 | pass 257 | 258 | def __str__(self): 259 | return '%f,%f,%f,%f,%f,%s' % (self.close, self.high, self.low, self.open, self.volume, self.timestamp) 260 | 261 | class TDAmeritradeAPI(): 262 | _sourceID = None # Note - Source ID must be provided by TD Ameritrade 263 | _version = '0.1' 264 | _active = False 265 | _sessionID = '' 266 | 267 | def __init__(self, sourceID): 268 | self._sourceID = sourceID 269 | 270 | def isActive(self, confirm=False): 271 | if self._active == True: 272 | # Confirm with server by calling KeepAlive 273 | if confirm: 274 | if self.keepAlive() == False: 275 | self.login() 276 | # TODO: add more robust checking here to make sure we're really logged in 277 | return True 278 | else: 279 | return False 280 | 281 | def keepAlive(self): 282 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 283 | conn.request('POST', '/apps/100/KeepAlive?source=%s' % self._sourceID) 284 | response = conn.getresponse() 285 | #print response.status, response.reason 286 | 287 | #print 'Getting response data...' 288 | data = response.read() 289 | #print 'Data:',data 290 | conn.close() 291 | 292 | if data.strip() == 'LoggedOn': 293 | return True 294 | elif data.strip() == 'InvalidSession': 295 | return False 296 | else: 297 | #print 'Unrecognized response: %s' % data 298 | pass 299 | 300 | def login(self, login, password): 301 | logging.debug('[tdapi] Entered login()') 302 | params = urllib.parse.urlencode({'source': self._sourceID, 'version': self._version}) 303 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 304 | body = urllib.parse.urlencode({'userid': login, 305 | 'password': password, 306 | 'source': self._sourceID, 307 | 'version': self._version}) 308 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 309 | #conn.set_debuglevel(100) 310 | conn.request('POST', '/apps/100/LogIn?'+params, body, headers) 311 | 312 | response = conn.getresponse() 313 | if response.status == 200: 314 | self._active = True 315 | else: 316 | self._active = False 317 | return False 318 | 319 | # The data response is an XML fragment. Log it. 320 | data = response.read() 321 | logging.debug('Login response:\n--------%s\n--------' % data) 322 | conn.close() 323 | 324 | # Make sure the login succeeded. First look for OK 325 | element = ElementTree.XML(data) 326 | try: 327 | result = element.findall('result')[0].text 328 | if result == 'FAIL': 329 | self._active = False 330 | return False 331 | elif result == 'OK': 332 | self._active = True 333 | else: 334 | logging.error('Unrecognized login result: %s' % result) 335 | return False 336 | 337 | # Now get the session ID 338 | self._sessionID = element.findall('xml-log-in')[0].findall('session-id')[0].text 339 | 340 | except: 341 | logging.error('Failed to parse login response.') 342 | return False 343 | 344 | 345 | def logout(self): 346 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 347 | conn.request('POST', '/apps/100/LogOut?source=%s' % self._sourceID) 348 | response = conn.getresponse() 349 | data = response.read() 350 | conn.close() 351 | 352 | self._active = False 353 | 354 | 355 | def getSessionID(self): 356 | return self._sessionID 357 | 358 | def getStreamerInfo(self, accountID=None): 359 | 360 | arguments = {'source': self._sourceID} 361 | if accountID != None: 362 | arguments['accountid'] = accountID 363 | params = urllib.parse.urlencode(arguments) 364 | 365 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 366 | #conn.set_debuglevel(100) 367 | conn.request('GET', '/apps/100/StreamerInfo?'+params) 368 | response = conn.getresponse() 369 | #print response.status, response.reason 370 | data = response.read() 371 | conn.close() 372 | #print 'Read %d bytes' % len(data) 373 | 374 | # We will need to create an ElementTree to process this XML response 375 | # TODO: handle exceptions 376 | element = ElementTree.XML(data) 377 | # Process XML response 378 | streamerInfo = {} 379 | try: 380 | children = element.findall('streamer-info')[0].getchildren() 381 | for c in children: 382 | streamerInfo[c.tag] = c.text 383 | except e: 384 | #print 'Error: failed to parse streamer-info response: %s', e 385 | return False 386 | 387 | #print 'Received streamer-info properties: %s' % streamerInfo 388 | 389 | return streamerInfo 390 | 391 | 392 | def getSnapshotQuote(self, tickers, assetType, detailed=False): 393 | logging.info('[tdapi.getSnapshotQuote] Enter') 394 | if len(tickers) > 300: 395 | logging.error('TODO: divide in batches of 300') 396 | 397 | if assetType not in ['stock','option','index','mutualfund']: 398 | logging.error('getSnapshotQuote: Unrecognized asset type %s' % assetType) 399 | return [] 400 | 401 | arguments = {'source': self._sourceID, 402 | 'symbol': str.join(',', tickers)} 403 | params = urllib.parse.urlencode(arguments) 404 | #print 'Arguments: ', arguments 405 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 406 | 407 | #conn.set_debuglevel(100) 408 | conn.request('GET', ('/apps/100/Quote;jsessionid=%s?' % self.getSessionID()) +params) 409 | response = conn.getresponse() 410 | data = response.read() 411 | conn.close() 412 | logging.info('[tdapi.getSnapshotQuote] Read %d bytes' % len(data)) 413 | 414 | quotes = {} 415 | # Perform basic processing regardless of quote type 416 | element = ElementTree.XML(data) 417 | try: 418 | result = element.findall('result')[0].text 419 | if result == 'FAIL': 420 | self._active = False 421 | return False 422 | elif result == 'OK': 423 | self._active = True 424 | else: 425 | logging.error('[tdapi.getSnapshotQuote] Unrecognized result: %s' % result) 426 | return {} 427 | 428 | # Now get the session ID 429 | #self._sessionID = element.findall('xml-log-in')[0].findall('session-id')[0].text 430 | 431 | except: 432 | logging.error('[tdapi.getSnapshotQuote] Failed to parse snapshot quote response.') 433 | return False 434 | if assetType == 'stock': 435 | try: 436 | quoteElements = element.findall('quote-list')[0].findall('quote') 437 | for i in quoteElements: 438 | symbol = i.findall('symbol')[0].text 439 | if detailed: 440 | q = StockQuote(i) # Create a quote object from etree 441 | quotes[symbol] = q 442 | else: 443 | last = float(i.findall('last')[0].text) 444 | quotes[symbol] = last 445 | 446 | except: 447 | logging.error('Failed to parse snapshot quote response') 448 | return {} 449 | 450 | else: 451 | logging.error('[tdapi.getSnapshotQuote] Asset type not supported: %s' % assetType) 452 | return quotes 453 | 454 | 455 | def getBinaryOptionChain(self, ticker): 456 | 457 | arguments = {'source': self._sourceID, 458 | 'symbol': ticker, 459 | 'range': 'ALL', 460 | 'quotes': 'true' 461 | } 462 | params = urllib.parse.urlencode(arguments) 463 | #print 'Arguments: ', arguments 464 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 465 | 466 | #conn.set_debuglevel(100) 467 | conn.request('GET', ('/apps/200/BinaryOptionChain;jsessionid=%s?' % self.getSessionID()) +params) 468 | response = conn.getresponse() 469 | data = response.read() 470 | conn.close() 471 | 472 | cursor = 0 473 | error = unpack('b', data[cursor:cursor+1])[0] 474 | cursor += 1 475 | # If there is an error, there will be an error length and corresponding error text 476 | if error != 0: 477 | errorLength = unpack('>h', data[cursor:cursor+2])[0] 478 | cursor += 2 479 | if errorLength > 0: 480 | errorText = data[cursor:cursor+errorLength] 481 | cursor += errorLength 482 | raise ValueError('[getBinaryOptionChain] Error: %s' % errorText) 483 | symbolLength = unpack('>h', data[cursor:cursor+2])[0] 484 | cursor += 2 485 | symbol = data[cursor:cursor+symbolLength].decode('utf-8') 486 | cursor += symbolLength 487 | symbolDescriptionLength = unpack('>h', data[cursor:cursor+2])[0] 488 | cursor += 2 489 | symbolDescription = data[cursor:cursor+symbolDescriptionLength].decode('utf-8') 490 | cursor += symbolDescriptionLength 491 | 492 | bid = unpack('>d', data[cursor:cursor+8])[0] 493 | cursor += 8 494 | ask = unpack('>d', data[cursor:cursor+8])[0] 495 | cursor += 8 496 | baSizeLength = unpack('>h', data[cursor:cursor+2])[0] 497 | cursor += 2 498 | baSize = data[cursor:cursor+baSizeLength] 499 | cursor += baSizeLength 500 | 501 | last = unpack('>d', data[cursor:cursor+8])[0] 502 | cursor += 8 503 | open = unpack('>d', data[cursor:cursor+8])[0] 504 | cursor += 8 505 | high = unpack('>d', data[cursor:cursor+8])[0] 506 | cursor += 8 507 | low = unpack('>d', data[cursor:cursor+8])[0] 508 | cursor += 8 509 | close = unpack('>d', data[cursor:cursor+8])[0] 510 | cursor += 8 511 | volume = unpack('>d', data[cursor:cursor+8])[0] 512 | cursor += 8 513 | change = unpack('>d', data[cursor:cursor+8])[0] 514 | cursor += 8 515 | 516 | rtFlag = chr(unpack('>H', data[cursor:cursor+2])[0]) 517 | cursor += 2 518 | 519 | qtLength = unpack('>H', data[cursor:cursor+2])[0] 520 | cursor += 2 521 | quoteTime = data[cursor:cursor+qtLength] 522 | cursor += qtLength 523 | rowCount = unpack('>i', data[cursor:cursor+4])[0] 524 | cursor += 4 525 | 526 | optionChain = [] 527 | for i in range(rowCount): 528 | if cursor > len(data): 529 | print('Error! Read too much data') 530 | break 531 | 532 | o = OptionChainElement() 533 | optionChain.append(o) 534 | # Option Date Length - Short - 2 535 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 536 | # Option Date - String - Variable 537 | o.optionDate = data[cursor:cursor+l]; cursor += l 538 | 539 | # Expiration Type Length - Short - 2 540 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 541 | # Expiration Type - String - Variable (R for Regular, L for LEAP) 542 | o.expirationType = data[cursor:cursor+l]; cursor += l 543 | 544 | # Strike Price - Double - 8 545 | o.strike = unpack('>d', data[cursor:cursor+8])[0]; cursor += 8 546 | 547 | # Standard Option Flag - Byte - 1 (1 = true, 0 = false) 548 | o.standardOptionFlag = unpack('b', data[cursor:cursor+1])[0]; cursor += 1 549 | 550 | # Put/Call Indicator - Char - 2 (P or C in unicode) 551 | o.pcIndicator = chr(unpack('>H', data[cursor:cursor+2])[0]); cursor += 2 552 | 553 | # Option Symbol Length - Short - 2 554 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 555 | # Option Symbol - String - Variable 556 | o.optionSymbol = data[cursor:cursor+l]; cursor += l 557 | 558 | # Option Description Length - Short - 2 559 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 560 | # Option Description - String - Variable 561 | o.optionDescription = data[cursor:cursor+l]; cursor += l 562 | # Bid - Double - 8 563 | o.bid = unpack('>d', data[cursor:cursor+8])[0]; cursor += 8 564 | # Ask - Double - 8 565 | o.ask = unpack('>d', data[cursor:cursor+8])[0]; cursor += 8 566 | # Bid/Ask Size Length - Short - 2 567 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 568 | # Bid/Ask Size - String - Variable 569 | o.baSize = data[cursor:cursor+l]; cursor += l 570 | # Last - Double - 8 571 | o.last = unpack('>d', data[cursor:cursor+8])[0]; cursor += 8 572 | 573 | # Last Trade Size Length - Short - 2 574 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 575 | # Last Trade Size - String - Variable 576 | o.lastTradeSize = data[cursor:cursor+l]; cursor += l 577 | # Last Trade Date Length - short - 2 578 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 579 | # Last Trade Date - String - Variable 580 | o.lastTradeDate = data[cursor:cursor+l]; cursor += l 581 | # Volume - Long - 8 582 | o.volume = unpack('>Q',data[cursor:cursor+8])[0]; cursor += 8 583 | 584 | # Open Interest - Integer - 4 585 | o.openInterest = unpack('>i', data[cursor:cursor+4])[0]; cursor += 4 586 | 587 | # RT Quote Flag - Byte - 1 (1=true, 0=false) 588 | o.rtQuoteFlag = unpack('b', data[cursor:cursor+1])[0]; cursor += 1 589 | o.setQuoteDateTime(quoteTime) 590 | 591 | # Underlying Symbol length - Short 2 592 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 593 | # Underlying Symbol - String - Variable 594 | o.underlyingSymbol = data[cursor:cursor+l].decode('utf-8'); cursor += l 595 | 596 | # Delta - Double- 8 597 | # Gamma - Double - 8 598 | # Theta - Double - 8 599 | # Vega - Double - 8 600 | # Rho - Double - 8 601 | # Implied Volatility - Double - 8 602 | # Time Value Index - Double - 8 603 | # Multiplier - Double - 8 604 | # Change - Double - 8 605 | # Change Percentage - Double - 8 606 | (o.delta, o.gamma, o.theta, o.vega, o.rho, o.impliedVolatility, o.tvIndex, 607 | o.multiplier, o.change, o.changePercentage) = \ 608 | unpack('>10d', data[cursor:cursor+80]); cursor += 80 609 | 610 | 611 | # ITM Flag - Byte - 1 (1 = true, 0 = false) 612 | # NTM Flag - Byte - 1 (1 = true, 0 = false) 613 | (o.itmFlag, o.ntmFlag) = unpack('2b', data[cursor:cursor+2]); cursor += 2 614 | 615 | # Theoretical value - Double - 8 616 | o.theoreticalValue = unpack('>d', data[cursor:cursor+8])[0]; cursor += 8 617 | 618 | # Deliverable Note Length - Short - 2 619 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 620 | # Deliverable Note - String - Variable 621 | o.deliverableNote = data[cursor:cursor+l]; cursor += l 622 | 623 | # CIL Dollar Amount - Double - 8 624 | # OP Cash Dollar Amount - Double - 8 625 | (o.cilDollarAmount, o.opCashDollarAmount) = \ 626 | unpack('>2d', data[cursor:cursor+16]); cursor += 16 627 | # Index Option Flag - Byte - 1 (1 = true, 0 = false) 628 | o.indexOptionFlag = unpack('b', data[cursor:cursor+1])[0]; cursor += 1 629 | # Number of Deliverables - Integer - 4 630 | numDeliverables = unpack('>i', data[cursor:cursor+4])[0]; cursor += 4 631 | 632 | for j in range(numDeliverables): 633 | # REPEATING block for each Deliverable 634 | # Deliverable Symbol Length - Short - 2 635 | l = unpack('>h', data[cursor:cursor+2])[0]; cursor += 2 636 | # Deliverable Symbol - String - Variable 637 | s = data[cursor:cursor+l]; cursor += l 638 | # Deliverable Shares - Integer - 4 639 | o.deliverables.append((s, unpack('>i', data[cursor:cursor+4])[0])); cursor += 4 640 | # END 641 | 642 | # Change all "nan" to None to make sure the oce is serializable 643 | for k in list(o.__dict__.keys()): 644 | if (type(o.__dict__[k]) == float) and math.isnan(o.__dict__[k]): 645 | logging.info('[tdapi.getBinaryOptionChain] Converting o[%s]=nan to None' % (k)) 646 | o.__dict__[k] = None 647 | 648 | return optionChain 649 | 650 | 651 | 652 | def getPriceHistory(self, ticker, intervalType='DAILY', intervalDuration='1', periodType='MONTH', 653 | period='1', startdate=None, enddate=None, extended=None): 654 | 655 | 656 | validPeriodTypes = [ 657 | 'DAY', 658 | 'MONTH', 659 | 'YEAR', 660 | 'YTD' 661 | ] 662 | 663 | validIntervalTypes = { 664 | 'DAY': ['MINUTE'], 665 | 'MONTH': ['DAILY', 'WEEKLY'], 666 | 'YEAR': ['DAILY', 'WEEKLY', 'MONTHLY'], 667 | 'YTD': ['DAILY', 'WEEKLY'] 668 | } 669 | 670 | arguments = {'source': self._sourceID, 671 | 'requestidentifiertype': 'SYMBOL', 672 | 'requestvalue': ticker, 673 | 'intervaltype': intervalType, 674 | 'intervalduration': intervalDuration, 675 | 'period': period, 676 | 'periodtype':periodType, 677 | 'startdate':startdate, 678 | 'enddate':enddate 679 | } 680 | # TODO: build params conditionally based on whether we're doing period-style request 681 | # TODO: support start and end dates 682 | validArgs = {} 683 | for k in list(arguments.keys()): 684 | if arguments[k] != None: 685 | validArgs[k] = arguments[k] 686 | params = urllib.parse.urlencode(validArgs) 687 | 688 | logging.getLogger("requests").setLevel(logging.WARNING) 689 | conn = http.client.HTTPSConnection('apis.tdameritrade.com') 690 | conn.set_debuglevel(0) 691 | 692 | conn.request('GET', '/apps/100/PriceHistory?'+params) 693 | response = conn.getresponse() 694 | if response.status != 200: 695 | #import pdb; pdb.set_trace() 696 | raise ValueError(response.reason) 697 | data = response.read() 698 | conn.close() 699 | 700 | # The first 15 bytes are the header 701 | # DATA TYPE DESCRIPTION 702 | # 00 00 00 01 4 bytes Symbol Count =1 703 | # 00 04 2 bytes Symbol Length = 4 704 | # 41 4D 54 44 4 bytes Symbol = AMTD 705 | # 00 1 byte Error code = 0 (OK) 706 | # 00 00 00 02 4 bytes Bar Count = 2 707 | 708 | cursor = 0 709 | symbolCount = unpack('>i', data[0:4])[0] 710 | if symbolCount > 1: 711 | fp = open('tdapi_debug_dump','wb') 712 | fp.write(data) 713 | fp.close() 714 | raise ValueError('Error - see tdapi_debug_dump') 715 | 716 | symbolLength = unpack('>h', data[4:6])[0] 717 | cursor = 6 718 | symbol = data[cursor:cursor+symbolLength] 719 | cursor += symbolLength 720 | error = unpack('b', data[cursor:cursor+1])[0] 721 | cursor += 1 722 | # If there is an error, there will be an error length and corresponding error text 723 | if error != 0: 724 | errorLength = unpack('>h', data[cursor:cursor+2])[0] 725 | # TODO: verify that this is correct below -- advance cursor for error length 726 | cursor += 2 727 | if errorLength > 0: 728 | errorText = data[cursor:cursor+errorLength] 729 | cursor += errorLength 730 | raise ValueError('[getPriceHistory] Error: %s' % errorText) 731 | 732 | barCount = unpack('>i', data[cursor:cursor+4])[0] 733 | cursor += 4 734 | 735 | # TODO: Add more rigorous checks on header data 736 | 737 | # Now we need to extract the bars 738 | bars = [] 739 | 740 | for i in range(barCount): 741 | # Make sure we still have enough data for a bar and a terminator (note only one terminator at the end) 742 | if cursor + 28 > len(data): 743 | raise ValueError('Trying to read %d bytes from %d total!' % (cursor+58, len(data))) 744 | C = unpack('>f', data[cursor:cursor+4])[0] 745 | cursor += 4 746 | H = unpack('>f', data[cursor:cursor+4])[0] 747 | cursor += 4 748 | L = unpack('>f', data[cursor:cursor+4])[0] 749 | cursor += 4 750 | O = unpack('>f', data[cursor:cursor+4])[0] 751 | cursor += 4 752 | V = unpack('>f', data[cursor:cursor+4])[0] * 100.0 753 | cursor += 4 754 | #T = time.gmtime(float(unpack('>Q',data[cursor:cursor+8])[0]) / 1000.0) # Returned in ms since the epoch 755 | T = datetime.datetime.utcfromtimestamp(float(unpack('>Q',data[cursor:cursor+8])[0]) / 1000.0) # Returned in ms since the epoch 756 | cursor += 8 757 | bars.append((O,H,L,C,V,T)) 758 | 759 | # Finally we should see a terminator of FF 760 | if data[cursor:cursor+2] != b'\xff\xff': 761 | fp = open('tdapi_debug_dump','wb') 762 | fp.write(data) 763 | fp.close() 764 | raise ValueError('Did not find terminator at hexdata[%d]! See tdapi_debug_dump' % cursor) 765 | 766 | df = pandas.DataFrame(data=bars, columns=['open','high','low','close','volume','timestamp']) 767 | 768 | return df 769 | --------------------------------------------------------------------------------