├── .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 |
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 | -
57 | About The Project
58 |
61 |
62 | -
63 | Getting Started
64 |
68 |
69 | - Usage
70 | - Roadmap
71 | - Contributing
72 | - License
73 | - Acknowledgements
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)
--------------------------------------------------------------------------------