├── LICENSE ├── .gitignore ├── setup.py ├── README.md └── ts └── client.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Reed 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # My Stuff 132 | config/ 133 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # load the README file. 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | 9 | # this will be my Library name. 10 | name='tradestation-python-api', 11 | 12 | # Want to make sure people know who made it. 13 | author='Alex Reed', 14 | 15 | # also an email they can use to reach out. 16 | author_email='coding.sigma@gmail.com', 17 | 18 | # I'm in alpha development still, so a compliant version number is a1. 19 | version='0.0.1', 20 | 21 | # here is a simple description of the library, this will appear when someone searches for the library on https://pypi.org/search 22 | description='A trading robot that utilizes the Tradestation API.', 23 | 24 | # I have a long description but that will just be my README file, note the variable up above where I read the file. 25 | long_description=long_description, 26 | 27 | # want to make sure that I specify the long description as MARKDOWN. 28 | long_description_content_type="text/markdown", 29 | 30 | # here is the URL you can find the code, this is just the GitHub URL. 31 | url='https://github.com/areed1192/tradestation-python-api', 32 | 33 | # there are some dependencies to use the library, so let's list them out. 34 | install_requires=[ 35 | 'requests', 36 | 'python-dateutil' 37 | ], 38 | 39 | # some keywords for my library. 40 | keywords='finance, tradestation, api, trading', 41 | 42 | # here are the packages I want "build." 43 | packages=find_packages(include=['tradestation'], exclude=['config.py']), 44 | 45 | # I also have some package data, like photos and JSON files, so I want to include those too. 46 | include_package_data=True, 47 | 48 | # additional classifiers that give some characteristics about the package. 49 | classifiers=[ 50 | 51 | # I want people to know it's still early stages. 52 | 'Development Status :: 3 - Alpha', 53 | 54 | # My Intended audience is mostly those who understand finance. 55 | 'Intended Audience :: Financial and Insurance Industry', 56 | 57 | # My License is MIT. 58 | 'License :: OSI Approved :: MIT License', 59 | 60 | # I wrote the client in English 61 | 'Natural Language :: English', 62 | 63 | # The client should work on all OS. 64 | 'Operating System :: OS Independent', 65 | 66 | # The client is intendend for PYTHON 3 67 | 'Programming Language :: Python :: 3' 68 | ], 69 | 70 | # you will need python 3.7 to use this libary. 71 | python_requires='>=3.8' 72 | 73 | ) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tradestation Python API 2 | 3 | A Python Client library for the TradeStation API. 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [What's in the API](#whats-in-the-api) 9 | - [Requirements](#requirements) 10 | - [API Key & Credentials](#api-key-and-credentials) 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Features](#features) 14 | - [Documentation & Resources](#documentation-and-resources) 15 | - [Support These Projects](#support-these-projects) 16 | 17 | ## Overview 18 | 19 | The unofficial Python API client library for TradeStation allows individuals with TradeStation accounts to manage trades, pull historical and real-time data, manage their accounts, create and modify orders all using the Python programming language. 20 | 21 | To learn more about the TradeStation API, please refer to the [official documentation](https://developer.tdameritrade.com/apis). 22 | 23 | ## What's in the API 24 | 25 | - Authentication - access tokens, refresh tokens, request authentication. 26 | - Accounts & Trading 27 | - Symbols 28 | - Index 29 | - Orders 30 | - Paper Trading 31 | - Quotes 32 | - Transaction History 33 | 34 | ## Requirements 35 | 36 | The following requirements must be met to use this API: 37 | 38 | - A TradeStation account, you'll need your account password and account number to use the API. 39 | - A TradeStation Developer API Key 40 | - A Redirect URI, sometimes called Redirect URL 41 | - Python 3.8 or later. 42 | 43 | ## API Key and Credentials 44 | 45 | Each TradeStation API request requires a TradeStation Developer API Key, a consumer ID, an account password, an account number, and a redirect URI. API Keys, consumer IDs, and redirect URIs are generated from the TradeStation developer portal. To set up and create your TradeStation developer account, please refer to the [official documentation](https://developer.tdameritrade.com/content/phase-1-authentication-update-xml-based-api). 46 | 47 | Additionally, to authenticate yourself using this library, you will need to provide your account number and password for your main TradeStation account. 48 | 49 | **Important:** Your account number, an account password, consumer ID, and API key should be kept secret. 50 | 51 | ## Installation 52 | 53 | ```bash 54 | pip install -e . 55 | ``` 56 | 57 | ## Usage 58 | 59 | This example demonstrates how to login to the API and demonstrates sending a request using the `quotes`, and `stream_bars_start_date` endpoint, using your API key. 60 | 61 | ```python 62 | # Import the client 63 | from ts.client import TradeStationClient 64 | 65 | # Create the Client. 66 | ts_client = TradeStationClient( 67 | username="USERNAME", 68 | client_id="CLIENT_ID", 69 | client_secret="CLIENT_SECRET", 70 | redirect_uri="REDIRECT_URI", 71 | paper_trading="PAPER_TRADING" 72 | ) 73 | 74 | # Get quotes for Oil Futures. 75 | ts_client.quotes(symbols=['@CL']) 76 | 77 | # Stream quotes for Amazon. 78 | ts_client.stream_quotes_changes(symbols=['AMZN']) 79 | 80 | # Stream bars for a certain date. 81 | ts_client.stream_bars_start_date( 82 | symbol='AMZN', 83 | interval=5, 84 | unit='Minute', 85 | start_date='02-25-2020', 86 | session='USEQPreAndPost' 87 | ) 88 | ``` 89 | 90 | ## Features 91 | 92 | ### Authentication Workflow Support 93 | 94 | Automatically will handle the authentication workflow for new users, returning users, and users with expired tokens (refresh token or access token). 95 | 96 | ### Request Validation 97 | 98 | For certain requests, in a limited fashion, it will help validate your request when possible. For example, when using the `get_bars` endpoint, it will automatically validate that the market you're requesting data from is one of the valid options. 99 | 100 | ## Documentation and Resources 101 | 102 | - [Overview](https://tradestation.github.io/api-docs/#section/Overview) 103 | - [Paper Trading](https://tradestation.github.io/api-docs/#section/Overview/SIM-vs-LIVE) 104 | - [Authentication](https://tradestation.github.io/api-docs/#section/Authentication) 105 | 106 | ## Support these Projects 107 | 108 | **Patreon:** 109 | Help support this project and future projects by donating to my [Patreon Page](https://www.patreon.com/sigmacoding). I'm always looking to add more content for individuals like yourself, unfortuantely some of the APIs I would require me to pay monthly fees. 110 | 111 | **YouTube:** 112 | If you'd like to watch more of my content, feel free to visit my YouTube channel [Sigma Coding](https://www.youtube.com/c/SigmaCoding). 113 | 114 | **Hire Me:** 115 | If you have a project, you think I can help you with feel free to reach out at coding.sigma@gmail.com 116 | -------------------------------------------------------------------------------- /ts/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import json 4 | 5 | import requests 6 | import urllib.parse 7 | 8 | from typing import List 9 | from typing import Dict 10 | from typing import Union 11 | from typing import Optional 12 | 13 | from datetime import date 14 | from datetime import datetime 15 | 16 | from dateutil.parser import parse 17 | 18 | 19 | class TradeStationClient(): 20 | 21 | """ 22 | Tradestation API Client Class. 23 | 24 | Implements OAuth 2.0 Authorization Code Grant workflow, handles configuration 25 | and state management, adds token for authenticated calls, and performs request 26 | to the TD Ameritrade API. 27 | """ 28 | 29 | def __init__(self, username: str, client_id: str, client_secret: str, redirect_uri: str, paper_trading: bool = True) -> None: 30 | """Initalizes the Tradestation Client object. 31 | 32 | Arguments: 33 | ---- 34 | username (str): The username of the account. 35 | 36 | client_id (str): The Client ID assigned to you during the App registration. This can 37 | be found at the app registration portal. 38 | 39 | client_secret (str): The Client Secret assigned to you during the App registration. This can 40 | be found at the app registration portal. 41 | 42 | redirect_uri (str): This is the redirect URL that you specified when you created your 43 | Tradestation Application. 44 | 45 | paper_trading (bool, optional): Specifies whether you want to use the simulation account or not. 46 | Defaults to True. 47 | 48 | Usage: 49 | ---- 50 | >>> tradestation_client = TradeStationClient( 51 | username=username, 52 | client_id=client_id, 53 | client_secret=client_secret, 54 | redirect_uri=redirect_uri, 55 | paper_trading=paper_trading 56 | ) 57 | >>> tradestation_client 58 | """ 59 | 60 | # define the configuration settings. 61 | self.config = { 62 | 'client_id': client_id, 63 | 'client_secret': client_secret, 64 | 'username': username, 65 | 'redirect_uri': redirect_uri, 66 | 'resource': 'https://api.tradestation.com', 67 | 'paper_resource': 'https://sim-api.tradestation.com', 68 | 'api_version': 'v2', 69 | 'paper_api_version': 'v2', 70 | 'auth_endpoint': 'https://api.tradestation.com/v2/Security/Authorize', 71 | 'cache_state': True, 72 | 'refresh_enabled': True, 73 | 'paper_trading': paper_trading 74 | } 75 | 76 | # initalize the client to either use paper trading account or regular account. 77 | if self.config['paper_trading']: 78 | self.paper_trading_mode = True 79 | else: 80 | self.paper_trading_mode = False 81 | 82 | # call the _state_manager method and update the state to init (initalized) 83 | self._state_manager('init') 84 | 85 | # define a new attribute called 'authstate' and initalize it to '' (Blank). This will be used by our login function. 86 | self.authstate = False 87 | 88 | def __repr__(self) -> str: 89 | """Defines the string representation of our TD Ameritrade Class instance. 90 | 91 | Returns: 92 | ---- 93 | (str): A string representation of the client. 94 | """ 95 | 96 | # Define the string representation. 97 | str_representation = ''.format( 98 | log_in=self.state['logged_in'], 99 | auth_state=self.authstate 100 | ) 101 | 102 | return str_representation 103 | 104 | def headers(self, mode: str = None) -> Dict: 105 | """Sets the headers for the request. 106 | 107 | Overview: 108 | ---- 109 | Returns a dictionary of default HTTP headers for calls to TradeStation API, 110 | in the headers we defined the Authorization and access token. 111 | 112 | Arguments: 113 | ---- 114 | mode (str): Defines the content-type for the headers dictionary. 115 | 116 | Returns: 117 | ---- 118 | (dict): The headers dictionary to be used in the request. 119 | """ 120 | 121 | # Grab the Access Token. 122 | token = self.state['access_token'] 123 | 124 | # Create the headers dictionary 125 | headers = { 126 | 'Authorization': 'Bearer {access_token}'.format(access_token=token) 127 | } 128 | 129 | # Set the Mode. 130 | if mode == 'application/json': 131 | headers['Content-type'] = 'application/json' 132 | elif mode == 'chunked': 133 | headers['Transfer-Encoding'] = 'Chunked' 134 | 135 | return headers 136 | 137 | def _api_endpoint(self, url: str) -> str: 138 | """Creates an API URL. 139 | 140 | Overview: 141 | ---- 142 | Convert relative endpoint (e.g., 'quotes') to full API endpoint. 143 | 144 | Arguments: 145 | ---- 146 | url (str): The URL that needs conversion to a full endpoint URL. 147 | 148 | Returns: 149 | --- 150 | (str): A full URL. 151 | """ 152 | 153 | # paper trading uses a different base url compared to regular trading. 154 | if self.paper_trading_mode: 155 | full_url = '/'.join([self.config['paper_resource'], 156 | self.config['paper_api_version'], url]) 157 | else: 158 | full_url = '/'.join([self.config['resource'], 159 | self.config['api_version'], url]) 160 | 161 | return full_url 162 | 163 | def _state_manager(self, action: str) -> None: 164 | """Handles the state. 165 | 166 | Overview: 167 | ---- 168 | Manages the self.state dictionary. Initalize State will set 169 | the properties to their default value. Save will save the 170 | current state if 'cache_state' is set to TRUE. 171 | 172 | Arguments: 173 | ---- 174 | name (str): action argument must of one of the following: 175 | 'init' -- Initalize State. 176 | 'save' -- Save the current state. 177 | """ 178 | 179 | # Define the initalized state, these are the default values. 180 | initialized_state = { 181 | 'access_token': None, 182 | 'refresh_token': None, 183 | 'access_token_expires_at': 0, 184 | 'access_token_expires_in': 0, 185 | 'logged_in': False 186 | } 187 | 188 | # Grab the current directory of the client file, that way we can store the JSON file in the same folder. 189 | dir_path = os.path.dirname(os.path.realpath(__file__)) 190 | filename = 'ts_state.json' 191 | file_path = os.path.join(dir_path, filename) 192 | 193 | # If the state is initalized. 194 | if action == 'init': 195 | 196 | # Initalize the state. 197 | self.state = initialized_state 198 | 199 | # If they allowed for caching and the file exist, load the file. 200 | if self.config['cache_state'] and os.path.isfile(file_path): 201 | with open(file=file_path, mode='r') as state_file: 202 | self.state.update(json.load(fp=state_file)) 203 | 204 | # If they didnt allow for caching delete the file. 205 | elif not self.config['cache_state'] and os.path.isfile(file_path): 206 | os.remove(file_path) 207 | 208 | # if they want to save it and have allowed for caching then load the file. 209 | elif action == 'save' and self.config['cache_state']: 210 | with open(file=file_path, mode='w+') as state_file: 211 | json.dump(obj=self.state, fp=state_file, indent=4) 212 | 213 | def login(self) -> bool: 214 | """Logs the user into a new session. 215 | 216 | Overview: 217 | --- 218 | Ask the user to authenticate themselves via the TD Ameritrade Authentication Portal. This will 219 | create a URL, display it for the User to go to and request that they paste the final URL into 220 | command window. 221 | 222 | Once the user is authenticated the API key is valide for 90 days, so refresh tokens may be used 223 | from this point, up to the 90 days. 224 | 225 | Returns: 226 | ---- 227 | (bool): `True` if the session was logged in, `False` otherwise. 228 | """ 229 | 230 | # if caching is enabled then attempt silent authentication. 231 | if self.config['cache_state']: 232 | 233 | # if it was successful, the user is authenticated. 234 | if self._silent_sso(): 235 | 236 | # update the authentication state 237 | self.authstate = True 238 | return True 239 | 240 | # Go through the authorization process. 241 | self._authorize() 242 | 243 | # Grab the access token. 244 | self._grab_access_token() 245 | 246 | # update the authentication state 247 | self.authstate = True 248 | 249 | return True 250 | 251 | def logout(self) -> None: 252 | """Clears the current TradeStation Connection state.""" 253 | 254 | # change state to initalized so they will have to either get a 255 | # new access token or refresh token next time they use the API 256 | self._state_manager('init') 257 | 258 | def _grab_access_token(self) -> bool: 259 | """Grabs an access token. 260 | 261 | Overview: 262 | ---- 263 | Access token handler for AuthCode Workflow. This takes the 264 | authorization code parsed from the auth endpoint to call the 265 | token endpoint and obtain an access token. 266 | 267 | Returns: 268 | ---- 269 | (bool): `True` if grabbing the access token was successful. `False` otherwise. 270 | """ 271 | 272 | # Parse the URL 273 | url_dict = urllib.parse.parse_qs(self.state['redirect_code']) 274 | 275 | # Convert the values to a list. 276 | url_values = list(url_dict.values()) 277 | 278 | # Grab the Code, which is stored in a list. 279 | url_code = url_values[0][0] 280 | 281 | # define the parameters of our access token post. 282 | data = { 283 | 'grant_type': 'authorization_code', 284 | 'client_id': self.config['client_id'], 285 | 'client_secret': self.config['client_secret'], 286 | 'code': url_code, 287 | 'redirect_uri': self.config['redirect_uri'] 288 | } 289 | 290 | # Post the data to the token endpoint and store the response. 291 | token_response = requests.post( 292 | url=self.config['auth_endpoint'], 293 | data=data, 294 | verify=True 295 | ) 296 | 297 | # Call the `_token_save` method to save the access token. 298 | if token_response.ok: 299 | self._token_save(response=token_response) 300 | return True 301 | else: 302 | return False 303 | 304 | def _silent_sso(self) -> bool: 305 | """Handles the silent authentication workflow. 306 | 307 | Overview: 308 | ---- 309 | Attempt a silent authentication, by checking whether current access token 310 | is valid and/or attempting to refresh it. Returns True if we have successfully 311 | stored a valid access token. 312 | 313 | Returns: 314 | ---- 315 | (bool): `True` if grabbing the silent authentication was successful. `False` otherwise. 316 | """ 317 | 318 | # if it's not expired we don't care. 319 | if self._token_validation(): 320 | return True 321 | 322 | # if the current access token is expired then try and refresh access token. 323 | elif self.state['refresh_token'] and self._grab_refresh_token(): 324 | return True 325 | 326 | # More than likely a first time login, so can't do silent authenticaiton. 327 | else: 328 | return False 329 | 330 | def _grab_refresh_token(self) -> bool: 331 | """Refreshes the current access token if it's expired. 332 | 333 | Returns: 334 | ---- 335 | (bool): `True` if grabbing the refresh token was successful. `False` otherwise. 336 | """ 337 | 338 | # Build the parameters of our request. 339 | data = { 340 | 'client_id': self.config['client_id'], 341 | 'client_secret': self.config['client_secret'], 342 | 'grant_type': 'refresh_token', 343 | 'response_type': 'token', 344 | 'refresh_token': self.state['refresh_token'] 345 | } 346 | 347 | # Make a post request to the token endpoint. 348 | response = requests.post( 349 | url=self.config['auth_endpoint'], 350 | data=data, 351 | verify=True 352 | ) 353 | 354 | # Save the token if the response was okay. 355 | if response.ok: 356 | self._token_save(response=response) 357 | return True 358 | else: 359 | return False 360 | 361 | def _token_save(self, response: requests.Response): 362 | """Saves an access token or refresh token. 363 | 364 | Overview: 365 | ---- 366 | Parses an access token from the response of a POST request and saves it 367 | in the state dictionary for future use. Additionally, it will store the 368 | expiration time and the refresh token. 369 | 370 | Arguments: 371 | ---- 372 | response (requests.Response): A response object recieved from the `token_refresh` or `_grab_access_token` 373 | methods. 374 | 375 | Returns: 376 | ---- 377 | (bool): `True` if saving the token was successful. `False` otherwise. 378 | """ 379 | 380 | # Parse the data. 381 | json_data = response.json() 382 | 383 | # Save the access token. 384 | if 'access_token' in json_data: 385 | self.state['access_token'] = json_data['access_token'] 386 | else: 387 | self.logout() 388 | return False 389 | 390 | # If there is a refresh token then grab it. 391 | if 'refresh_token' in json_data: 392 | self.state['refresh_token'] = json_data['refresh_token'] 393 | 394 | # Set the login state. 395 | self.state['logged_in'] = True 396 | 397 | # Store token expiration time. 398 | self.state['access_token_expires_in'] = json_data['expires_in'] 399 | self.state['access_token_expires_at'] = time.time() + \ 400 | int(json_data['expires_in']) 401 | 402 | self._state_manager('save') 403 | 404 | return True 405 | 406 | def _token_seconds(self) -> int: 407 | """Calculates when the token will expire. 408 | 409 | Overview: 410 | ---- 411 | Return the number of seconds until the current access token or refresh token 412 | will expire. The default value is access token because this is the most commonly used 413 | token during requests. 414 | 415 | Returns: 416 | ---- 417 | (int): The number of seconds till expiration 418 | """ 419 | 420 | # Calculate the token expire time. 421 | token_exp = time.time() >= self.state['access_token_expires_at'] 422 | 423 | # if the time to expiration is less than or equal to 0, return 0. 424 | if not self.state['refresh_token'] or token_exp: 425 | token_exp = 0 426 | else: 427 | token_exp = int(token_exp) 428 | 429 | return token_exp 430 | 431 | def _token_validation(self, nseconds: int = 5) -> None: 432 | """Validates the Access Token. 433 | 434 | Overview: 435 | ---- 436 | Verify the current access token is valid for at least N seconds, and 437 | if not then attempt to refresh it. Can be used to assure a valid token 438 | before making a call to the Tradestation API. 439 | 440 | Arguments: 441 | ---- 442 | nseconds (int): The minimum number of seconds the token has to be valid for before 443 | attempting to get a refresh token. 444 | """ 445 | 446 | if self._token_seconds() < nseconds and self.config['refresh_enabled']: 447 | self._grab_refresh_token() 448 | 449 | def _authorize(self) -> None: 450 | """Authorizes the session. 451 | 452 | Overview: 453 | ---- 454 | Initalizes the oAuth Workflow by creating the URL that 455 | allows the user to login to the Tradestation API using their credentials 456 | and then will parse the URL that they paste back into the terminal. 457 | """ 458 | 459 | # prepare the payload to login 460 | data = { 461 | 'response_type': 'code', 462 | 'redirect_uri': self.config['redirect_uri'], 463 | 'client_id': self.config['client_id'] 464 | } 465 | 466 | # url encode the data. 467 | params = urllib.parse.urlencode(data) 468 | 469 | # build the full URL for the authentication endpoint. 470 | url = 'https://api.tradestation.com/v2/authorize?' + params 471 | 472 | # aks the user to go to the URL provided, they will be prompted to authenticate themsevles. 473 | print('') 474 | print('='*80) 475 | print('') 476 | print('Please go to URL provided authorize your account: {}'.format(url)) 477 | print('') 478 | print('-'*80) 479 | 480 | # ask the user to take the final URL after authentication and paste here so we can parse. 481 | my_response = input('Paste the full URL redirect here: ') 482 | 483 | # store the redirect URL 484 | self.state['redirect_code'] = my_response 485 | 486 | def _handle_requests(self, url: str, method: str, headers: dict = {}, args: dict = None, stream: bool = False, payload: dict = None) -> dict: 487 | """[summary] 488 | 489 | Arguments: 490 | ---- 491 | url (str): [description] 492 | 493 | method (str): [description] 494 | 495 | headers (dict): [description] 496 | 497 | args (dict, optional): [description]. Defaults to None. 498 | 499 | stream (bool, optional): [description]. Defaults to False. 500 | 501 | payload (dict, optional): [description]. Defaults to None. 502 | 503 | Raises: 504 | ---- 505 | ValueError: [description] 506 | 507 | Returns: 508 | ---- 509 | dict: [description] 510 | """ 511 | 512 | streamed_content = [] 513 | if method == 'get': 514 | 515 | # handles the non-streaming GET requests. 516 | if stream == False: 517 | response = requests.get( 518 | url=url, headers=headers, params=args, verify=True) 519 | 520 | # handles the Streaming request. 521 | else: 522 | response = requests.get( 523 | url=url, headers=headers, params=args, verify=True, stream=True) 524 | for line in response.iter_lines(chunk_size=300): 525 | 526 | if 'END' not in line.decode() and line.decode() != '': 527 | try: 528 | streamed_content.append(json.loads(line)) 529 | except: 530 | print(line) 531 | 532 | elif method == 'post': 533 | 534 | if payload is None: 535 | response = requests.post( 536 | url=url, headers=headers, params=args, verify=True) 537 | else: 538 | response = requests.post( 539 | url=url, headers=headers, params=args, verify=True, json=payload) 540 | 541 | elif method == 'put': 542 | 543 | if payload is None: 544 | response = requests.put( 545 | url=url, headers=headers, params=args, verify=True) 546 | else: 547 | response = requests.put( 548 | url=url, headers=headers, params=args, verify=True, json=payload) 549 | 550 | elif method == 'delete': 551 | 552 | response = requests.delete( 553 | url=url, headers=headers, params=args, verify=True) 554 | 555 | else: 556 | raise ValueError( 557 | 'The type of request you are making is incorrect.') 558 | 559 | # grab the status code 560 | status_code = response.status_code 561 | 562 | # grab the response. headers. 563 | response_headers = response.headers 564 | 565 | if status_code == 200: 566 | 567 | if response_headers['Content-Type'] == 'application/json; charset=utf-8': 568 | return response.json() 569 | elif response_headers['Transfer-Encoding'] == 'chunked': 570 | 571 | return streamed_content 572 | 573 | else: 574 | # Error 575 | print('') 576 | print('-'*80) 577 | print("BAD REQUEST - STATUS CODE: {}".format(status_code)) 578 | print("RESPONSE URL: {}".format(response.url)) 579 | print("RESPONSE HEADERS: {}".format(response.headers)) 580 | print("RESPONSE TEXT: {}".format(response.text)) 581 | print('-'*80) 582 | print('') 583 | 584 | 585 | def user_accounts(self, user_id: str) -> dict: 586 | """Grabs all the accounts associated with the User. 587 | 588 | Arguments: 589 | ---- 590 | user_id (str): The Username of the account holder. 591 | 592 | Returns: 593 | ---- 594 | (dict): All the user accounts. 595 | """ 596 | 597 | # validate the token. 598 | self._token_validation() 599 | 600 | # define the endpoint. 601 | url_endpoint = self._api_endpoint( 602 | url='users/{username}/accounts'.format(username=user_id) 603 | ) 604 | 605 | # define the arguments 606 | params = { 607 | 'access_token': self.state['access_token'] 608 | } 609 | 610 | # grab the response. 611 | response = self._handle_requests( 612 | url=url_endpoint, 613 | method='get', 614 | args=params 615 | ) 616 | 617 | return response 618 | 619 | def account_balances(self, account_keys: List[str]) -> dict: 620 | """Grabs all the balances for each account provided. 621 | 622 | Args: 623 | ---- 624 | account_keys (List[str]): A list of account numbers. Can only be a max 625 | of 25 account numbers 626 | 627 | Raises: 628 | ---- 629 | ValueError: If the list is more than 25 account numbers will raise an error. 630 | 631 | Returns: 632 | ---- 633 | dict: A list of account balances for each of the accounts. 634 | """ 635 | 636 | if isinstance(account_keys, list): 637 | 638 | # validate the token. 639 | self._token_validation() 640 | 641 | # argument validation. 642 | if len(account_keys) == 0: 643 | raise ValueError( 644 | "You cannot pass through an empty list for account keys.") 645 | elif len(account_keys) > 0 and len(account_keys) <= 25: 646 | account_keys = ','.join(account_keys) 647 | elif len(account_keys) > 25: 648 | raise ValueError( 649 | "You cannot pass through more than 25 account keys.") 650 | 651 | # define the endpoint. 652 | url_endpoint = self._api_endpoint( 653 | url='accounts/{account_numbers}/balances'.format( 654 | account_numbers=account_keys) 655 | ) 656 | 657 | # define the arguments 658 | params = { 659 | 'access_token': self.state['access_token'] 660 | } 661 | 662 | # grab the response. 663 | response = self._handle_requests( 664 | url=url_endpoint, 665 | method='get', 666 | args=params 667 | ) 668 | 669 | return response 670 | 671 | else: 672 | raise ValueError("Account Keys, must be a list object") 673 | 674 | def account_positions(self, account_keys: List[str], symbols: List[str]) -> dict: 675 | """Grabs all the account positions. 676 | 677 | Arguments: 678 | ---- 679 | account_keys (List[str]): A list of account numbers.. 680 | 681 | symbols (List[str]): A list of ticker symbols, you want to return. 682 | 683 | Raises: 684 | ---- 685 | ValueError: If the list is more than 25 account numbers will raise an error. 686 | 687 | Returns: 688 | ---- 689 | dict: A list of account balances for each of the accounts. 690 | """ 691 | 692 | if isinstance(account_keys, list): 693 | 694 | # validate the token. 695 | self._token_validation() 696 | 697 | # argument validation, account keys. 698 | if len(account_keys) == 0: 699 | raise ValueError( 700 | "You cannot pass through an empty list for account keys.") 701 | elif len(account_keys) > 0 and len(account_keys) <= 25: 702 | account_keys = ','.join(account_keys) 703 | elif len(account_keys) > 25: 704 | raise ValueError( 705 | "You cannot pass through more than 25 account keys.") 706 | 707 | # argument validation, symbols. 708 | if symbols is not None: 709 | 710 | if len(symbols) == 0: 711 | raise ValueError( 712 | "You cannot pass through an empty symbols list for the filter.") 713 | else: 714 | 715 | symbols_formatted = [] 716 | for symbol in symbols: 717 | symbols_formatted.append( 718 | "Symbol eq '{}'".format(symbol) 719 | ) 720 | 721 | symbols = 'or '.join(symbols_formatted) 722 | params = { 723 | 'access_token': self.state['access_token'], 724 | '$filter': symbols 725 | } 726 | 727 | else: 728 | params = { 729 | 'access_token': self.state['access_token'] 730 | } 731 | 732 | # define the endpoint. 733 | url_endpoint = self._api_endpoint( 734 | url='accounts/{account_numbers}/positions'.format( 735 | account_numbers=account_keys 736 | ) 737 | ) 738 | 739 | # grab the response. 740 | response = self._handle_requests( 741 | url=url_endpoint, 742 | method='get', 743 | args=params 744 | ) 745 | 746 | return response 747 | 748 | else: 749 | raise ValueError("Account Keys, must be a list object") 750 | 751 | def account_orders(self, account_keys: List[str], since: int, page_size: int, page_number: int = 0) -> dict: 752 | """Grab all the account orders for a list of accounts. 753 | 754 | Overview: 755 | ---- 756 | This endpoint is used to grab all the order from a list of accounts provided. Additionally, 757 | each account will only go back 14 days when searching for orders. 758 | 759 | Arguments: 760 | ---- 761 | account_keys (List[str]): A list of account numbers. 762 | 763 | since (int): Number of days to look back, max is 14 days. 764 | 765 | page_size (int): The page size. 766 | 767 | page_number (int, optional): The page number to return if more than one. Defaults to 0. 768 | 769 | Raises: 770 | ---- 771 | ValueError: If the list is more than 25 account numbers will raise an error. 772 | 773 | Returns: 774 | ---- 775 | dict: A list of account balances for each of the accounts. 776 | """ 777 | 778 | if isinstance(account_keys, list): 779 | 780 | # validate the token. 781 | self._token_validation() 782 | 783 | # argument validation, account keys. 784 | if len(account_keys) == 0: 785 | raise ValueError( 786 | "You cannot pass through an empty list for account keys.") 787 | elif len(account_keys) > 0 and len(account_keys) <= 25: 788 | account_keys = ','.join(account_keys) 789 | elif len(account_keys) > 25: 790 | raise ValueError( 791 | "You cannot pass through more than 25 account keys.") 792 | 793 | # argument validation, SINCE 794 | if since: 795 | if since > 14: 796 | raise ValueError( 797 | "You can't get orders older than 14 days old.") 798 | elif since <= 0: 799 | raise ValueError( 800 | "You can't specify since as a 0 or a negative number.") 801 | 802 | today = date.today() 803 | today = date(year=today.year, month=today.month, day=since) 804 | date_format = today.strftime("%m/%d/%Y") 805 | 806 | else: 807 | date_format = None 808 | 809 | params = { 810 | 'access_token': self.state['access_token'], 811 | 'since': date_format, 812 | 'pageSize': page_size, 813 | 'pageNum': page_number 814 | } 815 | 816 | # define the endpoint. 817 | url_endpoint = self._api_endpoint( 818 | url='accounts/{account_numbers}/orders'.format( 819 | account_numbers=account_keys) 820 | ) 821 | 822 | # grab the response. 823 | response = self._handle_requests( 824 | url=url_endpoint, 825 | method='get', 826 | args=params 827 | ) 828 | 829 | return response 830 | 831 | else: 832 | raise ValueError("Account Keys, must be a list object") 833 | 834 | def symbol_info(self, symbol: str) -> dict: 835 | """Grabs the info for a particular symbol 836 | 837 | Arguments: 838 | ---- 839 | symbol (str): A ticker symbol. 840 | 841 | Raises: 842 | ---- 843 | ValueError: If no symbol is provided will raise an error. 844 | 845 | Returns: 846 | ---- 847 | dict: A dictionary containing the symbol info. 848 | """ 849 | 850 | # validate the token. 851 | self._token_validation() 852 | 853 | if symbol is None: 854 | raise ValueError("You must pass through a symbol.") 855 | 856 | # define the endpoint. 857 | url_endpoint = self._api_endpoint( 858 | url='data/symbol/{ticker_symbol}'.format(ticker_symbol=symbol) 859 | ) 860 | 861 | # define the arguments. 862 | params = { 863 | 'access_token': self.state['access_token'] 864 | } 865 | 866 | # grab the response. 867 | response = self._handle_requests( 868 | url=url_endpoint, 869 | method='get', 870 | args=params 871 | ) 872 | 873 | return response 874 | 875 | def quotes(self, symbols: List[str]) -> dict: 876 | """Grabs the quotes for a list of symbols. 877 | 878 | Arguments: 879 | ---- 880 | symbol (List[str]): A list of ticker symbols. 881 | 882 | Raises: 883 | ---- 884 | ValueError: If no symbol is provided will raise an error. 885 | 886 | Returns: 887 | ---- 888 | (dict): A dictionary containing the symbol quotes. 889 | """ 890 | 891 | # validate the token. 892 | self._token_validation() 893 | 894 | if symbols is None: 895 | raise ValueError("You must pass through at least one symbol.") 896 | 897 | symbols = ','.join(symbols) 898 | 899 | # define the endpoint. 900 | url_endpoint = self._api_endpoint( 901 | url='data/quote/{symbols}'.format(symbols=symbols)) 902 | 903 | # define the arguments. 904 | params = { 905 | 'access_token': self.state['access_token'] 906 | } 907 | 908 | # grab the response. 909 | response = self._handle_requests( 910 | url=url_endpoint, 911 | method='get', 912 | args=params 913 | ) 914 | 915 | return response 916 | 917 | def stream_quotes_changes(self, symbols=None): 918 | """Streams quote changes for a list of symbols. 919 | 920 | Arguments: 921 | ---- 922 | symbol (List[str]): A list of ticker symbols. 923 | 924 | Raises: 925 | ---- 926 | ValueError: If no symbol is provided will raise an error. 927 | 928 | Returns: 929 | ---- 930 | (dict): A dictionary containing the symbol quotes. 931 | """ 932 | 933 | # validate the token. 934 | self._token_validation() 935 | 936 | if symbols is None: 937 | raise ValueError("You must pass through at least one symbol.") 938 | 939 | symbols = ','.join(symbols) 940 | 941 | # define the endpoint. 942 | url_endpoint = self._api_endpoint( 943 | url='stream/quote/changes/{symbols}'.format(symbols=symbols)) 944 | 945 | # define the headers 946 | headers = { 947 | 'Accept': 'application/vnd.tradestation.streams+json' 948 | } 949 | 950 | # define the arguments. 951 | params = { 952 | 'access_token': self.state['access_token'] 953 | } 954 | 955 | # grab the response. 956 | response = self._handle_requests( 957 | url=url_endpoint, 958 | method='get', 959 | headers=headers, 960 | args=params, 961 | stream=True 962 | ) 963 | 964 | return response 965 | 966 | def stream_bars_start_date(self, symbol: str, interval: int, unit: str, start_date: str, session: str) -> dict: 967 | """Stream bars for a certain data range. 968 | 969 | Arguments: 970 | ---- 971 | symbol (str): A ticker symbol to stream bars. 972 | 973 | interval (int): The size of the bar. 974 | 975 | unit (str): The frequency of the bar. 976 | 977 | start_date (str): The start point of the streaming. 978 | 979 | session (str): Defines whether you want bars from post, pre, or current market. 980 | 981 | Raises: 982 | ---- 983 | ValueError: 984 | 985 | Returns: 986 | ---- 987 | (dict): A dictionary of quotes. 988 | """ 989 | 990 | # ['USEQPre','USEQPost','USEQPreAndPost','Default'] 991 | 992 | # validate the token. 993 | self._token_validation() 994 | 995 | if symbol is None: 996 | raise ValueError("You must pass through one symbol.") 997 | 998 | if unit not in ["Minute", "Daily", "Weekly", "Monthly"]: 999 | raise ValueError( 1000 | 'The value you passed through for `unit` is incorrect, it must be one of the following: ["Minute", "Daily", "Weekly", "Monthly"]') 1001 | 1002 | if interval != 1 and unit in ["Daily", "Weekly", "Monthly"]: 1003 | raise ValueError( 1004 | "The interval must be one for daily, weekly or monthly.") 1005 | elif interval > 1440: 1006 | raise ValueError("Interval must be less than or equal to 1440") 1007 | 1008 | # define the endpoint. 1009 | url_endpoint = self._api_endpoint( 1010 | url='stream/barchart/{symbol}/{interval}/{unit}/{start_date}'.format( 1011 | symbol=symbol, 1012 | interval=interval, 1013 | unit=unit, 1014 | start_date=start_date 1015 | ) 1016 | ) 1017 | 1018 | # define the arguments. 1019 | params = { 1020 | 'access_token': self.state['access_token'], 1021 | 'sessionTemplate': session 1022 | } 1023 | 1024 | # grab the response. 1025 | response = self._handle_requests( 1026 | url=url_endpoint, 1027 | method='get', 1028 | args=params, 1029 | stream=True 1030 | ) 1031 | 1032 | return response 1033 | 1034 | def stream_bars_date_range(self, symbol: str, interval: int, unit: str, start_date: str, end_date: str, session: str) -> dict: 1035 | """Stream bars for a certain data range. 1036 | 1037 | Arguments: 1038 | ---- 1039 | symbol (str): A ticker symbol to stream bars. 1040 | 1041 | interval (int): The size of the bar. 1042 | 1043 | unit (str): The frequency of the bar. 1044 | 1045 | start_date (str): The start point of the streaming. 1046 | 1047 | end_date (str): The end point of the streaming. 1048 | 1049 | session (str): Defines whether you want bars from post, pre, or current market. 1050 | 1051 | Raises: 1052 | ---- 1053 | ValueError: 1054 | 1055 | Returns: 1056 | ---- 1057 | (dict): A dictionary of quotes. 1058 | """ 1059 | 1060 | # validate the token. 1061 | self._token_validation() 1062 | 1063 | # validate the symbol 1064 | if symbol is None: 1065 | raise ValueError("You must pass through one symbol.") 1066 | 1067 | # validate the unit 1068 | if unit not in ["Minute", "Daily", "Weekly", "Monthly"]: 1069 | raise ValueError( 1070 | 'The value you passed through for `unit` is incorrect, it must be one of the following: ["Minute", "Daily", "Weekly", "Monthly"]') 1071 | 1072 | # validate the interval. 1073 | if interval != 1 and unit in ["Daily", "Weekly", "Monthly"]: 1074 | raise ValueError( 1075 | "The interval must be one for daily, weekly or monthly.") 1076 | elif interval > 1440: 1077 | raise ValueError("Interval must be less than or equal to 1440") 1078 | 1079 | # validate the session. 1080 | if session is not None and session not in ['USEQPre', 'USEQPost', 'USEQPreAndPost', 'Default']: 1081 | raise ValueError( 1082 | 'The value you passed through for `session` is incorrect, it must be one of the following: ["USEQPre","USEQPost","USEQPreAndPost","Default"]') 1083 | 1084 | # validate the START DATE. 1085 | if isinstance(start_date, datetime.datetime) or isinstance(start_date, datetime.date): 1086 | start_date_iso = start_date.isoformat() 1087 | elif isinstance(start_date, str): 1088 | datetime_parsed = parse(start_date) 1089 | start_date_iso = datetime_parsed.isoformat() 1090 | 1091 | # validate the END DATE. 1092 | if isinstance(end_date, datetime.datetime) or isinstance(start_date, datetime.date): 1093 | end_date_iso = end_date.isoformat() 1094 | 1095 | elif isinstance(end_date, str): 1096 | datetime_parsed = parse(end_date) 1097 | end_date_iso = datetime_parsed.isoformat() 1098 | 1099 | # define the endpoint. 1100 | url_endpoint = self._api_endpoint(url='stream/barchart/{symbol}/{interval}/{unit}/{start}/{end}'.format( 1101 | symbol=symbol, 1102 | interval=interval, 1103 | unit=unit, 1104 | start=start_date_iso, 1105 | end=end_date_iso 1106 | ) 1107 | ) 1108 | 1109 | # define the arguments. 1110 | params = { 1111 | 'access_token': self.state['access_token'], 1112 | 'sessionTemplate': session 1113 | } 1114 | 1115 | # grab the response. 1116 | response = self._handle_requests( 1117 | url=url_endpoint, 1118 | method='get', 1119 | args=params, 1120 | stream=True 1121 | ) 1122 | 1123 | return response 1124 | 1125 | def stream_bars_back(self, symbol: str, interval: int, unit: str, bar_back: int, last_date: str, session: str): 1126 | """Stream bars for a certain number of bars back. 1127 | 1128 | Arguments: 1129 | ---- 1130 | symbol (str): A ticker symbol to stream bars. 1131 | 1132 | interval (int): The size of the bar. 1133 | 1134 | unit (str): The frequency of the bar. 1135 | 1136 | bar_back (str): The number of bars back. 1137 | 1138 | last_date (str): The date from which to start going back. 1139 | 1140 | session (str): Defines whether you want bars from post, pre, or current market. 1141 | 1142 | Raises: 1143 | ---- 1144 | ValueError: 1145 | 1146 | Returns: 1147 | ---- 1148 | (dict): A dictionary of quotes. 1149 | """ 1150 | 1151 | # validate the token. 1152 | self._token_validation() 1153 | 1154 | # validate the symbol 1155 | if symbol is None: 1156 | raise ValueError("You must pass through one symbol.") 1157 | 1158 | # validate the unit 1159 | if unit not in ["Minute", "Daily", "Weekly", "Monthly"]: 1160 | raise ValueError( 1161 | 'The value you passed through for `unit` is incorrect, it must be one of the following: ["Minute", "Daily", "Weekly", "Monthly"]') 1162 | 1163 | # validate the interval. 1164 | if interval != 1 and unit in ["Daily", "Weekly", "Monthly"]: 1165 | raise ValueError( 1166 | "The interval must be one for daily, weekly or monthly.") 1167 | elif interval > 1440: 1168 | raise ValueError("Interval must be less than or equal to 1440") 1169 | 1170 | # validate the session. 1171 | if session is not None and session not in ['USEQPre', 'USEQPost', 'USEQPreAndPost', 'Default']: 1172 | raise ValueError( 1173 | 'The value you passed through for `session` is incorrect, it must be one of the following: ["USEQPre","USEQPost","USEQPreAndPost","Default"]') 1174 | 1175 | if bar_back > 157600: 1176 | raise ValueError("`bar_back` must be less than or equal to 157600") 1177 | 1178 | if isinstance(last_date, datetime.datetime): 1179 | last_date_iso = last_date.isoformat() 1180 | 1181 | elif isinstance(last_date, str): 1182 | datetime_parsed = parse(last_date) 1183 | last_date_iso = datetime_parsed.isoformat() 1184 | 1185 | # Define the endpoint. 1186 | url_endpoint = self._api_endpoint( 1187 | url='stream/barchart/{symbol}/{interval}/{unit}/{bar_back}/{last_date}'.format( 1188 | symbol=symbol, 1189 | interval=interval, 1190 | unit=unit, 1191 | bar_back=bar_back, 1192 | last_date_iso=last_date_iso 1193 | ) 1194 | ) 1195 | 1196 | # define the arguments. 1197 | params = { 1198 | 'access_token': self.state['access_token'], 1199 | 'sessionTemplate': session 1200 | } 1201 | 1202 | # grab the response. 1203 | response = self._handle_requests( 1204 | url=url_endpoint, 1205 | method='get', 1206 | args=params, 1207 | stream=True 1208 | ) 1209 | 1210 | return response 1211 | 1212 | def stream_bars_days_back(self, symbol: str, interval: int, unit: str, bar_back: int, last_date: str, session: str): 1213 | """Stream bars for a certain number of days back. 1214 | 1215 | Arguments: 1216 | ---- 1217 | symbol (str): A ticker symbol to stream bars. 1218 | 1219 | interval (int): The size of the bar. 1220 | 1221 | unit (str): The frequency of the bar. 1222 | 1223 | bar_back (str): The number of bars back. 1224 | 1225 | last_date (str): The date from which to start going back. 1226 | 1227 | session (str): Defines whether you want bars from post, pre, or current market. 1228 | 1229 | Raises: 1230 | ---- 1231 | ValueError: 1232 | 1233 | Returns: 1234 | ---- 1235 | (dict): A dictionary of quotes. 1236 | """ 1237 | 1238 | # validate the token. 1239 | self._token_validation() 1240 | 1241 | # validate the symbol 1242 | if symbol is None: 1243 | raise ValueError("You must pass through one symbol.") 1244 | 1245 | # validate the unit 1246 | if unit not in ["Minute", "Daily", "Weekly", "Monthly"]: 1247 | raise ValueError( 1248 | 'The value you passed through for `unit` is incorrect, it must be one of the following: ["Minute", "Daily", "Weekly", "Monthly"]') 1249 | 1250 | # validate the interval. 1251 | if interval != 1 and unit in ["Daily", "Weekly", "Monthly"]: 1252 | raise ValueError( 1253 | "The interval must be one for daily, weekly or monthly.") 1254 | elif interval > 1440: 1255 | raise ValueError("Interval must be less than or equal to 1440") 1256 | 1257 | # validate the session. 1258 | if session is not None and session not in ['USEQPre', 'USEQPost', 'USEQPreAndPost', 'Default']: 1259 | raise ValueError( 1260 | 'The value you passed through for `session` is incorrect, it must be one of the following: ["USEQPre","USEQPost","USEQPreAndPost","Default"]') 1261 | 1262 | if bar_back > 157600: 1263 | raise ValueError("`bar_back` must be less than or equal to 157600") 1264 | 1265 | if isinstance(last_date, datetime.datetime): 1266 | last_date_iso = last_date.isoformat() 1267 | 1268 | elif isinstance(last_date, str): 1269 | datetime_parsed = parse(last_date) 1270 | last_date_iso = datetime_parsed.isoformat() 1271 | 1272 | # define the endpoint. 1273 | url_endpoint = self._api_endpoint( 1274 | url='stream/barchart/{symbol}/{interval}/{unit}/{bar_back}/{last_date}'.format( 1275 | symbol=symbol, 1276 | interval=interval, 1277 | unit=unit, 1278 | bar_back=bar_back, 1279 | last_date=last_date_iso 1280 | ) 1281 | ) 1282 | 1283 | # Define the arguments. 1284 | params = { 1285 | 'access_token': self.state['access_token'], 1286 | 'sessionTemplate': session 1287 | } 1288 | 1289 | # grab the response.. 1290 | response = self._handle_requests( 1291 | url=url_endpoint, 1292 | method='get', 1293 | args=params, 1294 | stream=True 1295 | ) 1296 | 1297 | return response 1298 | 1299 | def stream_bars(self, symbol: str, interval: int, bar_back: int): 1300 | """Stream bars for a certain symbol. 1301 | 1302 | Arguments: 1303 | ---- 1304 | symbol (str): A ticker symbol to stream bars. 1305 | 1306 | interval (int): The size of the bar. 1307 | 1308 | unit (str): The frequency of the bar. 1309 | 1310 | Raises: 1311 | ---- 1312 | ValueError: 1313 | 1314 | Returns: 1315 | ---- 1316 | (dict): A dictionary of quotes. 1317 | """ 1318 | 1319 | # validate the token. 1320 | self._token_validation() 1321 | 1322 | # validate the symbol 1323 | if symbol is None: 1324 | raise ValueError("You must pass through one symbol.") 1325 | 1326 | if interval > 64999: 1327 | raise ValueError("Interval must be less than or equal to 64999") 1328 | 1329 | if bar_back > 10: 1330 | raise ValueError("`bar_back` must be less than or equal to 10") 1331 | 1332 | # define the endpoint. 1333 | url_endpoint = self._api_endpoint( 1334 | url='stream/tickbars/{symbol}/{interval}/{bar_back}'.format( 1335 | symbol=symbol, 1336 | interval=interval, 1337 | bar_back=bar_back 1338 | ) 1339 | ) 1340 | 1341 | # define the arguments. 1342 | params = { 1343 | 'access_token': self.state['access_token'] 1344 | } 1345 | 1346 | # grab the response. 1347 | response = self._handle_requests( 1348 | url=url_endpoint, 1349 | method='get', 1350 | args=params, 1351 | stream=True 1352 | ) 1353 | 1354 | return response 1355 | 1356 | def symbol_lists(self) -> dict: 1357 | """Returns a list of ticker symbols 1358 | 1359 | Returns: 1360 | ---- 1361 | (dict): A list of symbols. 1362 | """ 1363 | 1364 | # validate the token. 1365 | self._token_validation() 1366 | 1367 | # define the endpoint. 1368 | url_endpoint = self._api_endpoint(url='data/symbollists') 1369 | 1370 | # define the arguments. 1371 | params = { 1372 | 'access_token': self.state['access_token'] 1373 | } 1374 | 1375 | # grab the response. 1376 | response = self._handle_requests( 1377 | url=url_endpoint, 1378 | method='get', 1379 | args=params 1380 | ) 1381 | 1382 | return response 1383 | 1384 | def symbol_list(self, symbol_list_id: List[str]) -> dict: 1385 | """Grab a list of symbols. 1386 | 1387 | Arguments: 1388 | ---- 1389 | symbol_list_id (List[str]): A list of symbol. 1390 | 1391 | Returns: 1392 | ---- 1393 | dict: Return a list of symbols. 1394 | """ 1395 | 1396 | # validate the token. 1397 | self._token_validation() 1398 | 1399 | # define the endpoint. 1400 | url_endpoint = self._api_endpoint( 1401 | url='data/symbollists/{list_symbol}'.format( 1402 | list_symbol=symbol_list_id) 1403 | ) 1404 | 1405 | # define the arguments. 1406 | params = { 1407 | 'access_token': self.state['access_token'] 1408 | } 1409 | 1410 | # grab the response. 1411 | response = self._handle_requests( 1412 | url=url_endpoint, 1413 | method='get', 1414 | args=params 1415 | ) 1416 | 1417 | return response 1418 | 1419 | def symbols_from_symbol_list(self, symbol_list_id: List[str]) -> dict: 1420 | """Grab a list of symbols. 1421 | 1422 | Arguments: 1423 | ---- 1424 | symbol_list_id (List[str]): A list of symbol. 1425 | 1426 | Returns: 1427 | ---- 1428 | dict: Return a list of symbols. 1429 | """ 1430 | 1431 | # validate the token. 1432 | self._token_validation() 1433 | 1434 | # define the endpoint. 1435 | url_endpoint = self._api_endpoint( 1436 | url='data/symbollists/{list_id}/symbols'.format( 1437 | list_id=symbol_list_id) 1438 | ) 1439 | 1440 | # define the arguments. 1441 | params = { 1442 | 'access_token': self.state['access_token'] 1443 | } 1444 | 1445 | # grab the response. 1446 | response = self._handle_requests( 1447 | url=url_endpoint, 1448 | method='get', 1449 | args=params 1450 | ) 1451 | 1452 | return response 1453 | 1454 | def confirm_order(self, order: dict) -> dict: 1455 | """Confirm an order. 1456 | 1457 | Arguments: 1458 | ---- 1459 | order (dict): A dictionary for order. 1460 | 1461 | Returns: 1462 | ---- 1463 | dict: A confirmation of the order. 1464 | """ 1465 | # validate the token. 1466 | self._token_validation() 1467 | 1468 | # define the endpoint. 1469 | url_endpoint = self._api_endpoint(url='orders/confirm') 1470 | 1471 | # define the arguments. 1472 | params = { 1473 | 'access_token': self.state['access_token'] 1474 | } 1475 | 1476 | # grab the response. 1477 | response = self._handle_requests( 1478 | url=url_endpoint, method='post', args=params, payload=order) 1479 | 1480 | return response 1481 | 1482 | def submit_order(self, order: dict) -> dict: 1483 | """Submit an order. 1484 | 1485 | Arguments: 1486 | ---- 1487 | order (dict): A dictionary for order. 1488 | 1489 | Returns: 1490 | ---- 1491 | dict: A confirmation of the order. 1492 | """ 1493 | 1494 | # validate the token. 1495 | self._token_validation() 1496 | 1497 | # define the endpoint. 1498 | url_endpoint = self._api_endpoint(url='orders') 1499 | 1500 | # define the arguments. 1501 | params = { 1502 | 'access_token': self.state['access_token'] 1503 | } 1504 | 1505 | # grab the response. 1506 | response = self._handle_requests( 1507 | url=url_endpoint, 1508 | method='post', 1509 | args=params, 1510 | payload=order 1511 | ) 1512 | 1513 | return response 1514 | 1515 | def cancel_order(self, order_id: str) -> dict: 1516 | """Cancel an order. 1517 | 1518 | Arguments: 1519 | ---- 1520 | order_id (str): An order id. 1521 | 1522 | Returns: 1523 | ---- 1524 | dict: A confirmation of the cancel order. 1525 | """ 1526 | 1527 | # validate the token. 1528 | self._token_validation() 1529 | 1530 | # define the endpoint. 1531 | url_endpoint = self._api_endpoint( 1532 | url='orders/{order_id}'.format(order_id=order_id)) 1533 | 1534 | # define the arguments. 1535 | params = { 1536 | 'access_token': self.state['access_token'] 1537 | } 1538 | 1539 | # grab the response. 1540 | response = self._handle_requests( 1541 | url=url_endpoint, 1542 | method='delete', 1543 | args=params 1544 | ) 1545 | 1546 | return response 1547 | 1548 | def replace_order(self, order_id: str, new_order: dict) -> dict: 1549 | """Replace an order. 1550 | 1551 | Arguments: 1552 | ---- 1553 | order_id (str): An order id. 1554 | 1555 | order (dict): A dictionary for order. 1556 | 1557 | Returns: 1558 | ---- 1559 | dict: A confirmation of the replaced order. 1560 | """ 1561 | 1562 | # validate the token. 1563 | self._token_validation() 1564 | 1565 | # define the endpoint. 1566 | url_endpoint = self._api_endpoint( 1567 | url='orders/{order_id}'.format(order_id=order_id) 1568 | ) 1569 | 1570 | # define the arguments. 1571 | params = { 1572 | 'access_token': self.state['access_token'] 1573 | } 1574 | 1575 | # grab the response. 1576 | response = self._handle_requests( 1577 | url=url_endpoint, 1578 | method='put', 1579 | args=params, 1580 | payload=new_order 1581 | ) 1582 | 1583 | return response 1584 | 1585 | def confirm_group_order(self, orders: List[Dict]) -> dict: 1586 | """Confirm a list of orders. 1587 | 1588 | Arguments: 1589 | ---- 1590 | orders (List[dict]): A list of orders to confirm. 1591 | 1592 | Returns: 1593 | ---- 1594 | dict: A confirmation for all the orders. 1595 | """ 1596 | 1597 | # validate the token. 1598 | self._token_validation() 1599 | 1600 | # define the endpoint. 1601 | url_endpoint = self._api_endpoint(url='orders/groups/confirm') 1602 | 1603 | # define the arguments. 1604 | params = { 1605 | 'access_token': self.state['access_token'] 1606 | } 1607 | 1608 | # grab the response. 1609 | response = self._handle_requests( 1610 | url=url_endpoint, 1611 | method='post', 1612 | args=params, 1613 | payload=orders 1614 | ) 1615 | 1616 | return response 1617 | 1618 | def submit_group_order(self, orders: List[Dict]) -> dict: 1619 | """Submit a list of orders. 1620 | 1621 | Arguments: 1622 | ---- 1623 | orders (List[dict]): A list of orders to submit. 1624 | 1625 | Returns: 1626 | ---- 1627 | dict: A confirmation for all the orders. 1628 | """ 1629 | 1630 | # validate the token. 1631 | self._token_validation() 1632 | 1633 | # define the endpoint. 1634 | url_endpoint = self._api_endpoint(url='orders/groups') 1635 | 1636 | # define the arguments. 1637 | params = { 1638 | 'access_token': self.state['access_token'] 1639 | } 1640 | 1641 | # grab the response. 1642 | response = self._handle_requests( 1643 | url=url_endpoint, 1644 | method='post', 1645 | args=params, 1646 | payload=orders 1647 | ) 1648 | 1649 | return response 1650 | 1651 | def available_activation_triggers(self): 1652 | """Grabs all the Activiation Triggers. 1653 | 1654 | Returns: 1655 | ---- 1656 | (dict): A dictionary resource with all the activation triggers. 1657 | """ 1658 | 1659 | # validate the token. 1660 | self._token_validation() 1661 | 1662 | # define the endpoint. 1663 | url_endpoint = self._api_endpoint( 1664 | url='orderexecution/activationtriggers' 1665 | ) 1666 | 1667 | # define the arguments. 1668 | params = { 1669 | 'access_token': self.state['access_token'] 1670 | } 1671 | 1672 | # grab the response. 1673 | response = self._handle_requests( 1674 | url=url_endpoint, 1675 | method='get', 1676 | args=params 1677 | ) 1678 | 1679 | return response 1680 | 1681 | def available_exchanges(self) -> dict: 1682 | """Grabs all the exchanges provided by TradeStation. 1683 | 1684 | Returns: 1685 | ---- 1686 | (dict): A dictionary resource with all the exchanges listed. 1687 | """ 1688 | 1689 | # validate the token. 1690 | self._token_validation() 1691 | 1692 | # define the endpoint. 1693 | url_endpoint = self._api_endpoint(url='orderexecution/exchanges') 1694 | 1695 | # define the arguments. 1696 | params = { 1697 | 'access_token': self.state['access_token'] 1698 | } 1699 | 1700 | # grab the response. 1701 | response = self._handle_requests( 1702 | url=url_endpoint, 1703 | method='get', 1704 | args=params 1705 | ) 1706 | 1707 | return response 1708 | --------------------------------------------------------------------------------