├── .gitignore ├── README.md ├── find_sys_path.py ├── ibw ├── __init__.py ├── client.py └── clientportal.py ├── images └── IBTB_logo.png ├── robot ├── __init__.py ├── indicator.py ├── portfolio.py ├── stock_frame.py ├── trader.py └── trades.py ├── setup.py ├── tests ├── run_client.py └── test_ticker_signal.py └── write_config.py /.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 | # Add files that I don't want on GitHub. 132 | .vscode/ 133 | ibw/server_session.json 134 | .pypirc 135 | MANIFEST.in 136 | clientportal.gw/ 137 | clientportal.beta.gw/ 138 | config/ 139 | 140 | #More files to ignore 141 | .github/ 142 | order_record/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | [![Contributors][contributors-shield]][contributors-url] 23 | [![Forks][forks-shield]][forks-url] 24 | [![Stargazers][stars-shield]][stars-url] 25 | [![Issues][issues-shield]][issues-url] 26 | [![MIT License][license-shield]][license-url] 27 | [![LinkedIn][linkedin-shield]][linkedin-url] 28 | 29 | 30 |
31 |

32 | 33 | Logo 34 | 35 | 36 |

Interactive Brokers Trading Bot

37 | 38 |

39 | A Python library written to handle IB's Client Portal API, manage portfolio and execute trades. 40 |
41 | Explore the docs » 42 |
43 |
44 | View Demo 45 | · 46 | Report Bug 47 | · 48 | Request Feature 49 |

50 |

51 | 52 | 53 |
54 |

📚Table of Contents

55 |
    56 |
  1. 57 | About The Project 58 | 61 |
  2. 62 |
  3. 63 | Getting Started 64 | 68 |
  4. 69 |
  5. Usage
  6. 70 |
  7. Roadmap
  8. 71 |
  9. Contributing
  10. 72 |
  11. License
  12. 73 |
  13. Acknowledgements
  14. 74 |
75 |
76 | 77 | 78 | 79 | ## 💡About The Project 80 | 81 | 84 | 85 | This project is built entirely on Python, it combines other Interactive Brokers libraries written by other contributors as well as my own contribution in making algorithmic trading on Interactive Brokers possible. 86 | 87 | 91 | 92 | ### Built With 93 | 94 | - [Python](https://www.python.org/) 95 | 96 | 97 | 98 | ## 🎉Getting Started 99 | 100 | To get a local copy up and running follow these simple steps. 101 | 102 | ### 🔖 Prerequisites 103 | 104 | Before using this library, ensure you have Java installed and have an account with Interactive Brokers. Check out [Interactive Broker Client Portal Web API](https://interactivebrokers.github.io/cpwebapi/) for setting up. You can skip the download and unzip the CPI WebAPI step from the IB site as this step has been taken care off in the library. 105 | 106 | ### 🔧 Installation 107 | 108 | 1. Clone the repo 109 | ```sh 110 | git clone https://github.com/ProScriptSlinger/Interactive-Brokers-Trading-Bot.git 111 | ``` 112 | 2. Navigate to the working directory 113 | 114 | 3. In the terminal, run 115 | 116 | ``` 117 | python setup.py build 118 | ``` 119 | 120 | and then 121 | 122 | ``` 123 | python setup.py install 124 | ``` 125 | 126 | 4. Enter your IB credentials in write_config.py and run the script 127 | 128 | 5. Open run_client.py and run the script. It will download the clientportal.gw to the working directory. 129 | 130 | 6. Using Git Bash, navigate to the clientportal.gw folder and run 131 | 132 | ``` 133 | "bin/run.bat" "root/conf.yaml" 134 | ``` 135 | 136 | 7. Run run_client.py in tests and the bot should be up and running. 137 | 138 | 8. Follow the instructions on run_client.py to configure your trading bot. 139 | 140 | 141 | 142 | ## 📦 Usage 143 | 144 | To use it, study the revelant libraries, namely the python objects in robot/ folder. There are also some simple instructions in the run_client.py to get you up and running quick. 145 | 146 | 147 | 148 | ## 🚩 Roadmap 149 | 150 | See the [open issues](https://github.com/github_username/repo_name/issues) for a list of proposed features (and known issues). 151 | 152 | ### ✨ Milestone Summary 153 | 154 | | Status | Milestone | Goals | ETA | 155 | | :----: | :-------------------------------------------------------------------------------------------------------------------------------------- | :---: | :-----------: | 156 | | 🚀 | **[Implement the ability to associate tickers with different indicators and trigger levels](#implement-ticker-indicators-association)** | 1 / 1 | 15 April 2021 | 157 | 158 | ### Implement ticker indicators association 159 | 160 | > This milestone will be done when 161 | 162 | - Different signals can be attached to a ticker 163 | - All the indicators' signal can be checked independently, giving correct buy/sell signals 164 | 165 | 166 | 167 | ## 💝 Contributing 168 | 169 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 170 | 171 | 1. Fork the Project 172 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 173 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 174 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 175 | 5. Open a Pull Request 176 | 177 | 178 | 179 | ## 📜 License 180 | 181 | Distributed under the MIT License. See `LICENSE` for more information. 182 | -------------------------------------------------------------------------------- /find_sys_path.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | print(sys.path) -------------------------------------------------------------------------------- /ibw/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProScriptSlinger/Interactive-Brokers-Trading-Bot/aedb625e1921af90fad93563943f38ebd4eceb5a/ibw/__init__.py -------------------------------------------------------------------------------- /ibw/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ssl 4 | import json 5 | import time 6 | import urllib 7 | import urllib3 8 | import certifi 9 | import logging 10 | import pathlib 11 | import requests 12 | import textwrap 13 | import subprocess 14 | 15 | from typing import Union 16 | from typing import List 17 | from typing import Dict 18 | 19 | from urllib3.exceptions import InsecureRequestWarning 20 | from ibw.clientportal import ClientPortal 21 | 22 | urllib3.disable_warnings(category=InsecureRequestWarning) 23 | # http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) 24 | 25 | try: 26 | _create_unverified_https_context = ssl._create_unverified_context 27 | except AttributeError: 28 | # Legacy Python that doesn't verify HTTPS certificates by default 29 | pass 30 | else: 31 | # Handle target environment that doesn't support HTTPS verification 32 | ssl._create_default_https_context = _create_unverified_https_context 33 | 34 | logging.basicConfig( 35 | filename='app.log', 36 | format='%(levelname)s - %(name)s - %(message)s', 37 | level=logging.DEBUG 38 | ) 39 | 40 | # gateway_path = pathlib.Path('clientportal.gw').resolve() #Added this line to redirect clientportal.gw away from resoruces/clientportal.beta.gw 41 | 42 | class IBClient(): 43 | 44 | def __init__(self, username: str, account: str, client_gateway_path: str = None, is_server_running: bool = True) -> None: 45 | #Changed the "client_gatewat_path: str = None" to "client_gatewat_path: str = gateway_path " where "gateway_path = pathlib.Path('clientportal.gw').resolve()" as defined above 46 | 47 | """Initalizes a new instance of the IBClient Object. 48 | 49 | Arguments: 50 | ---- 51 | username {str} -- Your IB account username for either your paper or regular account. 52 | 53 | account {str} -- Your IB account number for either your paper or regular account. 54 | 55 | Keyword Arguments: 56 | ---- 57 | password {str} -- Your IB account password for either your paper or regular account. (default:{""}) 58 | 59 | Usage: 60 | ---- 61 | >>> ib_paper_session = IBClient( 62 | username='IB_PAPER_USERNAME', 63 | account='IB_PAPER_ACCOUNT', 64 | ) 65 | >>> ib_paper_session 66 | >>> ib_regular_session = IBClient( 67 | username='IB_REGULAR_USERNAME', 68 | account='IB_REGULAR_ACCOUNT', 69 | ) 70 | >>> ib_regular_session 71 | """ 72 | 73 | self.account = account 74 | self.username = username 75 | self.client_portal_client = ClientPortal() 76 | 77 | self.api_version = 'v1/' 78 | self._operating_system = sys.platform 79 | self.session_state_path: pathlib.Path = pathlib.Path(__file__).parent.joinpath('server_session.json').resolve() 80 | self.authenticated = False 81 | self._is_server_running = is_server_running 82 | 83 | # Define URL Components 84 | ib_gateway_host = r"https://localhost" 85 | ib_gateway_port = r"5000" 86 | self.ib_gateway_path = ib_gateway_host + ":" + ib_gateway_port 87 | self.backup_gateway_path = r"https://cdcdyn.interactivebrokers.com/portal.proxy" 88 | self.login_gateway_path = self.ib_gateway_path + "/sso/Login?forwardTo=22&RL=1&ip2loc=on" 89 | 90 | 91 | if client_gateway_path is None: 92 | # Grab the Client Portal Path. 93 | self.client_portal_folder: pathlib.Path = pathlib.Path(__file__).parents[1].joinpath( 94 | 'clientportal.gw' 95 | ).resolve() 96 | 97 | # See if it exists. 98 | if not self.client_portal_folder.exists(): 99 | print("The Client Portal Gateway doesn't exist. You need to download it before using the Library.") 100 | print("Downloading the Client Portal file...") 101 | self.client_portal_client.download_and_extract() 102 | 103 | else: 104 | 105 | self.client_portal_folder = client_gateway_path 106 | 107 | if not self._is_server_running: 108 | 109 | # Load the Server State. 110 | self.server_process = self._server_state(action='load') 111 | 112 | # Log the initial Info. 113 | logging.info(textwrap.dedent(''' 114 | ================= 115 | Initialize Client: 116 | ================= 117 | Server Process: {serv_proc} 118 | Operating System: {op_sys} 119 | Session State Path: {state_path} 120 | Client Portal Folder: {client_path} 121 | ''').format( 122 | serv_proc=self.server_process, 123 | op_sys=self._operating_system, 124 | state_path=self.session_state_path, 125 | client_path=self.client_portal_folder 126 | ) 127 | ) 128 | else: 129 | self.server_process = None 130 | 131 | 132 | def create_session(self, set_server=True) -> bool: 133 | """Creates a new session. 134 | 135 | Creates a new session with Interactive Broker using the credentials 136 | passed through when the Robot was initalized. 137 | 138 | Usage: 139 | ---- 140 | >>> ib_client = IBClient( 141 | username='IB_PAPER_username', 142 | password='IB_PAPER_PASSWORD', 143 | account='IB_PAPER_account', 144 | ) 145 | >>> server_response = ib_client.create_session() 146 | >>> server_response 147 | True 148 | 149 | Returns: 150 | ---- 151 | bool -- True if the session was created, False if wasn't created. 152 | """ 153 | 154 | # first let's check if the server is running, if it's not then we can start up. 155 | if self.server_process is None and not self._is_server_running: 156 | 157 | # If it's None we need to connect first. 158 | if set_server: 159 | self.connect(start_server=True, check_user_input=True) 160 | else: 161 | self.connect(start_server=True, check_user_input=False) 162 | return True 163 | 164 | # then make sure the server is updated. 165 | if self._set_server(): 166 | return True 167 | 168 | # Try and authenticate. 169 | auth_response = self.is_authenticated() 170 | 171 | # Log the initial Info. 172 | logging.info(textwrap.dedent(''' 173 | ================= 174 | Create Session: 175 | ================= 176 | Auth Response: {auth_resp} 177 | ''').format( 178 | auth_resp=auth_response, 179 | ) 180 | ) 181 | 182 | # Finally make sure we are authenticated. 183 | if 'authenticated' in auth_response.keys() and auth_response['authenticated'] and self._set_server(): 184 | self.authenticated = True 185 | return True 186 | else: 187 | # In this case don't connect, but prompt the user to log in again. 188 | self.connect(start_server=False) 189 | 190 | if self._set_server(): 191 | self.authenticated = True 192 | return True 193 | 194 | def _set_server(self) -> bool: 195 | """Sets the server info for the session. 196 | 197 | Sets the Server for the session, and if the server cannot be set then 198 | script will halt. Otherwise will return True to continue on in the script. 199 | 200 | Returns: 201 | ---- 202 | bool -- True if the server was set, False if wasn't 203 | """ 204 | success = '\nNew session has been created and authenticated. Requests will not be limited.\n'.upper() 205 | failure = '\nCould not create a new session that was authenticated, exiting script.\n'.upper() 206 | 207 | # Grab the Server accounts. 208 | server_account_content = self.server_accounts() 209 | 210 | # Try to do the quick way. 211 | if (server_account_content and 'accounts' in server_account_content): 212 | accounts = server_account_content['accounts'] 213 | if self.account in accounts: 214 | 215 | # Log the response. 216 | logging.debug(textwrap.dedent(''' 217 | ================= 218 | Set Server: 219 | ================= 220 | Server Response: {serv_resp} 221 | ''').format( 222 | serv_resp=server_account_content 223 | ) 224 | ) 225 | 226 | print(success) 227 | return True 228 | else: 229 | 230 | # Update the Server. 231 | server_update_content = self.update_server_account( 232 | account_id=self.account, 233 | check=False 234 | ) 235 | 236 | # Grab the accounts. 237 | server_account_content = self.server_accounts() 238 | 239 | # Log the response. 240 | logging.debug(textwrap.dedent(''' 241 | ================= 242 | Set Server: 243 | ================= 244 | Server Response: {serv_resp} 245 | Server Update Response: {auth_resp} 246 | ''').format( 247 | auth_resp=server_update_content, 248 | serv_resp=server_account_content 249 | ) 250 | ) 251 | 252 | # TO DO: Add check market hours here and then check for a mutual fund. 253 | if (server_account_content and 'accounts' in server_account_content) or (server_update_content and 'message' in server_update_content): 254 | print(success) 255 | return True 256 | else: 257 | print(failure) 258 | sys.exit() 259 | 260 | # # TO DO: Add check market hours here and then check for a mutual fund. 261 | # news = self.data_news(conid='265598') 262 | # if news and 'news' in news: 263 | # print(success) 264 | # return True 265 | # if server_account_content is not None and 'set' in server_update_content.keys() and server_update_content['set'] == True: 266 | # print(success) 267 | # return True 268 | # elif ('message' in server_update_content.keys()) and (server_update_content['message'] == 'Account already set'): 269 | # print(success) 270 | # return True 271 | # else: 272 | # print(failure) 273 | # sys.exit() 274 | 275 | def _server_state(self, action: str = 'save') -> Union[None, int]: 276 | """Determines the server state. 277 | 278 | Maintains the server state, so we can easily load a previous session, 279 | save a new session, or delete a closed session. 280 | 281 | Arguments: 282 | ---- 283 | action {str} -- The action you wish to take to the `json` file. Can be one of the following options: 284 | 285 | 1. save - saves the current state and overwrites the old one. 286 | 2. load - loads the previous state from a session that has a server still running. 287 | 3. delete - deletes the state because the server has been closed. 288 | 289 | Returns: 290 | ---- 291 | Union[None, int] -- The Process ID of the Server. 292 | """ 293 | 294 | # Define file components. 295 | file_exists = self.session_state_path.exists() 296 | 297 | # Log the response. 298 | logging.debug(textwrap.dedent(''' 299 | ================= 300 | Server State: 301 | ================= 302 | Server State: {state} 303 | State File: {exist} 304 | ''').format( 305 | state=action, 306 | exist=file_exists 307 | ) 308 | ) 309 | 310 | if action == 'save': 311 | 312 | # Save the State. 313 | with open(self.session_state_path, 'w') as server_file: 314 | json.dump( 315 | obj={'server_process_id': self.server_process}, 316 | fp=server_file 317 | ) 318 | 319 | # If we are loading check the file exists first. 320 | elif action == 'load' and file_exists: 321 | 322 | try: 323 | self.is_authenticated(check=True) 324 | check_proc_id = False 325 | except: 326 | check_proc_id = True 327 | 328 | # Load it. 329 | with open(self.session_state_path, 'r') as server_file: 330 | server_state = json.load(fp=server_file) 331 | 332 | # Grab the Process Id. 333 | proc_id = server_state['server_process_id'] 334 | 335 | # If it's running return the process ID. 336 | if check_proc_id: 337 | is_running = self._check_if_server_running(process_id=proc_id) 338 | else: 339 | is_running = True 340 | 341 | if is_running: 342 | return proc_id 343 | 344 | # Delete it. 345 | elif action == 'delete' and file_exists: 346 | self.session_state_path.unlink() 347 | 348 | def _check_if_server_running(self, process_id: str) -> bool: 349 | """Used to see if the Clientportal Gateway is running. 350 | 351 | Arguments: 352 | ---- 353 | process_id (str): The process ID of the clientportal. 354 | 355 | Returns: 356 | ---- 357 | bool: `True` if running, `False` otherwise. 358 | """ 359 | 360 | if self._operating_system == 'win32': 361 | 362 | # See if the Process is running. 363 | with os.popen('tasklist') as task_list: 364 | 365 | # Grab each task. 366 | for process in task_list.read().splitlines()[4:]: 367 | 368 | if str(process_id) in process: 369 | 370 | # Log the response. 371 | logging.debug(textwrap.dedent(''' 372 | ================= 373 | Server Process: 374 | ================= 375 | Process ID: {process} 376 | ''').format( 377 | process=process 378 | ) 379 | ) 380 | 381 | return True 382 | 383 | else: 384 | 385 | try: 386 | os.kill(process_id, 0) 387 | return True 388 | except OSError: 389 | return False 390 | 391 | def _check_authentication_user_input(self) -> bool: 392 | """Used to check the authentication of the Server. 393 | 394 | Returns: 395 | ---- 396 | bool: `True` if authenticated, `False` otherwise. 397 | """ 398 | 399 | max_retries = 0 400 | while (max_retries > 4 or self.authenticated == False): 401 | 402 | # Grab the User Request. 403 | user_input = input( 404 | 'Would you like to make an authenticated request (Yes/No)? ' 405 | ).upper() 406 | 407 | # If no, close the session. 408 | if user_input == 'NO': 409 | self.close_session() 410 | # Else try and see if we are authenticated. 411 | else: 412 | auth_response = self.is_authenticated(check=True) 413 | 414 | # Log the Auth Response. 415 | logging.debug('Check User Auth Inital: {auth_resp}'.format( 416 | auth_resp=auth_response 417 | ) 418 | ) 419 | 420 | if 'statusCode' in auth_response.keys() and auth_response['statusCode'] == 401: 421 | print("Session isn't connected, closing script.") 422 | self.close_session() 423 | 424 | elif 'authenticated' in auth_response.keys() and auth_response['authenticated'] == True: 425 | self.authenticated = True 426 | break 427 | 428 | elif 'authenticated' in auth_response.keys() and auth_response['authenticated'] == False: 429 | valid_resp = self.validate() 430 | reauth_resp = self.reauthenticate() 431 | auth_response = self.is_authenticated() 432 | 433 | try: 434 | serv_resp = self.server_accounts() 435 | if 'accounts' in serv_resp: 436 | self.authenticated = True 437 | 438 | # Log the response. 439 | logging.debug('Had to do Server Account Request: {auth_resp}'.format( 440 | auth_resp=serv_resp 441 | ) 442 | ) 443 | break 444 | except: 445 | pass 446 | 447 | logging.debug( 448 | ''' 449 | Validate Response: {valid_resp} 450 | Reauth Response: {reauth_resp} 451 | '''.format( 452 | valid_resp=valid_resp, 453 | reauth_resp=reauth_resp 454 | ) 455 | ) 456 | 457 | max_retries += 1 458 | 459 | return self.authenticated 460 | 461 | def _check_authentication_non_input(self) -> bool: 462 | """Runs the authentication protocol but without user input. 463 | 464 | Returns: 465 | ---- 466 | bool: `True` if authenticated, `False` otherwise. 467 | """ 468 | 469 | # Grab the auth response. 470 | auth_response = self.is_authenticated(check=True) 471 | 472 | # Log the Auth response. 473 | logging.debug('Check Non-User Auth Inital: {auth_resp}'.format( 474 | auth_resp=auth_response 475 | ) 476 | ) 477 | 478 | # Fail early, status code means we can't authenticate. 479 | if 'statusCode' in auth_response: 480 | print("Session isn't connected, closing script.") 481 | self.close_session() 482 | 483 | # Grab the Auth Response Flag. 484 | auth_response_value = auth_response.get('authenticated', None) 485 | 486 | # If it it's True we are good. 487 | if auth_response_value: 488 | self.authenticated = True 489 | 490 | # If not, try and reauthenticate. 491 | elif not auth_response_value: 492 | 493 | # Validate the session first. 494 | self.validate() 495 | 496 | # Then reauthenticate the session. 497 | reauth_response = self.reauthenticate() 498 | 499 | # See if it was triggered. 500 | if 'message' in reauth_response: 501 | self.authenticated = True 502 | else: 503 | self.authenticated = False 504 | 505 | def _start_server(self) -> str: 506 | """Starts the Server. 507 | 508 | Returns: 509 | ---- 510 | str: The Server Process ID. 511 | """ 512 | 513 | # windows will use the command line application. 514 | if self._operating_system == 'win32': 515 | IB_WEB_API_PROC = ["cmd", "/k", r"bin\run.bat", r"root\conf.yaml"] 516 | self.server_process = subprocess.Popen( 517 | args=IB_WEB_API_PROC, 518 | cwd=self.client_portal_folder, 519 | creationflags=subprocess.CREATE_NEW_CONSOLE 520 | ).pid 521 | 522 | # mac will use the terminal. 523 | elif self._operating_system == 'darwin': 524 | IB_WEB_API_PROC = [ 525 | "open", "-F", "-a", 526 | "Terminal", r"bin/run.sh", r"root/conf.yaml" 527 | ] 528 | self.server_process = subprocess.Popen( 529 | args=IB_WEB_API_PROC, 530 | cwd=self.client_portal_folder 531 | ).pid 532 | 533 | return self.server_process 534 | 535 | def connect(self, start_server: bool = True, check_user_input: bool = True) -> bool: 536 | """Connects the session with the API. 537 | 538 | Connects the session to the Interactive Broker API by, starting up the Client Portal Gateway, 539 | prompting the user to log in and then returns the results back to the `create_session` method. 540 | 541 | Arguments: 542 | ---- 543 | start_server {bool} -- True if the server isn't running but needs to be started, False if it 544 | is running and just needs to be authenticated. 545 | 546 | Returns: 547 | ---- 548 | bool -- `True` if it was connected. 549 | """ 550 | 551 | logging.debug('Running Client Folder at: {file_path}'.format( 552 | file_path=self.client_portal_folder)) 553 | 554 | # If needed, start the server and save the State. 555 | if start_server: 556 | self._start_server() 557 | self._server_state(action='save') 558 | 559 | # Display prompt if needed. 560 | if check_user_input: 561 | 562 | print(textwrap.dedent("""{lin_brk} 563 | The Interactive Broker server is currently starting up, so we can authenticate your session. 564 | STEP 1: GO TO THE FOLLOWING URL: {url} 565 | STEP 2: LOGIN TO YOUR account WITH YOUR username AND PASSWORD. 566 | STEP 3: WHEN YOU SEE `Client login succeeds` RETURN BACK TO THE TERMINAL AND TYPE `YES` TO CHECK IF THE SESSION IS AUTHENTICATED. 567 | SERVER IS RUNNING ON PROCESS ID: {proc_id} 568 | {lin_brk}""".format( 569 | lin_brk='-'*80, 570 | url=self.login_gateway_path, 571 | proc_id=self.server_process 572 | ) 573 | ) 574 | ) 575 | 576 | # Check the auth status 577 | auth_status = self._check_authentication_user_input() 578 | 579 | else: 580 | 581 | auth_status = True 582 | 583 | return auth_status 584 | 585 | def close_session(self) -> None: 586 | """Closes the current session and kills the server using Taskkill.""" 587 | 588 | print('\nCLOSING SERVER AND EXITING SCRIPT.') 589 | 590 | # Define the process. 591 | process = "TASKKILL /F /PID {proc_id} /T".format( 592 | proc_id=self.server_process 593 | ) 594 | 595 | # Kill the process. 596 | subprocess.call(process, creationflags=subprocess.DETACHED_PROCESS) 597 | 598 | # Delete the state 599 | self._server_state(action='delete') 600 | 601 | # and exit. 602 | sys.exit() 603 | 604 | def _headers(self, mode: str = 'json') -> Dict: 605 | """Builds the headers. 606 | 607 | Returns a dictionary of default HTTP headers for calls to Interactive 608 | Brokers API, in the headers we defined the Authorization and access 609 | token. 610 | 611 | Arguments: 612 | ---- 613 | mode {str} -- Defines the content-type for the headers dictionary. 614 | default is 'json'. Possible values are ['json','form'] 615 | 616 | Returns: 617 | ---- 618 | Dict 619 | """ 620 | 621 | if mode == 'json': 622 | headers = { 623 | 'Content-Type': 'application/json' 624 | } 625 | elif mode == 'form': 626 | headers = { 627 | 'Content-Type': 'application/x-www-form-urlencoded' 628 | } 629 | elif mode == 'none': 630 | headers = None 631 | 632 | return headers 633 | 634 | def _build_url(self, endpoint: str) -> str: 635 | """Builds a url for a request. 636 | 637 | Arguments: 638 | ---- 639 | endpoint {str} -- The URL that needs conversion to a full endpoint URL. 640 | 641 | Returns: 642 | ---- 643 | {srt} -- A full URL path. 644 | """ 645 | 646 | # otherwise build the URL 647 | return urllib.parse.unquote( 648 | urllib.parse.urljoin( 649 | self.ib_gateway_path, 650 | self.api_version 651 | ) + r'portal/' + endpoint 652 | ) 653 | 654 | def _make_request(self, endpoint: str, req_type: str, headers: str = 'json', params: dict = None, data: dict = None, json: dict = None) -> Dict: 655 | """Handles the request to the client. 656 | 657 | Handles all the requests made by the client and correctly organizes 658 | the information so it is sent correctly. Additionally it will also 659 | build the URL. 660 | 661 | Arguments: 662 | ---- 663 | endpoint {str} -- The endpoint we wish to request. 664 | 665 | req_type {str} -- Defines the type of request to be made. Can be one of four 666 | possible values ['GET','POST','DELETE','PUT'] 667 | 668 | params {dict} -- Any arguments that are to be sent along in the request. That 669 | could be parameters of a 'GET' request, or a data payload of a 670 | 'POST' request. 671 | 672 | Returns: 673 | ---- 674 | {Dict} -- A response dictionary. 675 | 676 | """ 677 | # First build the url. 678 | url = self._build_url(endpoint=endpoint) 679 | 680 | # Define the headers. 681 | headers = self._headers(mode=headers) 682 | 683 | # Make the request. 684 | if req_type == 'POST': 685 | response = requests.post(url=url, headers=headers, params=params, json=json, verify=False) 686 | elif req_type == 'GET': 687 | response = requests.get(url=url, headers=headers, params=params, json=json, verify=False) 688 | elif req_type == 'DELETE': 689 | response = requests.delete(url=url, headers=headers, params=params, json=json, verify=False) 690 | 691 | # grab the status code 692 | status_code = response.status_code 693 | 694 | # grab the response headers. 695 | response_headers = response.headers 696 | 697 | # Check to see if it was successful 698 | if response.ok: 699 | 700 | if response_headers.get('Content-Type','null') == 'application/json;charset=utf-8': 701 | data = response.json() 702 | else: 703 | data = response.json() 704 | 705 | # Log it. 706 | logging.debug(''' 707 | Response Text: {resp_text} 708 | Response URL: {resp_url} 709 | Response Code: {resp_code} 710 | Response JSON: {resp_json} 711 | Response Headers: {resp_headers} 712 | '''.format( 713 | resp_text=response.text, 714 | resp_url=response.url, 715 | resp_code=status_code, 716 | resp_json=data, 717 | resp_headers=response_headers 718 | ) 719 | ) 720 | 721 | return data 722 | 723 | # if it was a bad request print it out. 724 | elif not response.ok and url != 'https://localhost:5000/v1/portal/iserver/account': 725 | print(url) 726 | raise requests.HTTPError() 727 | 728 | def _prepare_arguments_list(self, parameter_list: List[str]) -> str: 729 | """Prepares the arguments for the request. 730 | 731 | Some endpoints can take multiple values for a parameter, this 732 | method takes that list and creates a valid string that can be 733 | used in an API request. The list can have either one index or 734 | multiple indexes. 735 | 736 | Arguments: 737 | ---- 738 | parameter_list {List} -- A list of paramater values assigned to an argument. 739 | 740 | Usage: 741 | ---- 742 | >>> SessionObject._prepare_arguments_list(parameter_list=['MSFT','SQ']) 743 | 744 | Returns: 745 | ---- 746 | {str} -- The joined list. 747 | 748 | """ 749 | 750 | # validate it's a list. 751 | if type(parameter_list) is list: 752 | 753 | # specify the delimiter and join the list. 754 | delimiter = ',' 755 | parameter_list = delimiter.join(parameter_list) 756 | 757 | return parameter_list 758 | 759 | """ 760 | SESSION ENDPOINTS 761 | """ 762 | 763 | def validate(self) -> Dict: 764 | """Validates the current session for the SSO user.""" 765 | 766 | # define request components 767 | endpoint = r'sso/validate' 768 | req_type = 'GET' 769 | content = self._make_request( 770 | endpoint=endpoint, 771 | req_type=req_type 772 | ) 773 | 774 | return content 775 | 776 | def tickle(self) -> Dict: 777 | """Keeps the session open. 778 | 779 | If the gateway has not received any requests for several minutes an open session will 780 | automatically timeout. The tickle endpoint pings the server to prevent the 781 | session from ending. 782 | """ 783 | 784 | # define request components 785 | endpoint = r'tickle' 786 | req_type = 'POST' 787 | content = self._make_request( 788 | endpoint=endpoint, 789 | req_type=req_type 790 | ) 791 | 792 | return content 793 | 794 | def logout(self) -> Dict: 795 | """Logs the session out. 796 | 797 | Overview: 798 | ---- 799 | Logs the user out of the gateway session. Any further 800 | activity requires re-authentication. 801 | 802 | Returns: 803 | ---- 804 | (dict): A logout response. 805 | """ 806 | 807 | # Define request components. 808 | endpoint = r'logout' 809 | req_type = 'POST' 810 | content = self._make_request( 811 | endpoint=endpoint, 812 | req_type=req_type 813 | ) 814 | 815 | return content 816 | 817 | def reauthenticate(self) -> Dict: 818 | """Reauthenticates an existing session. 819 | 820 | Overview: 821 | ---- 822 | Provides a way to reauthenticate to the Brokerage 823 | system as long as there is a valid SSO session, 824 | see /sso/validate. 825 | 826 | Returns: 827 | ---- 828 | (dict): A reauthentication response. 829 | """ 830 | 831 | # Define request components. 832 | endpoint = r'iserver/reauthenticate' 833 | req_type = 'POST' 834 | 835 | # Make the request. 836 | content = self._make_request( 837 | endpoint=endpoint, 838 | req_type=req_type 839 | ) 840 | 841 | return content 842 | 843 | def is_authenticated(self, check: bool = False) -> Dict: 844 | """Checks if session is authenticated. 845 | 846 | Overview: 847 | ---- 848 | Current Authentication status to the Brokerage system. Market Data and 849 | Trading is not possible if not authenticated, e.g. authenticated 850 | shows `False`. 851 | 852 | Returns: 853 | ---- 854 | (dict): A dictionary with an authentication flag. 855 | """ 856 | 857 | # define request components 858 | endpoint = 'iserver/auth/status' 859 | 860 | if not check: 861 | req_type = 'POST' 862 | else: 863 | req_type = 'GET' 864 | 865 | content = self._make_request( 866 | endpoint=endpoint, 867 | req_type=req_type, 868 | headers='none' 869 | ) 870 | 871 | return content 872 | 873 | def _fundamentals_summary(self, conid: str) -> Dict: 874 | """Grabs a financial summary of a company. 875 | 876 | Return a financial summary for specific Contract ID. The financial summary 877 | includes key ratios and descriptive components of the Contract ID. 878 | 879 | Arguments: 880 | ---- 881 | conid {str} -- The contract ID. 882 | 883 | Returns: 884 | ---- 885 | {Dict} -- The response dictionary. 886 | """ 887 | 888 | # define request components 889 | endpoint = 'iserver/fundamentals/{}/summary'.format(conid) 890 | req_type = 'GET' 891 | content = self._make_request( 892 | endpoint=endpoint, 893 | req_type=req_type 894 | ) 895 | 896 | return content 897 | 898 | def _fundamentals_financials(self, conid: str, financial_statement: str, period: str = 'annual') -> Dict: 899 | """Grabs fundamental financial data. 900 | 901 | Overview: 902 | ---- 903 | Return a financial summary for specific Contract ID. The financial summary 904 | includes key ratios and descriptive components of the Contract ID. 905 | 906 | Arguments: 907 | ---- 908 | conid (str): The contract ID. 909 | 910 | financial_statement (str): The specific financial statement you wish to request 911 | for the Contract ID. Possible values are ['balance','cash','income'] 912 | 913 | period (str, optional): The specific period you wish to see. 914 | Possible values are ['annual','quarter']. Defaults to 'annual'. 915 | 916 | Returns: 917 | ---- 918 | Dict: Financial data for the specified contract ID. 919 | """ 920 | 921 | # define the period 922 | if period == 'annual': 923 | period = True 924 | else: 925 | period = False 926 | 927 | # Build the arguments. 928 | params = { 929 | 'type': financial_statement, 930 | 'annual': period 931 | } 932 | 933 | # define request components 934 | endpoint = 'tws.proxy/fundamentals/financials/{}'.format(conid) 935 | req_type = 'GET' 936 | content = self._make_request( 937 | endpoint=endpoint, 938 | req_type=req_type, 939 | params=params 940 | ) 941 | 942 | return content 943 | 944 | def _fundamentals_key_ratios(self, conid: str) -> Dict: 945 | """Returns analyst ratings for a specific conid. 946 | 947 | NAME: conid 948 | DESC: The contract ID. 949 | TYPE: String 950 | """ 951 | 952 | # Build the arguments. 953 | params = { 954 | 'widgets': 'key_ratios' 955 | } 956 | 957 | # define request components 958 | endpoint = 'fundamentals/landing/{}'.format(conid) 959 | req_type = 'GET' 960 | content = self._make_request( 961 | endpoint=endpoint, 962 | req_type=req_type, 963 | params=params 964 | ) 965 | 966 | return content 967 | 968 | def _fundamentals_dividends(self, conid: str) -> Dict: 969 | """Returns analyst ratings for a specific conid. 970 | 971 | NAME: conid 972 | DESC: The contract ID. 973 | TYPE: String 974 | """ 975 | 976 | # Build the arguments. 977 | params = { 978 | 'widgets': 'dividends' 979 | } 980 | 981 | # define request components 982 | endpoint = 'fundamentals/landing/{}'.format(conid) 983 | req_type = 'GET' 984 | content = self._make_request( 985 | endpoint=endpoint, 986 | req_type=req_type, 987 | params=params 988 | ) 989 | 990 | return content 991 | 992 | def _fundamentals_esg(self, conid: str) -> Dict: 993 | """ 994 | Returns analyst ratings for a specific conid. 995 | 996 | NAME: conid 997 | DESC: The contract ID. 998 | TYPE: String 999 | 1000 | """ 1001 | 1002 | # Build the arguments. 1003 | params = { 1004 | 'widgets': 'esg' 1005 | } 1006 | 1007 | # define request components 1008 | endpoint = 'fundamentals/landing/{}'.format(conid) 1009 | req_type = 'GET' 1010 | content = self._make_request( 1011 | endpoint=endpoint, 1012 | req_type=req_type, 1013 | params=params 1014 | ) 1015 | 1016 | return content 1017 | 1018 | def _data_news(self, conid: str) -> Dict: 1019 | """ 1020 | Return a financial summary for specific Contract ID. The financial summary 1021 | includes key ratios and descriptive components of the Contract ID. 1022 | 1023 | NAME: conid 1024 | DESC: The contract ID. 1025 | TYPE: String 1026 | """ 1027 | 1028 | # Build the arguments. 1029 | params = { 1030 | 'widgets': 'news', 1031 | 'lang': 'en' 1032 | } 1033 | 1034 | # define request components 1035 | endpoint = 'fundamentals/landing/{}'.format(conid) 1036 | req_type = 'GET' 1037 | content = self._make_request( 1038 | endpoint=endpoint, 1039 | req_type=req_type, 1040 | params=params 1041 | ) 1042 | 1043 | return content 1044 | 1045 | def _data_ratings(self, conid: str) -> Dict: 1046 | """Returns analyst ratings for a specific conid. 1047 | 1048 | NAME: conid 1049 | DESC: The contract ID. 1050 | TYPE: String 1051 | """ 1052 | 1053 | # Build the arguments. 1054 | params = { 1055 | 'widgets': 'ratings' 1056 | } 1057 | 1058 | # define request components 1059 | endpoint = 'fundamentals/landing/{}'.format(conid) 1060 | req_type = 'GET' 1061 | content = self._make_request( 1062 | endpoint=endpoint, 1063 | req_type=req_type, 1064 | params=params 1065 | ) 1066 | 1067 | return content 1068 | 1069 | def _data_events(self, conid: str) -> Dict: 1070 | """Returns analyst ratings for a specific conid. 1071 | 1072 | NAME: conid 1073 | DESC: The contract ID. 1074 | TYPE: String 1075 | """ 1076 | 1077 | # Build the arguments. 1078 | params = { 1079 | 'widgets': 'ratings' 1080 | } 1081 | 1082 | # define request components 1083 | endpoint = 'fundamentals/landing/{}'.format(conid) 1084 | req_type = 'GET' 1085 | content = self._make_request( 1086 | endpoint=endpoint, 1087 | req_type=req_type, 1088 | params=params 1089 | ) 1090 | 1091 | return content 1092 | 1093 | def _data_ownership(self, conid: str) -> Dict: 1094 | """Returns analyst ratings for a specific conid. 1095 | 1096 | NAME: conid 1097 | DESC: The contract ID. 1098 | TYPE: String 1099 | """ 1100 | 1101 | # Build the arguments. 1102 | params = { 1103 | 'widgets': 'ownership' 1104 | } 1105 | 1106 | # define request components 1107 | endpoint = 'fundamentals/landing/{}'.format(conid) 1108 | req_type = 'GET' 1109 | content = self._make_request( 1110 | endpoint=endpoint, 1111 | req_type=req_type, 1112 | params=params 1113 | ) 1114 | 1115 | return content 1116 | 1117 | def _data_competitors(self, conid: str) -> Dict: 1118 | """Returns analyst ratings for a specific conid. 1119 | 1120 | NAME: conid 1121 | DESC: The contract ID. 1122 | TYPE: String 1123 | """ 1124 | 1125 | # Build the arguments. 1126 | params = { 1127 | 'widgets': 'competitors' 1128 | } 1129 | 1130 | # define request components 1131 | endpoint = 'fundamentals/landing/{}'.format(conid) 1132 | req_type = 'GET' 1133 | content = self._make_request( 1134 | endpoint=endpoint, 1135 | req_type=req_type, 1136 | params=params 1137 | ) 1138 | 1139 | return content 1140 | 1141 | def _data_analyst_forecast(self, conid: str) -> Dict: 1142 | """Returns analyst ratings for a specific conid. 1143 | 1144 | NAME: conid 1145 | DESC: The contract ID. 1146 | TYPE: String 1147 | """ 1148 | 1149 | # Build the arguments. 1150 | params = { 1151 | 'widgets': 'analyst_forecast' 1152 | } 1153 | 1154 | # define request components 1155 | endpoint = 'fundamentals/landing/{}'.format(conid) 1156 | req_type = 'GET' 1157 | content = self._make_request( 1158 | endpoint=endpoint, 1159 | req_type=req_type, 1160 | params=params 1161 | ) 1162 | 1163 | return content 1164 | 1165 | def market_data(self, conids: List[str], since: str, fields: List[str]) -> Dict: 1166 | """ 1167 | Get Market Data for the given conid(s). The end-point will return by 1168 | default bid, ask, last, change, change pct, close, listing exchange. 1169 | See response fields for a list of available fields that can be request 1170 | via fields argument. The endpoint /iserver/accounts should be called 1171 | prior to /iserver/marketdata/snapshot. To receive all available fields 1172 | the /snapshot endpoint will need to be called several times. 1173 | 1174 | NAME: conid 1175 | DESC: The list of contract IDs you wish to pull current quotes for. 1176 | TYPE: List 1177 | 1178 | NAME: since 1179 | DESC: Time period since which updates are required. 1180 | Uses epoch time with milliseconds. 1181 | TYPE: String 1182 | 1183 | NAME: fields 1184 | DESC: List of fields you wish to retrieve for each quote. 1185 | TYPE: List 1186 | """ 1187 | 1188 | # define request components 1189 | endpoint = 'iserver/marketdata/snapshot' 1190 | req_type = 'GET' 1191 | 1192 | # join the two list arguments so they are both a single string. 1193 | conids_joined = self._prepare_arguments_list(parameter_list=conids) 1194 | 1195 | if fields is not None: 1196 | fields_joined = ",".join(str(n) for n in fields) 1197 | else: 1198 | fields_joined = "" 1199 | 1200 | # define the parameters 1201 | if since is None: 1202 | params = { 1203 | 'conids': conids_joined, 1204 | 'fields': fields_joined 1205 | } 1206 | else: 1207 | params = { 1208 | 'conids': conids_joined, 1209 | 'since': since, 1210 | 'fields': fields_joined 1211 | } 1212 | 1213 | content = self._make_request( 1214 | endpoint=endpoint, 1215 | req_type=req_type, 1216 | params=params 1217 | ) 1218 | 1219 | return content 1220 | 1221 | def market_data_history(self, conid: str, period: str, bar: str) -> Dict: 1222 | """ 1223 | Get history of market Data for the given conid, length of data is controlled by period and 1224 | bar. e.g. 1y period with bar=1w returns 52 data points. 1225 | 1226 | NAME: conid 1227 | DESC: The contract ID for a given instrument. If you don't know the contract ID use the 1228 | `search_by_symbol_or_name` endpoint to retrieve it. 1229 | TYPE: String 1230 | 1231 | NAME: period 1232 | DESC: Specifies the period of look back. For example 1y means looking back 1 year from today. 1233 | Possible values are ['1d','1w','1m','1y'] 1234 | TYPE: String 1235 | 1236 | NAME: bar 1237 | DESC: Specifies granularity of data. For example, if bar = '1h' the data will be at an hourly level. 1238 | Possible values are ['5min','1h','1w'] 1239 | TYPE: String 1240 | """ 1241 | 1242 | # define request components 1243 | endpoint = 'iserver/marketdata/history' 1244 | req_type = 'GET' 1245 | params = { 1246 | 'conid': conid, 1247 | 'period': period, 1248 | 'bar': bar 1249 | } 1250 | 1251 | content = self._make_request( 1252 | endpoint=endpoint, 1253 | req_type=req_type, 1254 | params=params 1255 | ) 1256 | 1257 | return content 1258 | 1259 | def server_accounts(self): 1260 | """ 1261 | Returns a list of accounts the user has trading access to, their 1262 | respective aliases and the currently selected account. Note this 1263 | endpoint must be called before modifying an order or querying 1264 | open orders. 1265 | """ 1266 | 1267 | # define request components 1268 | endpoint = 'iserver/accounts' 1269 | req_type = 'GET' 1270 | content = self._make_request( 1271 | endpoint=endpoint, 1272 | req_type=req_type 1273 | ) 1274 | 1275 | return content 1276 | 1277 | def update_server_account(self, account_id: str, check: bool = False) -> Dict: 1278 | """ 1279 | If an user has multiple accounts, and user wants to get orders, trades, 1280 | etc. of an account other than currently selected account, then user 1281 | can update the currently selected account using this API and then can 1282 | fetch required information for the newly updated account. 1283 | 1284 | NAME: account_id 1285 | DESC: The account ID you wish to set for the API Session. This will be used to 1286 | grab historical data and make orders. 1287 | TYPE: String 1288 | """ 1289 | 1290 | # define request components 1291 | endpoint = 'iserver/account' 1292 | req_type = 'POST' 1293 | params = { 1294 | 'acctId': account_id 1295 | } 1296 | 1297 | content = self._make_request( 1298 | endpoint=endpoint, 1299 | req_type=req_type, 1300 | params=params 1301 | ) 1302 | 1303 | return content 1304 | 1305 | def server_account_pnl(self): 1306 | """ 1307 | Returns an object containing PnLfor the selected account and its models 1308 | (if any). 1309 | """ 1310 | 1311 | # define request components 1312 | endpoint = 'iserver/account/pnl/partitioned' 1313 | req_type = 'GET' 1314 | content = self._make_request( 1315 | endpoint=endpoint, 1316 | req_type=req_type 1317 | ) 1318 | 1319 | return content 1320 | 1321 | def symbol_search(self, symbol: str) -> Dict: 1322 | """ 1323 | Performs a symbol search for a given symbol and returns 1324 | information related to the symbol including the contract id. 1325 | """ 1326 | 1327 | # define the request components 1328 | endpoint = 'iserver/secdef/search' 1329 | req_type = 'POST' 1330 | payload = { 1331 | 'symbol': symbol 1332 | } 1333 | 1334 | content = self._make_request( 1335 | endpoint=endpoint, 1336 | req_type=req_type, 1337 | json=payload 1338 | ) 1339 | 1340 | return content 1341 | 1342 | def contract_details(self, conid: str) -> Dict: 1343 | """ 1344 | Get contract details, you can use this to prefill your order before you submit an order. 1345 | 1346 | NAME: conid 1347 | DESC: The contract ID you wish to get details for. 1348 | TYPE: String 1349 | 1350 | RTYPE: Dictionary 1351 | """ 1352 | 1353 | # define the request components 1354 | endpoint = '/iserver/contract/{conid}/info'.format(conid=conid) 1355 | req_type = 'GET' 1356 | content = self._make_request( 1357 | endpoint=endpoint, 1358 | req_type=req_type 1359 | ) 1360 | 1361 | return content 1362 | 1363 | def contracts_definitions(self, conids: List[str]) -> Dict: 1364 | """ 1365 | Returns a list of security definitions for the given conids. 1366 | 1367 | NAME: conids 1368 | DESC: A list of contract IDs you wish to get details for. 1369 | TYPE: List 1370 | 1371 | RTYPE: Dictionary 1372 | """ 1373 | 1374 | # Define the request components. 1375 | endpoint = '/trsrv/secdef' 1376 | req_type = 'POST' 1377 | payload = { 1378 | 'conids': conids 1379 | } 1380 | 1381 | content = self._make_request( 1382 | endpoint=endpoint, 1383 | req_type=req_type, 1384 | json=payload 1385 | ) 1386 | 1387 | return content 1388 | 1389 | def futures_search(self, symbols: List[str]) -> Dict: 1390 | """ 1391 | Returns a list of non-expired future contracts for given symbol(s). 1392 | 1393 | NAME: Symbol 1394 | DESC: List of case-sensitive symbols separated by comma. 1395 | TYPE: List 1396 | 1397 | RTYPE: Dictionary 1398 | """ 1399 | 1400 | # define the request components 1401 | endpoint = '/trsrv/futures' 1402 | req_type = 'GET' 1403 | params = { 1404 | 'symbols': '{}'.format(','.join(symbols)) 1405 | } 1406 | 1407 | content = self._make_request( 1408 | endpoint=endpoint, 1409 | req_type=req_type, 1410 | params=params 1411 | ) 1412 | 1413 | return content 1414 | 1415 | def symbols_search_list(self, symbols: List[str]) -> Dict: 1416 | """ 1417 | Returns a list of non-expired future contracts for given symbol(s). 1418 | 1419 | NAME: Symbol 1420 | DESC: List of case-sensitive symbols separated by comma. 1421 | TYPE: List 1422 | 1423 | RTYPE: Dictionary 1424 | """ 1425 | 1426 | # define the request components 1427 | endpoint = '/trsrv/stocks' 1428 | req_type = 'GET' 1429 | params = {'symbols': '{}'.format(','.join(symbols))} 1430 | content = self._make_request( 1431 | endpoint=endpoint, 1432 | req_type=req_type, 1433 | params=params 1434 | ) 1435 | 1436 | return content 1437 | 1438 | def portfolio_accounts(self): 1439 | """ 1440 | In non-tiered account structures, returns a list of accounts for which the 1441 | user can view position and account information. This endpoint must be called prior 1442 | to calling other /portfolio endpoints for those accounts. For querying a list of accounts 1443 | which the user can trade, see /iserver/accounts. For a list of subaccounts in tiered account 1444 | structures (e.g. financial advisor or ibroker accounts) see /portfolio/subaccounts. 1445 | 1446 | """ 1447 | 1448 | # define request components 1449 | endpoint = 'portfolio/accounts' 1450 | req_type = 'GET' 1451 | content = self._make_request( 1452 | endpoint=endpoint, 1453 | req_type=req_type 1454 | ) 1455 | 1456 | return content 1457 | 1458 | def portfolio_sub_accounts(self): 1459 | """ 1460 | Used in tiered account structures (such as financial advisor and ibroker accounts) to return a 1461 | list of sub-accounts for which the user can view position and account-related information. This 1462 | endpoint must be called prior to calling other /portfolio endpoints for those subaccounts. To 1463 | query a list of accounts the user can trade, see /iserver/accounts. 1464 | 1465 | """ 1466 | 1467 | # define request components 1468 | endpoint = r'​portfolio/subaccounts' 1469 | req_type = 'GET' 1470 | content = self._make_request( 1471 | endpoint=endpoint, 1472 | req_type=req_type 1473 | ) 1474 | 1475 | return content 1476 | 1477 | def portfolio_account_info(self, account_id: str) -> Dict: 1478 | """ 1479 | Used in tiered account structures (such as financial advisor and ibroker accounts) to return a 1480 | list of sub-accounts for which the user can view position and account-related information. This 1481 | endpoint must be called prior to calling other /portfolio endpoints for those subaccounts. To 1482 | query a list of accounts the user can trade, see /iserver/accounts. 1483 | 1484 | NAME: account_id 1485 | DESC: The account ID you wish to return info for. 1486 | TYPE: String 1487 | """ 1488 | 1489 | # define request components 1490 | endpoint = r'portfolio/{}/meta'.format(account_id) 1491 | req_type = 'GET' 1492 | content = self._make_request( 1493 | endpoint=endpoint, 1494 | req_type=req_type 1495 | ) 1496 | 1497 | return content 1498 | 1499 | def portfolio_account_summary(self, account_id: str) -> Dict: 1500 | """ 1501 | Returns information about margin, cash balances and other information 1502 | related to specified account. See also /portfolio/{accountId}/ledger. 1503 | /portfolio/accounts or /portfolio/subaccounts must be called 1504 | prior to this endpoint. 1505 | 1506 | NAME: account_id 1507 | DESC: The account ID you wish to return info for. 1508 | TYPE: String 1509 | """ 1510 | 1511 | # define request components 1512 | endpoint = r'portfolio/{}/summary'.format(account_id) 1513 | req_type = 'GET' 1514 | content = self._make_request(endpoint=endpoint, req_type=req_type) 1515 | 1516 | return content 1517 | 1518 | def portfolio_account_ledger(self, account_id: str) -> Dict: 1519 | """ 1520 | Information regarding settled cash, cash balances, etc. in the account's 1521 | base currency and any other cash balances hold in other currencies. /portfolio/accounts 1522 | or /portfolio/subaccounts must be called prior to this endpoint. The list of supported 1523 | currencies is available at https://www.interactivebrokers.com/en/index.php?f=3185. 1524 | 1525 | NAME: account_id 1526 | DESC: The account ID you wish to return info for. 1527 | TYPE: String 1528 | """ 1529 | 1530 | # define request components 1531 | endpoint = r'portfolio/{}/ledger'.format(account_id) 1532 | req_type = 'GET' 1533 | content = self._make_request( 1534 | endpoint=endpoint, 1535 | req_type=req_type 1536 | ) 1537 | 1538 | return content 1539 | 1540 | def portfolio_account_allocation(self, account_id: str) -> Dict: 1541 | """ 1542 | Information about the account's portfolio allocation by Asset Class, Industry and 1543 | Category. /portfolio/accounts or /portfolio/subaccounts must be called prior to 1544 | this endpoint. 1545 | 1546 | NAME: account_id 1547 | DESC: The account ID you wish to return info for. 1548 | TYPE: String 1549 | """ 1550 | 1551 | # define request components 1552 | endpoint = r'portfolio/{}/allocation'.format(account_id) 1553 | req_type = 'GET' 1554 | content = self._make_request( 1555 | endpoint=endpoint, 1556 | req_type=req_type 1557 | ) 1558 | 1559 | return content 1560 | 1561 | def portfolio_accounts_allocation(self, account_ids: List[str]) -> Dict: 1562 | """ 1563 | Similar to /portfolio/{accountId}/allocation but returns a consolidated view of of all the 1564 | accounts returned by /portfolio/accounts. /portfolio/accounts or /portfolio/subaccounts must 1565 | be called prior to this endpoint. 1566 | 1567 | NAME: account_ids 1568 | DESC: A list of Account IDs you wish to return alloacation info for. 1569 | TYPE: List 1570 | """ 1571 | 1572 | # define request components 1573 | endpoint = r'portfolio/allocation' 1574 | req_type = 'POST' 1575 | payload = account_ids 1576 | content = self._make_request( 1577 | endpoint=endpoint, 1578 | req_type=req_type, 1579 | json=payload 1580 | ) 1581 | 1582 | return content 1583 | 1584 | def portfolio_account_positions(self, account_id: str, page_id: int = 0) -> Dict: 1585 | """ 1586 | Returns a list of positions for the given account. The endpoint supports paging, 1587 | page's default size is 30 positions. /portfolio/accounts or /portfolio/subaccounts 1588 | must be called prior to this endpoint. 1589 | 1590 | NAME: account_id 1591 | DESC: The account ID you wish to return positions for. 1592 | TYPE: String 1593 | 1594 | NAME: page_id 1595 | DESC: The page you wish to return if there are more than 1. The 1596 | default value is `0`. 1597 | TYPE: String 1598 | 1599 | ADDITIONAL ARGUMENTS NEED TO BE ADDED!!!!! 1600 | """ 1601 | 1602 | # define request components 1603 | endpoint = r'portfolio/{}/positions/{}'.format(account_id, page_id) 1604 | req_type = 'GET' 1605 | content = self._make_request( 1606 | endpoint=endpoint, 1607 | req_type=req_type 1608 | ) 1609 | 1610 | return content 1611 | 1612 | def portfolio_account_position(self, account_id: str, conid: str) -> Dict: 1613 | """ 1614 | Returns a list of all positions matching the conid. For portfolio models the conid 1615 | could be in more than one model, returning an array with the name of the model it 1616 | belongs to. /portfolio/accounts or /portfolio/subaccounts must be called prior to 1617 | this endpoint. 1618 | 1619 | NAME: account_id 1620 | DESC: The account ID you wish to return positions for. 1621 | TYPE: String 1622 | 1623 | NAME: conid 1624 | DESC: The contract ID you wish to find matching positions for. 1625 | TYPE: String 1626 | """ 1627 | 1628 | # Define request components. 1629 | endpoint = r'portfolio/{}/position/{}'.format(account_id, conid) 1630 | req_type = 'GET' 1631 | content = self._make_request( 1632 | endpoint=endpoint, 1633 | req_type=req_type 1634 | ) 1635 | 1636 | return content 1637 | 1638 | def portfolio_positions_invalidate(self, account_id: str) -> Dict: 1639 | """ 1640 | Invalidates the backend cache of the Portfolio. ??? 1641 | 1642 | NAME: account_id 1643 | DESC: The account ID you wish to return positions for. 1644 | TYPE: String 1645 | """ 1646 | 1647 | # Define request components. 1648 | endpoint = r'portfolio/{}/positions/invalidate'.format(account_id) 1649 | req_type = 'POST' 1650 | content = self._make_request( 1651 | endpoint=endpoint, 1652 | req_type=req_type 1653 | ) 1654 | 1655 | return content 1656 | 1657 | def portfolio_positions(self, conid: str) -> Dict: 1658 | """ 1659 | Returns an object of all positions matching the conid for all the selected accounts. 1660 | For portfolio models the conid could be in more than one model, returning an array 1661 | with the name of the model it belongs to. /portfolio/accounts or /portfolio/subaccounts 1662 | must be called prior to this endpoint. 1663 | 1664 | NAME: conid 1665 | DESC: The contract ID you wish to find matching positions for. 1666 | TYPE: String 1667 | """ 1668 | 1669 | # Define request components. 1670 | endpoint = r'portfolio/positions/{}'.format(conid) 1671 | req_type = 'GET' 1672 | content = self._make_request( 1673 | endpoint=endpoint, 1674 | req_type=req_type 1675 | ) 1676 | 1677 | return content 1678 | 1679 | def trades(self): 1680 | """ 1681 | Returns a list of trades for the currently selected account for current day and 1682 | six previous days. 1683 | """ 1684 | 1685 | # define request components 1686 | endpoint = r'iserver/account/trades' 1687 | req_type = 'GET' 1688 | content = self._make_request( 1689 | endpoint=endpoint, 1690 | req_type=req_type 1691 | ) 1692 | 1693 | return content 1694 | 1695 | def get_live_orders(self): 1696 | """ 1697 | The end-point is meant to be used in polling mode, e.g. requesting every 1698 | x seconds. The response will contain two objects, one is notification, the 1699 | other is orders. Orders is the list of orders (cancelled, filled, submitted) 1700 | with activity in the current day. Notifications contains information about 1701 | execute orders as they happen, see status field. 1702 | """ 1703 | 1704 | # define request components 1705 | endpoint = r'iserver/account/orders' 1706 | req_type = 'GET' 1707 | content = self._make_request( 1708 | endpoint=endpoint, 1709 | req_type=req_type 1710 | ) 1711 | 1712 | return content 1713 | 1714 | def get_order_status(self,trade_id:str): 1715 | """ 1716 | The end-point is meant to be used in polling mode, e.g. requesting every 1717 | x seconds. It queries the status of a live order with IB's order_id. 1718 | The response will contain a list of informaiton about the order. 1719 | """ 1720 | 1721 | # define request components 1722 | endpoint = r'iserver/account/order/status/{}'.format(trade_id) 1723 | req_type = 'GET' 1724 | content = self._make_request( 1725 | endpoint=endpoint, 1726 | req_type=req_type 1727 | ) 1728 | 1729 | return content 1730 | 1731 | def place_order(self, account_id: str, order: dict) -> Dict: 1732 | """ 1733 | Please note here, sometimes this end-point alone can't make sure you submit the order 1734 | successfully, you could receive some questions in the response, you have to to answer 1735 | them in order to submit the order successfully. You can use "/iserver/reply/{replyid}" 1736 | end-point to answer questions. 1737 | 1738 | NAME: account_id 1739 | DESC: The account ID you wish to place an order for. 1740 | TYPE: String 1741 | 1742 | NAME: order 1743 | DESC: Either an IBOrder object or a dictionary with the specified payload. 1744 | TYPE: IBOrder or Dict 1745 | """ 1746 | 1747 | if type(order) is dict: 1748 | order = order 1749 | else: 1750 | order = order.create_order() 1751 | 1752 | # define request components 1753 | endpoint = r'iserver/account/{}/order'.format(account_id) 1754 | req_type = 'POST' 1755 | content = self._make_request( 1756 | endpoint=endpoint, 1757 | req_type=req_type, 1758 | json=order 1759 | ) 1760 | 1761 | return content 1762 | 1763 | def place_orders(self, account_id: str, orders: List[Dict]) -> Dict: 1764 | """ 1765 | An extension of the `place_order` endpoint but allows for a list of orders. Those orders may be 1766 | either a list of dictionary objects or a list of IBOrder objects. 1767 | 1768 | NAME: account_id 1769 | DESC: The account ID you wish to place an order for. 1770 | TYPE: String 1771 | 1772 | NAME: orders 1773 | DESC: Either a list of IBOrder objects or a list of dictionaries with the specified payload. 1774 | TYPE: List or List 1775 | """ 1776 | 1777 | # EXTENDED THIS 1778 | if type(orders) is list: 1779 | orders = orders 1780 | else: 1781 | orders = orders 1782 | 1783 | # define request components 1784 | endpoint = r'iserver/account/{}/orders'.format(account_id) 1785 | req_type = 'POST' 1786 | content = self._make_request( 1787 | endpoint=endpoint, 1788 | req_type=req_type, 1789 | json=orders 1790 | ) 1791 | 1792 | return content 1793 | 1794 | def place_order_scenario(self, account_id: str, order: dict) -> Dict: 1795 | """ 1796 | This end-point allows you to preview order without actually submitting the 1797 | order and you can get commission information in the response. 1798 | 1799 | NAME: account_id 1800 | DESC: The account ID you wish to place an order for. 1801 | TYPE: String 1802 | 1803 | NAME: order 1804 | DESC: Either an IBOrder object or a dictionary with the specified payload. 1805 | TYPE: IBOrder or Dict 1806 | """ 1807 | 1808 | if type(order) is dict: 1809 | order = order 1810 | else: 1811 | order = order.create_order() 1812 | 1813 | # define request components 1814 | endpoint = r'iserver/account/{}/order/whatif'.format(account_id) 1815 | req_type = 'POST' 1816 | content = self._make_request( 1817 | endpoint=endpoint, 1818 | req_type=req_type, 1819 | json=order 1820 | ) 1821 | 1822 | return content 1823 | 1824 | def place_order_reply(self, reply_id: str = None, reply: bool = True): 1825 | """ 1826 | An extension of the `place_order` endpoint but allows for a list of orders. Those orders may be 1827 | either a list of dictionary objects or a list of IBOrder objects. 1828 | 1829 | NAME: account_id 1830 | DESC: The account ID you wish to place an order for. 1831 | TYPE: String 1832 | 1833 | NAME: orders 1834 | DESC: Either a list of IBOrder objects or a list of dictionaries with the specified payload. 1835 | TYPE: List or List 1836 | """ 1837 | 1838 | # define request components 1839 | endpoint = r'iserver/reply/{}'.format(reply_id) 1840 | req_type = 'POST' 1841 | reply = { 1842 | 'confirmed': reply 1843 | } 1844 | 1845 | content = self._make_request( 1846 | endpoint=endpoint, 1847 | req_type=req_type, 1848 | json=reply 1849 | ) 1850 | 1851 | return content 1852 | 1853 | def modify_order(self, account_id: str, customer_order_id: str, order: dict) -> Dict: 1854 | """ 1855 | Modifies an open order. The /iserver/accounts endpoint must first 1856 | be called. 1857 | 1858 | NAME: account_id 1859 | DESC: The account ID you wish to place an order for. 1860 | TYPE: String 1861 | 1862 | NAME: customer_order_id 1863 | DESC: The customer order ID for the order you wish to MODIFY. 1864 | TYPE: String 1865 | 1866 | NAME: order 1867 | DESC: Either an IBOrder object or a dictionary with the specified payload. 1868 | TYPE: IBOrder or Dict 1869 | """ 1870 | 1871 | if type(order) is dict: 1872 | order = order 1873 | else: 1874 | order = order.create_order() 1875 | 1876 | # define request components 1877 | endpoint = r'iserver/account/{}/order/{}'.format( 1878 | account_id, customer_order_id) 1879 | req_type = 'POST' 1880 | content = self._make_request( 1881 | endpoint=endpoint, 1882 | req_type=req_type, 1883 | json=order 1884 | ) 1885 | 1886 | return content 1887 | 1888 | def delete_order(self, account_id: str, customer_order_id: str) -> Dict: 1889 | """Deletes the order specified by the customer order ID. 1890 | 1891 | NAME: account_id 1892 | DESC: The account ID you wish to place an order for. 1893 | TYPE: String 1894 | 1895 | NAME: customer_order_id 1896 | DESC: The customer order ID for the order you wish to DELETE. 1897 | TYPE: String 1898 | """ 1899 | 1900 | # define request components 1901 | endpoint = r'iserver/account/{}/order/{}'.format( 1902 | account_id, customer_order_id) 1903 | req_type = 'DELETE' 1904 | content = self._make_request( 1905 | endpoint=endpoint, 1906 | req_type=req_type 1907 | ) 1908 | 1909 | return content 1910 | 1911 | def get_scanners(self): 1912 | """Returns an object contains four lists contain all parameters for scanners. 1913 | 1914 | RTYPE Dictionary 1915 | """ 1916 | # define request components 1917 | endpoint = r'iserver/scanner/params' 1918 | req_type = 'GET' 1919 | content = self._make_request( 1920 | endpoint=endpoint, 1921 | req_type=req_type 1922 | ) 1923 | 1924 | return content 1925 | 1926 | def run_scanner(self, instrument: str, scanner_type: str, location: str, size: str = '25', filters: List[dict] = None) -> Dict: 1927 | """Run a scanner to get a list of contracts. 1928 | 1929 | NAME: instrument 1930 | DESC: The type of financial instrument you want to scan for. 1931 | TYPE: String 1932 | 1933 | NAME: scanner_type 1934 | DESC: The Type of scanner you wish to run, defined by the scanner code. 1935 | TYPE: String 1936 | 1937 | NAME: location 1938 | DESC: The geographic location you wish to run the scan. For example (STK.US.MAJOR) 1939 | TYPE: String 1940 | 1941 | NAME: size 1942 | DESC: The number of results to return back. Defaults to 25. 1943 | TYPE: String 1944 | 1945 | NAME: filters 1946 | DESC: A list of dictionaries where the key is the filter you wish to set and the value is the value you want set 1947 | for that filter. 1948 | TYPE: List 1949 | 1950 | RTYPE Dictionary 1951 | """ 1952 | 1953 | # define request components 1954 | endpoint = r'iserver/scanner/run' 1955 | req_type = 'POST' 1956 | payload = { 1957 | "instrument": instrument, 1958 | "type": scanner_type, 1959 | "filter": filters, 1960 | "location": location, 1961 | "size": size 1962 | } 1963 | 1964 | content = self._make_request( 1965 | endpoint=endpoint, 1966 | req_type=req_type, 1967 | json=payload 1968 | ) 1969 | 1970 | return content 1971 | 1972 | def customer_info(self) -> Dict: 1973 | """Returns Applicant Id with all owner related entities 1974 | 1975 | RTYPE Dictionary 1976 | """ 1977 | 1978 | # define request components 1979 | endpoint = r'ibcust/entity/info' 1980 | req_type = 'GET' 1981 | content = self._make_request( 1982 | endpoint=endpoint, 1983 | req_type=req_type 1984 | ) 1985 | 1986 | return content 1987 | 1988 | def get_unread_messages(self) -> Dict: 1989 | """Returns the unread messages associated with the account. 1990 | 1991 | RTYPE Dictionary 1992 | """ 1993 | 1994 | # define request components 1995 | endpoint = r'fyi/unreadnumber' 1996 | req_type = 'GET' 1997 | content = self._make_request( 1998 | endpoint=endpoint, 1999 | req_type=req_type 2000 | ) 2001 | 2002 | return content 2003 | 2004 | def get_subscriptions(self) -> Dict: 2005 | """Return the current choices of subscriptions, we can toggle the option. 2006 | 2007 | RTYPE Dictionary 2008 | """ 2009 | 2010 | # define request components 2011 | endpoint = r'fyi/settings' 2012 | req_type = 'GET' 2013 | content = self._make_request( 2014 | endpoint=endpoint, 2015 | req_type=req_type 2016 | ) 2017 | 2018 | return content 2019 | 2020 | def change_subscriptions_status(self, type_code: str, enable: bool = True) -> Dict: 2021 | """Turns the subscription on or off. 2022 | 2023 | NAME: type_code 2024 | DESC: The subscription code you wish to change the status for. 2025 | TYPE: String 2026 | 2027 | NAME: enable 2028 | DESC: True if you want the subscription turned on, False if you want it turned of. 2029 | TYPE: Boolean 2030 | 2031 | RTYPE Dictionary 2032 | """ 2033 | 2034 | # define request components 2035 | endpoint = r'fyi/settings/{}' 2036 | req_type = 'POST' 2037 | payload = {'enable': enable} 2038 | content = self._make_request( 2039 | endpoint=endpoint, 2040 | req_type=req_type, 2041 | json=payload 2042 | ) 2043 | 2044 | return content 2045 | 2046 | def subscriptions_disclaimer(self, type_code: str) -> Dict: 2047 | """Returns the disclaimer for the specified subscription. 2048 | 2049 | NAME: type_code 2050 | DESC: The subscription code you wish to change the status for. 2051 | TYPE: String 2052 | 2053 | RTYPE Dictionary 2054 | """ 2055 | 2056 | # define request components 2057 | endpoint = r'fyi/disclaimer/{}' 2058 | req_type = 'GET' 2059 | content = self._make_request( 2060 | endpoint=endpoint, 2061 | req_type=req_type 2062 | ) 2063 | 2064 | return content 2065 | 2066 | def mark_subscriptions_disclaimer(self, type_code: str) -> Dict: 2067 | """Sets the specified disclaimer to read. 2068 | 2069 | NAME: type_code 2070 | DESC: The subscription code you wish to change the status for. 2071 | TYPE: String 2072 | 2073 | RTYPE Dictionary 2074 | """ 2075 | 2076 | # define request components 2077 | endpoint = r'fyi/disclaimer/{}' 2078 | req_type = 'PUT' 2079 | content = self._make_request( 2080 | endpoint=endpoint, 2081 | req_type=req_type 2082 | ) 2083 | 2084 | return content 2085 | 2086 | def subscriptions_delivery_options(self): 2087 | """Options for sending fyis to email and other devices. 2088 | 2089 | RTYPE Dictionary 2090 | """ 2091 | 2092 | # define request components 2093 | endpoint = r'fyi/deliveryoptions' 2094 | req_type = 'GET' 2095 | content = self._make_request( 2096 | endpoint=endpoint, 2097 | req_type=req_type 2098 | ) 2099 | 2100 | return content 2101 | 2102 | def mutual_funds_portfolios_and_fees(self, conid: str) -> Dict: 2103 | """Grab the Fees and objectives for a specified mutual fund. 2104 | 2105 | NAME: conid 2106 | DESC: The Contract ID for the mutual fund. 2107 | TYPE: String 2108 | 2109 | RTYPE Dictionary 2110 | """ 2111 | 2112 | # define request components 2113 | endpoint = r'fundamentals/mf_profile_and_fees/{mutual_fund_id}'.format( 2114 | mutual_fund_id=conid) 2115 | req_type = 'GET' 2116 | content = self._make_request( 2117 | endpoint=endpoint, 2118 | req_type=req_type 2119 | ) 2120 | 2121 | return content 2122 | 2123 | def mutual_funds_performance(self, conid: str, risk_period: str, yield_period: str, statistic_period: str) -> Dict: 2124 | """Grab the Lip Rating for a specified mutual fund. 2125 | 2126 | NAME: conid 2127 | DESC: The Contract ID for the mutual fund. 2128 | TYPE: String 2129 | 2130 | NAME: yield_period 2131 | DESC: The Period threshold for yield information 2132 | possible values: ['6M', '1Y', '3Y', '5Y', '10Y'] 2133 | TYPE: String 2134 | 2135 | NAME: risk_period 2136 | DESC: The Period threshold for risk information 2137 | possible values: ['6M', '1Y', '3Y', '5Y', '10Y'] 2138 | TYPE: String 2139 | 2140 | NAME: statistic_period 2141 | DESC: The Period threshold for statistic information 2142 | possible values: ['6M', '1Y', '3Y', '5Y', '10Y'] 2143 | TYPE: String 2144 | 2145 | RTYPE Dictionary 2146 | """ 2147 | 2148 | # define request components 2149 | endpoint = r'fundamentals/mf_performance/{mutual_fund_id}'.format( 2150 | mutual_fund_id=conid) 2151 | req_type = 'GET' 2152 | params = { 2153 | 'risk_period': None, 2154 | 'yield_period': None, 2155 | 'statistic_period': None 2156 | } 2157 | content = self._make_request( 2158 | endpoint=endpoint, 2159 | req_type=req_type, 2160 | params=params 2161 | ) 2162 | 2163 | return content 2164 | -------------------------------------------------------------------------------- /ibw/clientportal.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pathlib 3 | import requests 4 | import zipfile 5 | 6 | 7 | class ClientPortal(): 8 | 9 | def does_clientportal_directory_exist(self) -> bool: 10 | """Used to determine if the clientportal folder exist. 11 | 12 | Returns: 13 | bool: `True` if it exists, `False` otherwise. 14 | """ 15 | 16 | # Grab the clientportal folder. 17 | clientportal_folder: pathlib.Path = pathlib.Path(__file__).parent.joinpath( 18 | 'clientportal.gw' 19 | ).resolve() 20 | 21 | return clientportal_folder.exists() 22 | 23 | def make_clientportal_directory(self) -> None: 24 | """Makes the clientportal.gw folder if it doesn't exist.""" 25 | 26 | if not self.does_clientportal_directory_exist: 27 | clientportal_folder: pathlib.Path = pathlib.Path(__file__).parent.joinpath( 28 | 'clientportal.gw' 29 | ).resolve() 30 | clientportal_folder.mkdir(parents=True) 31 | 32 | def download_folder(self) -> str: 33 | """Defines the folder to download the Client Portal to. 34 | 35 | Returns: 36 | str: The path to the folder. 37 | """ 38 | 39 | # Define the download folder. 40 | download_folder = pathlib.Path(__file__).parent.joinpath( 41 | 'clientportal.gw' 42 | ).resolve() 43 | 44 | return download_folder 45 | 46 | def download_client_portal(self) -> requests.Response: 47 | """Downloads the Client Portal from Interactive Brokers. 48 | 49 | Returns: 50 | requests.Response: A response object with clientportal content. 51 | """ 52 | 53 | # Request the Client Portal 54 | response = requests.get( 55 | url='https://download2.interactivebrokers.com/portal/clientportal.gw.zip' 56 | ) 57 | 58 | return response 59 | 60 | def create_zip_file(self, response_content: requests.Response) -> zipfile.ZipFile: 61 | """Creates a zip file to house the client portal content. 62 | 63 | Arguments: 64 | ---- 65 | response_content (requests.Response): The response object with the 66 | client portal content. 67 | 68 | Returns: 69 | ---- 70 | zipfile.ZipFile: A zip file object with the Client Portal. 71 | """ 72 | 73 | # Download the Zip File. 74 | zip_file_content = zipfile.ZipFile( 75 | io.BytesIO(response_content.content) 76 | ) 77 | 78 | return zip_file_content 79 | 80 | def extract_zip_file(self, zip_file: zipfile.ZipFile) -> None: 81 | """Extracts the Zip File. 82 | 83 | Arguments: 84 | ---- 85 | zip_file (zipfile.ZipFile): The client portal zip file to be extracted. 86 | """ 87 | 88 | # Extract the Content to the new folder. 89 | zip_file.extractall(path="clientportal.gw") 90 | 91 | def download_and_extract(self) -> None: 92 | """Downloads and extracts the client portal object.""" 93 | 94 | # Make the resource directory if needed. 95 | self.make_clientportal_directory() 96 | 97 | # Download it. 98 | client_portal_response = self.download_client_portal() 99 | 100 | # Create a zip file. 101 | client_portal_zip = self.create_zip_file( 102 | response_content=client_portal_response 103 | ) 104 | 105 | # Extract it. 106 | self.extract_zip_file(zip_file=client_portal_zip) 107 | -------------------------------------------------------------------------------- /images/IBTB_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProScriptSlinger/Interactive-Brokers-Trading-Bot/aedb625e1921af90fad93563943f38ebd4eceb5a/images/IBTB_logo.png -------------------------------------------------------------------------------- /robot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProScriptSlinger/Interactive-Brokers-Trading-Bot/aedb625e1921af90fad93563943f38ebd4eceb5a/robot/__init__.py -------------------------------------------------------------------------------- /robot/indicator.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import numpy as np 3 | import pandas as pd 4 | 5 | from typing import Any 6 | from typing import List 7 | from typing import Dict 8 | from typing import Union 9 | from typing import Optional 10 | from typing import Tuple 11 | 12 | import robot.stock_frame as stock_frame 13 | 14 | class Indicators(): 15 | def __init__(self, price_df: stock_frame.StockFrame) -> None: 16 | 17 | self._stock_frame: stock_frame.StockFrame = price_df 18 | self._price_groups = self._stock_frame.symbol_groups 19 | self._current_indicators = {} #Instead of asking the user to call all the functions again when a new data row comes in, a wrapper is used to update each column 20 | #Indicators that user has assigned to the stock frame 21 | self._indicator_signals = {} #A dictionary of all the signals 22 | self._frame = self._stock_frame.frame 23 | 24 | self._indicators_comp_key = [] 25 | self._indicators_key = [] 26 | 27 | # For ticker_indicators 28 | self._ticker_indicator_signals = {} 29 | self._ticker_indicators_comp_key = [] 30 | self._ticker_indicators_key = [] 31 | 32 | def set_indicator_signal(self, indicator:str, buy: float, sell: float, condition_buy: Any, condition_sell: Any, buy_max: float = None, sell_max: float = None 33 | , condition_buy_max: Any = None, condition_sell_max: Any = None): 34 | #Each indicator has a buy signal and a sell signal, numeric threshold and operator (e.g. <,>) 35 | """Used to set an indicator where one indicator crosses above or below a certain numerical threshold. 36 | Arguments: 37 | ---- 38 | indicator {str} -- The indicator key, for example `ema` or `sma`. 39 | buy {float} -- The buy signal threshold for the indicator. 40 | 41 | sell {float} -- The sell signal threshold for the indicator. 42 | condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would 43 | represent greater than or from the `operator` module it would represent `operator.gt`. 44 | 45 | condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would 46 | represent greater than or from the `operator` module it would represent `operator.gt`. 47 | buy_max {float} -- If the buy threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 48 | This means if the signal exceeds this amount it WILL NOT PURCHASE THE INSTRUMENT. (defaults to None). 49 | 50 | sell_max {float} -- If the sell threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 51 | This means if the signal exceeds this amount it WILL NOT SELL THE INSTRUMENT. (defaults to None). 52 | condition_buy_max {str} -- The operator which is used to evaluate the `buy_max` condition. For example, `">"` would 53 | represent greater than or from the `operator` module it would represent `operator.gt`. (defaults to None). 54 | 55 | condition_sell_max {str} -- The operator which is used to evaluate the `sell_max` condition. For example, `">"` would 56 | represent greater than or from the `operator` module it would represent `operator.gt`. (defaults to None). 57 | """ 58 | # Add the key if it doesn't exist. If there is no signal for that indicator, set a template. 59 | if indicator not in self._indicator_signals: 60 | self._indicator_signals[indicator] = {} 61 | self._indicators_key.append(indicator) 62 | 63 | # Add the signals. 64 | self._indicator_signals[indicator]['buy'] = buy 65 | self._indicator_signals[indicator]['sell'] = sell 66 | self._indicator_signals[indicator]['buy_operator'] = condition_buy 67 | self._indicator_signals[indicator]['sell_operator'] = condition_sell 68 | 69 | # Add the max signals 70 | self._indicator_signals[indicator]['buy_max'] = buy_max 71 | self._indicator_signals[indicator]['sell_max'] = sell_max 72 | self._indicator_signals[indicator]['buy_operator_max'] = condition_buy_max 73 | self._indicator_signals[indicator]['sell_operator_max'] = condition_sell_max 74 | 75 | # An improved version of set_indicator_signal() as this allows indicator or strategy to be ticker-specific 76 | def set_ticker_indicator_signal(self, ticker:str, indicator:str, buy_cash_quantity:float, buy:float, sell:float, condition_buy: Any, condition_sell: Any, \ 77 | close_position_when_sell:bool=True, buy_max: float = None, sell_max: float = None, condition_buy_max: Any = None, condition_sell_max: Any = None): 78 | """Used to set an indicator for a ticker where one indicator crosses above or below a certain numerical threshold. 79 | 80 | Args: 81 | ticker (str): The ticker which you wish to set an indicator on 82 | indicator (str): The indicator key, e.g. 'ema','sma' 83 | buy_cash_quantity (float): The total amount of cash which you wish to allocate on this strategy 84 | buy (float): The buy signal threshold for the indicator 85 | sell (float): The sell signal threshold for the indicator 86 | condition_buy (Any): The operator which is used to evaluate the `buy` condition. For example, `">"` would 87 | represent greater than or from the `operator` module it would represent `operator.gt` 88 | condition_sell (Any): The operator which is used to evaluate the `sell` condition. For example, `">"` would 89 | represent greater than or from the `operator` module it would represent `operator.gt` 90 | close_position_when_sell (bool, optional): Sell all the positions held for that ticker when selling. Defaults to True. 91 | buy_max (float, optional): If the buy threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 92 | This means if the signal exceeds this amount it WILL NOT PURCHASE THE INSTRUMENT. Defaults to None. 93 | sell_max (float, optional): If the sell threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 94 | This means if the signal exceeds this amount it WILL NOT SELL THE INSTRUMENT. Defaults to None. 95 | condition_buy_max (Any, optional): The operator which is used to evaluate the `buy_max` condition. For example, `">"` would 96 | represent greater than or from the `operator` module it would represent `operator.gt` 97 | condition_sell_max (Any, optional): The operator which is used to evaluate the `sell_max` condition. For example, `">"` would 98 | represent greater than or from the `operator` module it would represent `operator.gt`. Defaults to None. 99 | """ 100 | 101 | # Check if ticker exists in the self._ticker_indicator_signals 102 | if ticker not in self._ticker_indicator_signals: 103 | self._ticker_indicator_signals[ticker] = {} 104 | 105 | # Check if indicator already exists in the dictionary 106 | if indicator not in self._ticker_indicator_signals[ticker]: 107 | self._ticker_indicator_signals[ticker][indicator] = {} 108 | self._ticker_indicators_key.append((ticker,indicator)) 109 | 110 | # Add the signals 111 | self._ticker_indicator_signals[ticker][indicator]['buy_cash_quantity'] = buy_cash_quantity 112 | self._ticker_indicator_signals[ticker][indicator]['close_position_when_sell'] = close_position_when_sell 113 | self._ticker_indicator_signals[ticker][indicator]['buy'] = buy 114 | self._ticker_indicator_signals[ticker][indicator]['sell'] = sell 115 | self._ticker_indicator_signals[ticker][indicator]['buy_operator'] = condition_buy 116 | self._ticker_indicator_signals[ticker][indicator]['sell_operator'] = condition_sell 117 | 118 | # Add the max signals 119 | self._ticker_indicator_signals[ticker][indicator]['buy_max'] = buy_max 120 | self._ticker_indicator_signals[ticker][indicator]['sell_max'] = sell_max 121 | self._ticker_indicator_signals[ticker][indicator]['buy_operator_max'] = condition_buy_max 122 | self._ticker_indicator_signals[ticker][indicator]['sell_operator_max'] = condition_sell_max 123 | 124 | 125 | #Another method for creating a signal would be when one indicator crosses above or below another indicator, so we need to compare the 2 here 126 | def set_indicator_signal_compare(self,indicator_1:str, indicator_2:str, condition_buy: Any, condition_sell: Any) -> None: 127 | """Used to set an indicator where one indicator is compared to another indicator. 128 | Overview: 129 | ---- 130 | Some trading strategies depend on comparing one indicator to another indicator. 131 | For example, the Simple Moving Average crossing above or below the Exponential 132 | Moving Average. This will be used to help build those strategies that depend 133 | on this type of structure. 134 | Arguments: 135 | ---- 136 | indicator_1 {str} -- The first indicator key, for example `ema` or `sma`. 137 | indicator_2 {str} -- The second indicator key, this is the indicator we will compare to. For example, 138 | is the `sma` greater than the `ema`. 139 | condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would 140 | represent greater than or from the `operator` module it would represent `operator.gt`. 141 | 142 | condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would 143 | represent greater than or from the `operator` module it would represent `operator.gt`. 144 | """ 145 | 146 | #define the key 147 | key = "{ind_1}_comp_{ind_2}".format( 148 | ind_1 = indicator_1, 149 | ind_2 = indicator_2 150 | ) 151 | 152 | #Add the key if it doesn't exist 153 | if key not in self._indicator_signals: 154 | self._indicator_signals[key] = {} 155 | self._indicators_comp_key.append(key) 156 | 157 | #Grab the dicionary 158 | indicator_dict = self._indicator_signals[key] 159 | 160 | #Add the signals 161 | indicator_dict['type'] = 'comparison' 162 | indicator_dict['indicator_1'] = indicator_1 163 | indicator_dict['indicator_2'] = indicator_2 164 | indicator_dict['buy_operator'] = condition_buy 165 | indicator_dict['sell_operator'] = condition_sell 166 | 167 | # An improved version of set_indicator_signal_compare() as this allows indicator to be ticker-specific 168 | def set_ticker_indicator_signal_compare(self,ticker:str,buy_cash_quantity:float,indicator_1:str, indicator_2:str, condition_buy: Any, condition_sell: Any, \ 169 | close_position_when_sell:bool=True) -> None: 170 | """Used to set an indicator where one indicator is compared to another indicator. 171 | Overview: 172 | ---- 173 | Some trading strategies depend on comparing one indicator to another indicator. 174 | For example, the Simple Moving Average crossing above or below the Exponential 175 | Moving Average. This will be used to help build those strategies that depend 176 | on this type of structure. 177 | Arguments: 178 | ---- 179 | ticker {str} -- Ticker 180 | buy_cash_quantity (float): The total amount of cash which you wish to allocate on this strategy 181 | indicator_1 {str} -- The first indicator key, for example `ema` or `sma`. 182 | indicator_2 {str} -- The second indicator key, this is the indicator we will compare to. For example, 183 | is the `sma` greater than the `ema`. 184 | condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would 185 | represent greater than or from the `operator` module it would represent `operator.gt`. 186 | condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would 187 | represent greater than or from the `operator` module it would represent `operator.gt`. 188 | close_position_when_sell {bool, optional} -- Sell all the positions held for that ticker when selling. Defaults to True. 189 | """ 190 | # Check if ticker exists in the self._ticker_indicator_signals 191 | if ticker not in self._ticker_indicator_signals: 192 | self._ticker_indicator_signals[ticker] = {} 193 | 194 | # Create a key 195 | key = tuple(ticker,f"{indicator_1}_comp_{indicator_2}") 196 | 197 | # Check if the key already exists in the dictionary 198 | if key not in self._ticker_indicator_signals[ticker]: 199 | self._ticker_indicator_signals[ticker][key] = {} 200 | self._ticker_indicators_comp_key.append(key) 201 | 202 | # Grab the key dictionary 203 | indicator_dict = self._ticker_indicator_signals[ticker][key] 204 | 205 | #Add the signals 206 | indicator_dict['type'] = 'comparison' 207 | indicator_dict['indicator_1'] = indicator_1 208 | indicator_dict['indicator_2'] = indicator_2 209 | indicator_dict['buy_operator'] = condition_buy 210 | indicator_dict['sell_operator'] = condition_sell 211 | indicator_dict['buy_cash_quantity'] = buy_cash_quantity 212 | indicator_dict['close_position_when_sell'] = close_position_when_sell 213 | 214 | 215 | def get_indicator_signal(self,indicator:str = None) -> Dict: 216 | """Return the raw Pandas Dataframe Object. 217 | Arguments: 218 | ---- 219 | indicator {Optional[str]} -- The indicator key, for example `ema` or `sma`. 220 | Returns: 221 | ---- 222 | {dict} -- Either all of the indicators or the specified indicator. 223 | """ 224 | if indicator and indicator in self._indicator_signals: #if user passes in indicator and it is in the indicator_signals dictionary 225 | return self._indicator_signals[indicator] 226 | else: #if user does not pass in any indicator, return all of them 227 | return self._indicator_signals 228 | 229 | @property 230 | def price_df(self) -> pd.DataFrame: 231 | return self._frame 232 | 233 | @price_df.setter 234 | def price_df(self,price_df:pd.DataFrame) -> None: 235 | self._frame = price_df 236 | 237 | def change_in_price(self,column_name:str = 'change_in_price') -> pd.DataFrame: 238 | """Calaculate the change in close price 239 | 240 | Args: 241 | column_name (str, optional): Pass in a value if you wish to change the column name. Defaults to 'change_in_price'. 242 | 243 | Returns: 244 | pd.DataFrame: Returns a pd dataframe with added column 'change_in_price' 245 | """ 246 | locals_data = locals() #Capture information passed through as arguments in a local symbol table, it changes depending where you can it 247 | del locals_data['self'] #delete the 'self' key as it doesn't matter, we only care about the arguments we pass through besides 'self' 248 | 249 | self._current_indicators[column_name] = {} #Create a new dictionary with key 'change_in_price' to be placed in our current indicators dictionary 250 | self._current_indicators[column_name]['args'] = locals_data #Create a new dictionary with key 'args' to be placed in our _current_indicators[column_name] dict 251 | #The values are the arguments passed to the function, so it saves all our arguments passed to an object 252 | self._current_indicators[column_name]['func'] = self.change_in_price #Storing the function so it can be called again 253 | 254 | #Calculating the actual indicator 255 | self._frame[column_name] = self._frame['close'].transform( 256 | lambda x: x.diff() #Calculate the change in price 257 | ) 258 | 259 | return self._frame 260 | 261 | # RSI 262 | def rsi(self,period:int,method:str='wilders',column_name:str = 'rsi') ->pd.DataFrame: 263 | """RSI (Relative Strength Index) measures the magnitude of recent price changes to evaluate overbought or 264 | oversold conditions in the price of a stock or other asset. Traders may sell when RSI>0.7 and buy when RSI<0.3. 265 | 266 | Args: 267 | period (int): The period used to calculate the exponential moving average. A typical value would be 14. 268 | method (str, optional): Method used to calculate rsi. Defaults to 'wilders'. 269 | column_name (str, optional): Pass in a value if you wish to change the column name. Defaults to 'rsi'. 270 | 271 | Returns: 272 | pd.DataFrame: Returns a pd dataframe with added column 'rsi_period' 273 | """ 274 | locals_data = locals() 275 | del locals_data['self'] 276 | 277 | # column_name = column_name + '_' + str(period) 278 | self._current_indicators[column_name] = {} 279 | self._current_indicators[column_name]['args'] = locals_data 280 | self._current_indicators[column_name]['func'] = self.rsi 281 | 282 | #Since RSI indicator require change in price, check whether change in price column exists first, if not, create it by calling change_in_price() 283 | if 'change_in_price' not in self._frame.columns: 284 | self.change_in_price() 285 | 286 | self._frame['up_day'] = self._price_groups['change_in_price'].transform( 287 | lambda x: np.where(x>=0,x,0) #Return elements chosen from x or y depending on condition, if x>=0, x=x, elif x < 0, return 0, only keep positive values 288 | ) 289 | 290 | self._frame['down_day'] = self._price_groups['change_in_price'].transform( 291 | lambda x: np.where(x<0,x.abs(),0) #Return elements chosen from x or y depending on condition, if x<=0, x=x.abs(), elif x > 0, return 0, only keep negative values 292 | ) 293 | 294 | self._frame['ewma_up'] = self._price_groups['up_day'].transform( 295 | lambda x: x.ewm(com = period-1).mean() #Give rolling average on up_day 296 | ) 297 | 298 | self._frame['ewma_down'] = self._price_groups['down_day'].transform( 299 | lambda x: x.ewm(com = period-1).mean() #Give rolling average on up_day 300 | ) 301 | relative_strength = self._frame['ewma_up']/self._frame['ewma_down'] 302 | relative_strength_index = 100.0 - (100.0/ (1.0 + relative_strength)) #Using RSI formula 303 | 304 | self._frame[column_name] = np.where(relative_strength_index==0,100, relative_strength_index) # Deal with cases when rsi = 0 305 | 306 | # Clean up before sending back. Delete all the unnessary columns and just leave 'rsi' in place 307 | self._frame.drop( 308 | labels=['ewma_up', 'ewma_down', 'down_day', 'up_day', 'change_in_price'], 309 | axis=1, 310 | inplace=True 311 | ) 312 | 313 | return self._frame 314 | 315 | # Simple moving average 316 | def sma(self, period:int,column_name:str = 'sma') -> pd.DataFrame: 317 | """SMA (Simple Moving Average) meausres the trend of price movement over a defined period. 318 | 319 | Args: 320 | period (int): The period used to calculate the sma. Typical values would be 5,10,20 and 50 321 | column_name (str, optional): Pass in a value if you wish to change the column name. Defaults to 'sma'. 322 | 323 | Returns: 324 | pd.DataFrame: Returns a pd dataframe with added column 'sma_period' 325 | """ 326 | locals_data = locals() 327 | del locals_data['self'] 328 | 329 | #column_name = column_name + '_' + str(period) 330 | self._current_indicators[column_name] = {} 331 | self._current_indicators[column_name]['args'] = locals_data 332 | self._current_indicators[column_name]['func'] = self.sma 333 | 334 | self._frame[column_name] = self._price_groups['close'].transform( 335 | lambda x: x.rolling(window=period).mean() 336 | ) 337 | 338 | return self._frame 339 | # Exponential Moving Average 340 | def ema(self, period:int, alpha: float = 0.0,column_name:str = 'ema') -> pd.DataFrame: 341 | """EMA (Exponential Moving Average) 342 | 343 | Args: 344 | period (int): The period used to calculate ema. Typical value: 12/26 for short term, 50/200 for long term 345 | alpha (float, optional): [description]. Defaults to 0.0. 346 | column_name (str, optional): Pass in a value if you wish to change the column name. Defaults to 'ema'. 347 | 348 | Returns: 349 | pd.DataFrame: Returns a pd dataframe with added column 'ema_period' 350 | """ 351 | locals_data = locals() 352 | del locals_data['self'] 353 | 354 | #column_name = column_name + '_' + period 355 | self._current_indicators[column_name] = {} 356 | self._current_indicators[column_name]['args'] = locals_data 357 | self._current_indicators[column_name]['func'] = self.ema 358 | 359 | self._frame[column_name] = self._price_groups['close'].transform( 360 | lambda x: x.ewm(span=period).mean() 361 | ) 362 | 363 | return self._frame 364 | 365 | # MACD 366 | def macd(self,fast_period:int = 12,slow_period:int = 26,column_name:str = 'macd') -> pd.DataFrame: 367 | """MACD(Moving Average Convergence Divergence) is a trend following momentum indicator that shows the 368 | relationships between 2 moving averages, tpically ema. Traders may buy the security when 'macd' crosses 369 | above the 'macd_signal' line and sell when 'macd' goes below the 'macd_signal' line. 370 | 371 | 372 | Args: 373 | fast_period (int, optional): The period used to calculate the ema of a small window. Defaults to 12. 374 | slow_period (int, optional): The period used to calculate the ema of a long window. Defaults to 26. 375 | column_name (str, optional): The name of column. Defaults to 'macd'. 376 | 377 | Returns: 378 | pd.DataFrame: returns a pd Dataframe with added columns 'macd_fast','macd_slow','macd' and 'macd_signal' 379 | """ 380 | locals_data = locals() 381 | del locals_data['self'] 382 | 383 | self._current_indicators[column_name] = {} 384 | self._current_indicators[column_name]['args'] = locals_data 385 | self._current_indicators[column_name]['func'] = self.macd 386 | 387 | # Calculate fast moving macd 388 | self._frame['macd_fast'] = self._frame['close'].transform( 389 | lambda x: x.ewm(span = fast_period, min_periods = fast_period).mean() 390 | ) 391 | 392 | # Calculate slow moving macd 393 | self._frame['macd_slow'] = self._frame['close'].transform( 394 | lambda x: x.ewm(span = slow_period, min_periods = slow_period).mean() 395 | ) 396 | 397 | # Calculate the difference between fast and slow macd 398 | self._frame['macd'] = self._frame['macd_fast'] - self._frame['macd_slow'] 399 | 400 | # Calculate the exponential moving average of the macd_diff 401 | self._frame['macd_signal'] = self._frame['macd'].transform( 402 | lambda x: x.ewm(span=9,min_periods=8).mean() 403 | ) 404 | 405 | return self._frame 406 | 407 | # VWAP 408 | def vwap(self,column_name='vwap') -> pd.DataFrame: 409 | """VWAP is the volumn weighted average price, typically used to calculate 410 | the average price a security has traded at throughout the day/minute. It 411 | provides insight into both the trend and value of a security. 412 | 413 | Returns: 414 | pd.DataFrame: Returns a pd Dataframe with added column 'vwap' 415 | """ 416 | locals_data = locals() 417 | del locals_data['self'] 418 | 419 | self._current_indicators[column_name] = {} 420 | self._current_indicators[column_name]['args'] = locals_data 421 | self._current_indicators[column_name]['func'] = self.vwap 422 | 423 | high = self._frame['high'] 424 | low = self._frame['low'] 425 | close = self._frame['close'] 426 | volume = self._frame['volume'] 427 | 428 | self._frame['vwap'] = (volume*(high+low+close)/3).cumsum() / volume.cumsum() 429 | return self._frame 430 | 431 | 432 | #refresh all the indicators every time a new row is added 433 | def refresh(self): 434 | #First update the groups 435 | self._price_groups = self._stock_frame.symbol_groups #Data related to one symbol is in a symbol_group 436 | 437 | #Loop through all the stored indicators 438 | for indicator in self._current_indicators: 439 | 440 | indicator_arguments = self._current_indicators[indicator]['args'] 441 | indicator_function = self._current_indicators[indicator]['func'] 442 | 443 | #Update the columns 444 | indicator_function(**indicator_arguments) # ** is used to unpack the indicator_arguments dictionary for passing them as arguments, google 'python dictionary unpacking' 445 | 446 | #Check whether the signals have been flagged for the indicators, if there is a buy/sell signal generated , then return the last row of dataframe. If not, return None. 447 | def check_signals(self) -> Union[pd.DataFrame,None]: #Union returns either one or the other 448 | """Checks to see if any signals have been generated. 449 | Returns: 450 | ---- 451 | {Union[pd.DataFrame, None]} -- If signals are generated then a pandas.DataFrame 452 | is returned otherwise nothing is returned. 453 | """ 454 | signals_df = self._stock_frame._check_signals( 455 | indicators=self._indicator_signals, 456 | indicators_comp_key=self._indicators_comp_key, 457 | indicators_key=self._indicators_key 458 | ) 459 | return signals_df 460 | 461 | # Check whether signals have been flagged for the ticker indicators, if there is buy/sell signal generated, a dict containing buy or sell instruction will be returned. If not, retrun None. 462 | def check_ticker_signals(self) -> Dict: 463 | """Called by the indicator object which will invoke stock_frame object's function _check_ticker_signals()\ 464 | It checks whether any buy/sell signal have been generated. 465 | 466 | Returns: 467 | Dict: Containing 'buys' or 'sells' if signals have been met. Otherwise, return empty dict 468 | """ 469 | signals_dict = self._stock_frame._check_ticker_signals( 470 | ticker_indicators=self._ticker_indicator_signals, 471 | ticker_indicators_key=self._ticker_indicators_key 472 | ) 473 | return signals_dict 474 | 475 | 476 | 477 | 478 | 479 | -------------------------------------------------------------------------------- /robot/portfolio.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from typing import Dict 3 | from typing import Union 4 | from typing import Optional 5 | from typing import Tuple 6 | 7 | from ibw.client import IBClient 8 | 9 | import robot.stock_frame as stock_frame 10 | import pandas as pd 11 | import numpy as np 12 | 13 | 14 | class Portfolio(): 15 | 16 | def __init__(self,account_id = Optional[str]): 17 | """Initalizes a new instance of the Portfolio object. 18 | Keyword Arguments: 19 | ---- 20 | account_number {str} -- An accout number to associate with the Portfolio. (default: {None}) 21 | """ 22 | self.account = account_id 23 | self.positions = {} 24 | self.positions_count = 0 25 | 26 | self._ib_client: IBClient = None 27 | self._stock_frame : stock_frame.StockFrame = None 28 | 29 | 30 | #Create an add_position function to add positions to portfolio 31 | def add_position(self,symbol:str, asset_type:str, purchase_date:Optional[str],order_status: str, quantity: float = 0.0, purchase_price: float = 0.0, ) -> Dict: 32 | """Adds a single new position to the the portfolio. 33 | Arguments: 34 | ---- 35 | symbol {str} -- The Symbol of the Financial Instrument. Example: 'AAPL' or '/ES' 36 | 37 | asset_type {str} -- The type of the financial instrument to be added. For example, 38 | 'STK','OPT','WAR','IOPT','CFD','BAG'. 39 | purchase_date {str} -- This is optional, must be in ISO format e.g. yyyy-mm-dd 40 | purchase_price {float} -- The purchase price, default is 0.0 41 | quantity -- The number of shares bought 42 | 43 | Returns: 44 | ---- 45 | {dict} -- a dictionary object that represents a position in the portfolio 46 | """ 47 | self.positions[symbol] = {} 48 | self.positions[symbol]['symbol'] = symbol 49 | self.positions[symbol]['asset_type'] = asset_type 50 | self.positions[symbol]['purchase_price'] = purchase_price 51 | self.positions[symbol]['quantity'] = quantity 52 | self.positions[symbol]['purchase_date'] = purchase_date 53 | self.positions[symbol]['order_status'] = order_status 54 | 55 | if purchase_date: 56 | self.positions[symbol]['ownership_status'] = True 57 | else: 58 | self.positions[symbol]['ownership_status'] = False 59 | 60 | return self.positions[symbol] 61 | 62 | def add_positions(self,positions:List[dict]) -> dict: 63 | if isinstance(positions,list): 64 | for position in positions: 65 | self.add_position( 66 | symbol=position['symbol'], 67 | asset_type=position['asset_type'], 68 | purchase_date=position.get('purchase_date',None), #If 'puchase_date' is not passed through, set to None 69 | purchase_price=position.get('purchase_price',0.0), 70 | quantity=position.get('quantity',0.0), 71 | order_status=position['order_status'] 72 | ) 73 | return self.positions 74 | else: 75 | raise TypeError("Positions must be a list of dictionaries!") 76 | 77 | def remove_position(self,symbol:str) -> Tuple[bool,str]: 78 | if symbol in self.positions: 79 | del self.positions[symbol] 80 | return (True,"Symbol {symbol} was successfully removed.".format(symbol=symbol)) 81 | else: 82 | return (False,"Symbol {symbol} doesn't exist in the portfolio.".format(symbol=symbol)) 83 | 84 | def in_portfolio(self,symbol:str) -> bool: 85 | if symbol in self.positions: 86 | return True 87 | else: 88 | return False 89 | 90 | def is_profitable(self,symbol:str, current_price:float) -> bool: 91 | if self.in_portfolio(symbol=symbol): 92 | #Grab the purchase price 93 | purchase_price = self.positions[symbol]['purchase_price'] #Select the purchase_price for a symbol row 94 | #Check if symbol is in portfolio 95 | if current_price > purchase_price: 96 | return True 97 | else: 98 | return False 99 | else: 100 | raise ValueError("Symbol {symbol} is not in the portfolio.".format(symbol=symbol)) 101 | 102 | def get_ownership_status(self,symbol:str) -> bool: 103 | """Gets the ownership status for a position in the portfolio. 104 | Arguments: 105 | ---- 106 | symbol {str} -- The symbol you want to grab the ownership status for. 107 | Returns: 108 | ---- 109 | {bool} -- `True` if the we own the position, `False` if we do not own it. 110 | """ 111 | if self.in_portfolio(symbol=symbol) and self.positions[symbol]['ownership_status']: 112 | return self.positions[symbol]['ownership_status'] 113 | else: 114 | return False 115 | 116 | def set_ownership_status(self, symbol: str, ownership: bool) -> None: 117 | """Sets the ownership status for a position in the portfolio. 118 | Arguments: 119 | ---- 120 | symbol {str} -- The symbol you want to change the ownership status for. 121 | ownership {bool} -- The ownership status you want the symbol to have. Can either 122 | be `True` or `False`. 123 | Raises: 124 | ---- 125 | KeyError: If the symbol does not exist in the portfolio it will return an error. 126 | """ 127 | 128 | if self.in_portfolio(symbol=symbol): 129 | self.positions[symbol]['ownership_status'] = ownership 130 | else: 131 | raise KeyError( 132 | "Can't set ownership status, as you do not have the symbol in your portfolio." 133 | ) 134 | 135 | def total_allocation(self): 136 | pass 137 | 138 | def risk_exposure(self): 139 | pass 140 | 141 | def total_market_value(self): 142 | pass -------------------------------------------------------------------------------- /robot/stock_frame.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time, timezone 2 | 3 | from typing import List 4 | from typing import Dict 5 | from typing import Union 6 | 7 | import numpy as np 8 | import pandas as pd 9 | from pandas.core.groupby import DataFrameGroupBy 10 | from pandas.core.window import RollingGroupby 11 | 12 | 13 | class StockFrame(): 14 | 15 | def __init__(self, data: List[Dict]) -> None: 16 | """Initalizes the Stock Data Frame Object. 17 | Arguments: 18 | ---- 19 | data {List[Dict]} -- The data to convert to a frame. Normally, this is 20 | returned from the historical prices endpoint. 21 | """ 22 | self._data = data 23 | self._frame: pd.DataFrame = self.create_frame() 24 | self._symbol_groups: DataFrameGroupBy = None 25 | self._symbol_rolling_groups: RollingGroupby = None 26 | 27 | @property 28 | def frame(self) -> pd.DataFrame: 29 | return self._frame 30 | 31 | @property 32 | def symbol_groups(self) -> DataFrameGroupBy: 33 | self._symbol_groups = self._frame.groupby( 34 | by='symbol', 35 | as_index=False, 36 | sort=True 37 | ) 38 | 39 | return self._symbol_groups 40 | 41 | def symbol_rolling_groups(self,size:int) -> RollingGroupby: 42 | #"size specifies the window size" 43 | 44 | if not self._symbol_groups: #if there is no _symbol_groups object 45 | self.symbol_groups 46 | 47 | self._symbol_rolling_groups = self._symbol_groups.rolling(size) 48 | 49 | return self._symbol_rolling_groups 50 | 51 | def create_frame(self) -> pd.DataFrame: #Initialise dataframe 52 | #Create a dataframe 53 | price_df = pd.DataFrame(data=self._data) 54 | price_df = self._parse_datatime_column(price_df=price_df) #Take timestamp column of every row, make it a pandas 55 | price_df = self._set_multi_index(price_df=price_df) 56 | 57 | return price_df 58 | 59 | def _parse_datatime_column(self,price_df:pd.DataFrame) -> pd.DataFrame: 60 | price_df['datetime'] = pd.to_datetime(price_df['datetime'], unit = 'ms', origin = 'unix') #Parse unix epoch timestamp to date time 61 | return price_df 62 | 63 | def _set_multi_index(self, price_df:pd.DataFrame) -> pd.DataFrame: 64 | price_df = price_df.set_index(keys=['symbol','datetime']) 65 | return price_df 66 | 67 | def add_rows(self, data:dict) -> None: #Add qoute from results of get_historical_prices() to dataframe 68 | """Adds a new row to our StockFrame. 69 | Arguments: 70 | ---- 71 | data {Dict} -- A list of quotes. 72 | Usage: 73 | ---- 74 | >>> # Create a StockFrame object. 75 | >>> stock_frame = trading_robot.create_stock_frame( 76 | data=historical_prices['aggregated'] 77 | ) 78 | >>> fake_data = { 79 | "datetime": 1586390396750, 80 | "symbol": "MSFT", 81 | "close": 165.7, 82 | "open": 165.67, 83 | "high": 166.67, 84 | "low": 163.5, 85 | "volume": 48318234 86 | } 87 | >>> # Add to the Stock Frame. 88 | >>> stock_frame.add_rows(data=fake_data) 89 | """ 90 | column_names = ['open','close','high','low','volume'] #Headers of the columns in stock dataframe 91 | 92 | for quote in data: 93 | #Parse the timestamp 94 | time_stamp = pd.to_datetime( 95 | quote['datetime'], #timestamp from IB in epoch format, see IB Client Portal API docs /portal/iserver/marketdata/snapshot for data return of price request 96 | unit='ms', 97 | origin='unix' 98 | ) 99 | symbol = quote['symbol'] 100 | #Define our index 101 | row_id = (symbol,time_stamp) #Tuple with 2 elements, symbols and time_stamp which is fixed 102 | 103 | #Define our values, see IB Client Portal API docs /portal/iserver/marketdata/snapshot for data return of price request 104 | ######### NEED TO CHANGE IT TO HISTORICAL MARKET PRICE RATHER THAN CURRENT PRICE 105 | row_values = [ 106 | quote['open'], 107 | quote['close'], 108 | quote['high'], 109 | quote['low'], 110 | quote['volume'], 111 | ] 112 | 113 | #New row 114 | new_row = pd.Series(data=row_values) 115 | 116 | #Add row 117 | self.frame.loc[row_id,column_names] = new_row.values 118 | self.frame.sort_index(inplace=True) 119 | 120 | #Check whehter an indicator exists in the stock frame dataframe 121 | def do_indicator_exist(self, column_names: List[str]) -> bool: 122 | """Checks to see if the indicator columns specified exist. 123 | Overview: 124 | ---- 125 | The user can add multiple indicator columns to their StockFrame object 126 | and in some cases we will need to modify those columns before making trades. 127 | In those situations, this method, will help us check if those columns exist 128 | before proceeding on in the code. 129 | Arguments: 130 | ---- 131 | column_names {List[str]} -- A list of column names that will be checked. 132 | Raises: 133 | ---- 134 | KeyError: If a column is not found in the StockFrame, a KeyError will be raised. 135 | Returns: 136 | ---- 137 | bool -- `True` if all the columns exist. 138 | """ 139 | 140 | if set(column_names).issubset(self._frame.columns): 141 | return True 142 | else: 143 | raise KeyError("The following indicator columns are missing from the StockFrame: {missing_columns}".format( 144 | missing_columns=set(column_names).difference( 145 | self._frame.columns) 146 | )) 147 | 148 | 149 | #Check whether the conditions for the indicators are met. If it's met, it will return the last row for each symbol in the StockFrame and compare the indicator column 150 | #values with the conditions specidied 151 | def _check_signals(self, indicators:Dict,indicators_comp_key:List[str],indicators_key: List[str]) -> Union[pd.DataFrame,None]: 152 | """Returns the last row of the StockFrame if conditions are met. 153 | Overview: 154 | ---- 155 | Before a trade is executed, we must check to make sure if the 156 | conditions that warrant a `buy` or `sell` signal are met. This 157 | method will take last row for each symbol in the StockFrame and 158 | compare the indicator column values with the conditions specified 159 | by the user. 160 | If the conditions are met the row will be returned back to the user. 161 | Arguments: 162 | ---- 163 | indicators {dict} -- A dictionary containing all the indicators to be checked 164 | along with their buy and sell criteria. 165 | indicators_comp_key List[str] -- A list of the indicators where we are comparing 166 | one indicator to another indicator. 167 | indicators_key List[str] -- A list of the indicators where we are comparing 168 | one indicator to a numerical value. 169 | Returns: 170 | ---- 171 | {Union[pd.DataFrame, None]} -- If signals are generated then, a pandas.DataFrame object 172 | will be returned. If no signals are found then nothing will be returned. 173 | """ 174 | 175 | #Get the last row of every symbol_groups 176 | last_rows = self._symbol_groups.tail(1) 177 | 178 | #Define a dictionary of conditions 179 | conditions = {} 180 | 181 | #Check to see if all the columns for the indicators specified exists 182 | if self.do_indicator_exist(column_names=indicators_key): 183 | 184 | #Loop through every indicator using its key 185 | for indicator in indicators_key: 186 | 187 | #Define new column which is the value in the last row of indicator 188 | column = last_rows[indicator] 189 | 190 | #Grab the buy and sell condition of an indicator from the indicators arguments, e.g. self._indicator_signals:Dict in Indicator class 191 | buy_condition_target = indicators[indicator]['buy'] 192 | sell_condition_target = indicators[indicator]['sell'] 193 | 194 | buy_condition_operator = indicators[indicator]['buy_operator'] 195 | sell_condition_operator = indicators[indicator]['sell_operator'] 196 | 197 | #Set up conditions for buy and sell, i.e. one conditiona would be value in 'column' compared 198 | condition_1: pd.Series = buy_condition_operator( 199 | column, buy_condition_target #compare the value of last role against the buy_conditiona_target 200 | ) 201 | condition_2: pd.Series = sell_condition_operator( 202 | column, sell_condition_target 203 | ) 204 | 205 | condition_1 = condition_1.where(lambda x: x==True).dropna() #Keep the columns when condition_1 is met, i.e. when column is (buy_condition_operator) than buy_condition_target 206 | condition_2 = condition_2.where(lambda x: x==True).dropna() 207 | 208 | conditions['buys'] = condition_1 #Store the value of the indicator in a dictionary when the condition is met, it will later be returned 209 | conditions['sells'] = condition_2 210 | 211 | #Store the indicators in a list 212 | check_indicators = [] 213 | 214 | #Check whether the indicator exists in indicators_comp_key 215 | for indicator in indicators_comp_key: 216 | #Split the indicators into 2 parts by '_comp_' so we can check if both exist 217 | parts = indicator.split('_comp_') 218 | check_indicators+= parts 219 | 220 | if self.do_indicator_exist(column_names=check_indicators): 221 | for indicator in indicators_comp_key: 222 | # Split the indicators. 223 | parts = indicator.split('_comp_') 224 | 225 | #Grab the indicators that need to be compared 226 | indicator_1 = last_rows[parts[0]] 227 | indicator_2 = last_rows[parts[1]] 228 | 229 | #If we have a buy operator, grab it 230 | if indicators['indicator']['buy_operator']: 231 | buy_condition_operator = indicators['indicator']['buy_operator'] 232 | 233 | #Grab the condition 234 | condition_1 : pd.Series = buy_condition_operator( 235 | indicator_1, indicator_2 236 | ) 237 | # Keep the one's that aren't null. 238 | condition_1 = condition_1.where(lambda x: x == True).dropna() 239 | 240 | #Add it as a buy signal 241 | conditions['buy'] = condition_1 242 | 243 | #If we have a sell operator, grab it 244 | if indicators['indicator']['sell_operator']: 245 | buy_condition_operator = indicators['indicator']['sell_operator'] 246 | 247 | #Grab the condition 248 | condition_2 : pd.Series = sell_condition_operator( 249 | indicator_1, indicator_2 250 | ) 251 | # Keep the one's that aren't null. 252 | condition_2 = condition_2.where(lambda x: x == True).dropna() 253 | 254 | #Add it as a buy signal 255 | conditions['sell'] = condition_2 256 | return conditions 257 | 258 | # Check whether the conditions for the indicators associated with ticker has been met. If it's met, it will \ 259 | # return the last row for each symbol in the StockFrame and compare the indicator column values with the conditions specidied. 260 | def _check_ticker_signals(self, ticker_indicators:Dict, ticker_indicators_comp_key:List[tuple], ticker_indicators_key:List[tuple]) -> Dict: 261 | """Returns a dict containing buy & sell information if conditions are met by the ticker indicators. 262 | Overview: 263 | ---- 264 | Before a trade is executed, we must check to make sure if the 265 | conditions that warrant a `buy` or `sell` signal are met. This 266 | method will take last row for each symbol in the StockFrame and 267 | compare the indicator column values with the conditions specified 268 | by the user. 269 | If the conditions are met, a dictionary containing necessary information for buy & sell will be returned. 270 | 271 | Args: 272 | ticker_indicators (Dict): A dictionary containing all the ticker indicators, ie. Indicator.__ticker_indicator_signals 273 | ticker_indicators_comp_key (List[tuple]): A list containing tuple(ticker,comp_indicator), i.e. ('APPL',"macd_comp_macd_signal") 274 | ticker_indicators_key (List[tuple]): A list containing tuple(ticker,indicator), ie. Indicator._ticker_indicators_key 275 | 276 | Returns: 277 | Dict: If conditions have been met, dict will contain 2 additional dict called 'buys' & 'sells'. If not, dict will contain nothing 278 | """ 279 | 280 | #Define a dictionary of conditions 281 | conditions = {} 282 | 283 | # First, form a list with all the indicator names from the 2nd element in ticker_indicators_key:List 284 | # Check to see if all the indicator columns exist 285 | if self.do_indicator_exist(column_names=[pair[1] for pair in ticker_indicators_key]): 286 | 287 | #Loop through every tuple in ticker_indicators_key which is a list 288 | for ticker_indicator in ticker_indicators_key: 289 | # The first element of tuple is the ticker and the second element of tuple contains the name of indicator 290 | ticker = ticker_indicator[0] 291 | indicator = ticker_indicator[1] 292 | 293 | # Get the last row of the specified ticker group 294 | last_row = self._symbol_groups.get_group(ticker).tail(1) 295 | 296 | # Select the indicator cell as target for comparison later 297 | target_cell = last_row[indicator] 298 | 299 | #Grab the buy and sell condition of an ticker_indicator from the function arguments, e.g. self._ticker_indicator_signals:Dict in Indicator class 300 | buy_condition_target = ticker_indicators[ticker][indicator]['buy'] 301 | sell_condition_target = ticker_indicators[ticker][indicator]['sell'] 302 | 303 | buy_condition_operator = ticker_indicators[ticker][indicator]['buy_operator'] 304 | sell_condition_operator = ticker_indicators[ticker][indicator]['sell_operator'] 305 | 306 | if buy_condition_operator(target_cell, buy_condition_target): 307 | # If the buy condition has been met, append key-value pair to conditions['buys'] 308 | # The key would be the ticker and the value would be the buy_cash_quantity which can be used to calculate quantity in process_signal() 309 | conditions['buys'].update({ticker:ticker_indicators[ticker][indicator]['buy_cash_quantity']}) 310 | 311 | if sell_condition_operator(target_cell, sell_condition_target): 312 | # If the sell condition has been met, append key-value pair to conditions['sells'] 313 | # The key would be the ticker and the value would be close_position_when_sold:bool, this will be passed onto process_signal() 314 | conditions['sells'].update({ticker:ticker_indicators[ticker][indicator]['close_position_when_sell']}) 315 | 316 | # Check comparison indicators 317 | # Store the comparison indicators in a list 318 | check_indicators = [] 319 | 320 | for ticker,comp_key in ticker_indicators_comp_key: 321 | #Split the indicators into 2 parts by '_comp_' so we can check if both exist 322 | parts = comp_key.split('_comp_') 323 | check_indicators+= parts 324 | 325 | # Check to see if all the indicator columns exist 326 | if self.do_indicator_exist(column_names=check_indicators): 327 | #Loop through every tuple in ticker_indicators_key which is a list 328 | for ticker_comp_indicator in ticker_indicators_comp_key: 329 | # Check whether it is a normal indicator or comparison indicator by checking whether _comp_ exists in 2nd element of ticker_in 330 | # The first element of tuple is the ticker and the second element of tuple contains the name of indicator 331 | ticker = ticker_comp_indicator[0] 332 | comp_indicator = ticker_comp_indicator[1] 333 | 334 | # Split the indicators. 335 | parts = indicator.split('_comp_') 336 | 337 | #Grab the last row of thr indicators that need to be compared 338 | last_row = self._symbol_groups.get_group(ticker).tail(1) 339 | 340 | # Select the indicator cell for indicator 1 as target for comparison later 341 | target_cell_1 = last_row[parts[0]] 342 | 343 | # Select the indicator cell for indicator 2 as target for comparison later 344 | target_cell_2 = last_row[parts[1]] 345 | 346 | if buy_condition_operator(target_cell_1, target_cell_2): 347 | # If the buy condition has been met, append key-value pair to conditions['buys'] 348 | # The key would be the ticker and the value would be the buy_cash_quantity which can be used to calculate quantity in process_signal() 349 | conditions['buys'].update({ticker:ticker_indicators[ticker][ticker_comp_indicator]['buy_cash_quantity']}) 350 | 351 | if sell_condition_operator(target_cell_1, target_cell_2): 352 | # If the sell condition has been met, append key-value pair to conditions['sells'] 353 | # The key would be the ticker and the value would be close_position_when_sold:bool, this will be passed onto process_signal() 354 | conditions['sells'].update({ticker:ticker_indicators[ticker][ticker_comp_indicator]['close_position_when_sell']}) 355 | 356 | return conditions -------------------------------------------------------------------------------- /robot/trader.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time as time_true 3 | import pprint 4 | import pathlib 5 | import pandas as pd 6 | import robot.stock_frame as stock_frame 7 | import robot.trades as trades 8 | import robot.portfolio as portfolio 9 | 10 | from datetime import time 11 | from datetime import datetime 12 | from datetime import timezone 13 | from datetime import timedelta 14 | 15 | from typing import List 16 | from typing import Dict 17 | from typing import Union 18 | from typing import Optional 19 | from ibw.client import IBClient 20 | from configparser import ConfigParser 21 | 22 | #gateway_path = pathlib.Path('clientportal.gw').resolve() #Added this line to redirect clientportal.gw away from resoruces/clientportal.beta.gw 23 | 24 | class Trader(): 25 | 26 | def __init__(self, username: str, account: str , client_gateway_path: str = None, is_server_running: bool = True): 27 | """ 28 | USAGE: 29 | Specify the paper and regular account details and gateway path before creating an object 30 | e.g. 31 | # Grab configuration values. 32 | config = ConfigParser() 33 | file_path = pathlib.Path('config/config.ini').resolve() 34 | config.read(file_path) 35 | 36 | # Load the details. 37 | paper_account = config.get('main', 'PAPER_ACCOUNT') 38 | paper_username = config.get('main', 'PAPER_USERNAME') 39 | regular_account = config.get('main','REGULAR_ACCOUNT') 40 | regular_username = config.get('main','REGULAR_USERNAME') 41 | 42 | #Specify path 43 | gateway_path = pathlib.Path('clientportal.gw').resolve() 44 | 45 | >>> ib_paper_session = IBClient( 46 | username='paper_username', 47 | account='paper_account', 48 | ) 49 | """ 50 | #Change username and account to go from paper account to regular account 51 | self.username = username 52 | self.account = account 53 | #self.client_gateway_path = client_gateway_path 54 | self.is_paper_trading = True #Remember to change it when switch to regular account 55 | self.session: IBClient = self._create_session() ### self.seesion = ib_client ### 56 | self._account_data:pd.DataFrame = self._get_account_data() #Get account data 57 | self.historical_prices = {} #A historical prices dictionary for all interested stocks 58 | self.stock_frame:stock_frame.StockFrame = None 59 | self.portfolio:portfolio.Portfolio = None 60 | self.trades = {} # A dictionary of all the trades that belongs to the trader 61 | 62 | @property 63 | def account_data(self) -> pd.DataFrame: 64 | return self._account_data 65 | 66 | def _create_session(self) -> IBClient: 67 | """Start a new session. Go to initiate an IBClient object and the session will be passed onto trader object 68 | Creates a new session with the IB Client API and logs the user into 69 | the new session. 70 | Returns: 71 | ---- 72 | IBClient -- A IBClient object with an authenticated sessions. 73 | """ 74 | ib_client = IBClient( 75 | username = self.username, 76 | account = self.account, 77 | is_server_running=True 78 | ) 79 | 80 | #Start a new session 81 | ib_client.create_session() 82 | 83 | return ib_client 84 | 85 | 86 | def _get_account_data(self) -> pd.DataFrame: 87 | #Has to call /iserver/accounts before anything, make a request with ib_client.portfolio_accounts() 88 | portfolio_accounts = self.session.portfolio_accounts() 89 | 90 | portfolio_ledger = self.session.portfolio_account_ledger(account_id=self.account) 91 | 92 | column_names = ['account number','currency','cash balance','stock value','net liquidation value','realised PnL','unrealised PnL',] 93 | #create a pandas df with columns stated by column_names 94 | 95 | account_df = pd.DataFrame(columns=column_names) 96 | 97 | for item in portfolio_ledger: 98 | #Parse the timestamp 99 | time_stamp = pd.to_datetime( 100 | portfolio_ledger[item]['timestamp'], #timestamp from IB in epoch format, see IB Client Portal API docs /portal/iserver/marketdata/snapshot for data return of price request 101 | unit='s', 102 | origin='unix' 103 | ) 104 | 105 | #Define currency 106 | currency = portfolio_ledger[item]['currency'] 107 | #Define our index 108 | row_id = (time_stamp,currency) #Tuple with 2 elements, time_stamp and currency which is fixed 109 | 110 | row_values = [ 111 | portfolio_ledger[item]['acctcode'], 112 | portfolio_ledger[item]['currency'], 113 | portfolio_ledger[item]['cashbalance'], 114 | portfolio_ledger[item]['stockmarketvalue'], 115 | portfolio_ledger[item]['netliquidationvalue'], 116 | portfolio_ledger[item]['realizedpnl'], 117 | portfolio_ledger[item]['unrealizedpnl'] 118 | ] 119 | 120 | #New row 121 | new_row = pd.Series(data=row_values,index=account_df.columns,name=row_id) 122 | 123 | #Add row 124 | account_df = account_df.append(new_row) 125 | 126 | #return dataframe 127 | return account_df 128 | 129 | def contract_details_by_symbols(self,symbols:List[str]=None) -> pd.DataFrame: 130 | #Search for the conid for a symnbol and get basic info about the instruments 131 | #With /portal/iserver/secdef/search 132 | 133 | column_names = ['symbol','company','company header','conid','exchange','security type'] 134 | #Create a pandas df with column names staed in column_names 135 | symbol_to_conid_df = pd.DataFrame(columns=column_names) 136 | 137 | for symbol in symbols: 138 | symbol_results = self.session.symbol_search(symbol=symbol) 139 | for item in symbol_results: 140 | #Obtain the 'secType' in 'sections' by normalising it making it into a list 141 | normalized_sectype_list = pd.json_normalize(item['sections']) 142 | normalized_sectype_list = normalized_sectype_list['secType'].tolist() 143 | 144 | row_values=[ 145 | item['symbol'], 146 | item['companyName'], 147 | item['companyHeader'], #str(Company Name - Exchange) 148 | item['conid'], 149 | item['description'], #Exchange 150 | normalized_sectype_list #List containing all securities type 151 | ] 152 | 153 | #Define our index 154 | row_id = (item['symbol'],item['description']) #Tuple with 2 elements, symbol and exchange which is fixed 155 | 156 | #New row 157 | new_row = pd.Series(data=row_values,index=symbol_to_conid_df.columns,name=row_id) 158 | #Add row 159 | symbol_to_conid_df = symbol_to_conid_df.append(new_row) 160 | 161 | return symbol_to_conid_df 162 | 163 | def symbol_to_conid(self,symbol:str,exchange:List[str]) -> str: 164 | """Use this to find the conid of a symbol. It will return the conid for a specified symbol. 165 | Keep the list of exchange as short as possible as this function aims to return one conid for a symbol. 166 | Arguments: 167 | ---- 168 | symbol {str} -- The symbol/ticker that you wish to look up 169 | 170 | exchange {list[str]} -- The list of exchanges that you wish to trade in. The exchanges can be 171 | `NASDAQ`,`NYSE`,`MEXI` but there are many more. 172 | It is a good practive to keep the list of exchange to the primary exchanges 173 | you trade in to prevent conflicts. E.g. if you put in both `NASDAQ` and `MEXI` in the list of exchange for `AAPL`, 174 | it will return the first conid found even though Apple is listed on both exchanges. 175 | 176 | 177 | Returns: 178 | ---- 179 | {str} -- The conid for the specified symbol 180 | """ 181 | symbol_results = self.session.symbol_search(symbol=symbol) 182 | for item in symbol_results: 183 | if item['description'] in exchange: 184 | return item['conid'] 185 | 186 | #If nothing is returned by this point, it means the symbol is not in the exchanges provided. 187 | raise ValueError("{} is not in the list of exchanges you provided".format(symbol)) 188 | 189 | def get_current_quotes(self,conids:List[str]=None) -> Dict: 190 | #Get the current price for a list of conids 191 | """ 192 | After querying symbol_to_conid and have a dataframe returned, 193 | select the 'conid' column of the specific row id with symbol, exchange 194 | then pass them to a list to get the current qoutes for them 195 | """ 196 | quote_fields = ['55','31'] #qoute_feilds to indicate information wanted,'55' is symbol,'31' is last price 197 | current_quotes = self.session.market_data( 198 | conids=conids, 199 | since='0', 200 | fields=quote_fields 201 | 202 | ) 203 | 204 | current_quotes_dict = dict() 205 | for item in current_quotes: 206 | if '31' in item.keys(): # Check if the last price exists for a symbol 207 | current_quotes_dict.update({item['55']:item['31']}) 208 | else: 209 | current_quotes_dict.update({item['55']:None}) 210 | 211 | return current_quotes_dict 212 | 213 | 214 | def get_historical_prices(self,period:str,bar:str,conids:List[str]=None) -> List[Dict]: 215 | #Get historical prices for a list of conids 216 | """ 217 | Get history of market Data for the given conid, length of data is controlled by period and 218 | bar. e.g. 1y period with bar=1w returns 52 data points. 219 | 220 | NAME: conids 221 | DESC: The contract ID for a given instrument. You can pass it a list of conids for all the interesetd stocks 222 | TYPE: List 223 | 224 | NAME: period 225 | DESC: Specifies the period of look back. For example 1y means looking back 1 year from today. 226 | Possible values are ['1d','1w','1m','1y'] 227 | TYPE: String 228 | 229 | NAME: bar 230 | DESC: Specifies granularity of data. For example, if bar = '1h' the data will be at an hourly level. 231 | Possible values are ['1min','5min','1h','1w'] 232 | TYPE: String 233 | 234 | """ 235 | #List of new_prices for each symbol 236 | new_prices = [] 237 | 238 | for conid in conids: 239 | historical_prices = self.session.market_data_history( 240 | conid=conid, 241 | period=period, 242 | bar=bar 243 | ) 244 | 245 | #Obtain symbol for each query 246 | symbol = historical_prices['symbol'] 247 | self.historical_prices[symbol]= {} #Create a dictionary which will be a propety of trader object 248 | self.historical_prices[symbol]['candles'] = historical_prices['data'] 249 | 250 | 251 | #Extract candle data from historical_prices['data'] 252 | for candle in historical_prices['data']: 253 | #Parse the timestamp 254 | # time_stamp = pd.to_datetime( 255 | # candle['t'], #timestamp from IB in epoch format, see IB Client Portal API docs /portal/iserver/marketdata/snapshot for data return of price request 256 | # unit='ms', 257 | # origin='unix' 258 | # ) 259 | new_price_dict = {} #This is a mini dictionary for every candle, refer to /portal/iserver/marketdata/history 260 | new_price_dict['symbol'] = symbol 261 | new_price_dict['datetime'] = candle['t'] #Parse it to datetime timestamp later in stockframe 262 | new_price_dict['open'] = candle['o'] 263 | new_price_dict['close'] = candle['c'] 264 | new_price_dict['high'] = candle['h'] 265 | new_price_dict['low'] = candle['l'] 266 | new_price_dict['volume'] = candle['v'] 267 | new_prices.append(new_price_dict) 268 | 269 | self.historical_prices['aggregated'] = new_prices 270 | 271 | return self.historical_prices 272 | #Get latest candle 273 | def get_latest_candle(self,bar='1min',conids=List[str]) -> List[Dict]: 274 | """ 275 | Get latest candle of a list of stocks, the default bar is '1min' 276 | 277 | NAME: bar 278 | DESC: Specifies granularity of data. For example, if bar = '1h' the data will be at an hourly level. Default is '1min'. 279 | Possible values are ['1min','5min','1h','1w'] 280 | TYPE: String 281 | 282 | NAME: conids 283 | DESC: A list of conids of interested stock 284 | TYPE: List of strings 285 | 286 | """ 287 | #define period based on bar, since we will be extracting final candle in historical_prices, out only constraint is period > bar 288 | if 'min' in bar: 289 | period = '1h' 290 | elif 'h' in bar: 291 | period = '1d' 292 | elif 'w' in bar: 293 | period = '1m' 294 | else: 295 | raise ValueError('Bar parameter does not contain min,h or w strings.') 296 | latest_prices = [] 297 | 298 | for conid in conids: 299 | try: 300 | historical_prices = self.session.market_data_history( 301 | conid=conid, 302 | period=period, 303 | bar=bar 304 | ) 305 | except: 306 | #Sleep for 1sec then retry 307 | time_true.sleep(1) 308 | historical_prices = self.session.market_data_history( 309 | conid=conid, 310 | period=period, 311 | bar=bar 312 | ) 313 | #Obtain symbol for each query 314 | symbol = historical_prices['symbol'] 315 | 316 | for candle in historical_prices['data'][-1:]: 317 | new_price_dict = {} #This is a mini dictionary for every candle, refer to /portal/iserver/marketdata/history 318 | new_price_dict['symbol'] = symbol 319 | new_price_dict['datetime'] = candle['t'] #Parse it to datetime timestamp later in stockframe 320 | new_price_dict['open'] = candle['o'] 321 | new_price_dict['close'] = candle['c'] 322 | new_price_dict['high'] = candle['h'] 323 | new_price_dict['low'] = candle['l'] 324 | new_price_dict['volume'] = candle['v'] 325 | latest_prices.append(new_price_dict) 326 | 327 | return latest_prices 328 | 329 | def wait_till_next_candle(self,last_bar_timestamp:pd.DatetimeIndex) -> None: 330 | last_bar_time = last_bar_timestamp.to_pydatetime()[0].replace(tzinfo=timezone.utc) #Convert it into a python datetime format and make sure it is in utc time zone 331 | #Because data doesn't come out at 0s at the minute, it will take another 30s for the data to arrive, set refresh at 30s 332 | last_bar_time = last_bar_time + timedelta(seconds=30.0) 333 | 334 | next_bar_time = last_bar_time + timedelta(seconds=60.0) 335 | curr_bar_time = datetime.now(tz=timezone.utc) 336 | 337 | #Because IB only offers delayed data by 15 mins without market subscription, delayed_time takes care off this by 338 | #shifting curr_bar_time forward by 15 mins to take of the delayed data, this variable is named delayed_curr_bar_time 339 | delayed_time = -timedelta(minutes=15) 340 | delayed_curr_bar_time = curr_bar_time + delayed_time 341 | 342 | last_bar_timestamp = int(last_bar_time.timestamp()) 343 | next_bar_timestamp = int(next_bar_time.timestamp()) 344 | #curr_bar_timestamp = int(curr_bar_time.timestamp()) #Not used because delayed_curr_bar_timestamp is used instead 345 | delayed_curr_bar_timestamp = int(delayed_curr_bar_time.timestamp()) 346 | 347 | #time_to_wait_now = next_bar_timestamp - curr_bar_timestamp 348 | time_to_wait_now = next_bar_timestamp - delayed_curr_bar_timestamp 349 | 350 | if time_to_wait_now < 0: 351 | time_to_wait_now = 0 352 | 353 | print("=" * 80) 354 | print("Pausing for the next bar") 355 | print("-" * 80) 356 | print("Curr Time: {time_curr}".format( 357 | time_curr=curr_bar_time.strftime("%Y-%m-%d %H:%M:%S") 358 | ) 359 | ) 360 | print("Delayed Curr Time: {delayed_time_curr}".format( 361 | delayed_time_curr=delayed_curr_bar_time.strftime("%Y-%m-%d %H:%M:%S") 362 | ) 363 | ) 364 | print("Next Time: {time_next}".format( 365 | time_next=next_bar_time.strftime("%Y-%m-%d %H:%M:%S") 366 | ) 367 | ) 368 | print("Sleep Time: {seconds}".format(seconds=time_to_wait_now)) 369 | print("-" * 80) 370 | print('') 371 | 372 | time_true.sleep(time_to_wait_now) 373 | 374 | #Create a stock frame for trader class 375 | def create_stock_frame(self,data: List[Dict]) -> stock_frame.StockFrame: 376 | """Generates a new stock frame object 377 | Arguments: 378 | ---- 379 | data{List[dict]} -- The data to add to the StockFrame object, it can be the results obtained from get_historical_prices(), e.g. self.historical_prices['aggregated'] 380 | 381 | Returns: 382 | ---- 383 | StockFrame -- A multi-index pandas data frame built for trading. 384 | """ 385 | 386 | #Create the frame 387 | self.stock_frame = stock_frame.StockFrame(data=data) 388 | return self.stock_frame 389 | 390 | #Obtain account positions data which will then be passed to the portfolio object to generate a portfolio dataframe 391 | def load_positions(self) -> List[Dict]: 392 | """Load all the existing positions from IB to the Portfolio object 393 | Arguments: 394 | ---- 395 | None 396 | 397 | Returns: 398 | ---- 399 | data{List[dict]} -- List of Dictionary containing information about every positions the account holds 400 | 401 | Usage: 402 | ---- 403 | >>> trader = Trader( 404 | username=paper_username, 405 | account=paper_account, 406 | client_gateway_path=gateway_path 407 | ) 408 | >>> trader_portfolio = trader.create_portfolio() 409 | >>> trader.load_positions() 410 | 411 | """ 412 | account_positions = self.session.portfolio_account_positions( 413 | account_id=self.account, 414 | page_id=0 415 | ) 416 | for position in account_positions: 417 | 418 | # Sometimes there isn't the key 'ticker' in the position dictionary, in which case \ 419 | # use 'contractDesc' instead 420 | if 'ticker' not in position: 421 | position['ticker'] = position['contractDesc'] 422 | 423 | self.portfolio.add_position( 424 | symbol=position['ticker'], 425 | asset_type=position['assetClass'], 426 | purchase_date="Unknown", 427 | order_status="Filled", #If it is in the positions list, it is filled 428 | quantity=position['position'], 429 | purchase_price=position['mktPrice'] 430 | ) 431 | 432 | # Set the ownership_status to True as not providing date has set it to False originally 433 | self.portfolio.set_ownership_status(symbol=position['ticker'],ownership=True) 434 | 435 | return account_positions 436 | 437 | def create_portfolio(self) -> portfolio.Portfolio: 438 | """Creates a new portfoliio 439 | 440 | Creates a Portfolio Object to help store and organise positions as they are added or removed. 441 | 442 | Usage: 443 | ---- 444 | trader = Trader( 445 | username=paper_username, 446 | account=paper_account, 447 | is_server_running=True 448 | ) 449 | 450 | trader_portfolio = trader.create_portfolio() 451 | """ 452 | self.portfolio = portfolio.Portfolio(account_id=self.account) 453 | 454 | #Assign the client 455 | self.portfolio._ib_client = self.session 456 | 457 | return self.portfolio 458 | 459 | 460 | def create_trade(self,account_id:Optional[str], local_trade_id:str, conid:str, ticker:str, security_type:str, order_type: str, side:str, duration:str , 461 | price:float = 0.0, quantity:float = 0.0,outsideRTH:bool=False) -> trades.Trade: 462 | """Initalizes a new instance of a Trade Object. 463 | This helps simplify the process of building an order by using pre-built templates that can be 464 | easily modified to incorporate more complex strategies. 465 | Keyword Arguments: 466 | ---- 467 | account_id {str} -- It is optional. It should be one of the accounts returned 468 | by /iserver/accounts. If not passed, the first one in the list is selected. 469 | 470 | trade_id {str} -- Optional, if left blank, a unqiue identification code will be automatically generated 471 | 472 | conid {str} -- conid is the identifier of the security you want to trade, you can find 473 | the conid with /iserver/secdef/search 474 | 475 | ticker {str} -- Ticker symbol for the asset 476 | 477 | security_type {str} -- The order's security/asset type, can be one of the following 478 | [`STK`,`OPT`,`WAR`,`IOPT`,`CFD`,`BAG`] 479 | 480 | order_type {str} -- The type of order you would like to create. Can be 481 | one of the following: [`MKT`, `LMT`, `STP`, `STP_LIMIT`] 482 | 483 | side {str} -- The side the trade will take, can be one of the 484 | following: [`BUY`, `SELL`] 485 | duration {str} -- The tif/duration of order, can be one of the following: [`DAY`,`GTC`] 486 | 487 | price {float} -- For `MKT`, this is optional. For `LMT`, this is the limit price. For `STP`, 488 | this is the stop price 489 | 490 | quantity {float} -- The quantity of assets to buy 491 | 492 | outsideRTH {bool} -- Execute outside trading hours if True, default is False 493 | 494 | Usage: 495 | ---- 496 | >>> trader = Trader( 497 | username=paper_username, 498 | account=paper_account, 499 | client_gateway_path=gateway_path 500 | ) 501 | >>> new_trade = trader.create_trade( 502 | account_id=paper_account, 503 | trade_id=None, 504 | conid='', 505 | ticker='', 506 | security_type='STK', 507 | order_type='LMT', 508 | side='BUY', 509 | duration='DAY', 510 | price=0.0, 511 | quantity=0.0 512 | ) 513 | >>> new_trade 514 | 515 | Returns: 516 | ---- 517 | Trade -- A pyrobot.Trade object with the specified template. 518 | """ 519 | 520 | #Initialise a Trade object 521 | trade = trades.Trade() 522 | 523 | #Create a new order 524 | trade.create_order( 525 | account_id=account_id, 526 | local_trade_id=local_trade_id, 527 | conid=conid, 528 | ticker=ticker, 529 | security_type=security_type, 530 | order_type=order_type, 531 | side=side, 532 | duration=duration, 533 | price=price, 534 | quantity=quantity 535 | ) 536 | 537 | #Set the Client 538 | trade.account = self.account 539 | trade._ib_client = self.session 540 | 541 | local_trade_id = trade.local_trade_id 542 | self.trades[local_trade_id] = trade 543 | 544 | return trade 545 | 546 | def process_signal(self,signals:pd.Series,exchange:list,order_type:str = 'MKT') -> List[dict]: 547 | """ Process the signal after we have obtained the signal through indicator.check_sigals() 548 | It will create establish the Trade Objects and create orders for buy and sell signals 549 | 550 | Arguments: 551 | ---- 552 | signals {pd.Dataframe} -- The signals returned by Indicator object's check_signals() 553 | 554 | exchange {list[str]} -- The list of exchanges that you wish to trade in. The exchanges can be 555 | `NASDAQ`,`NYSE`,`MEXI` but there are many more. 556 | It is a good practive to keep the list of exchange to the primary exchanges 557 | you trade in to prevent conflicts. E.g. if you put in both `NASDAQ` and `MEXI` in the list of exchange for `AAPL`, 558 | it will return the first conid found even though Apple is listed on both exchanges. 559 | 560 | order_type {str} -- The order type of executing signal, `MKT` or `LMT`, the default is 561 | `MKT` and support for `LMT` is not added yet 562 | 563 | Returns: 564 | ---- 565 | {list[dict]} -- A list of order responses will be returned 566 | """ 567 | 568 | # Extract buys and sells signal from signals 569 | buys:pd.Series = signals['buys'] 570 | sells:pd.Series = signals['sells'] 571 | 572 | # Establish order_response list 573 | order_responses = [] 574 | 575 | # Check if we have buy signals 576 | if not buys.empty: 577 | # Grab the buy symbols 578 | symbol_list = buys.index.get_level_values(0).to_list() 579 | 580 | #Loop through each symbol in buy signals 581 | for symbol in symbol_list: 582 | # Obtain the conid for the symbol 583 | conid = self.symbol_to_conid(symbol=symbol,exchange=exchange) 584 | 585 | # Check if position already exists in Portfolio object, only proceed buy signal if it is not in portfolio 586 | if self.portfolio.in_portfolio(symbol) is False: 587 | #Create a Trade object for symbol that doesn't exist in Portfolio.positions 588 | trade_obj: trades.Trade = self.create_trade( 589 | account_id=self.account, 590 | local_trade_id=None, 591 | conid=conid, 592 | ticker=symbol, 593 | security_type='STK', 594 | order_type=order_type, 595 | side='BUY', 596 | duration='DAY', 597 | price=None, 598 | quantity=1.0 599 | ) 600 | 601 | # Preview the order 602 | preview_order_response = trade_obj.preview_order() 603 | 604 | # Execute the order 605 | execute_order_response = trade_obj.place_order(ignore_warning=True) 606 | 607 | # Save the exexcute_order_response into a dictionary 608 | order_response = { 609 | 'symbol': symbol, 610 | 'local_trade_id':execute_order_response[0]['local_order_id'], 611 | 'trade_id':execute_order_response[0]['order_id'], 612 | 'message':execute_order_response[0]['text'], 613 | 'order_status':execute_order_response[0]['order_status'], 614 | 'warning_message':execute_order_response[0]['warning_message'] 615 | } 616 | 617 | # Sleep for 0.1 seconds to make sure order is executed on IB server 618 | time_true.sleep(0.1) 619 | 620 | # Query order to find out market order, price and other info 621 | order_status_response = self.session.get_order_status(trade_id=execute_order_response[0]['order_id']) 622 | order_price = float(order_status_response['exit_strategy_display_price']) 623 | order_quantity = float(order_status_response['size']) 624 | order_status = order_status_response['order_status'] 625 | order_asset_type = order_status_response['sec_type'] 626 | 627 | # Obtain the time now 628 | time_now = datetime.now(tz=timezone.utc).replace(microsecond=0).isoformat() 629 | 630 | # Add this position onto our Portfolio Object with the data obtained from order_status_response 631 | portfolio_position_dict = self.portfolio.add_position( 632 | symbol=symbol, 633 | asset_type=order_asset_type, 634 | purchase_date=time_now, 635 | purchase_price=order_price, 636 | quantity=order_quantity, 637 | order_status=order_status 638 | # Ownership_status is automatically set to when purchase_date is supplied 639 | ) 640 | 641 | # IMPLEMENT WAIT UNTIL ORDER IS FILLED? # 642 | 643 | 644 | # Append the order_response above to the main order_responses list 645 | order_responses.append(order_response) 646 | 647 | # Check if we have any sells signals 648 | elif not sells.empty: 649 | 650 | # Grab the sell symbols 651 | symbol_list = buys.index.get_level_values(0).to_list() 652 | 653 | #Loop through each symbol in sell signals 654 | for symbol in symbol_list: 655 | # Obtain the conid for the symbol 656 | conid = self.symbol_to_conid(symbol=symbol,exchange=exchange) 657 | 658 | # Check if position already exists in Portfolio object, only proceed sell signal if it is in portfolio 659 | if self.portfolio.in_portfolio(symbol): 660 | 661 | #Check if we own the position in portfolio 662 | if self.portfolio.positions[symbol]['ownership_status']: 663 | # Set ownership_status to False as we are selling it 664 | self.portfolio.set_ownership_status(symbol=symbol,ownership=False) 665 | 666 | # Create a trade_obj to sell it 667 | trade_obj: trades.Trade = self.create_trade( 668 | account_id=self.account, 669 | local_trade_id=None, 670 | conid=conid, 671 | ticker=symbol, 672 | security_type='STK', 673 | order_type=order_type, 674 | side='SELL', 675 | duration='DAY', 676 | price=None, 677 | quantity=self.portfolio.positions[symbol]['quantity'] 678 | ) 679 | 680 | # Preview the order 681 | preview_order_response = trade_obj.preview_order() 682 | 683 | # Execute the order 684 | execute_order_response = trade_obj.place_order(ignore_warning=True) 685 | 686 | # Save the exexcute_order_response into a dictionary 687 | order_response = { 688 | 'symbol': symbol, 689 | 'local_trade_id':execute_order_response[0]['local_order_id'], 690 | 'trade_id':execute_order_response[0]['order_id'], 691 | 'order_status':execute_order_response[0]['order_status'], 692 | } 693 | 694 | 695 | # Sleep for 0.1 seconds to make sure order is executed on IB server 696 | time_true.sleep(0.1) 697 | 698 | # Set positions[symbol]['quantity] to 0 and update order_status 699 | self.portfolio.positions[symbol]['quantity'] = 0 700 | self.portfolio.positions[symbol]['order_status'] = execute_order_response[0]['order_status'] 701 | 702 | order_responses.append(order_response) 703 | 704 | return order_responses 705 | 706 | # A function similar to process_signal() used to process ticker specific signals 707 | def process_ticker_signal(self,ticker_signals:Dict,exchange:List,order_type:str='MKT') -> List[dict]: 708 | 709 | # Extract buys and sells signal from signals 710 | buys:dict = ticker_signals['buys'] 711 | sells:dict = ticker_signals['sells'] 712 | 713 | # Establish order_response list 714 | order_responses = [] 715 | 716 | # Check if there are any buys signals 717 | if buys: 718 | # Loop through each key value pair in dict 719 | for ticker,buy_cash_quantity in buys.items(): 720 | # Obtain the conid for the symbol 721 | conid = self.symbol_to_conid(symbol=ticker,exchange=exchange) 722 | 723 | # Check if position already exists in Portfolio object, only proceed buy signal if it is not in portfolio 724 | if self.portfolio.in_portfolio(ticker) is False: 725 | 726 | quantity = 0.0 727 | quantity = self.calculate_buy_quantity(ticker=ticker,conid=conid,buy_cash_quantity=buy_cash_quantity) 728 | 729 | # Check if a quantity has been calculated 730 | if quantity != 0.0: 731 | # Create a Trade object for symbol that doesn't exist in Portfolio.positions 732 | # Purchase with the quantity calculated 733 | trade_obj: trades.Trade = self.create_trade( 734 | account_id=self.account, 735 | local_trade_id=None, 736 | conid=conid, 737 | ticker=ticker, 738 | security_type='STK', 739 | order_type=order_type, 740 | side='BUY', 741 | duration='DAY', 742 | price=None, 743 | quantity=quantity 744 | ) 745 | 746 | # Preview the order 747 | preview_order_response = trade_obj.preview_order() 748 | 749 | # Execute the order 750 | execute_order_response = trade_obj.place_order(ignore_warning=True) 751 | 752 | # Save the exexcute_order_response into a dictionary 753 | order_response = { 754 | 'symbol': ticker, 755 | 'local_trade_id':execute_order_response[0]['local_order_id'], 756 | 'trade_id':execute_order_response[0]['order_id'], 757 | 'message':execute_order_response[0]['text'], 758 | 'order_status':execute_order_response[0]['order_status'], 759 | 'warning_message':execute_order_response[0]['warning_message'] 760 | } 761 | 762 | # Sleep for 0.1 seconds to make sure order is executed on IB server 763 | time_true.sleep(0.1) 764 | 765 | # Query order to find out market order, price and other info 766 | order_status_response = self.session.get_order_status(trade_id=execute_order_response[0]['order_id']) 767 | order_price = float(order_status_response['exit_strategy_display_price']) 768 | order_quantity = float(order_status_response['size']) 769 | order_status = order_status_response['order_status'] 770 | order_asset_type = order_status_response['sec_type'] 771 | 772 | # Obtain the time now 773 | time_now = datetime.now(tz=timezone.utc).replace(microsecond=0).isoformat() 774 | 775 | # Add this position onto our Portfolio Object with the data obtained from order_status_response 776 | portfolio_position_dict = self.portfolio.add_position( 777 | symbol=ticker, 778 | asset_type=order_asset_type, 779 | purchase_date=time_now, 780 | purchase_price=order_price, 781 | quantity=order_quantity, 782 | order_status=order_status 783 | # Ownership_status is automatically set to when purchase_date is supplied 784 | ) 785 | 786 | # IMPLEMENT WAIT UNTIL ORDER IS FILLED? # 787 | 788 | 789 | # Append the order_response above to the main order_responses list 790 | order_responses.append(order_response) 791 | else: 792 | pprint(f"Current quote for {ticker} is {quantity} which means it cannot be obtained,\ 793 | no order has been placed as a result.") 794 | 795 | # Check if we have any sells signals 796 | elif sells: 797 | # Loop through each key value pair in dict 798 | for ticker,close_position_when_sell in sells.items(): 799 | # Obtain the conid for the symbol 800 | conid = self.symbol_to_conid(symbol=ticker,exchange=exchange) 801 | 802 | # Check if position already exists in Portfolio object, only proceed sell signal if it is in portfolio 803 | if self.portfolio.in_portfolio(ticker): 804 | 805 | #Check if we own the position in portfolio 806 | if self.portfolio.positions[ticker]['ownership_status']: 807 | # Set ownership_status to False as we are selling it 808 | self.portfolio.set_ownership_status(symbol=ticker,ownership=False) 809 | 810 | # Check if we want to close the position when selling 811 | # Logic needs to be implemented when close_position_when_sell == False 812 | if close_position_when_sell: 813 | quantity = self.portfolio.positions[ticker]['quantity'] 814 | else: 815 | # Not yet implemented, simply sell position even when it results to False for now 816 | quantity = self.portfolio.positions[ticker]['quantity'] 817 | 818 | # Create a trade_obj to sell it 819 | trade_obj: trades.Trade = self.create_trade( 820 | account_id=self.account, 821 | local_trade_id=None, 822 | conid=conid, 823 | ticker=ticker, 824 | security_type='STK', 825 | order_type=order_type, 826 | side='SELL', 827 | duration='DAY', 828 | price=None, # price can be None when selling with market order 829 | quantity=quantity 830 | ) 831 | 832 | # Preview the order 833 | preview_order_response = trade_obj.preview_order() 834 | 835 | # Execute the order 836 | execute_order_response = trade_obj.place_order(ignore_warning=True) 837 | 838 | # Save the exexcute_order_response into a dictionary 839 | order_response = { 840 | 'symbol': ticker, 841 | 'local_trade_id':execute_order_response[0]['local_order_id'], 842 | 'trade_id':execute_order_response[0]['order_id'], 843 | 'order_status':execute_order_response[0]['order_status'], 844 | } 845 | 846 | 847 | # Sleep for 0.1 seconds to make sure order is executed on IB server 848 | time_true.sleep(0.1) 849 | 850 | # Set positions[symbol]['quantity] to 0 and update order_status 851 | self.portfolio.positions[ticker]['quantity'] = 0 852 | self.portfolio.positions[ticker]['order_status'] = execute_order_response[0]['order_status'] 853 | 854 | order_responses.append(order_response) 855 | 856 | return order_responses 857 | 858 | 859 | def calculate_buy_quantity(self,ticker:str,conid:str,buy_cash_quantity:float) -> Union[float,None]: 860 | """Calculate the quantity of stock to buy based on the latest quote and the total buy cash. 861 | 862 | Args: 863 | ticker (str): Ticker 864 | conid (str): The conid for the ticker 865 | buy_cash_quantity (float): Total cash allocation for this purchase 866 | 867 | Returns: 868 | Union[float,None]: Depending on whether current quote can be obtained, it returns the quantity \ 869 | or None 870 | """ 871 | # First query the latest quote 872 | quotes_dict = self.get_current_quotes(conids=[conid]) 873 | 874 | # Check if the dict contains latest quote data 875 | if quotes_dict.get(ticker): 876 | # Convert the price to a float 877 | current_price = float(quotes_dict.get(ticker)) 878 | # Calculate quantity 879 | quantity = buy_cash_quantity/current_price 880 | # Round it to 2 d.p 881 | return round(quantity,2) 882 | else: 883 | return None 884 | 885 | 886 | def update_order_status(self) -> Dict: 887 | """Query and update all the live orders on IB. 888 | The end-point is meant to be used in polling mode, e.g. requesting every 889 | x seconds. The response will contain two objects, one is notification, the 890 | other is orders. Orders is the list of orders (cancelled, filled, submitted) 891 | with activity in the current day. Notifications contains information about 892 | execute orders as they happen, see status field. 893 | 894 | Returns: 895 | {dict} -- A dictionary containing all the live orders 896 | """ 897 | live_order_response = self.session.get_live_orders() 898 | 899 | # Check if live_order_response contains any data to update 900 | if live_order_response['snapshot'] is True: 901 | # Loop through all the live orders 902 | for order in live_order_response['orders']: 903 | print(order) 904 | symbol = order['ticker'] 905 | # Check if the order is in portfolio position 906 | if self.portfolio.in_portfolio(symbol=symbol): 907 | # Check if the order has order_status 'Submitted' ir 'PreSubmitted' 908 | if self.portfolio.positions[symbol]['order_status'] is 'Submitted' or 'PreSubmitted': 909 | self.portfolio.positions[symbol]['order_status'] = order['status'] 910 | 911 | return live_order_response 912 | 913 | -------------------------------------------------------------------------------- /robot/trades.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import json 4 | import re 5 | import pathlib 6 | 7 | from typing import Tuple 8 | from typing import Dict 9 | from typing import List 10 | from typing import Optional 11 | from typing import Union 12 | from datetime import datetime 13 | from datetime import timezone 14 | from ibw.client import IBClient 15 | 16 | class Trade(): 17 | """ 18 | Object Type: 19 | ---- 20 | `robot.Trade` 21 | Overview: 22 | ---- 23 | Reprsents the Trade Object which is used to create new trades, 24 | add customizations to them, and easily modify existing content. 25 | """ 26 | def __init__(self): 27 | """Initalizes a new order.""" 28 | self.account = "" 29 | self.order_instructions = {} 30 | self.local_trade_id = "" #Local trade ID 31 | self.trade_id = "" #order_id given by IB 32 | self.side = "" #Long/short 33 | self.side_opposite = "" #Opposite of self.side 34 | self.quantity = 0.0 35 | self.total_cost = 0.0 36 | self.conid = 0 37 | self.order_status = "" 38 | 39 | self._order_response = {} 40 | self._triggered_added = False 41 | self._multi_leg = False 42 | self._ib_client:IBClient = None 43 | 44 | def create_order(self, account_id:Optional[str], local_trade_id:str, conid:str, ticker:str, security_type:str, order_type: str, side:str, duration:str , 45 | price:float = 0.0, quantity:float = 0.0,outsideRTH:bool=False) -> dict: 46 | """Creates a new Trade object template. 47 | A trade object is a template that can be used to help build complex trades 48 | that normally are prone to errors when writing the JSON. Additionally, it 49 | will help the process of storing trades easier. 50 | Please note here, sometimes this end-point alone can't make sure you submit the order 51 | successfully, you could receive some questions in the response, you have to to answer 52 | them in order to submit the order successfully. You can use "/iserver/reply/{replyid}" 53 | end-point to answer questions. 54 | Arguments: 55 | ---- 56 | account_id {str} -- It is optional. It should be one of the accounts returned by /iserver/accounts. 57 | If not passed, the first one in the list is selected. 58 | local_trade_id {str} -- Optional, if left blank, a unqiue identification code will be automatically generated 59 | conid {str} -- conid is the identifier of the security you want to trade, you can find the conid with /iserver/secdef/search 60 | ticker {str} -- Ticker symbol for the asset 61 | security_type {str} -- The order's security/asset type, can be one of the following 62 | ['STK','OPT','WAR','IOPT','CFD','BAG'] 63 | order_type {str} -- The type of order you would like to create. Can be 64 | one of the following: ['MKT', 'LMT', 'STP', 'STP_LIMIT'] 65 | side {str} -- The side the trade will take, can be one of the 66 | following: ['BUY', 'SELL'] 67 | duration {str} -- The tif/duration of order, can be one of the following: ['DAY','GTC'] 68 | price {float} -- For 'MKT', this is optional. For 'lmt', this is the limit price. For 'STP', 69 | this is the stop price 70 | quantity {float} -- The quantity of assets to buy 71 | outsideRTH {bool} -- Execute outside trading hours if True, default is False 72 | Returns: 73 | ---- 74 | {dict} -- Returns a dictionary containing 'id' and 'message'.if the message is a question, 75 | you have to reply to question in order to submit the order successfully. See more in the "/iserver/reply/{replyid}" endpoint. 76 | """ 77 | 78 | #Set the other required parameters for the api call, refer to api manual or inspect source code when placing a order 79 | isClose = False 80 | referrer = "QuickTrade" 81 | useAdaptive = False 82 | 83 | #Check if conid has been passed through as the test will miss it if conid == '' 84 | if (conid==None or conid==''): 85 | raise ValueError("Conid is none or has no value") 86 | 87 | #Combine conid and ticker to form 'secType' parameter 88 | secType = str(conid) + ":" + security_type 89 | #secType = separator.join([conid,security_type]) 90 | 91 | #Build a default cOID based on arguments passed if local_trade_id is None 92 | if local_trade_id is None: 93 | current_timestamp = str(int(datetime.now(tz=timezone.utc).timestamp())) 94 | local_trade_id = ticker + '_' + side + '_' + str(price) + '_' + current_timestamp 95 | 96 | #Convert conid from str to integer as IB requires conid to be integer 97 | conid=int(conid) 98 | 99 | order_dict = { 100 | 'acctId': account_id, 101 | 'cOID': local_trade_id, 102 | 'isClose': isClose, 103 | #Lisiting exchange is not passed as it is optional, smart routing is used 104 | 'orderType': order_type, 105 | 'outsideRTH': outsideRTH, 106 | 'conid': conid, 107 | 'price': price, 108 | 'quantity': quantity, 109 | 'referrer': referrer, 110 | 'secType':secType, 111 | 'side': side, 112 | 'ticker': ticker, 113 | 'tif': duration, 114 | 'useAdaptive': useAdaptive 115 | } 116 | #Check if all values have been filled 117 | for key,value in order_dict.items(): 118 | if (value == '' or (type(value)!=bool and value == 0.0)): #Because python evaluates False as 0, check type to prevent False be mistaken as unfilled value 119 | print(value) 120 | raise ValueError("order_dict has unfilled values. {key} is not filled, the current value is {value}".format(key=key,value=value)) 121 | 122 | #Make a reference on all data passed through 123 | self.symbol = ticker 124 | self.conid = conid 125 | self.quantity = quantity 126 | self.price = price 127 | self.order_type = order_type 128 | self.asset_type = security_type 129 | #Assigned order_dict to a self attribute which can be called to place order 130 | self.order_instructions = order_dict 131 | self.local_trade_id = local_trade_id 132 | self.side = side 133 | #Set self.side_opposite 134 | if self.side=='BUY': 135 | self.side_opposite = 'SELL' 136 | else: 137 | self.side_opposite = 'BUY' 138 | 139 | return order_dict 140 | 141 | def preview_order(self) -> Dict: 142 | """Preview an order 143 | After a order has been created with create_order(), the order can be 144 | passed to the IB server to check and be reviewed before placing the order. 145 | Arguments: 146 | ---- 147 | 148 | Returns: 149 | ---- 150 | {dict} -- A dictionary with keys: 'amount','equity','initial','maintenance','warn','error' 151 | 152 | """ 153 | 154 | #Check if order_instructions exists 155 | if self.order_instructions: 156 | #Call place_order_scenario() in IBClient for order preview 157 | preview_order_dict = self._ib_client.place_order_scenario(account_id=self.account,order=self.order_instructions) 158 | 159 | #Using the return from preview to set some attributes 160 | if preview_order_dict['error'] is not None: 161 | raise RuntimeError("There is an error in the order. Error: {}".format(preview_order_dict['error'])) 162 | 163 | self.order_status = "Not submitted" 164 | #Remove non numeric value from preview_order_dict['amount']['total'] to store it as string 165 | total_cost = re.sub(r'[^\d.]+', '', preview_order_dict['amount']['total']) 166 | print("Total cost of order: {}".format(total_cost)) 167 | self.total_cost = float(total_cost) 168 | 169 | return preview_order_dict 170 | else: 171 | raise TypeError("order_dict is not in a form of a dictionary.") 172 | 173 | def place_order(self,ignore_warning=False) -> Dict: 174 | """Place an order 175 | Ideally, preview_order() should be called to check that order_instructions has no issues. 176 | place_order() uses order_instructions dictionary which is an attribute of Trade object to execute the order. 177 | Please note here, sometimes this endpoint alone can't make sure you submit the order successfully, 178 | you could receive some questions in the response, you have to to answer them in order to 179 | submit the order successfully. You can use "/iserver/reply/{replyid}" endpoint to answer questions. 180 | Arguments: 181 | ---- 182 | ignore_warning {bool} -- IB will require confirmation to place order if user has no live data subscription. 183 | Set this to True if you acknowledge the warning and an automatic reply will be sent to IB. It also handles 184 | a number of scenarios, check the code in the trades.py for clarification. 185 | Returns: 186 | ---- 187 | {dict} -- A dictionary containing the 'id' and 'message'. If the message is a question, 188 | you have to reply to question in order to submit the order successfully, see more in the 189 | "/iserver/reply/{replyid}" endpoint 190 | """ 191 | #Check if self.order_instructions exist 192 | if self.order_instructions: 193 | place_order_dict = self._ib_client.place_order(account_id=self.account,order=self.order_instructions) 194 | 195 | #Sometimes, IB will return with a warning about trading without market data sub and prompt a reply 196 | if 'message' in place_order_dict[0].keys() and 'o354' in place_order_dict[0]['messageIds']: 197 | #Check whether the response require a reply by seeing if 'message' is a key of response and 'messageIds' == 'o354' 198 | #'o354' is the warning code for trading without real time data 199 | print("Warning of trading without live data has been ignored!") 200 | reply_id = place_order_dict[0]['id'] 201 | #Send an automatic reply to authorise trade 202 | place_order_dict = self._ib_client.place_order_reply(reply_id=reply_id,reply=True) 203 | 204 | elif 'message' in place_order_dict[0].keys() and 'o163' in place_order_dict[0]['messageIds']: 205 | # Sometimes, if an limit order has been submitted and the limit price exceeds the current price by 206 | # the percentage constraint of 3%, IB will send an warning with 'messageIds' == 'o163'. 207 | # This case will also be handled when ignore_warning is False. 208 | print("Warning of limit price exceeds the current price by more than 3 percent has been ignored!") 209 | reply_id = place_order_dict[0]['id'] 210 | #Send an automatic reply to authorise trade 211 | place_order_dict = self._ib_client.place_order_reply(reply_id=reply_id,reply=True) 212 | 213 | 214 | print(place_order_dict) 215 | if any(condition in place_order_dict[0]['order_status'] for condition in ['Submitted','PreSubmitted','Filled']): 216 | #Add data to Trade object if 'order_status' is either 'Submitted', 'Filled' or 'PreSubmitted' 217 | 218 | self.trade_id = place_order_dict[0]['order_id'] 219 | self.order_status = place_order_dict[0]['order_status'] 220 | 221 | #Record the trade and log it down to json file 222 | self.add_to_order_record() 223 | return place_order_dict 224 | else: 225 | message = "Order hasn't been placed and might require additional input." 226 | raise RuntimeError(message + "Order Status: {}".format(place_order_dict['order_status'])) 227 | 228 | else: 229 | raise TypeError("self.order_instructions is undefined, please create the order first.") 230 | 231 | def add_to_order_record(self) -> None: 232 | """ 233 | Save the order details onto a json file so orders can be viewed later 234 | """ 235 | #Establish the location of order_record.json 236 | record_path = pathlib.Path('order_record/orders.jsonc') 237 | 238 | # Convert IB_Client details in self object into strings so they can be read, 239 | # We can't just dump the IBClient object into a json file 240 | def default(obj): 241 | if isinstance(obj,IBClient): 242 | return str(obj) 243 | # # Create a directory called 'order_records' in working directory and create a JSON file called 'orders.jsonc' 244 | # # Initialise the file with empty list 245 | # with open(file=record_path,mode='w+') as order_file: 246 | # json.dump( 247 | # obj=[], #Set up empty list 248 | # fp=order_file, 249 | # indent=4 250 | # ) 251 | 252 | # After a directory is created, open the JSON file and append new data 253 | with open(file=record_path,mode='a') as order_file_json: 254 | data = self.__dict__ 255 | order_file_json.write(json.dumps(data,indent=4,default=default)) 256 | order_file_json.close() 257 | 258 | def cancel_order(self) -> dict: 259 | """ 260 | Cancel an open order that has not been filled. Uses the self attribute trade_id to cancel order. 261 | 262 | Returns: 263 | ---- 264 | {dict} -- A dictionary object that has keys 'order_id', 'msg','conid','account' 265 | """ 266 | if self.trade_id: 267 | #Check the order_status to see the status of the order 268 | if self.order_status=="PreSubmitted": 269 | #if order is pre-submitted and not filled, then cancel the order 270 | response = self._ib_client.delete_order(account_id=self.account,customer_order_id=self.trade_id) 271 | self.order_status = "Cancelled" 272 | return response 273 | elif self.order_status == "Filled": 274 | #if order is filled already, it can't be cancelled 275 | raise RuntimeError("{} has been filled already so it cannot be cancelled.".format(self.trade_id)) 276 | elif self.order_status == "Cancelled": 277 | raise RuntimeError("{} has already been cancelled so it cannot be cancelled again.".format(self.trade_id)) 278 | else: 279 | raise RuntimeError("The order_status of {} is not specidie/is not defined. Please check the status through IB" 280 | .format(self.trade_id)) 281 | 282 | else: 283 | RuntimeError("self.trade_id is undefined.") 284 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import setup, find_packages 3 | 4 | 5 | # with open("README.md", "r") as fh: 6 | # long_description = fh.read() 7 | 8 | setup( 9 | 10 | # Library Name. 11 | name='IB_Trading_Bot', 12 | 13 | # Want to make sure people know who made it. 14 | author='Vincent Ho, Alex Reed', 15 | 16 | # also an email they can use to reach out. 17 | author_email='coding.sigma@gmail.com', 18 | 19 | # read this as MAJOR VERSION 0, MINOR VERSION 1, MAINTENANCE VERSION 0 20 | version='0.1.0', 21 | description='A python client library for the Interactive Broker Web API.', 22 | 23 | # I have a long description but that will just be my README file. 24 | long_description="A Python library written to handle IB's Client Portal API, manage portfolio and execute trades.", 25 | 26 | # want to make sure that I specify the long description as MARKDOWN. 27 | long_description_content_type="text/markdown", 28 | 29 | # here is the URL you can find the code. 30 | url='https://github.com/Vincentho711/Interactive-Brokers-Trading-Bot', 31 | 32 | # there are some dependencies to use the library, so let's list them out. 33 | install_requires=[ 34 | 'certifi>=2019.11.28', 35 | 'requests>=2.22.0', 36 | 'urllib3>=1.25.3' 37 | ], 38 | 39 | # here are the packages I want "build." 40 | packages=find_packages(include=['ibw']), 41 | 42 | # additional classifiers that give some characteristics about the package. 43 | classifiers=[ 44 | 45 | # I want people to know it's still early stages. 46 | 'Development Status :: 3 - Alpha', 47 | 48 | # My Intended audience is mostly those who understand finance. 49 | 'Intended Audience :: Financial and Insurance Industry', 50 | 51 | # My License is MIT. 52 | 'License :: OSI Approved :: MIT License', 53 | 54 | # I wrote the client in English 55 | 'Natural Language :: English', 56 | 57 | # The client should work on all OS. 58 | 'Operating System :: OS Independent', 59 | 60 | # The client is intendend for PYTHON 3 61 | 'Programming Language :: Python :: 3' 62 | ], 63 | 64 | # you will need python 3.7 to use this libary. 65 | python_requires='>3.7' 66 | ) 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/run_client.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pandas as pd 3 | import json 4 | import pathlib 5 | import operator 6 | import time as time_true 7 | import robot.indicator as indicator 8 | import robot.trader as trader 9 | import robot.stock_frame as stock_frame 10 | import robot.portfolio as portfolio 11 | import robot.trades as trades 12 | 13 | from pprint import pprint 14 | from datetime import time 15 | from datetime import datetime 16 | from datetime import timezone 17 | from configparser import ConfigParser 18 | 19 | from ibw.client import IBClient 20 | 21 | # First, the script setup.py has to be run once to configure the libraries when you use this library \ 22 | # for the first time. 23 | # Before running the run_client.py script, ensure that there is a config.ini file with the correct account info. 24 | # If you don't have this yet. Enter your credentials in wirte_config.py and run it. 25 | # A clientportal.gw will be created within the parent directory when it is run the first time. 26 | # Running it the first time will result in an error code as a local server has not been set up. 27 | # Kill the script, then head to the clientportal.gw in file explorer, run the command \ 28 | # "bin/run.bat" "root/conf.yaml" in Git Bash to start the local server. 29 | # Go to "localhost:5000" in your preferred browser and log in with the same credentials provided \ 30 | # in config.ini. 31 | # Keep the browser opened. 32 | # After the browser displays "client login succeeds", you can run the following script and the bot \ 33 | # take control of the operation. 34 | # See https://interactivebrokers.github.io/cpwebapi/ for the detail setup, java has to be installed on \ 35 | # your local machine to run properly. 36 | 37 | # Grab configuration values. 38 | config = ConfigParser() 39 | file_path = pathlib.Path('config/config.ini').resolve() 40 | config.read(file_path) 41 | 42 | # Load the details. 43 | paper_account = config.get('main', 'PAPER_ACCOUNT') 44 | paper_username = config.get('main', 'PAPER_USERNAME') 45 | regular_account = config.get('main','REGULAR_ACCOUNT') 46 | regular_username = config.get('main','REGULAR_USERNAME') 47 | 48 | # Create a new trader object 49 | trader = trader.Trader( 50 | username=paper_username, 51 | account=paper_account 52 | ) 53 | 54 | # Grabbing account data 55 | pprint("Account details: ") 56 | pprint("-"*80) 57 | pprint(trader._account_data) 58 | pprint("="*80) 59 | 60 | 61 | # Grabbing contract detials with symbols() 62 | query_symbols = ['AAPL','MSFT','SCHW'] 63 | pprint("Contract details by symbols: ") 64 | pprint("-"*80) 65 | pprint(trader.contract_details_by_symbols(query_symbols)) 66 | pprint("="*80) 67 | 68 | # Grabbing the conid for a specific symbol 69 | # List the exchanges you trade in, best to keep it concise to prevent errors 70 | exchange_list = ['NASDAQ','NYSE'] 71 | query_symbol = 'TSLA' 72 | pprint("Symbol to conid: ") 73 | pprint("-"*80) 74 | response = trader.symbol_to_conid(symbol=query_symbol,exchange=exchange_list) 75 | pprint("The conid for {} is {}".format(query_symbol,response)) 76 | pprint("="*80) 77 | 78 | # Grab a current qoute 79 | query_symbol = 'TSLA' 80 | quote_response = trader.get_current_quotes(conids=trader.symbol_to_conid(query_symbol,exchange_list)) 81 | pprint("Current quote for {}: ".format(query_symbol)) 82 | pprint("-"*80) 83 | pprint(quote_response) 84 | pprint("="*80) 85 | # Alternatively, the function supports passing in a list of conids 86 | # Sometimes, there will be an error here, kill and rerun the script and the problem should go away 87 | # If the problem persists after a few times, try chanching the query_conids to something else 88 | query_conids = ['265598','15124833'] 89 | pprint("Current quote for {}: ".format(query_conids)) 90 | pprint("-"*80) 91 | quote_response_dict = trader.get_current_quotes(conids=query_conids) 92 | pprint(quote_response_dict) 93 | pprint("="*80) 94 | 95 | # Grab historical price for a list of stocks 96 | query_conids = ['265598','272093'] 97 | pprint("Historical price for {}: ".format(query_conids)) 98 | pprint("-"*80) 99 | historical_price_response = trader.get_historical_prices( 100 | period='30d', 101 | bar='1d', 102 | conids=query_conids 103 | ) 104 | pprint(historical_price_response) 105 | pprint("="*80) 106 | 107 | # The functions above are utility functions that can be queried in any part of the loop to obtain info. 108 | # Below is the main code required to run the bot 109 | # ---------------------------------------------------------------------------------------------------- 110 | # Example of how to use the trading bot 111 | # 1. Idenify the stocks of interest and the exhanges they are in, pass the conids for those stocks in\ 112 | # a list and their relevant exchanges 113 | # 2. Create a trader object which we did in the beginning 114 | # 3. Query historical prices of the a list of stocks you would like your trading bot to trade 115 | # 4. Create a stock frame object which will include the historical data you just queried 116 | # 5. Create a indicator object which will check for buy and sell signals 117 | # 6. Add the relevant indicators your wish to check for 118 | # 7. Create a portfolio object which holds all the positions and check whether trades have been executed 119 | # 8. Add buy and sell signals for the choosen indicators 120 | # 9. Load all the exisiting positions from IB that has not been sold 121 | # 10.In a loop, keep querying the latest bar of the stocks of interested when it comes out \ 122 | # and it will calculate the newest value for your indicators automatically 123 | # 11.Add the latest bar to the stock frame 124 | # 12.Refresh the stock frame so all the indicators get calcualted 125 | # 13.Check signals in indicators to see if they have met the predefined buy and sell signals 126 | # 14.Process the signal if there is any stocks that has met the buy and sell signals, this will \ 127 | # execute the traes as well 128 | # 15.Query the order status from IB and update the status in portfolio 129 | # 16.Grab the latest timestamp and store it as a variable so a sleep function can be initiated 130 | # 17.Put bot into sleep unitl the next bar comes out and run from 9. again. Currently, the bot refreshes\ 131 | # every minute. 132 | #--------------------------------------------------------------------------------------------------------- 133 | # Main code 134 | # 1. Identify stocks of interest, use trader.symbol_to_conid() to find the conid associated with a ticker if\ 135 | # needed 136 | conids_list = ['265598','272093'] 137 | exchange_list = ['NYSE','NASDAQ'] 138 | 139 | # 2. Create a trader object 140 | trader = trader.Trader( 141 | username=paper_username, 142 | account=paper_account 143 | ) 144 | 145 | # 3. Query historical prices of stocks of interest 146 | historical_prices_list = trader.get_historical_prices( 147 | period='30d', 148 | bar='1d', 149 | conids=conids_list 150 | ) 151 | 152 | # 4. Create a stock frame object 153 | stock_frame_client = trader.create_stock_frame(trader.historical_prices['aggregated']) 154 | 155 | # 5. Create a indicator object and populate it historical prices 156 | indicator_client = indicator.Indicators( 157 | price_df= stock_frame_client 158 | ) 159 | 160 | 161 | 162 | # 6. Add any indicators, in here, we will add the RSI indicator 163 | indicator_client.rsi(period=14) 164 | 165 | # 7. Add the buy and sell signal for the indicator 166 | indicator_client.set_indicator_signal( 167 | indicator='rsi', 168 | buy=30.0, # Buy when RSI drops below 30 169 | sell=70.0, # Sell when RSI climbs above 70 170 | condition_buy=operator.ge, # Greater or equal to operator 171 | condition_sell=operator.le 172 | ) 173 | 174 | # You can see a list of the signals set using indicator_client._indicators_key and \ 175 | # indicator_client._indicators_signals 176 | pprint("Indicators key: ") 177 | pprint("-"*80) 178 | pprint(indicator_client._indicators_key) 179 | pprint("="*80) 180 | pprint("Indicators signals: ") 181 | pprint("-"*80) 182 | pprint(indicator_client._indicator_signals) 183 | pprint("="*80) 184 | 185 | # You can also query the indicator's stock frame: 186 | pprint("Indicator's stock frame: ") 187 | pprint("-"*80) 188 | pprint(indicator_client._frame.tail(20)) 189 | pprint("="*80) 190 | 191 | # 8. Create a portfolio object 192 | trader.create_portfolio() 193 | 194 | # 9. Load all the existing positions from IB 195 | positions_list = trader.load_positions() 196 | pprint("Positions List: ") 197 | pprint("-"*80) 198 | pprint(positions_list) 199 | pprint("="*80) 200 | 201 | # 10. Main Loop 202 | while (True): 203 | 204 | # 11. Grab the latest bar 205 | latest_candle = trader.get_latest_candle( 206 | bar='1min', 207 | conids=conids_list 208 | ) 209 | 210 | # 11. Add the latest bar to the stock frame 211 | stock_frame_client.add_rows(data=latest_candle) 212 | 213 | # 12. Refresh the indicator object so that all indicators values get calculated 214 | indicator_client.refresh() 215 | 216 | # 13. Check signals in indicators to see whether any signals have been met by the latest candle 217 | signals = indicator_client.check_signals() 218 | buys = signals['buys'].to_list() 219 | sells = signals['sells'].to_list() 220 | pprint("Buy signals: {} ".format(buys)) 221 | pprint("Sells signals: {} ".format(sells)) 222 | 223 | # 14. Process the signals if there are any buys or sells signals 224 | # The default method of trade will be using market order and any errors will be suppreseed. 225 | # It may still require user input to overide any warnings, see the function itself for more info 226 | process_signal_response = trader.process_signal(signals=signals,exchange=exchange_list) 227 | pprint(process_signal_response) 228 | 229 | # 15. Update the orders status of any orders that have been placed but not filled in the portfolio object 230 | update_order_status = trader.update_order_status() 231 | pprint(update_order_status) 232 | 233 | # You can choose to display all exsisting positions here 234 | pprint("-"*80) 235 | pprint("Positions:") 236 | pprint("="*50) 237 | pprint(trader.portfolio.positions) 238 | 239 | # 16. Grab the latest timestamp in the stock frame 240 | latest_candle_timestamp = trader.stock_frame.frame.tail(1).index.get_level_values(1) 241 | pprint("-"*80) 242 | pprint("Latest timestamp:") 243 | pprint("="*50) 244 | pprint(latest_candle_timestamp) 245 | 246 | # 17. Put bot into sleep until next candle comes out 247 | trader.wait_till_next_candle(last_bar_timestamp=latest_candle_timestamp) 248 | 249 | # END OF LOOP 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /tests/test_ticker_signal.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pandas as pd 3 | import json 4 | import pathlib 5 | import operator 6 | import time as time_true 7 | 8 | from pprint import pprint 9 | from datetime import time 10 | from datetime import datetime 11 | from datetime import timezone 12 | from configparser import ConfigParser 13 | 14 | from ibw.client import IBClient 15 | from robot.trader import Trader 16 | from robot.stock_frame import StockFrame 17 | from robot.indicator import Indicators 18 | from robot.portfolio import Portfolio 19 | from robot.trades import Trade 20 | 21 | 22 | # This script is used to test the newly implemented functions associated to ticker indicators 23 | 24 | # Grab configuration values. 25 | config = ConfigParser() 26 | file_path = pathlib.Path('config/config.ini').resolve() 27 | config.read(file_path) 28 | 29 | # Load the details. 30 | paper_account = config.get('main', 'PAPER_ACCOUNT') 31 | paper_username = config.get('main', 'PAPER_USERNAME') 32 | regular_account = config.get('main','REGULAR_ACCOUNT') 33 | regular_username = config.get('main','REGULAR_USERNAME') 34 | 35 | # Create a new trader object 36 | trader = Trader( 37 | username=paper_username, 38 | account=paper_account 39 | ) 40 | 41 | # Grabbing account data 42 | pprint("Account details: ") 43 | pprint("-"*80) 44 | pprint(trader._account_data) 45 | pprint("="*80) 46 | 47 | # '2665586' = 'AAPL', '272093' = 'MSFT' 48 | conids_list = ['265598','272093'] 49 | exchange_list = ['NYSE','NASDAQ'] 50 | 51 | # Query historical prices for stocks of interest 52 | historical_prices_list = trader.get_historical_prices( 53 | period='30d', 54 | bar='1d', 55 | conids=conids_list 56 | ) 57 | 58 | # Create a stock frame object 59 | stock_frame_client = trader.create_stock_frame(trader.historical_prices['aggregated']) 60 | 61 | # Create a indicator object 62 | indicator_client = Indicators( 63 | price_df=stock_frame_client 64 | ) 65 | 66 | # Add an indicator 67 | indicator_client.rsi(period=14) 68 | 69 | # Associate rsi indicator to the ticker 'AAPL' 70 | indicator_client.set_ticker_indicator_signal( 71 | ticker='AAPL', 72 | indicator='rsi', 73 | buy_cash_quantity=50.0, 74 | close_position_when_sell=True, 75 | buy=30.0, 76 | sell=70.0, 77 | condition_buy=operator.ge, 78 | condition_sell=operator.le, 79 | ) 80 | 81 | # Add a MACD indicator 82 | indicator_client.macd(fast_period=12,slow_period=26) 83 | 84 | # Associate macd indicator to the ticker 'AAPL' 85 | indicator_client.set_ticker_indicator_signal( 86 | ticker='AAPL', 87 | indicator='macd', 88 | buy_cash_quantity=100.0, 89 | close_position_when_sell=True, 90 | 91 | ) 92 | # Check indicators 93 | pprint("Ticker indicators key: ") 94 | pprint("-"*80) 95 | pprint(indicator_client._ticker_indicators_key) 96 | pprint("="*80) 97 | 98 | # Print every indicator associated with every ticker 99 | for ticker in indicator_client._ticker_indicator_signals: 100 | pprint(f"Indicators for {ticker}:") 101 | pprint("-"*80) 102 | for count, indicator in enumerate(indicator_client._ticker_indicator_signals[ticker],start=1): 103 | pprint(f"{count}: {str(indicator)}") 104 | pprint(indicator_client._ticker_indicator_signals[ticker][indicator]) 105 | pprint("="*80) 106 | pprint("") 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /write_config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from configparser import ConfigParser 3 | 4 | config = ConfigParser() 5 | 6 | config.add_section('main') 7 | 8 | config.set('main', 'REGULAR_ACCOUNT', 'ENTER_YOUR_REGULAR_ACCOUNT_HERE') 9 | config.set('main', 'REGULAR_PASSWORD', 'ENTER_YOUR_REGULAR_PASSWORD') 10 | config.set('main', 'REGULAR_USERNAME', 'ENTER_YOUR_REGULAR_USERNAME') 11 | 12 | config.set('main', 'PAPER_ACCOUNT', 'ENTER_YOUR_PAPER_ACCOUNT_HERE') 13 | config.set('main', 'PAPER_PASSWORD', 'ENTER_YOUR_PAPER_PASSWORD_HERE') 14 | config.set('main', 'PAPER_USERNAME', 'ENTER_YOUR_PAPER_USERNAME_HERE') 15 | 16 | new_directory = pathlib.Path("config/").mkdir(parents=True, exist_ok=True) 17 | 18 | with open('config/config.ini', 'w+') as f: 19 | config.write(f) --------------------------------------------------------------------------------