From dcb12f2da0aaf39630884ed923e3eb1b45eaad4b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 11 Oct 2020 10:29:38 +0100 Subject: [PATCH 001/126] Add interface `_AccountListDelegae` - Implemented abstract property `accounts` as getter of list of account IDs. - Implemented abstract function `on_account_list_update` as callback on list of account IDs being updated. --- ibpy_native/interfaces/delegates/__init__.py | 2 ++ ibpy_native/interfaces/delegates/accounts.py | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 ibpy_native/interfaces/delegates/__init__.py create mode 100644 ibpy_native/interfaces/delegates/accounts.py diff --git a/ibpy_native/interfaces/delegates/__init__.py b/ibpy_native/interfaces/delegates/__init__.py new file mode 100644 index 0000000..7fe89a2 --- /dev/null +++ b/ibpy_native/interfaces/delegates/__init__.py @@ -0,0 +1,2 @@ +"""Interfaces of delegates.""" +from .accounts import _AccountListDelegate diff --git a/ibpy_native/interfaces/delegates/accounts.py b/ibpy_native/interfaces/delegates/accounts.py new file mode 100644 index 0000000..60d6956 --- /dev/null +++ b/ibpy_native/interfaces/delegates/accounts.py @@ -0,0 +1,25 @@ +"""Internal delegate module for accounts & portfolio related features.""" +import abc +from typing import List + +class _AccountListDelegate(metaclass=abc.ABCMeta): + """Internal delegate protocol for accounts & portfolio related features.""" + @property + @abc.abstractmethod + def accounts(self) -> List[str]: + """Abstract getter of a list of account IDs. + + This property should be implemented to return the internal account ID + list. + """ + return NotImplemented + + @abc.abstractmethod + def on_account_list_update(self, account_list: List[str]): + """Callback on `_IBWrapper.managedAccounts` is triggered by IB API. + + Args: + account_list (:obj:`List[str]`): List of proceeded account IDs + updated from IB. + """ + return NotImplemented From 1c11aaeda65d35a67de9f5ce7ad6bcea127a73d6 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 9 Nov 2020 17:43:46 +0000 Subject: [PATCH 002/126] Rename notification listener - Refactored parameter name and variable name in `__init__` function of `_IBWrapper` for the `NotificationListener`. --- ibpy_native/bridge.py | 4 +++- ibpy_native/internal/wrapper.py | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index d064867..4309d84 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -28,7 +28,9 @@ def __init__(self, host='127.0.0.1', port=4001, client_id=1, auto_conn=True, self._port = port self._client_id = client_id - self._wrapper = ib_wrapper._IBWrapper(listener=notification_listener) + self._wrapper = ib_wrapper._IBWrapper( + notification_listener=notification_listener + ) self._client = ib_client._IBClient(wrapper=self._wrapper) if auto_conn: diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 3514995..3d075e6 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -16,9 +16,15 @@ class _IBWrapper(wrapper.EWrapper): _req_queue: Dict[int, fq._FinishableQueue] = {} - def __init__(self, - listener: Optional[listeners.NotificationListener] = None): - self._listener: Optional[listeners.NotificationListener] = listener + def __init__( + self, + notification_listener: Optional[ + listeners.NotificationListener + ] = None + ): + self._notification_listener: Optional[ + listeners.NotificationListener + ] = notification_listener super().__init__() @@ -85,7 +91,6 @@ def get_request_queue_no_throw(self, req_id: int) -> \ """ return self._req_queue[req_id] if req_id in self._req_queue else None - # Error handling def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. @@ -93,8 +98,9 @@ def set_on_notify_listener(self, listener: listeners.NotificationListener): listener (ibpy_native.interfaces.listeners.NotificationListener): Listener for IB notifications. """ - self._listener = listener + self._notification_listener = listener + # Error handling def error(self, reqId, errorCode, errorString): # override method err = error.IBError(rid=reqId, err_code=errorCode, err_str=errorString) @@ -103,8 +109,11 @@ def error(self, reqId, errorCode, errorString): if reqId is not -1: self._req_queue[reqId].put(element=err) else: - if self._listener is not None: - self._listener.on_notify(msg_code=errorCode, msg=errorString) + if self._notification_listener is not None: + self._notification_listener.on_notify( + msg_code=errorCode, + msg=errorString + ) # Get contract details def contractDetails(self, reqId, contractDetails): From e2d8f046d2cf6e303f3c7d4b96aeb20e5461debc Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 13 Nov 2020 00:19:44 +0000 Subject: [PATCH 003/126] `_IBWrapper` utilies`_AccountListDelegate` - Implemented `_AccountListDelegate` utilisation in `_IBWrapper` - Added optional variable `_AccountListDelegate` `_account_list_delegate` in `_IBWrapper`. - Added setter `set_account_list_delegate`. - Added `MockAccountListDelegate` in `tests.toolkit.utils` for test usages. - Added unittest coverage for `_AccountListDelegate` utilisation in `_IBWrapper`. --- ibpy_native/internal/wrapper.py | 15 +++++++++++++++ tests/internal/test_ib_wrapper.py | 12 ++++++++++++ tests/toolkit/utils.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 3d075e6..7ab3059 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -6,6 +6,7 @@ from ibapi import wrapper from ibpy_native import error +from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -14,6 +15,9 @@ class _IBWrapper(wrapper.EWrapper): TWS instance. """ + # Delegates + _account_list_delegate: Optional[delegates._AccountListDelegate] = None + _req_queue: Dict[int, fq._FinishableQueue] = {} def __init__( @@ -91,6 +95,17 @@ def get_request_queue_no_throw(self, req_id: int) -> \ """ return self._req_queue[req_id] if req_id in self._req_queue else None + # Setters + def set_account_list_delegate(self, + delegate: delegates._AccountListDelegate): + """Setter for optional `_AccountListDelegate`. + + Args: + delegate (ibpy_native.interfaces.delegates._AccountListDelegate): + Delegate for managing IB account list. + """ + self._account_list_delegate = delegate + def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 011421c..b632bff 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -59,6 +59,18 @@ async def test_next_req_id(self): await f_queue.get() self.assertEqual(self._wrapper.next_req_id, 1) + def test_account_list_delegate(self): + """Test `_AccountListDelegate` implementation.""" + mock_delegate = utils.MockAccountListDelegate() + + self._wrapper.set_account_list_delegate(delegate=mock_delegate) + mock_delegate.on_account_list_update( + account_list=['DU0000140', 'DU0000141'] + ) + + print(mock_delegate.accounts) + self.assertTrue(mock_delegate.accounts) + def test_notification_listener(self): """Test notification listener approach.""" class MockListener(listeners.NotificationListener): diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 156fb82..1ff03f7 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -1,10 +1,12 @@ """Utilities for making unittests easier to write.""" +# pylint: disable=protected-access import asyncio from typing import List, Union from ibapi import wrapper as ib_wrapper from ibpy_native import error +from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners def async_test(fn): @@ -17,6 +19,18 @@ def wrapper(*args, **kwargs): return wrapper +class MockAccountListDelegate(delegates._AccountListDelegate): + """Mock accounts delegate""" + + _account_list: List[str] = [] + + @property + def accounts(self) -> List[str]: + return self._account_list + + def on_account_list_update(self, account_list: List[str]): + self._account_list = account_list + class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" ticks: List[Union[ib_wrapper.HistoricalTick, From 35782686d4ea7151f1eb1fa1ab88fe0f5651c701 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 15 Nov 2020 06:47:31 +0000 Subject: [PATCH 004/126] Handles managed accounts request responses - Overridden function `managedAccounts` in `_IBWrapper` to handle the responses for `client.reqManagedAccts()`. - Implemented unit test in `TestIBWrapper` to cover the overridden function in `_IBWrapper`. --- ibpy_native/internal/wrapper.py | 13 +++++++++++++ tests/internal/test_ib_wrapper.py | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 7ab3059..e13b84e 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -130,6 +130,19 @@ def error(self, reqId, errorCode, errorString): msg=errorString ) + # Accounts & portfolio + def managedAccounts(self, accountsList: str): + # override method + # Trim the spaces in `accountsList` received + trimmed = "".join(accountsList.split()) + # Separate different account IDs into a list + account_list = trimmed.split(',') + + if self._account_list_delegate is not None: + self._account_list_delegate.on_account_list_update( + account_list=account_list + ) + # Get contract details def contractDetails(self, reqId, contractDetails): # override method diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index b632bff..124b0ea 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -1,7 +1,8 @@ """Unit tests for module `ibpy_native.wrapper`.""" # pylint: disable=protected-access -import os +import asyncio import enum +import os import threading import unittest @@ -91,6 +92,19 @@ def on_notify(self, msg_code: int, msg: str): self.assertTrue(mock_listener.triggered) + @utils.async_test + async def test_managed_accounts(self): + """ Test overridden function `managedAccounts`.""" + mock_delegate = utils.MockAccountListDelegate() + + self._wrapper.set_account_list_delegate(delegate=mock_delegate) + self._client.reqManagedAccts() + + await asyncio.sleep(1) + + print(mock_delegate.accounts) + self.assertTrue(mock_delegate.accounts) + @utils.async_test async def test_historical_ticks(self): """Test overridden function `historicalTicks`.""" From c773f83e322c0c33b2612d79f0adca6f415ea5f7 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 28 Dec 2020 12:47:53 +0000 Subject: [PATCH 005/126] Add model `Account` - Added module `ibpy_native.account` & model class `Account` to manage the properties of IB account (e.g. account ID). - Modified the `_AccountListDelegate` to have the property `accounts` returns a list of `Account` instance instead of `str`. - Renamed module `ibpy_native.interfaces.delegates.accounts` to `ibpy_native.interfaces.delegates.account`. - Made corresponding changes to places using `_AccountListDelegate`. --- ibpy_native/account.py | 31 +++++++++++++++++++ ibpy_native/interfaces/delegates/__init__.py | 2 +- .../delegates/{accounts.py => account.py} | 9 +++--- tests/toolkit/utils.py | 9 ++++-- 4 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 ibpy_native/account.py rename ibpy_native/interfaces/delegates/{accounts.py => account.py} (75%) diff --git a/ibpy_native/account.py b/ibpy_native/account.py new file mode 100644 index 0000000..5fc77fc --- /dev/null +++ b/ibpy_native/account.py @@ -0,0 +1,31 @@ +"""IB account related resources.""" +from typing_extensions import Final + +class Account: + """Model class for individual IB account. + + Attributes: + account_id (:obj:`Final[str]`): Account ID received from IB Gateway. + """ + account_id: Final[str] + _destroy_flag: bool = False + + def __init__(self, account_id: str): + self.account_id = account_id + + @property + def destory_flag(self) -> bool: + """Flag to indicate if this account instance is going to be destoried. + + Returns: + bool: `True` if the corresponding account on IB is no longer + available. No further action should be perfromed with this + account instance if it's the case. + """ + return self._destroy_flag + + def destory(self): + """Marks the instance will be destoried and no further action should be + performed on this instance. + """ + self._destroy_flag = True diff --git a/ibpy_native/interfaces/delegates/__init__.py b/ibpy_native/interfaces/delegates/__init__.py index 7fe89a2..3b1b078 100644 --- a/ibpy_native/interfaces/delegates/__init__.py +++ b/ibpy_native/interfaces/delegates/__init__.py @@ -1,2 +1,2 @@ """Interfaces of delegates.""" -from .accounts import _AccountListDelegate +from .account import _AccountListDelegate diff --git a/ibpy_native/interfaces/delegates/accounts.py b/ibpy_native/interfaces/delegates/account.py similarity index 75% rename from ibpy_native/interfaces/delegates/accounts.py rename to ibpy_native/interfaces/delegates/account.py index 60d6956..4367f1e 100644 --- a/ibpy_native/interfaces/delegates/accounts.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -2,15 +2,16 @@ import abc from typing import List +from ibpy_native import account + class _AccountListDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @property @abc.abstractmethod - def accounts(self) -> List[str]: - """Abstract getter of a list of account IDs. + def accounts(self) -> List[account.Account]: + """Abstract getter of a list of `Account` instance. - This property should be implemented to return the internal account ID - list. + This property should be implemented to return the IB account list. """ return NotImplemented diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 1ff03f7..61f14c4 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -5,6 +5,7 @@ from ibapi import wrapper as ib_wrapper +from ibpy_native import account from ibpy_native import error from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners @@ -22,14 +23,16 @@ def wrapper(*args, **kwargs): class MockAccountListDelegate(delegates._AccountListDelegate): """Mock accounts delegate""" - _account_list: List[str] = [] + _account_list: List[account.Account] = [] @property - def accounts(self) -> List[str]: + def accounts(self) -> List[account.Account]: return self._account_list def on_account_list_update(self, account_list: List[str]): - self._account_list = account_list + # self._account_list = account_list + for account_id in account_list: + self._account_list.append(account.Account(account_id)) class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" From b5c33cf40a9c061e6874459b86c3a2c654879238 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 30 Dec 2020 11:58:03 +0000 Subject: [PATCH 006/126] Relocate model `Account` - Moved model class `Account` from module `ibpy_native.account` to `ibpy_native.models.account` to avoid circular import from happening. - Made mandatory changes on corresponding modules. --- ibpy_native/interfaces/delegates/account.py | 4 ++-- ibpy_native/models/__init__.py | 1 + ibpy_native/{ => models}/account.py | 4 ++-- tests/toolkit/utils.py | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 ibpy_native/models/__init__.py rename ibpy_native/{ => models}/account.py (92%) diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 4367f1e..afe72ac 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -2,13 +2,13 @@ import abc from typing import List -from ibpy_native import account +from ibpy_native import models class _AccountListDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @property @abc.abstractmethod - def accounts(self) -> List[account.Account]: + def accounts(self) -> List[models.Account]: """Abstract getter of a list of `Account` instance. This property should be implemented to return the IB account list. diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py new file mode 100644 index 0000000..301bf3a --- /dev/null +++ b/ibpy_native/models/__init__.py @@ -0,0 +1 @@ +from .account import Account diff --git a/ibpy_native/account.py b/ibpy_native/models/account.py similarity index 92% rename from ibpy_native/account.py rename to ibpy_native/models/account.py index 5fc77fc..1bb237a 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/models/account.py @@ -1,4 +1,4 @@ -"""IB account related resources.""" +"""Models for IB account(s).""" from typing_extensions import Final class Account: @@ -28,4 +28,4 @@ def destory(self): """Marks the instance will be destoried and no further action should be performed on this instance. """ - self._destroy_flag = True + self._destroy_flag = True \ No newline at end of file diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 61f14c4..7afdb51 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -5,8 +5,8 @@ from ibapi import wrapper as ib_wrapper -from ibpy_native import account from ibpy_native import error +from ibpy_native import models from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners @@ -23,16 +23,16 @@ def wrapper(*args, **kwargs): class MockAccountListDelegate(delegates._AccountListDelegate): """Mock accounts delegate""" - _account_list: List[account.Account] = [] + _account_list: List[models.Account] = [] @property - def accounts(self) -> List[account.Account]: + def accounts(self) -> List[models.Account]: return self._account_list def on_account_list_update(self, account_list: List[str]): # self._account_list = account_list for account_id in account_list: - self._account_list.append(account.Account(account_id)) + self._account_list.append(models.Account(account_id)) class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" From 3ac8478554ed65d156c2e0b533dcc3b9e2305149 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 1 Jan 2021 20:29:19 +0000 Subject: [PATCH 007/126] Create `AccountsManager` - Created implementation of `ibpy_native.delegates._AccountListDelegate` as a manager to manage all IB accounts under the same username logged-in on IB Gateway. - Created corresponding unittest case & module `tests.test_account`. --- ibpy_native/account.py | 66 ++++++++++++++++++++++++++++++++++++++++++ tests/test_account.py | 45 ++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 ibpy_native/account.py create mode 100644 tests/test_account.py diff --git a/ibpy_native/account.py b/ibpy_native/account.py new file mode 100644 index 0000000..4fc13d8 --- /dev/null +++ b/ibpy_native/account.py @@ -0,0 +1,66 @@ +"""IB account related resources.""" +# pylint: disable=protected-access +from typing import List, Optional + +from ibpy_native import models +from ibpy_native.interfaces import delegates + +class AccountsManager(delegates._AccountListDelegate): + """Class to manage all IB accounts under the same username logged-in on + IB Gateway. + """ + def __init__(self, accounts: Optional[List[models.Account]] = None): + self._accounts: List[models.Account] = ([] if accounts is None + else accounts) + + @property + def accounts(self) -> List[models.Account]: + """List of IB account(s) available under the same username logged in + on the IB Gateway. + + Returns: + List[ibpy_native.account.Account]: List of available IB account(s). + """ + # Implements `delegates._AccountListDelegate` + return self._accounts + + def on_account_list_update(self, account_list: List[str]): + """Callback function for internal API callback + `_IBWrapper.managedAccounts`. + + Checks the existing account list for update(s) to the list. Terminates + action(s) or subscription(s) on account(s) which is/are no longer + available and removes from account list. + + Args: + account_list (:obj:`List[str]`): List of proceeded account IDs + updated from IB. + """ + # Implements `delegates._AccountListDelegate` + if self._accounts: + # Deep clone the existing account list for operation as modification + # while iterating a list will cause unexpected result. + copied_list = self._accounts.copy() + + for account in self._accounts: + if account.account_id not in account_list: + # Terminates action(s) or subscription(s) on account in + # existing account list but not the newly received list. + copied_list.remove(account) + + for acc_id in account_list: + if not any(ac.account_id == acc_id for ac in copied_list): + # Adds account appears in received list but not existing + # list to the account list so it can be managed by this + # framework. + copied_list.append(models.Account(account_id=acc_id)) + + # Clears the list in instance scope then extends it instead of + # reassigning its' pointer to the deep cloned list as the original + # list may be referencing by the user via public property + # `accounts`. + self._accounts.clear() + self._accounts.extend(copied_list) + else: + for acc_id in account_list: + self._accounts.append(models.Account(account_id=acc_id)) diff --git a/tests/test_account.py b/tests/test_account.py new file mode 100644 index 0000000..c82d3b7 --- /dev/null +++ b/tests/test_account.py @@ -0,0 +1,45 @@ +"""Unit tests for module `ibpy_native.account`.""" +import unittest + +from ibpy_native import account +from ibpy_native import models + +_MOCK_AC_140: str = 'DU0000140' +_MOCK_AC_141: str = 'DU0000141' +_MOCK_AC_142: str = 'DU0000142' +_MOCK_AC_143: str = 'DU0000143' + +class TestAccountsManager(unittest.TestCase): + """Unit tests for class `AccountsManager`.""" + + def setUp(self): + self._manager = account.AccountsManager() + + def test_on_account_list_update(self): + """Test the implementation of function `on_account_list_update`.""" + self._manager.on_account_list_update( + account_list=[_MOCK_AC_140, _MOCK_AC_141] + ) + + self.assertEqual(len(self._manager.accounts), 2) + self.assertEqual(self._manager.accounts[0].account_id, _MOCK_AC_140) + self.assertEqual(self._manager.accounts[1].account_id, _MOCK_AC_141) + + def test_on_account_list_update_existing_list(self): + """Test the implementation of function `on_account_list_update` with an + non empty account list in the `AccountsManager` instance. + """ + # Prepends data into account list for test. + self._manager = account.AccountsManager( + accounts=[models.Account(account_id=_MOCK_AC_140), + models.Account(account_id=_MOCK_AC_142), + models.Account(account_id=_MOCK_AC_143)] + ) + + self._manager.on_account_list_update( + account_list=[_MOCK_AC_140, _MOCK_AC_141] + ) + + self.assertEqual(len(self._manager.accounts), 2) + self.assertEqual(self._manager.accounts[0].account_id, _MOCK_AC_140) + self.assertEqual(self._manager.accounts[1].account_id, _MOCK_AC_141) From 1a25cca1411479bf9fedddcffce67aa7d89e5b86 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sat, 2 Jan 2021 19:10:23 +0000 Subject: [PATCH 008/126] Utilise `AccountsManager` - Updated constructor of `ibpy_native.bridge.IBBridge` to enable user passing an optional `ibpy_native.account.AccountsManager` into the framework. - Added readonly property `accounts_manager` so it can be retrieve elsewhere. - Added corresponding unit test case. --- ibpy_native/bridge.py | 29 ++++++++++++++++++++++++----- tests/test_ib_bridge.py | 8 ++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 4309d84..a8147f7 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -11,6 +11,7 @@ from ibapi import contract as ib_contract +from ibpy_native import account from ibpy_native import error from ibpy_native.interfaces import listeners from ibpy_native.internal import client as ib_client @@ -19,23 +20,41 @@ from ibpy_native.utils import datatype as dt class IBBridge: - """Public class to bridge between `ibpy-native` & IB API""" - - def __init__(self, host='127.0.0.1', port=4001, client_id=1, auto_conn=True, - notification_listener: \ - Optional[listeners.NotificationListener] = None): + """Public class to bridge between `ibpy-native` & IB API.""" + + def __init__( + self, host: str = '127.0.0.1', port: int = 4001, + client_id: int = 1, auto_conn: bool = True, + notification_listener: \ + Optional[listeners.NotificationListener] = None, + accounts_manager: Optional[account.AccountsManager] = None + ): self._host = host self._port = port self._client_id = client_id + self._accounts_manager = ( + account.AccountsManager() if accounts_manager is None + else accounts_manager + ) self._wrapper = ib_wrapper._IBWrapper( notification_listener=notification_listener ) + self._wrapper.set_account_list_delegate(delegate=self._accounts_manager) + self._client = ib_client._IBClient(wrapper=self._wrapper) if auto_conn: self.connect() + # Properties + @property + def accounts_manager(self) -> account.AccountsManager: + """:obj:`account.AccountsManager`: Instance that stores & manages all IB + account(s) related data. + """ + return self._accounts_manager + # Setters @staticmethod def set_timezone(tz: datetime.tzinfo): diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 4ed829d..4abaaa2 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -64,6 +64,14 @@ def setUpClass(cls): host=TEST_HOST, port=TEST_PORT, client_id=TEST_ID ) + @utils.async_test + async def test_accounts_manager(self): + """Test if the `AccountsManager` is properly set up and has the + account(s) received from IB Gateway once logged in. + """ + await asyncio.sleep(0.5) # Wait for the Gateway to return account ID(s). + self.assertTrue(self._bridge.accounts_manager.accounts) + def test_set_timezone(self): """Test function `set_timezone`.""" ibpy_native.IBBridge.set_timezone(tz=pytz.timezone('Asia/Hong_Kong')) From f840e50da30fac0360a81006183a4382be04ae27 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 3 Jan 2021 12:39:47 +0000 Subject: [PATCH 009/126] Remove useless print - Removed `print` line in unit test case `TestIBWrapper.test_managed_accounts`. --- tests/internal/test_ib_wrapper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 124b0ea..bcaa65c 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -102,7 +102,6 @@ async def test_managed_accounts(self): await asyncio.sleep(1) - print(mock_delegate.accounts) self.assertTrue(mock_delegate.accounts) @utils.async_test From a5eb2adfbd3a0c3e075e122dd09c98e4c9851d25 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 3 Jan 2021 12:53:43 +0000 Subject: [PATCH 010/126] Change attribute scope - Changed the scope of attribute `_account_list_delegate` in `_IBWrapper` form class to instance only. --- ibpy_native/internal/wrapper.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index e13b84e..76f031b 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -14,21 +14,18 @@ class _IBWrapper(wrapper.EWrapper): """The wrapper deals with the action coming back from the IB gateway or TWS instance. """ - - # Delegates - _account_list_delegate: Optional[delegates._AccountListDelegate] = None - _req_queue: Dict[int, fq._FinishableQueue] = {} def __init__( self, notification_listener: Optional[ - listeners.NotificationListener - ] = None + listeners.NotificationListener] = None ): + self._account_list_delegate: Optional[ + delegates._AccountListDelegate] = None + self._notification_listener: Optional[ - listeners.NotificationListener - ] = notification_listener + listeners.NotificationListener] = notification_listener super().__init__() From 7d87d41c8a83ad5295760dc2dd54697e79e32811 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 3 Jan 2021 14:32:48 +0000 Subject: [PATCH 011/126] Change `_req_queue` scope - Changed the scope of `_req_queue` in `_IBWrapper` from class to instance only and initial its' value (an empty dictionary) to avoid issue(s) while multiple `_IBWrapper` instances are being created. --- ibpy_native/internal/wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 76f031b..8f714d6 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -14,13 +14,13 @@ class _IBWrapper(wrapper.EWrapper): """The wrapper deals with the action coming back from the IB gateway or TWS instance. """ - _req_queue: Dict[int, fq._FinishableQueue] = {} - def __init__( self, notification_listener: Optional[ listeners.NotificationListener] = None ): + self._req_queue: Dict[int, fq._FinishableQueue] = {} + self._account_list_delegate: Optional[ delegates._AccountListDelegate] = None From b742f9c4b96554526d5409eb6b400f9351698f4a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 15 Jan 2021 18:03:07 +0000 Subject: [PATCH 012/126] Override account update callbacks - Overridden callback functions for `IBApi.EClient.reqAccountUpdates`. - It's only printing the info for now. - Setup unittest cases to test out the callbacks for account updates and keep them here as they will be used later anyway. --- ibpy_native/internal/wrapper.py | 36 ++++++++++++++++++++++- tests/internal/test_ib_wrapper.py | 48 ++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 8f714d6..088604e 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -3,6 +3,7 @@ import queue from typing import Dict, List, Optional, Union +from ibapi import contract as ib_contract from ibapi import wrapper from ibpy_native import error @@ -127,7 +128,7 @@ def error(self, reqId, errorCode, errorString): msg=errorString ) - # Accounts & portfolio + #region - Accounts & portfolio def managedAccounts(self, accountsList: str): # override method # Trim the spaces in `accountsList` received @@ -140,6 +141,39 @@ def managedAccounts(self, accountsList: str): account_list=account_list ) + #region - account updates + def updateAccountValue(self, key: str, val: str, currency: str, + accountName: str): + # override method + super().updateAccountValue(key, val, currency, accountName) + print(f'Key: {key}\nValue: {val}\nCurrency: {currency}\n' + f'Account Name: {accountName}\n') + + def updatePortfolio(self, contract: ib_contract.Contract, position: float, + marketPrice: float, marketValue: float, + averageCost: float, unrealizedPNL: float, + realizedPNL: float, accountName: str): + # override method + print(contract) + print(f'Position: {position}\nMarket Price: {marketPrice}\n' + f'Market Value: {marketValue}\nAvg Cost: {averageCost}\n' + f'Unrealized P&L: {unrealizedPNL}\nRealized P&L: {realizedPNL}\n' + f'Account Name: {accountName}') + + def updateAccountTime(self, timeStamp: str): + # override method + super().updateAccountTime(timeStamp) + + print(timeStamp) + + def accountDownloadEnd(self, accountName: str): + # override method + super().accountDownloadEnd(accountName) + + print('end') + #endregion - account updates + #endregion - Accounts & portfolio + # Get contract details def contractDetails(self, reqId, contractDetails): # override method diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index bcaa65c..55d110b 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -45,6 +45,7 @@ def setUpClass(cls): setattr(cls._client, "_thread", thread) + # _IBWrapper specifics @utils.async_test async def test_next_req_id(self): """Test retrieval of next usable request ID.""" @@ -92,9 +93,10 @@ def on_notify(self, msg_code: int, msg: str): self.assertTrue(mock_listener.triggered) + #region - IB account related @utils.async_test async def test_managed_accounts(self): - """ Test overridden function `managedAccounts`.""" + """Test overridden function `managedAccounts`.""" mock_delegate = utils.MockAccountListDelegate() self._wrapper.set_account_list_delegate(delegate=mock_delegate) @@ -104,6 +106,38 @@ async def test_managed_accounts(self): self.assertTrue(mock_delegate.accounts) + #region - account updates + @utils.async_test + async def test_update_account_value(self): + """Test overridden function `updateAccountValue`.""" + await self._start_account_updates() + + await self._cancel_account_updates() + + @utils.async_test + async def test_update_portfolio(self): + """Test overridden function `updatePortfolio`.""" + await self._start_account_updates() + + await self._cancel_account_updates() + + @utils.async_test + async def test_update_account_time(self): + """Test overridden function `updateAccountTime`.""" + await self._start_account_updates() + + await self._cancel_account_updates() + + @utils.async_test + async def test_account_download_end(self): + """Test overridden function `accountDownloadEnd`""" + await self._start_account_updates() + + await self._cancel_account_updates() + #endregion - account updates + #endregion - IB account related + + # Historical market data @utils.async_test async def test_historical_ticks(self): """Test overridden function `historicalTicks`.""" @@ -286,3 +320,15 @@ async def test_tick_by_tick_mid_point(self): @classmethod def tearDownClass(cls): cls._client.disconnect() + + #region - Private functions + async def _start_account_updates(self): + self._client.reqAccountUpdates(subscribe=True, + acctCode=os.getenv("IB_ACC_ID", "")) + await asyncio.sleep(1) + + async def _cancel_account_updates(self): + self._client.reqAccountUpdates(subscribe=False, + acctCode=os.getenv("IB_ACC_ID", "")) + await asyncio.sleep(0.5) + #endregion From 2d3f4288fcec9a046505658bbb158f4987883e6a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 05:34:14 +0000 Subject: [PATCH 013/126] Add account updates queue property - Added property `_account_updates_queue` in interface `_AccountListDelegate` and class `AccountsManager`. --- ibpy_native/account.py | 12 ++++++++++++ ibpy_native/interfaces/delegates/account.py | 13 +++++++++++++ tests/toolkit/utils.py | 13 +++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 4fc13d8..659281f 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -1,9 +1,11 @@ """IB account related resources.""" # pylint: disable=protected-access +import queue from typing import List, Optional from ibpy_native import models from ibpy_native.interfaces import delegates +from ibpy_native.utils import finishable_queue as fq class AccountsManager(delegates._AccountListDelegate): """Class to manage all IB accounts under the same username logged-in on @@ -12,6 +14,9 @@ class AccountsManager(delegates._AccountListDelegate): def __init__(self, accounts: Optional[List[models.Account]] = None): self._accounts: List[models.Account] = ([] if accounts is None else accounts) + self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( + queue_to_finish=queue.Queue() + ) @property def accounts(self) -> List[models.Account]: @@ -24,6 +29,13 @@ def accounts(self) -> List[models.Account]: # Implements `delegates._AccountListDelegate` return self._accounts + @property + def account_updates_queue(self) -> fq._FinishableQueue: + """":obj:`ibpy_native.utils.finishable_queue._FinishableQueue`: + The queue that stores account updates data from IB Gateway. + """ + return self._account_updates_queue + def on_account_list_update(self, account_list: List[str]): """Callback function for internal API callback `_IBWrapper.managedAccounts`. diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index afe72ac..7697e45 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -1,8 +1,10 @@ """Internal delegate module for accounts & portfolio related features.""" +# pylint: disable=protected-access import abc from typing import List from ibpy_native import models +from ibpy_native.utils import finishable_queue as fq class _AccountListDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @@ -15,6 +17,17 @@ def accounts(self) -> List[models.Account]: """ return NotImplemented + @property + @abc.abstractmethod + def account_updates_queue(self) -> fq._FinishableQueue: + """Abstract getter of the queue designed to handle account updates + data from IB gateway. + + This property should be implemented to return the `_FinishableQueue` + object. + """ + return NotImplemented + @abc.abstractmethod def on_account_list_update(self, account_list: List[str]): """Callback on `_IBWrapper.managedAccounts` is triggered by IB API. diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 7afdb51..4ee3c76 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -1,6 +1,7 @@ """Utilities for making unittests easier to write.""" # pylint: disable=protected-access import asyncio +import queue from typing import List, Union from ibapi import wrapper as ib_wrapper @@ -9,6 +10,7 @@ from ibpy_native import models from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners +from ibpy_native.utils import finishable_queue as fq def async_test(fn): # pylint: disable=invalid-name @@ -23,14 +25,21 @@ def wrapper(*args, **kwargs): class MockAccountListDelegate(delegates._AccountListDelegate): """Mock accounts delegate""" - _account_list: List[models.Account] = [] + def __init__(self): + self._account_list: List[models.Account] = [] + self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( + queue_to_finish=queue.Queue() + ) @property def accounts(self) -> List[models.Account]: return self._account_list + @property + def account_updates_queue(self) -> fq._FinishableQueue: + return self._account_updates_queue + def on_account_list_update(self, account_list: List[str]): - # self._account_list = account_list for account_id in account_list: self._account_list.append(models.Account(account_id)) From cbd409fae1cd074c07ead9e6ddd6dd96317eeaa1 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 14:57:02 +0000 Subject: [PATCH 014/126] Add models for account updates - Added model classes `RawAccountValueData` & `RawPortfolioData` to organise updates received from account updates request. --- ibpy_native/models/__init__.py | 2 ++ ibpy_native/models/account.py | 53 ++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py index 301bf3a..6212072 100644 --- a/ibpy_native/models/__init__.py +++ b/ibpy_native/models/__init__.py @@ -1 +1,3 @@ from .account import Account +from .account import RawAccountValueData +from .account import RawPortfolioData diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 1bb237a..f5ef632 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -1,5 +1,8 @@ """Models for IB account(s).""" -from typing_extensions import Final +import dataclasses +from typing_extensions import final, Final + +from ibapi import contract as ib_contract class Account: """Model class for individual IB account. @@ -28,4 +31,50 @@ def destory(self): """Marks the instance will be destoried and no further action should be performed on this instance. """ - self._destroy_flag = True \ No newline at end of file + self._destroy_flag = True + +@final +@dataclasses.dataclass +class RawAccountValueData: + """Model class for account value updates received in callback + `ibpy_native.internal.wrapper.IBWrapper.updateAccountValue`. + + Attributes: + account (str): Account ID that the data belongs to. + currency (str): The currency on which the value is expressed. + key (str): The value being updated. See `TWS API doc`_ for the full + list of `key`. + val (str): Up-to-date value. + + .. _TWS API doc: + https://interactivebrokers.github.io/tws-api/interfaceIBApi_1_1EWrapper.html#ae15a34084d9f26f279abd0bdeab1b9b5 + """ + account: str + currency: str + key: str + val: str + +@final +@dataclasses.dataclass +class RawPortfolioData: + """Model class for portfolio updates received in callback + `ibpy_native.internal.wrapper.IBWrapper.updatePortfolio`. + + Attributes: + account (str): Account ID that the data belongs to. + contract (:obj:`ibapi.contract.Contract`): The contract for which a + position is held. + position (float): The number of positions held. + market_price (float): Instrument's unitary price. + market_val (float): Total market value of the instrument. + avg_cost (float): The average cost of the positions held. + unrealised_pnl (float): Unrealised profit and loss. + realised_pnl (float): Realised profit and loss. + """ + account: str + contract: ib_contract.Contract + market_price: float + market_val: float + avg_cost: float + unrealised_pnl: float + realised_pnl: float From 2971ed4eb287b2640ec248003a0a026750022803 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 15:58:38 +0000 Subject: [PATCH 015/126] Implement account updates callbacks - Overridden callback functions `updateAccountValue`, `updatePortfolio`, and `updateAccountTime` to put the updates received into the queue if the delegate responsible to account updates is available. --- ibpy_native/internal/wrapper.py | 38 ++++---- tests/internal/test_ib_wrapper.py | 149 ++++++++++++++++++------------ 2 files changed, 107 insertions(+), 80 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 088604e..1200e1f 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -7,6 +7,7 @@ from ibapi import wrapper from ibpy_native import error +from ibpy_native import models from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -128,6 +129,7 @@ def error(self, reqId, errorCode, errorString): msg=errorString ) + #region - Override functions from `wrapper.EWrapper` #region - Accounts & portfolio def managedAccounts(self, accountsList: str): # override method @@ -144,33 +146,28 @@ def managedAccounts(self, accountsList: str): #region - account updates def updateAccountValue(self, key: str, val: str, currency: str, accountName: str): - # override method - super().updateAccountValue(key, val, currency, accountName) - print(f'Key: {key}\nValue: {val}\nCurrency: {currency}\n' - f'Account Name: {accountName}\n') + if self._account_list_delegate: + data = models.RawAccountValueData( + account=accountName, currency=currency, key=key, val=val + ) + self._account_list_delegate.account_updates_queue.put(data) def updatePortfolio(self, contract: ib_contract.Contract, position: float, marketPrice: float, marketValue: float, averageCost: float, unrealizedPNL: float, realizedPNL: float, accountName: str): - # override method - print(contract) - print(f'Position: {position}\nMarket Price: {marketPrice}\n' - f'Market Value: {marketValue}\nAvg Cost: {averageCost}\n' - f'Unrealized P&L: {unrealizedPNL}\nRealized P&L: {realizedPNL}\n' - f'Account Name: {accountName}') + if self._account_list_delegate: + data = models.RawPortfolioData( + account=accountName, contract=contract, + market_price=marketPrice, market_val=marketValue, + avg_cost=averageCost, unrealised_pnl=unrealizedPNL, + realised_pnl=realizedPNL + ) + self._account_list_delegate.account_updates_queue.put(data) def updateAccountTime(self, timeStamp: str): - # override method - super().updateAccountTime(timeStamp) - - print(timeStamp) - - def accountDownloadEnd(self, accountName: str): - # override method - super().accountDownloadEnd(accountName) - - print('end') + if self._account_list_delegate: + self._account_list_delegate.account_updates_queue.put(timeStamp) #endregion - account updates #endregion - Accounts & portfolio @@ -246,6 +243,7 @@ def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): record.price = midPoint self._handle_live_ticks(req_id=reqId, tick=record) + #endregion - Override functions from `wrapper.EWrapper` ## Private functions def __init_req_queue(self, req_id: int): diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 55d110b..814568d 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -8,6 +8,7 @@ from ibapi import wrapper as ib_wrapper +from ibpy_native import models from ibpy_native.interfaces import listeners from ibpy_native.internal import client as ibpy_client from ibpy_native.internal import wrapper as ibpy_wrapper @@ -61,18 +62,6 @@ async def test_next_req_id(self): await f_queue.get() self.assertEqual(self._wrapper.next_req_id, 1) - def test_account_list_delegate(self): - """Test `_AccountListDelegate` implementation.""" - mock_delegate = utils.MockAccountListDelegate() - - self._wrapper.set_account_list_delegate(delegate=mock_delegate) - mock_delegate.on_account_list_update( - account_list=['DU0000140', 'DU0000141'] - ) - - print(mock_delegate.accounts) - self.assertTrue(mock_delegate.accounts) - def test_notification_listener(self): """Test notification listener approach.""" class MockListener(listeners.NotificationListener): @@ -93,50 +82,6 @@ def on_notify(self, msg_code: int, msg: str): self.assertTrue(mock_listener.triggered) - #region - IB account related - @utils.async_test - async def test_managed_accounts(self): - """Test overridden function `managedAccounts`.""" - mock_delegate = utils.MockAccountListDelegate() - - self._wrapper.set_account_list_delegate(delegate=mock_delegate) - self._client.reqManagedAccts() - - await asyncio.sleep(1) - - self.assertTrue(mock_delegate.accounts) - - #region - account updates - @utils.async_test - async def test_update_account_value(self): - """Test overridden function `updateAccountValue`.""" - await self._start_account_updates() - - await self._cancel_account_updates() - - @utils.async_test - async def test_update_portfolio(self): - """Test overridden function `updatePortfolio`.""" - await self._start_account_updates() - - await self._cancel_account_updates() - - @utils.async_test - async def test_update_account_time(self): - """Test overridden function `updateAccountTime`.""" - await self._start_account_updates() - - await self._cancel_account_updates() - - @utils.async_test - async def test_account_download_end(self): - """Test overridden function `accountDownloadEnd`""" - await self._start_account_updates() - - await self._cancel_account_updates() - #endregion - account updates - #endregion - IB account related - # Historical market data @utils.async_test async def test_historical_ticks(self): @@ -321,14 +266,98 @@ async def test_tick_by_tick_mid_point(self): def tearDownClass(cls): cls._client.disconnect() +class TestAccountAndPortfolioData(unittest.TestCase): + """Unit tests for account and portfolio data related callbacks & functions + in class `ibpy_native.internal.wrapper._IBWrapper`. + + Connection with IB Gateway is required for this test suit. + """ + @classmethod + def setUpClass(cls): + cls.wrapper = ibpy_wrapper._IBWrapper() + cls.client = ibpy_client._IBClient(cls.wrapper) + + cls.client.connect( + os.getenv('IB_HOST', '127.0.0.1'), + int(os.getenv('IB_PORT', '4002')), + 1001 + ) + + thread = threading.Thread(target=cls.client.run) + thread.start() + + setattr(cls.client, "_thread", thread) + + def setUp(self): + self.mock_delegate = utils.MockAccountListDelegate() + self.wrapper.set_account_list_delegate(self.mock_delegate) + + def test_account_list_delegate(self): + """Test `_AccountListDelegate` implementation.""" + self.mock_delegate.on_account_list_update( + account_list=['DU0000140', 'DU0000141'] + ) + + self.assertTrue(self.mock_delegate.accounts) + + @utils.async_test + async def test_managed_accounts(self): + """Test overridden function `managedAccounts`.""" + self.client.reqManagedAccts() + + await asyncio.sleep(1) + + self.assertTrue(self.mock_delegate.accounts) + + #region - account updates + @utils.async_test + async def test_update_account_value(self): + """Test overridden function `updateAccountValue`.""" + await self._start_account_updates() + await self._cancel_account_updates() + + result: list = await self.mock_delegate.account_updates_queue.get() + self.assertTrue( + any(isinstance(data, models.RawAccountValueData) for data in result) + ) + + @utils.async_test + async def test_update_portfolio(self): + """Test overridden function `updatePortfolio`.""" + await self._start_account_updates() + await self._cancel_account_updates() + + result: list = await self.mock_delegate.account_updates_queue.get() + self.assertTrue( + any(isinstance(data, models.RawPortfolioData) for data in result) + ) + + @utils.async_test + async def test_update_account_time(self): + """Test overridden function `updateAccountTime`.""" + await self._start_account_updates() + await self._cancel_account_updates() + + result: list = await self.mock_delegate.account_updates_queue.get() + self.assertRegex( + next(data for data in result if isinstance(data, str)), + r"\d{2}:\d{2}" + ) + #endregion - account updates + + @classmethod + def tearDownClass(cls): + cls.client.disconnect() + #region - Private functions async def _start_account_updates(self): - self._client.reqAccountUpdates(subscribe=True, - acctCode=os.getenv("IB_ACC_ID", "")) + self.client.reqAccountUpdates(subscribe=True, + acctCode=os.getenv("IB_ACC_ID", "")) await asyncio.sleep(1) async def _cancel_account_updates(self): - self._client.reqAccountUpdates(subscribe=False, - acctCode=os.getenv("IB_ACC_ID", "")) + self.client.reqAccountUpdates(subscribe=False, + acctCode=os.getenv("IB_ACC_ID", "")) await asyncio.sleep(0.5) + self.mock_delegate.account_updates_queue.put(fq._Status.FINISHED) #endregion From 0f202dc887f058a2b2a193fecc6cec1d3286edac Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 16:45:51 +0000 Subject: [PATCH 016/126] Rename `_AccountListDelegate` - Renamed the delegate to `_AccountManagementDelegate` as it no longer responsibles to handle the account list only. - Refactored all relevant members. --- ibpy_native/account.py | 2 +- ibpy_native/bridge.py | 2 +- ibpy_native/interfaces/delegates/__init__.py | 2 +- ibpy_native/interfaces/delegates/account.py | 2 +- ibpy_native/internal/wrapper.py | 27 ++++++++++---------- tests/internal/test_ib_wrapper.py | 8 +++--- tests/toolkit/utils.py | 2 +- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 659281f..4b33a79 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -7,7 +7,7 @@ from ibpy_native.interfaces import delegates from ibpy_native.utils import finishable_queue as fq -class AccountsManager(delegates._AccountListDelegate): +class AccountsManager(delegates._AccountManagementDelegate): """Class to manage all IB accounts under the same username logged-in on IB Gateway. """ diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index a8147f7..d755eed 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -40,7 +40,7 @@ def __init__( self._wrapper = ib_wrapper._IBWrapper( notification_listener=notification_listener ) - self._wrapper.set_account_list_delegate(delegate=self._accounts_manager) + self._wrapper.set_account_management_delegate(delegate=self._accounts_manager) self._client = ib_client._IBClient(wrapper=self._wrapper) diff --git a/ibpy_native/interfaces/delegates/__init__.py b/ibpy_native/interfaces/delegates/__init__.py index 3b1b078..060c54b 100644 --- a/ibpy_native/interfaces/delegates/__init__.py +++ b/ibpy_native/interfaces/delegates/__init__.py @@ -1,2 +1,2 @@ """Interfaces of delegates.""" -from .account import _AccountListDelegate +from .account import _AccountManagementDelegate diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 7697e45..68bdedf 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -6,7 +6,7 @@ from ibpy_native import models from ibpy_native.utils import finishable_queue as fq -class _AccountListDelegate(metaclass=abc.ABCMeta): +class _AccountManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @property @abc.abstractmethod diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 1200e1f..51b468b 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -23,8 +23,8 @@ def __init__( ): self._req_queue: Dict[int, fq._FinishableQueue] = {} - self._account_list_delegate: Optional[ - delegates._AccountListDelegate] = None + self._ac_man_delegate: Optional[ + delegates._AccountManagementDelegate] = None self._notification_listener: Optional[ listeners.NotificationListener] = notification_listener @@ -95,15 +95,16 @@ def get_request_queue_no_throw(self, req_id: int) -> \ return self._req_queue[req_id] if req_id in self._req_queue else None # Setters - def set_account_list_delegate(self, - delegate: delegates._AccountListDelegate): + def set_account_management_delegate(self, + delegate: delegates + ._AccountManagementDelegate): """Setter for optional `_AccountListDelegate`. Args: delegate (ibpy_native.interfaces.delegates._AccountListDelegate): Delegate for managing IB account list. """ - self._account_list_delegate = delegate + self._ac_man_delegate = delegate def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. @@ -138,36 +139,36 @@ def managedAccounts(self, accountsList: str): # Separate different account IDs into a list account_list = trimmed.split(',') - if self._account_list_delegate is not None: - self._account_list_delegate.on_account_list_update( + if self._ac_man_delegate is not None: + self._ac_man_delegate.on_account_list_update( account_list=account_list ) #region - account updates def updateAccountValue(self, key: str, val: str, currency: str, accountName: str): - if self._account_list_delegate: + if self._ac_man_delegate: data = models.RawAccountValueData( account=accountName, currency=currency, key=key, val=val ) - self._account_list_delegate.account_updates_queue.put(data) + self._ac_man_delegate.account_updates_queue.put(data) def updatePortfolio(self, contract: ib_contract.Contract, position: float, marketPrice: float, marketValue: float, averageCost: float, unrealizedPNL: float, realizedPNL: float, accountName: str): - if self._account_list_delegate: + if self._ac_man_delegate: data = models.RawPortfolioData( account=accountName, contract=contract, market_price=marketPrice, market_val=marketValue, avg_cost=averageCost, unrealised_pnl=unrealizedPNL, realised_pnl=realizedPNL ) - self._account_list_delegate.account_updates_queue.put(data) + self._ac_man_delegate.account_updates_queue.put(data) def updateAccountTime(self, timeStamp: str): - if self._account_list_delegate: - self._account_list_delegate.account_updates_queue.put(timeStamp) + if self._ac_man_delegate: + self._ac_man_delegate.account_updates_queue.put(timeStamp) #endregion - account updates #endregion - Accounts & portfolio diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 814568d..141c4f5 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -289,11 +289,11 @@ def setUpClass(cls): setattr(cls.client, "_thread", thread) def setUp(self): - self.mock_delegate = utils.MockAccountListDelegate() - self.wrapper.set_account_list_delegate(self.mock_delegate) + self.mock_delegate = utils.MockAccountManagementDelegate() + self.wrapper.set_account_management_delegate(self.mock_delegate) - def test_account_list_delegate(self): - """Test `_AccountListDelegate` implementation.""" + def test_account_management_delegate(self): + """Test `_AccountManagementDelegate` implementation.""" self.mock_delegate.on_account_list_update( account_list=['DU0000140', 'DU0000141'] ) diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 4ee3c76..6f40bbc 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -22,7 +22,7 @@ def wrapper(*args, **kwargs): return wrapper -class MockAccountListDelegate(delegates._AccountListDelegate): +class MockAccountManagementDelegate(delegates._AccountManagementDelegate): """Mock accounts delegate""" def __init__(self): From 752735abdeda794bd4d7ebe116baffef3201da95 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 16:50:28 +0000 Subject: [PATCH 017/126] Add docstring --- ibpy_native/models/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py index 6212072..1c32f88 100644 --- a/ibpy_native/models/__init__.py +++ b/ibpy_native/models/__init__.py @@ -1,3 +1,4 @@ +"""Expose models on package level.""" from .account import Account from .account import RawAccountValueData from .account import RawPortfolioData From b659fba59ae24ec52325d84edef8233d8027f221 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 22:11:50 +0000 Subject: [PATCH 018/126] Thread-safe `_FinishableQueue` - Make `_FinishableQueue` thread-safe by adding a thread lock and locks the thread while updating the value of `self._status`. --- ibpy_native/utils/finishable_queue.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index 2e083c0..2eb0b92 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -2,6 +2,7 @@ import asyncio import enum import queue +import threading from typing import Iterator, Any @@ -14,14 +15,16 @@ class _Status(enum.Enum): TIMEOUT = 408 class _FinishableQueue(): - """This class takes a built-in `Queue` object to handle the async tasks by - managing its' status based on elements retrieve from the `Queue` object. + """Thread-safe class that takes a built-in `queue.Queue` object to handle + the async tasks by managing its' status based on elements retrieve from the + `Queue` object. Args: queue_to_finish (:obj:`queue.Queue`): queue object assigned to handle the async task """ def __init__(self, queue_to_finish: queue.Queue): + self._lock = threading.Lock() self._queue = queue_to_finish self._status = _Status.STARTED @@ -75,10 +78,12 @@ async def get(self) -> list: ) if current_element is _Status.FINISHED: - self._status = _Status.FINISHED + with self._lock: + self._status = _Status.FINISHED else: if isinstance(current_element, BaseException): - self._status = _Status.ERROR + with self._lock: + self._status = _Status.ERROR contents_of_queue.append(current_element) @@ -96,8 +101,10 @@ async def stream(self) -> Iterator[Any]: ) if current_element is _Status.FINISHED: - self._status = current_element + with self._lock: + self._status = current_element elif isinstance(current_element, BaseException): - self._status = _Status.ERROR + with self._lock: + self._status = _Status.ERROR yield current_element From b45e301c63769a30346ec15da397a585c67c6b9e Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 25 Jan 2021 23:33:12 +0000 Subject: [PATCH 019/126] Modify `_FinishableQueue` status - Added status `_Status.INIT` to represent the `_FinishableQueue` is newly initialised and untouched. - Renamed status `_Status.STARTED` to `_Status.READY`. --- ibpy_native/utils/finishable_queue.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index 2eb0b92..4643ef5 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -9,7 +9,8 @@ # Queue status class _Status(enum.Enum): """Status codes for `_FinishableQueue`""" - STARTED = 103 + INIT = 0 + READY = 103 ERROR = 500 FINISHED = 200 TIMEOUT = 408 @@ -26,16 +27,13 @@ class _FinishableQueue(): def __init__(self, queue_to_finish: queue.Queue): self._lock = threading.Lock() self._queue = queue_to_finish - self._status = _Status.STARTED + self._status = _Status.INIT @property def status(self) -> _Status: - """Get status of the finishable queue. - - Returns: - ibpy_native.utils.finishable_queue._Status: Enum `_Status` - represents either the queue has been started, finished, - timeout, or encountered error. + """:obj:`ibpy_native.utils.finishable_queue._Status`: Status represents + wether the queue is newly initialised, ready for use, finished, + timeout, or encountered error. """ return self._status @@ -56,10 +54,14 @@ def reset(self): status is marked as either `TIMEOUT` or `FINISHED` """ if self.finished: - self._status = _Status.STARTED + self._status = _Status.READY def put(self, element: Any): """Setter to put element to internal synchronised queue.""" + if self._status is _Status.INIT: + with self._lock: + self._status = _Status.READY + self._queue.put(element) async def get(self) -> list: From 191b4cd58a5d9f2759a0f4218b8697478884bafc Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 26 Jan 2021 14:57:48 +0000 Subject: [PATCH 020/126] Account model handles account info - Implements getter & setter functions to handle the account's information received from IB Gateway. - Create unit test assert the implementation of newly created getter & setter. --- ibpy_native/models/account.py | 45 +++++++++++++++++++++++++++++++++++ tests/models/__init__.py | 0 tests/models/test_account.py | 28 ++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/models/__init__.py create mode 100644 tests/models/test_account.py diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index f5ef632..2dd830b 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -1,5 +1,8 @@ """Models for IB account(s).""" import dataclasses +import threading +from typing import Dict, Optional, Union + from typing_extensions import final, Final from ibapi import contract as ib_contract @@ -14,6 +17,9 @@ class Account: _destroy_flag: bool = False def __init__(self, account_id: str): + self._lock = threading.Lock() + self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} + self.account_id = account_id @property @@ -27,6 +33,45 @@ def destory_flag(self) -> bool: """ return self._destroy_flag + def get_account_value(self, key: str, currency: str = "") -> Optional[str]: + """Returns the value of specified account's information. + + Args: + key (str): The account info to retrieve. + currency (:obj:`str`, optional): The currency on which the value + is expressed. Defaults to empty string. + + Returns: + :obj:`str`, optional: Value of specified account's information. + `None` if no info found with the specified `key` & `currency`. + """ + if key in self._account_values: + if currency in self._account_values[key]: + return self._account_values[key] if ( + isinstance(self._account_values[key], str) + ) else self._account_values[key][currency] + + return None + + def update_account_value(self, key: str, currency: str = "", val: str = ""): + """Thread-safe setter function to update the account value. + + Args: + key (str): The value being updated. + currency (:obj:`str`, optional): The currency on which the value + is expressed. Defaults to empty string. + val (:obj:`str`, optional): Up-to-date value. Defaults to empty + string. + """ + with self._lock: + if not currency: + self._account_values[key] = val + else: + if key not in self._account_values: + self._account_values[key]: Dict[str, str] = {} + + self._account_values[key][currency] = val + def destory(self): """Marks the instance will be destoried and no further action should be performed on this instance. diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/test_account.py b/tests/models/test_account.py new file mode 100644 index 0000000..ec6c135 --- /dev/null +++ b/tests/models/test_account.py @@ -0,0 +1,28 @@ +"""Unit test for module `ibpy_native.models.account`.""" +import unittest + +from ibpy_native.models import account + +class TestAccountModel(unittest.TestCase): + """Unit test for model class `Account`""" + + def setUp(self): + self.account = account.Account(account_id="DU0000140") + + def test_get_set_account_value(self): + """Test the implementation of account value's getter & setter.""" + self.account.update_account_value(key="AccountReady", + currency="", + val="true") + self.account.update_account_value(key="AvailableFunds", + currency="BASE", + val="25000") + self.assertEqual("true", + self.account.get_account_value(key="AccountReady")) + self.assertEqual("25000", + self.account.get_account_value(key="AvailableFunds", + currency="BASE")) + self.assertIsNone(self.account.get_account_value(key="AvailableFunds", + currency="USD")) + self.assertIsNone(self.account.get_account_value(key="AccountCode")) + \ No newline at end of file From 0184b22b9c21c1ef1e907a963daecf2e764218cc Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 26 Jan 2021 15:22:56 +0000 Subject: [PATCH 021/126] Thread-safe `Account` model - Thread lock while updating the value of `self._destroy_flag`. --- ibpy_native/models/account.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 2dd830b..3b23a06 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -13,14 +13,13 @@ class Account: Attributes: account_id (:obj:`Final[str]`): Account ID received from IB Gateway. """ - account_id: Final[str] - _destroy_flag: bool = False - def __init__(self, account_id: str): self._lock = threading.Lock() + self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} + self._destroy_flag = False - self.account_id = account_id + self.account_id: Final[str] = account_id @property def destory_flag(self) -> bool: @@ -76,7 +75,8 @@ def destory(self): """Marks the instance will be destoried and no further action should be performed on this instance. """ - self._destroy_flag = True + with self._lock: + self._destroy_flag = True @final @dataclasses.dataclass From 2a80f42d7df2b8c5b1abf17a93e0cd179e120667 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 26 Jan 2021 15:27:18 +0000 Subject: [PATCH 022/126] Fix typo "destory" --- ibpy_native/models/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 3b23a06..cf95805 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -22,8 +22,8 @@ def __init__(self, account_id: str): self.account_id: Final[str] = account_id @property - def destory_flag(self) -> bool: - """Flag to indicate if this account instance is going to be destoried. + def destroy_flag(self) -> bool: + """Flag to indicate if this account instance is going to be destroyed. Returns: bool: `True` if the corresponding account on IB is no longer From 29d4304c388c5750fd92564a850637b810f695d2 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 26 Jan 2021 15:34:19 +0000 Subject: [PATCH 023/126] Refactor `account_id` to property --- ibpy_native/models/account.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index cf95805..f9c59a6 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -3,23 +3,24 @@ import threading from typing import Dict, Optional, Union -from typing_extensions import final, Final +from typing_extensions import final from ibapi import contract as ib_contract class Account: - """Model class for individual IB account. + """Model class for individual IB account.""" - Attributes: - account_id (:obj:`Final[str]`): Account ID received from IB Gateway. - """ def __init__(self, account_id: str): self._lock = threading.Lock() + self._account_id = account_id self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} self._destroy_flag = False - self.account_id: Final[str] = account_id + @property + def account_id(self) -> str: + """str: Account ID received from IB Gateway.""" + return self._account_id @property def destroy_flag(self) -> bool: From a2d3a069bc1eae74e2bd622482e6dbb4780a1c2f Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 26 Jan 2021 15:53:14 +0000 Subject: [PATCH 024/126] Add boolean `account_ready` - Added property `account_ready` to save the ready status received from IB Gateway. --- ibpy_native/models/account.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index f9c59a6..314fd51 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -14,6 +14,7 @@ def __init__(self, account_id: str): self._lock = threading.Lock() self._account_id = account_id + self._account_ready = False self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} self._destroy_flag = False @@ -22,6 +23,18 @@ def account_id(self) -> str: """str: Account ID received from IB Gateway.""" return self._account_id + @property + def account_ready(self) -> bool: + """bool: If false, the account value stored can be out of date or + incorrect. + """ + return self._account_ready + + @account_ready.setter + def account_ready(self, status: bool): + with self._lock: + self._account_ready = status + @property def destroy_flag(self) -> bool: """Flag to indicate if this account instance is going to be destroyed. From d065b3706b3ed0acf6ab9bfac1942dd1b7d03877 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 29 Jan 2021 08:33:15 +0000 Subject: [PATCH 025/126] Add model `Position` - Added model class `Position` in module `morels.account` for storing the processed account position data. --- ibpy_native/models/__init__.py | 1 + ibpy_native/models/account.py | 118 +++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py index 1c32f88..8e442d5 100644 --- a/ibpy_native/models/__init__.py +++ b/ibpy_native/models/__init__.py @@ -1,4 +1,5 @@ """Expose models on package level.""" from .account import Account +from .account import Position from .account import RawAccountValueData from .account import RawPortfolioData diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 314fd51..adf9821 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -1,11 +1,14 @@ """Models for IB account(s).""" +# pylint: disable=protected-access, too-many-instance-attributes import dataclasses +import datetime import threading from typing import Dict, Optional, Union from typing_extensions import final from ibapi import contract as ib_contract +from ibpy_native.internal import client as ib_client class Account: """Model class for individual IB account.""" @@ -92,6 +95,121 @@ def destory(self): with self._lock: self._destroy_flag = True +@final +class Position: + """Thread-safe model class for portfolio data. + + Args: + contract (:obj:`ibapi.contract.Contract`): The contract for which a + position is held. + pos (float): The number of positions held. + mk_price (float): Instrument's unitary price. + mk_val (float): Total market value of the instrument. + avg_cost (float): The average cost of the positions held. + un_pnl (float): Unrealised profit and loss. + r_pnl (float): Realised profit and loss. + """ + def __init__(self, contract: ib_contract.Contract, pos: float, + mk_price: float, mk_val: float, avg_cost: float, + un_pnl: float, r_pnl: float): + self._lock = threading.Lock() + + self._contract = contract + self._pos = pos + self._mk_price = mk_price + self._mk_val = mk_val + self._avg_cost = avg_cost + self._un_pnl = un_pnl + self._r_pnl = r_pnl + self._last_update = ib_client._IBClient.TZ.localize( + dt=datetime.datetime.now() + ) + + @property + def contract(self) -> ib_contract.Contract: + """:obj:`ibapi.contract.Contract`: The contract for which a position + is held. + """ + return self._contract + + @property + def position(self) -> float: + """float: The number of positions held.""" + return self._pos + + @position.setter + def position(self, val: float): + with self._lock: + self._pos = val + + @property + def market_price(self) -> float: + """float: Instrument's unitary price.""" + return self._mk_price + + @market_price.setter + def market_price(self, val: float): + with self._lock: + self._mk_price = val + + @property + def market_value(self) -> float: + """float: Total market value of the instrument.""" + return self._mk_val + + @market_value.setter + def market_value(self, val: float): + with self._lock: + self._mk_val = val + + @property + def avg_cost(self) -> float: + """float: The average of the positions held.""" + return self._avg_cost + + @avg_cost.setter + def avg_cost(self, val: float): + with self._lock: + self._avg_cost = val + + @property + def unrealised_pnl(self) -> float: + """float: Unrealised profit and loss.""" + return self._un_pnl + + @unrealised_pnl.setter + def unrealised_pnl(self, val: float): + with self._lock: + self._un_pnl = val + + @property + def realised_pnl(self) -> float: + """float: Realised profit and loss.""" + return self._r_pnl + + @realised_pnl.setter + def realised_pnl(self, val: float): + with self._lock: + self._r_pnl = val + + @property + def last_update_time(self) -> datetime.datetime: + """:obj:`datetime.datetime`: The last time on which the position data + was updated. + + Note: + This property will always be converted to an aware object + based on the timezone set via `ibpy_native.bridge.IBBridge.`. + """ + return self._last_update + + @last_update_time.setter + def last_update_time(self, val: datetime.datetime): + converted = ib_client._IBClient.TZ.localize(dt=val) + + with self._lock: + self._last_update = converted + @final @dataclasses.dataclass class RawAccountValueData: From d46df7e2ca32a6eaa26323456aa652d7e07e2c9b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 29 Jan 2021 17:06:19 +0000 Subject: [PATCH 026/126] Separated module to avoid import issues - Separated module `ibpy_native.models.account` into 3 to prevent Python import issues such as circular import. --- ibpy_native/models/__init__.py | 6 +- ibpy_native/models/account.py | 161 -------------------------------- ibpy_native/models/portfolio.py | 121 ++++++++++++++++++++++++ ibpy_native/models/raw_data.py | 52 +++++++++++ 4 files changed, 176 insertions(+), 164 deletions(-) create mode 100644 ibpy_native/models/portfolio.py create mode 100644 ibpy_native/models/raw_data.py diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py index 8e442d5..b0557e0 100644 --- a/ibpy_native/models/__init__.py +++ b/ibpy_native/models/__init__.py @@ -1,5 +1,5 @@ """Expose models on package level.""" from .account import Account -from .account import Position -from .account import RawAccountValueData -from .account import RawPortfolioData +from .portfolio import Position +from .raw_data import RawAccountValueData +from .raw_data import RawPortfolioData diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index adf9821..c28e337 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -94,164 +94,3 @@ def destory(self): """ with self._lock: self._destroy_flag = True - -@final -class Position: - """Thread-safe model class for portfolio data. - - Args: - contract (:obj:`ibapi.contract.Contract`): The contract for which a - position is held. - pos (float): The number of positions held. - mk_price (float): Instrument's unitary price. - mk_val (float): Total market value of the instrument. - avg_cost (float): The average cost of the positions held. - un_pnl (float): Unrealised profit and loss. - r_pnl (float): Realised profit and loss. - """ - def __init__(self, contract: ib_contract.Contract, pos: float, - mk_price: float, mk_val: float, avg_cost: float, - un_pnl: float, r_pnl: float): - self._lock = threading.Lock() - - self._contract = contract - self._pos = pos - self._mk_price = mk_price - self._mk_val = mk_val - self._avg_cost = avg_cost - self._un_pnl = un_pnl - self._r_pnl = r_pnl - self._last_update = ib_client._IBClient.TZ.localize( - dt=datetime.datetime.now() - ) - - @property - def contract(self) -> ib_contract.Contract: - """:obj:`ibapi.contract.Contract`: The contract for which a position - is held. - """ - return self._contract - - @property - def position(self) -> float: - """float: The number of positions held.""" - return self._pos - - @position.setter - def position(self, val: float): - with self._lock: - self._pos = val - - @property - def market_price(self) -> float: - """float: Instrument's unitary price.""" - return self._mk_price - - @market_price.setter - def market_price(self, val: float): - with self._lock: - self._mk_price = val - - @property - def market_value(self) -> float: - """float: Total market value of the instrument.""" - return self._mk_val - - @market_value.setter - def market_value(self, val: float): - with self._lock: - self._mk_val = val - - @property - def avg_cost(self) -> float: - """float: The average of the positions held.""" - return self._avg_cost - - @avg_cost.setter - def avg_cost(self, val: float): - with self._lock: - self._avg_cost = val - - @property - def unrealised_pnl(self) -> float: - """float: Unrealised profit and loss.""" - return self._un_pnl - - @unrealised_pnl.setter - def unrealised_pnl(self, val: float): - with self._lock: - self._un_pnl = val - - @property - def realised_pnl(self) -> float: - """float: Realised profit and loss.""" - return self._r_pnl - - @realised_pnl.setter - def realised_pnl(self, val: float): - with self._lock: - self._r_pnl = val - - @property - def last_update_time(self) -> datetime.datetime: - """:obj:`datetime.datetime`: The last time on which the position data - was updated. - - Note: - This property will always be converted to an aware object - based on the timezone set via `ibpy_native.bridge.IBBridge.`. - """ - return self._last_update - - @last_update_time.setter - def last_update_time(self, val: datetime.datetime): - converted = ib_client._IBClient.TZ.localize(dt=val) - - with self._lock: - self._last_update = converted - -@final -@dataclasses.dataclass -class RawAccountValueData: - """Model class for account value updates received in callback - `ibpy_native.internal.wrapper.IBWrapper.updateAccountValue`. - - Attributes: - account (str): Account ID that the data belongs to. - currency (str): The currency on which the value is expressed. - key (str): The value being updated. See `TWS API doc`_ for the full - list of `key`. - val (str): Up-to-date value. - - .. _TWS API doc: - https://interactivebrokers.github.io/tws-api/interfaceIBApi_1_1EWrapper.html#ae15a34084d9f26f279abd0bdeab1b9b5 - """ - account: str - currency: str - key: str - val: str - -@final -@dataclasses.dataclass -class RawPortfolioData: - """Model class for portfolio updates received in callback - `ibpy_native.internal.wrapper.IBWrapper.updatePortfolio`. - - Attributes: - account (str): Account ID that the data belongs to. - contract (:obj:`ibapi.contract.Contract`): The contract for which a - position is held. - position (float): The number of positions held. - market_price (float): Instrument's unitary price. - market_val (float): Total market value of the instrument. - avg_cost (float): The average cost of the positions held. - unrealised_pnl (float): Unrealised profit and loss. - realised_pnl (float): Realised profit and loss. - """ - account: str - contract: ib_contract.Contract - market_price: float - market_val: float - avg_cost: float - unrealised_pnl: float - realised_pnl: float diff --git a/ibpy_native/models/portfolio.py b/ibpy_native/models/portfolio.py new file mode 100644 index 0000000..fd25861 --- /dev/null +++ b/ibpy_native/models/portfolio.py @@ -0,0 +1,121 @@ +"""Model classes for account portfolio related data.""" +# pylint: disable=too-many-instance-attributes +import datetime +import threading + +from typing_extensions import final + +from ibapi import contract as ib_contract + +@final +class Position: + """Thread-safe model class for portfolio data. + + Args: + contract (:obj:`ibapi.contract.Contract`): The contract for which a + position is held. + pos (float): The number of positions held. + mk_price (float): Instrument's unitary price. + mk_val (float): Total market value of the instrument. + avg_cost (float): The average cost of the positions held. + un_pnl (float): Unrealised profit and loss. + r_pnl (float): Realised profit and loss. + """ + def __init__(self, contract: ib_contract.Contract, pos: float, + mk_price: float, mk_val: float, avg_cost: float, + un_pnl: float, r_pnl: float): + self._lock = threading.Lock() + + self._contract = contract + self._pos = pos + self._mk_price = mk_price + self._mk_val = mk_val + self._avg_cost = avg_cost + self._un_pnl = un_pnl + self._r_pnl = r_pnl + self._last_update = datetime.datetime.now() + + @property + def contract(self) -> ib_contract.Contract: + """:obj:`ibapi.contract.Contract`: The contract for which a position + is held. + """ + return self._contract + + @property + def position(self) -> float: + """float: The number of positions held.""" + return self._pos + + @position.setter + def position(self, val: float): + with self._lock: + self._pos = val + + @property + def market_price(self) -> float: + """float: Instrument's unitary price.""" + return self._mk_price + + @market_price.setter + def market_price(self, val: float): + with self._lock: + self._mk_price = val + + @property + def market_value(self) -> float: + """float: Total market value of the instrument.""" + return self._mk_val + + @market_value.setter + def market_value(self, val: float): + with self._lock: + self._mk_val = val + + @property + def avg_cost(self) -> float: + """float: The average of the positions held.""" + return self._avg_cost + + @avg_cost.setter + def avg_cost(self, val: float): + with self._lock: + self._avg_cost = val + + @property + def unrealised_pnl(self) -> float: + """float: Unrealised profit and loss.""" + return self._un_pnl + + @unrealised_pnl.setter + def unrealised_pnl(self, val: float): + with self._lock: + self._un_pnl = val + + @property + def realised_pnl(self) -> float: + """float: Realised profit and loss.""" + return self._r_pnl + + @realised_pnl.setter + def realised_pnl(self, val: float): + with self._lock: + self._r_pnl = val + + @property + def last_update_time(self) -> datetime.datetime: + """:obj:`datetime.datetime`: The last time on which the position data + was updated. + + Note: + This property will always be converted to an aware object + based on the timezone set via `ibpy_native.bridge.IBBridge.`. + """ + return self._last_update + + @last_update_time.setter + def last_update_time(self, val: datetime.datetime): + # converted = ib_client._IBClient.TZ.localize(dt=val) + + with self._lock: + self._last_update = val diff --git a/ibpy_native/models/raw_data.py b/ibpy_native/models/raw_data.py new file mode 100644 index 0000000..7f36fe7 --- /dev/null +++ b/ibpy_native/models/raw_data.py @@ -0,0 +1,52 @@ +"""Model classes of raw data passed from IB Gateway to the wrapper.""" +import dataclasses + +from typing_extensions import final + +from ibapi import contract as ib_contract + +@final +@dataclasses.dataclass +class RawAccountValueData: + """Model class for account value updates received in callback + `ibpy_native.internal.wrapper.IBWrapper.updateAccountValue`. + + Attributes: + account (str): Account ID that the data belongs to. + currency (str): The currency on which the value is expressed. + key (str): The value being updated. See `TWS API doc`_ for the full + list of `key`. + val (str): Up-to-date value. + + .. _TWS API doc: + https://interactivebrokers.github.io/tws-api/interfaceIBApi_1_1EWrapper.html#ae15a34084d9f26f279abd0bdeab1b9b5 + """ + account: str + currency: str + key: str + val: str + +@final +@dataclasses.dataclass +class RawPortfolioData: + """Model class for portfolio updates received in callback + `ibpy_native.internal.wrapper.IBWrapper.updatePortfolio`. + + Attributes: + account (str): Account ID that the data belongs to. + contract (:obj:`ibapi.contract.Contract`): The contract for which a + position is held. + position (float): The number of positions held. + market_price (float): Instrument's unitary price. + market_val (float): Total market value of the instrument. + avg_cost (float): The average cost of the positions held. + unrealised_pnl (float): Unrealised profit and loss. + realised_pnl (float): Realised profit and loss. + """ + account: str + contract: ib_contract.Contract + market_price: float + market_val: float + avg_cost: float + unrealised_pnl: float + realised_pnl: float From ccc2a139de611d41506722f0358480fd9b176947 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Fri, 29 Jan 2021 18:04:09 +0000 Subject: [PATCH 027/126] Add missing field - Added missing variable `position` in model `RawPortfolioData`. --- ibpy_native/internal/wrapper.py | 6 +++--- ibpy_native/models/raw_data.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 51b468b..fd3e49c 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -160,9 +160,9 @@ def updatePortfolio(self, contract: ib_contract.Contract, position: float, if self._ac_man_delegate: data = models.RawPortfolioData( account=accountName, contract=contract, - market_price=marketPrice, market_val=marketValue, - avg_cost=averageCost, unrealised_pnl=unrealizedPNL, - realised_pnl=realizedPNL + position=position, market_price=marketPrice, + market_val=marketValue, avg_cost=averageCost, + unrealised_pnl=unrealizedPNL, realised_pnl=realizedPNL ) self._ac_man_delegate.account_updates_queue.put(data) diff --git a/ibpy_native/models/raw_data.py b/ibpy_native/models/raw_data.py index 7f36fe7..c030fb3 100644 --- a/ibpy_native/models/raw_data.py +++ b/ibpy_native/models/raw_data.py @@ -45,6 +45,7 @@ class RawPortfolioData: """ account: str contract: ib_contract.Contract + position: float market_price: float market_val: float avg_cost: float From cf53f1f00afdcf0f11fa8aa5ccbc80c63ee1fadf Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 31 Jan 2021 11:07:46 +0000 Subject: [PATCH 028/126] Handle portfolio data update - Implemented thread-safe function `update_portfolio` in model `Account` to handle data update for positions held. --- ibpy_native/models/account.py | 52 ++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index c28e337..61b478e 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -1,14 +1,9 @@ """Models for IB account(s).""" -# pylint: disable=protected-access, too-many-instance-attributes -import dataclasses -import datetime import threading from typing import Dict, Optional, Union -from typing_extensions import final - -from ibapi import contract as ib_contract -from ibpy_native.internal import client as ib_client +from ibpy_native.models import portfolio +from ibpy_native.models import raw_data class Account: """Model class for individual IB account.""" @@ -19,6 +14,8 @@ def __init__(self, account_id: str): self._account_id = account_id self._account_ready = False self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} + self._portfolio: Dict[int, portfolio.Position] = {} + self._destroy_flag = False @property @@ -38,6 +35,15 @@ def account_ready(self, status: bool): with self._lock: self._account_ready = status + @property + def positions(self) -> Dict[int, portfolio.Position]: + """:obj:`dict` of :obj:`ibpy_native.models.account.Position`: + Dictionary of positions held by the account this instance representing. + Using the unique IB contract identifier + (`ibapi.contract.Contract.ConId`) as key. + """ + return self._portfolio + @property def destroy_flag(self) -> bool: """Flag to indicate if this account instance is going to be destroyed. @@ -88,6 +94,38 @@ def update_account_value(self, key: str, currency: str = "", val: str = ""): self._account_values[key][currency] = val + def update_portfolio(self, contract_id: int, + data: raw_data.RawPortfolioData): + """Thread-safe setter function to update the positions held by the + account the instance is representing. + + Args: + contract_id (int): The unique IB contract identifier of the contract + of which a position is held. + data (:obj:`ibpy_native.models.RawPortfolioData`): The raw profolio + data received from IB Gateway. + """ + with self._lock: + if contract_id in self._portfolio: + position: portfolio.Position = self._portfolio[contract_id] + # Updates the existing position object stored in dictionary + position.contract = data.contract + position.position = data.position + position.market_price = data.market_price + position.market_value = data.market_val + position.avg_cost = data.avg_cost + position.unrealised_pnl = data.unrealised_pnl + position.realised_pnl = data.realised_pnl + else: + position = portfolio.Position(contract=data.contract, + pos=data.position, + mk_price=data.market_price, + mk_val=data.market_val, + avg_cost=data.avg_cost, + un_pnl=data.unrealised_pnl, + r_pnl=data.realised_pnl) + self._portfolio[contract_id] = position + def destory(self): """Marks the instance will be destoried and no further action should be performed on this instance. From 82a0865735a8649a25ecb4bbcb8869a1ff5fb581 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 31 Jan 2021 16:33:40 +0000 Subject: [PATCH 029/126] Change data type of `last_update_time`. - Changed the data type of `last_update_time` to `datetime.time` as the date is not important and no date will be received from IB Gateway but the time only anyway. --- ibpy_native/models/account.py | 14 ++++++++++++++ ibpy_native/models/portfolio.py | 18 +++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 61b478e..37937dd 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -1,4 +1,5 @@ """Models for IB account(s).""" +import datetime import threading from typing import Dict, Optional, Union @@ -15,6 +16,7 @@ def __init__(self, account_id: str): self._account_ready = False self._account_values: Dict[str, Union[str, Dict[str, str]]] = {} self._portfolio: Dict[int, portfolio.Position] = {} + self._last_update: Optional[datetime.time] = None self._destroy_flag = False @@ -44,6 +46,18 @@ def positions(self) -> Dict[int, portfolio.Position]: """ return self._portfolio + @property + def last_update_time(self) -> Optional[datetime.time]: + """:obj:`datetime.time`, optional: The last update system time + for the account values. `None` if update time is not yet received from + IB Gateway. + """ + return self._last_update + + @last_update_time.setter + def last_update_time(self, val: datetime.time): + self._last_update = val + @property def destroy_flag(self) -> bool: """Flag to indicate if this account instance is going to be destroyed. diff --git a/ibpy_native/models/portfolio.py b/ibpy_native/models/portfolio.py index fd25861..c2249a8 100644 --- a/ibpy_native/models/portfolio.py +++ b/ibpy_native/models/portfolio.py @@ -2,6 +2,7 @@ # pylint: disable=too-many-instance-attributes import datetime import threading +from typing import Optional from typing_extensions import final @@ -33,7 +34,7 @@ def __init__(self, contract: ib_contract.Contract, pos: float, self._avg_cost = avg_cost self._un_pnl = un_pnl self._r_pnl = r_pnl - self._last_update = datetime.datetime.now() + self._last_update: Optional[datetime.time] = None @property def contract(self) -> ib_contract.Contract: @@ -103,19 +104,14 @@ def realised_pnl(self, val: float): self._r_pnl = val @property - def last_update_time(self) -> datetime.datetime: - """:obj:`datetime.datetime`: The last time on which the position data - was updated. - - Note: - This property will always be converted to an aware object - based on the timezone set via `ibpy_native.bridge.IBBridge.`. + def last_update_time(self) -> Optional[datetime.time]: + """:obj:`datetime.time`: The last time on which the position data + was updated. `None` if the update time is not yet received from IB + Gateway. """ return self._last_update @last_update_time.setter - def last_update_time(self, val: datetime.datetime): - # converted = ib_client._IBClient.TZ.localize(dt=val) - + def last_update_time(self, val: datetime.time): with self._lock: self._last_update = val From eb5e66825f5e6eb39afc5bf30983bc6ea1275d01 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 1 Feb 2021 10:59:48 +0000 Subject: [PATCH 030/126] Handles account updates - Implemented functions in `AccountsManager` to handle account updates from IB Gateway. --- ibpy_native/account.py | 97 +++++++++++++++++++++++++++++++++++++++++- tests/test_account.py | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 4b33a79..806e31b 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -1,10 +1,14 @@ """IB account related resources.""" # pylint: disable=protected-access +import asyncio +import datetime +import re import queue -from typing import List, Optional +from typing import List, Optional, Union from ibpy_native import models from ibpy_native.interfaces import delegates +from ibpy_native.internal import client as ib_client from ibpy_native.utils import finishable_queue as fq class AccountsManager(delegates._AccountManagementDelegate): @@ -76,3 +80,94 @@ def on_account_list_update(self, account_list: List[str]): else: for acc_id in account_list: self._accounts.append(models.Account(account_id=acc_id)) + + async def sub_account_updates(self, account_id: str): + """Subscribes to account updates. + + Args: + account_id (str): The account to subscribe for updates. + """ + try: + # Check if the specified account ID is associated with one of the + # accounts being managed by this account manager. + account = next(ac for ac in self._accounts + if ac.account_id == account_id) + except StopIteration: + return + + await self._prevent_multi_account_updates() + + last_elm: Optional[Union[models.RawAccountValueData, + models.RawPortfolioData]] = None + + async for elm in self._account_updates_queue.stream(): + if isinstance(elm, (models.RawAccountValueData, + models.RawPortfolioData)): + if elm.account is not account_id: + # Skip the current element incase the data received doesn't + # belong to the account specified, which shouldn't happen + # at all but just in case. + continue + + if isinstance(elm, models.RawAccountValueData): + self._update_account_value(account=account, data=elm) + elif isinstance(elm, models.RawPortfolioData): + account.update_portfolio(contract_id=elm.contract.conId, + data=elm) + elif isinstance(elm, str): + if last_elm is None: + # This case should not happen as the account update time + # is always received after the updated data. + continue + + if re.fullmatch(r"\d{2}:\d{2}", elm): + time = datetime.datetime.strptime(elm, "%H:%M").time() + time = time.replace(tzinfo=ib_client._IBClient.TZ) + + if isinstance(last_elm, (str, models.RawAccountValueData)): + # This timestamp represents the last update system time + # of the account values updated. + account.last_update_time = time + elif isinstance(last_elm, models.RawPortfolioData): + # This timestamp represents the last update system time + # of the portfolio data updated. + account.positions[ + last_elm.contract.conId + ].last_update_time = time + else: + # In case if there's any unexpected element being passed + # into this queue. + continue + + last_elm = elm + + def unsub_account_updates(self): + """Unsubscribes to account updates.""" + self._account_updates_queue.put(fq._Status.FINISHED) + + #region - Private functions + async def _prevent_multi_account_updates(self): + """Prevent multi subscriptions of account updates by verifying the + `self._account_updates_queue` is finished or not as the API + `ibapi.EClient.reqAccountUpdates` is designed as only one account at a + time can be subscribed at a time. + """ + if self._account_updates_queue.status is fq._Status.INIT: + # Returns as no account updates request has been made before. + return + + if not self._account_updates_queue.finished: + self.unsub_account_updates() + + while not self._account_updates_queue.finished: + await asyncio.sleep(0.1) + + def _update_account_value(self, account: models.Account, + data: models.RawAccountValueData): + if data.key == "AccountReady": + account.account_ready = (data.val == "true") + else: + account.update_account_value(key=data.key, + currency=data.currency, + val=data.val) + #endregion - Private functions diff --git a/tests/test_account.py b/tests/test_account.py index c82d3b7..e7cf04b 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,8 +1,16 @@ """Unit tests for module `ibpy_native.account`.""" +# pylint: disable=protected-access +import asyncio +import datetime import unittest +from ibapi import contract as ib_contract from ibpy_native import account from ibpy_native import models +from ibpy_native.internal import client as ib_client +from ibpy_native.utils import finishable_queue as fq + +from tests.toolkit import utils _MOCK_AC_140: str = 'DU0000140' _MOCK_AC_141: str = 'DU0000141' @@ -43,3 +51,77 @@ def test_on_account_list_update_existing_list(self): self.assertEqual(len(self._manager.accounts), 2) self.assertEqual(self._manager.accounts[0].account_id, _MOCK_AC_140) self.assertEqual(self._manager.accounts[1].account_id, _MOCK_AC_141) + + @utils.async_test + async def test_sub_account_updates(self): + """Test the implementation of `sub_account_updates`.""" + self._manager.on_account_list_update(account_list=[_MOCK_AC_140]) + + updates_receiver = asyncio.create_task( + self._manager.sub_account_updates(account_id=_MOCK_AC_140) + ) + asyncio.create_task(self._simulate_account_updates( + account_id=_MOCK_AC_140)) + + await updates_receiver + + # Assert account value + self.assertEqual( + "25000", + self._manager.accounts[0].get_account_value(key="AvailableFunds", + currency="BASE") + ) + self.assertEqual(datetime.time(hour=10, minute=10, + tzinfo=ib_client._IBClient.TZ), + self._manager.accounts[0].last_update_time) + + # Assert portfolio data + self.assertEqual(8, self._manager.accounts[0].positions[0].avg_cost) + self.assertEqual(datetime.time(hour=10, minute=7, + tzinfo=ib_client._IBClient.TZ), + self._manager.accounts[0].positions[0].last_update_time) + self.assertEqual(20689, + (self._manager.accounts[0].positions[412888950] + .market_price)) + self.assertEqual(datetime.time(hour=10, minute=11, + tzinfo=ib_client._IBClient.TZ), + (self._manager.accounts[0].positions[412888950] + .last_update_time)) + + #region - Private functions + async def _simulate_account_updates(self, account_id: str): + self._manager.account_updates_queue.put( + models.RawAccountValueData(account=account_id, + currency="BASE", + key="AvailableFunds", + val="25000") + ) + self._manager.account_updates_queue.put( + models.RawPortfolioData(account=account_id, + contract=ib_contract.Contract(), + position=1, + market_price=10.5, + market_val=10.5, + avg_cost=8, + unrealised_pnl=2.5, + realised_pnl=0) + ) + self._manager.account_updates_queue.put("10:07") + self._manager.account_updates_queue.put("10:10") + + second_contract = ib_contract.Contract() + second_contract.conId = 412888950 + self._manager.account_updates_queue.put( + models.RawPortfolioData(account=account_id, + contract=second_contract, + position=1, + market_price=20689, + market_val=20689, + avg_cost=20600, + unrealised_pnl=89, + realised_pnl=0) + ) + self._manager.account_updates_queue.put("10:11") + + self._manager.account_updates_queue.put(fq._Status.FINISHED) + #endregion - Private functions From 3a71cd8f64ebe162b5f8a554a6b3e55d2a4bfa80 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 2 Feb 2021 02:39:38 +0000 Subject: [PATCH 031/126] Add abstract functions - Added abstract functions in delegate `_AccountManagementDelegate` to align with the class `AccountsManager`. --- ibpy_native/interfaces/delegates/account.py | 17 +++++++++++++++++ tests/toolkit/utils.py | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 68bdedf..2f8bed6 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -37,3 +37,20 @@ def on_account_list_update(self, account_list: List[str]): updated from IB. """ return NotImplemented + + @abc.abstractmethod + async def sub_account_updates(self, account_id: str): + """Abstract function to start receiving account updates from IB + Gateway. + + Args: + account_id (str): The account to subscribe for updates. + """ + return NotImplemented + + @abc.abstractmethod + async def unsub_account_updates(self): + """Abstract function to stop receiving account updates from IB Gateway + from an on-going account updates subscription. + """ + return NotImplemented diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 6f40bbc..7a2ada8 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -24,7 +24,6 @@ def wrapper(*args, **kwargs): class MockAccountManagementDelegate(delegates._AccountManagementDelegate): """Mock accounts delegate""" - def __init__(self): self._account_list: List[models.Account] = [] self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( @@ -43,6 +42,12 @@ def on_account_list_update(self, account_list: List[str]): for account_id in account_list: self._account_list.append(models.Account(account_id)) + async def sub_account_updates(self, account_id: str): + pass + + async def unsub_account_updates(self): + pass + class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" ticks: List[Union[ib_wrapper.HistoricalTick, From 50982ff683dda499cc9a40ad196eef95e28a4276 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 2 Feb 2021 05:18:14 +0000 Subject: [PATCH 032/126] Async func unsub account updates - Set function `unsub_account_updates` as `async` function to align with the delegate being implemented. --- ibpy_native/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 806e31b..3f55379 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -141,7 +141,7 @@ async def sub_account_updates(self, account_id: str): last_elm = elm - def unsub_account_updates(self): + async def unsub_account_updates(self): """Unsubscribes to account updates.""" self._account_updates_queue.put(fq._Status.FINISHED) From db87744154532849f8ee17a581b8b90f713aceb5 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 2 Feb 2021 17:41:04 +0000 Subject: [PATCH 033/126] Refactor account list to dict - Refactored list `_accounts` in `AccountsManager` to `dict` so it's much easier to retrieve specified account object. --- ibpy_native/account.py | 66 ++++++++++----------- ibpy_native/interfaces/delegates/account.py | 9 +-- tests/test_account.py | 45 ++++++++------ tests/toolkit/utils.py | 8 +-- 4 files changed, 65 insertions(+), 63 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 3f55379..756d0e1 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -4,7 +4,7 @@ import datetime import re import queue -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from ibpy_native import models from ibpy_native.interfaces import delegates @@ -14,21 +14,24 @@ class AccountsManager(delegates._AccountManagementDelegate): """Class to manage all IB accounts under the same username logged-in on IB Gateway. + + Args: + accounts (:obj:`Dict[str, ibpy_native.models.Account]`, optional): + Pre-populated accounts dictionary intended for test only. Defaults + to `None`. """ - def __init__(self, accounts: Optional[List[models.Account]] = None): - self._accounts: List[models.Account] = ([] if accounts is None - else accounts) + def __init__(self, accounts: Optional[Dict[str, models.Account]]=None): + self._accounts: Dict[str, models.Account] = ({} if accounts is None + else accounts) self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( queue_to_finish=queue.Queue() ) @property - def accounts(self) -> List[models.Account]: - """List of IB account(s) available under the same username logged in - on the IB Gateway. - - Returns: - List[ibpy_native.account.Account]: List of available IB account(s). + def accounts(self) -> Dict[str, models.Account]: + """:obj:`Dict[str, ibpy_native.models.Account]`: Dictionary of IB + account(s) available under the same username logged in on the IB + Gateway. Account IDs are used as keys. """ # Implements `delegates._AccountListDelegate` return self._accounts @@ -54,47 +57,38 @@ def on_account_list_update(self, account_list: List[str]): """ # Implements `delegates._AccountListDelegate` if self._accounts: - # Deep clone the existing account list for operation as modification - # while iterating a list will cause unexpected result. - copied_list = self._accounts.copy() + # Deep clone the existing account dict for operation as modification + # while iterating a iteratable will cause unexpected result. + copied_dict: [str, models.Account] = self._accounts.copy() - for account in self._accounts: - if account.account_id not in account_list: - # Terminates action(s) or subscription(s) on account in - # existing account list but not the newly received list. - copied_list.remove(account) + for acc_id in self._accounts: + if acc_id not in account_list: + del copied_dict[acc_id] for acc_id in account_list: - if not any(ac.account_id == acc_id for ac in copied_list): + # if not any(ac.account_id == acc_id for ac in copied_dict): + if acc_id not in copied_dict: # Adds account appears in received list but not existing - # list to the account list so it can be managed by this - # framework. - copied_list.append(models.Account(account_id=acc_id)) + # account dict so it can be managed by this framework. + copied_dict[acc_id] = models.Account(account_id=acc_id) - # Clears the list in instance scope then extends it instead of + # Clears the dict in instance scope then updates it instead of # reassigning its' pointer to the deep cloned list as the original # list may be referencing by the user via public property # `accounts`. self._accounts.clear() - self._accounts.extend(copied_list) + self._accounts.update(copied_dict) else: for acc_id in account_list: - self._accounts.append(models.Account(account_id=acc_id)) + self._accounts[acc_id] = models.Account(account_id=acc_id) - async def sub_account_updates(self, account_id: str): + async def sub_account_updates(self, account: models.Account): """Subscribes to account updates. Args: - account_id (str): The account to subscribe for updates. + account (:obj:`ibpy_native.models.Account`): The account to + subscribe for updates. """ - try: - # Check if the specified account ID is associated with one of the - # accounts being managed by this account manager. - account = next(ac for ac in self._accounts - if ac.account_id == account_id) - except StopIteration: - return - await self._prevent_multi_account_updates() last_elm: Optional[Union[models.RawAccountValueData, @@ -103,7 +97,7 @@ async def sub_account_updates(self, account_id: str): async for elm in self._account_updates_queue.stream(): if isinstance(elm, (models.RawAccountValueData, models.RawPortfolioData)): - if elm.account is not account_id: + if elm.account != account.account_id: # Skip the current element incase the data received doesn't # belong to the account specified, which shouldn't happen # at all but just in case. diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 2f8bed6..d0587bd 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -1,7 +1,7 @@ """Internal delegate module for accounts & portfolio related features.""" # pylint: disable=protected-access import abc -from typing import List +from typing import Dict, List from ibpy_native import models from ibpy_native.utils import finishable_queue as fq @@ -10,7 +10,7 @@ class _AccountManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @property @abc.abstractmethod - def accounts(self) -> List[models.Account]: + def accounts(self) -> Dict[str, models.Account]: """Abstract getter of a list of `Account` instance. This property should be implemented to return the IB account list. @@ -39,12 +39,13 @@ def on_account_list_update(self, account_list: List[str]): return NotImplemented @abc.abstractmethod - async def sub_account_updates(self, account_id: str): + async def sub_account_updates(self, account: models.Account): """Abstract function to start receiving account updates from IB Gateway. Args: - account_id (str): The account to subscribe for updates. + account (:obj:`ibpy_native.models.Account`): The account to + subscribe for updates. """ return NotImplemented diff --git a/tests/test_account.py b/tests/test_account.py index e7cf04b..30b5f8f 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -30,18 +30,20 @@ def test_on_account_list_update(self): ) self.assertEqual(len(self._manager.accounts), 2) - self.assertEqual(self._manager.accounts[0].account_id, _MOCK_AC_140) - self.assertEqual(self._manager.accounts[1].account_id, _MOCK_AC_141) + self.assertEqual(self._manager.accounts[_MOCK_AC_140].account_id, + _MOCK_AC_140) + self.assertEqual(self._manager.accounts[_MOCK_AC_141].account_id, + _MOCK_AC_141) def test_on_account_list_update_existing_list(self): - """Test the implementation of function `on_account_list_update` with an + """Test the implementation of function `on_account_list_update` with non empty account list in the `AccountsManager` instance. """ # Prepends data into account list for test. self._manager = account.AccountsManager( - accounts=[models.Account(account_id=_MOCK_AC_140), - models.Account(account_id=_MOCK_AC_142), - models.Account(account_id=_MOCK_AC_143)] + accounts={_MOCK_AC_140: models.Account(account_id=_MOCK_AC_140), + _MOCK_AC_142: models.Account(account_id=_MOCK_AC_142), + _MOCK_AC_143: models.Account(account_id=_MOCK_AC_143)} ) self._manager.on_account_list_update( @@ -49,8 +51,10 @@ def test_on_account_list_update_existing_list(self): ) self.assertEqual(len(self._manager.accounts), 2) - self.assertEqual(self._manager.accounts[0].account_id, _MOCK_AC_140) - self.assertEqual(self._manager.accounts[1].account_id, _MOCK_AC_141) + with self.assertRaises(KeyError): + _ = self._manager.accounts[_MOCK_AC_142] + with self.assertRaises(KeyError): + _ = self._manager.accounts[_MOCK_AC_143] @utils.async_test async def test_sub_account_updates(self): @@ -58,7 +62,8 @@ async def test_sub_account_updates(self): self._manager.on_account_list_update(account_list=[_MOCK_AC_140]) updates_receiver = asyncio.create_task( - self._manager.sub_account_updates(account_id=_MOCK_AC_140) + self._manager.sub_account_updates( + account=self._manager.accounts[_MOCK_AC_140]) ) asyncio.create_task(self._simulate_account_updates( account_id=_MOCK_AC_140)) @@ -68,25 +73,27 @@ async def test_sub_account_updates(self): # Assert account value self.assertEqual( "25000", - self._manager.accounts[0].get_account_value(key="AvailableFunds", + self._manager.accounts[_MOCK_AC_140].get_account_value(key="AvailableFunds", currency="BASE") ) self.assertEqual(datetime.time(hour=10, minute=10, tzinfo=ib_client._IBClient.TZ), - self._manager.accounts[0].last_update_time) + self._manager.accounts[_MOCK_AC_140].last_update_time) # Assert portfolio data - self.assertEqual(8, self._manager.accounts[0].positions[0].avg_cost) - self.assertEqual(datetime.time(hour=10, minute=7, - tzinfo=ib_client._IBClient.TZ), - self._manager.accounts[0].positions[0].last_update_time) + self.assertEqual( + 8, self._manager.accounts[_MOCK_AC_140].positions[0].avg_cost) + self.assertEqual( + datetime.time(hour=10, minute=7, tzinfo=ib_client._IBClient.TZ), + self._manager.accounts[_MOCK_AC_140].positions[0].last_update_time + ) self.assertEqual(20689, - (self._manager.accounts[0].positions[412888950] - .market_price)) + (self._manager.accounts[_MOCK_AC_140] + .positions[412888950].market_price)) self.assertEqual(datetime.time(hour=10, minute=11, tzinfo=ib_client._IBClient.TZ), - (self._manager.accounts[0].positions[412888950] - .last_update_time)) + (self._manager.accounts[_MOCK_AC_140] + .positions[412888950].last_update_time)) #region - Private functions async def _simulate_account_updates(self, account_id: str): diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 7a2ada8..4fc819f 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import asyncio import queue -from typing import List, Union +from typing import Dict, List, Union from ibapi import wrapper as ib_wrapper @@ -25,13 +25,13 @@ def wrapper(*args, **kwargs): class MockAccountManagementDelegate(delegates._AccountManagementDelegate): """Mock accounts delegate""" def __init__(self): - self._account_list: List[models.Account] = [] + self._account_list: Dict[str, models.Account] = [] self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( queue_to_finish=queue.Queue() ) @property - def accounts(self) -> List[models.Account]: + def accounts(self) -> Dict[str, models.Account]: return self._account_list @property @@ -42,7 +42,7 @@ def on_account_list_update(self, account_list: List[str]): for account_id in account_list: self._account_list.append(models.Account(account_id)) - async def sub_account_updates(self, account_id: str): + async def sub_account_updates(self, account: models.Account): pass async def unsub_account_updates(self): From db6c1820b4f22ee8c8ef0ccb0b7bb9babfcd355a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 06:50:55 +0000 Subject: [PATCH 034/126] `IBBridge` func account updates - Implemented functions `sub_account_updates` & `unsub_account_updates` in class `IBBridge`. - Implemented corresponding unit tests. --- ibpy_native/bridge.py | 43 +++++++++++++++++++++++++++++++++++------ tests/test_ib_bridge.py | 21 +++++++++++++++++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index d755eed..b329115 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -11,8 +11,9 @@ from ibapi import contract as ib_contract -from ibpy_native import account +from ibpy_native import account as ib_account from ibpy_native import error +from ibpy_native import models from ibpy_native.interfaces import listeners from ibpy_native.internal import client as ib_client from ibpy_native.internal import wrapper as ib_wrapper @@ -27,20 +28,21 @@ def __init__( client_id: int = 1, auto_conn: bool = True, notification_listener: \ Optional[listeners.NotificationListener] = None, - accounts_manager: Optional[account.AccountsManager] = None + accounts_manager: Optional[ib_account.AccountsManager] = None ): self._host = host self._port = port self._client_id = client_id self._accounts_manager = ( - account.AccountsManager() if accounts_manager is None + ib_account.AccountsManager() if accounts_manager is None else accounts_manager ) self._wrapper = ib_wrapper._IBWrapper( notification_listener=notification_listener ) - self._wrapper.set_account_management_delegate(delegate=self._accounts_manager) + self._wrapper.set_account_management_delegate( + delegate=self._accounts_manager) self._client = ib_client._IBClient(wrapper=self._wrapper) @@ -49,7 +51,7 @@ def __init__( # Properties @property - def accounts_manager(self) -> account.AccountsManager: + def accounts_manager(self) -> ib_account.AccountsManager: """:obj:`account.AccountsManager`: Instance that stores & manages all IB account(s) related data. """ @@ -104,7 +106,35 @@ def disconnect(self): """ self._client.disconnect() - ## Interacts with IB APIs + #region - Interacts with IB APIs + #region - IB account related + async def sub_account_updates(self, account: models.Account): + """Subscribes to account updates from IB. + + Args: + account (:obj:`models.Account`): Account object retrieved from + `AccountsManager`. + """ + asyncio.create_task(self._accounts_manager.sub_account_updates( + account=account)) + self._client.reqAccountUpdates(subscribe=True, + acctCode=account.account_id) + + async def unsub_account_updates(self, + account: Optional[models.Account]=None): + """Stop receiving account updates from IB. + + Args: + account (:obj:`models.Account`, optional): Account that's currently + subscribed for account updates. + """ + self._client.reqAccountUpdates( + subscribe=False, + acctCode="" if account is None else account.account_id + ) + await self._accounts_manager.unsub_account_updates() + #endregion - IB account related + # Contracts @sphinx.deprecated( version='0.2.0', @@ -467,3 +497,4 @@ def stop_live_ticks_stream(self, stream_id: int): self._client.cancel_live_ticks_stream(req_id=stream_id) except error.IBError as err: raise err + #endregion - Interacts with IB APIs diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 4abaaa2..bf0b1da 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -13,7 +13,7 @@ from ibpy_native import error from ibpy_native.interfaces import listeners from ibpy_native.internal import client as ibpy_client -from ibpy_native.utils import datatype as dt +from ibpy_native.utils import datatype as dt, finishable_queue as fq from tests.toolkit import sample_contracts from tests.toolkit import utils @@ -103,6 +103,25 @@ def on_notify(self, msg_code: int, msg: str): reqId=-1, errorCode=1100, errorString="MOCK MSG" ) + #region - IB account related + @utils.async_test + async def test_account_updates(self): + """Test functions `sub_acccount_updates` & `unsub_account_updates`.""" + await asyncio.sleep(0.5) + account = self._bridge.accounts_manager.accounts[os.getenv("IB_ACC_ID")] + + await self._bridge.sub_account_updates(account=account) + await asyncio.sleep(0.5) + await self._bridge.unsub_account_updates(account=account) + await asyncio.sleep(0.5) + + self.assertTrue(account.account_ready) + self.assertEqual( + self._bridge.accounts_manager.account_updates_queue.status, + fq._Status.FINISHED + ) + #endregion - IB account related + @utils.async_test async def test_get_us_stock_contract(self): """Test function `get_us_stock_contract`.""" From 6fc36b730064e600724ecec37a4791c0a3bff18e Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 07:33:51 +0000 Subject: [PATCH 035/126] Add func `req_managed_accounts` - Implemented function `req_managed_accounts` on `IBBridge` & corresponding unit test to fetch the accounts manually. --- ibpy_native/bridge.py | 4 ++++ tests/test_ib_bridge.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index b329115..7bfe188 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -108,6 +108,10 @@ def disconnect(self): #region - Interacts with IB APIs #region - IB account related + def req_managed_accounts(self): + """Fetch the accounts handle by the username logged in on IB Gateway.""" + self._client.reqManagedAccts() + async def sub_account_updates(self, account: models.Account): """Subscribes to account updates from IB. diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index bf0b1da..6ecf2bc 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -104,6 +104,18 @@ def on_notify(self, msg_code: int, msg: str): ) #region - IB account related + @utils.async_test + async def test_req_managed_accounts(self): + """Test function `req_managed_accounts`.""" + await asyncio.sleep(0.5) + # Clean up the already filled dict. + self._bridge.accounts_manager.accounts.clear() + + self._bridge.req_managed_accounts() + + await asyncio.sleep(0.5) + self.assertTrue(self._bridge.accounts_manager.accounts) + @utils.async_test async def test_account_updates(self): """Test functions `sub_acccount_updates` & `unsub_account_updates`.""" From e8dbe48331cff6e145d08ea2fa405be7789fd464 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 08:56:11 +0000 Subject: [PATCH 036/126] Delete cmd script - Deleted folder `cmd` and everthing inside. --- cmd/__init__.py | 0 cmd/fetch_us_historical_ticks.py | 263 ------------------------------- 2 files changed, 263 deletions(-) delete mode 100644 cmd/__init__.py delete mode 100644 cmd/fetch_us_historical_ticks.py diff --git a/cmd/__init__.py b/cmd/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/fetch_us_historical_ticks.py b/cmd/fetch_us_historical_ticks.py deleted file mode 100644 index 4e257f3..0000000 --- a/cmd/fetch_us_historical_ticks.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Script to fetch historical tick data from IB.""" -import argparse -import time -import sys -from datetime import datetime, timedelta -from pathlib import Path - -import pandas as pd -from ibpy_native import IBBridge -from ibpy_native.client import IBClient -from ibpy_native.error import IBError -from ibpy_native.utils import const -from ibapi.wrapper import Contract - -class _FetchCmd: - # pylint: disable=protected-access - @classmethod - def invoke(cls): - """Invokes the command actions.""" - cmd = cls.build_cmd() - args = cmd.parse_args() - args.func(args) - - @classmethod - def build_cmd(cls) -> argparse.ArgumentParser: - """Build the command line interface for historical ticks fetcher.""" - - # Create the top level parser - parser = argparse.ArgumentParser( - description="Fetch the historical ticks data for specified " - "instrument from IB", - formatter_class=argparse.RawTextHelpFormatter - ) - # Arguments for Gateway/TWS config - parser.add_argument( - '-s', '--server-ip', dest='host', default='127.0.0.1', metavar='', - help="specifies the IP of the running TWS/IB Gateway\n" - "(default: 127.0.0.1)" - ) - parser.add_argument( - '-p', '--port', dest='port', default=4001, metavar='', type=int, - help="specifies the socket port of TWS/IB Gateway\n" - "(default: 4001)" - ) - parser.add_argument( - '-i', '--client-id', dest='client_id', default=1, - metavar='', type=int, - help="specifies the client ID for TWS/IB Gateway instance\n" - "(default: 1)" - ) - parser.add_argument( - 'symbol', help="specifies the symbol of the instrument" - ) - sec_type_parsers = parser.add_subparsers( - metavar='sec_type', required=True, - help="specifies the security type to fetch" - ) - - # Create sub-commands - cls._stk_cmd(sec_type_parsers) - cls._fut_cmd(sec_type_parsers) - - return parser - - @classmethod - def _add_common_args(cls, parser: argparse.ArgumentParser): - parser.add_argument( - '-dt', '--data-type', choices=['MIDPOINT', 'BIDASK', 'TRADES'], - dest='data_type', default='TRADES', metavar='', type=str.upper, - help="type of tick data requires\n" - "options: {MIDPOINT, BIDASK, TRADES}\n" - "(default: TRADES)" - ) - parser.add_argument( - '-f', '--from', dest='ft', metavar='', - help="specifies the start date time of the ticks to be fetched\n" - "format: {yyyyMMdd HH:mm:ss} (i.e. \"20200513 15:18:00\")\n" - "* uses TWS/IB Gateway timezone specificed at login\n" - "(default: from earliest available tick)" - ) - parser.add_argument( - '-t', '--to', dest='to', metavar='', - help="specifies the end date time of the ticks to be fetched\n" - "format: {yyyyMMdd HH:mm:ss} (i.e. \"20200513 15:18:00\")\n" - "* uses TWS/IB Gateway timezone specificed at login\n" - "(default: to latest available tick)" - ) - parser.add_argument( - '--timeout', dest='timeout', default=120, - metavar='', type=int, - help="specifies the second(s) to wait for the request\n" - "(default: 120s)", - ) - parser.add_argument( - '-o', '--out', dest='out', metavar='', - help="specifies the destination path & filename for the CSV file " - "of tick data received\n" - "(default: ./{data_type}/{symbol}-{sec_type}-{exchange}-{currency})" - ".csv" - ) - - # Sub-command builders - @classmethod - def _stk_cmd(cls, parent: argparse._SubParsersAction): - """Create the parser for the "stk" command.""" - parser: argparse.ArgumentParser = parent.add_parser( - 'stk', aliases=['stock'], help="" - ) - - cls._add_common_args(parser) - parser.set_defaults(func=cls._stk_actions) - - @classmethod - def _fut_cmd(cls, parent: argparse._SubParsersAction): - """Create the parser for the "fut" command.""" - parser: argparse.ArgumentParser = parent.add_parser( - 'fut', aliases=['futures'], help="" - ) - parser.formatter_class = argparse.RawTextHelpFormatter - - parser.add_argument( - 'contract_month', type=cls._validate_contract_month_format, - help="specifies the contract month or date\n" - "format: {yyyyMM(dd)} (i.e. 202003 or 20200320)" - ) - cls._add_common_args(parser) - parser.set_defaults(func=cls._fut_actions) - - @classmethod - def _validate_contract_month_format(cls, value) -> int: - int_value = int(value) - - if (len(value) != 6 and len(value) != 8) or not value.isdecimal(): - raise argparse.ArgumentTypeError( - "%s is an invalid contract month" % value - ) - - return int_value - - # Sub-command functions - @classmethod - def _stk_actions(cls, args): - bridge = cls._connect_ib_bridge( - host=args.host, port=args.port, client_id=args.client_id - ) - - try: - contract = bridge.get_us_stock_contract(symbol=args.symbol) - cls._cmd_actions(bridge, contract, args) - except IBError as err: - cls._handles_ib_error(bridge, err) - - @classmethod - def _fut_actions(cls, args): - bridge = cls._connect_ib_bridge( - host=args.host, port=args.port, client_id=args.client_id - ) - - try: - contract = bridge.get_us_future_contract( - symbol=args.symbol, contract_month=str(args.contract_month) - ) - cls._cmd_actions(bridge, contract, args) - except IBError as err: - cls._handles_ib_error(bridge, err) - - # Sub-command common actions - @classmethod - def _connect_ib_bridge(cls, host: str, port: int, - client_id: int) -> IBBridge: - print("Connecting to %s:%d..." % (host, port)) - bridge = IBBridge(host=host, port=port, client_id=client_id) - - # Wait for connection - timeout = time.monotonic() + 10 - while time.monotonic() < timeout: - if bridge.is_connected(): - print("Connected") - - return bridge - - print("Failed to connect to a running TWS/IB Gateway instance") - sys.exit(1) - - @classmethod - def _handles_ib_error(cls, bridge: IBBridge, err: IBError): - print(err) - - if bridge.is_connected(): - bridge.disconnect() - - sys.exit(err.errorCode) - - @classmethod - def _cmd_actions(cls, bridge: IBBridge, contract: Contract, args): - print(contract) - - start_time: datetime = None - end_time: datetime = datetime.now() - - if args.ft: - start_time = datetime.strptime(args.ft, const._IB.TIME_FMT) - if args.to: - end_time = datetime.strptime(args.to, const._IB.TIME_FMT) - - if contract.lastTradeDateOrContractMonth != '': - last_trade_time = datetime.strptime( - contract.lastTradeDateOrContractMonth, '%Y%m%d' - ) - last_trade_time = last_trade_time.replace( - hour=23, minute=59, second=59 - ) + timedelta(seconds=1) - - if end_time > last_trade_time: - end_time = last_trade_time - - if args.data_type == 'BIDASK': - args.data_type = 'BID_ASK' - - fetch_result = bridge.get_historical_ticks( - contract=contract, start=start_time, end=end_time, - data_type=args.data_type, attempts=-1, timeout=args.timeout - ) - - if fetch_result['completed']: - print("Data fetch completed") - - if bridge.is_connected(): - bridge.disconnect() - - # Export to csv - ticks = [] - - for tick in fetch_result['ticks']: - content = vars(tick) - content = { - 'readableTime': - datetime.fromtimestamp(tick.time).astimezone(IBClient.TZ), - **content - } - ticks.append(content) - - data_frame = pd.DataFrame(ticks) - - if args.out is None: - output_file = Path( - f'./{args.data_type}/' - f'{contract.symbol}-{contract.secType}-{contract.exchange}' - f'-{contract.currency}.csv' - ) - else: - output_file = Path(args.out) - - output_file.parent.mkdir(parents=True, exist_ok=True) - - data_frame.to_csv(output_file) - - print(f"Data exported to {output_file.absolute()}") - - sys.exit() - -if __name__ == '__main__': - _FetchCmd.invoke() From 9c099086c4ecf363710c64f90e6c88d432fc749e Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 09:01:36 +0000 Subject: [PATCH 037/126] Remove deprecated functions - Removed deprecated contract related functions from `IBBridge`. --- ibpy_native/bridge.py | 93 ----------------------------------------- tests/test_ib_bridge.py | 19 --------- 2 files changed, 112 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 7bfe188..bdf9014 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -7,8 +7,6 @@ import threading from typing import List, Optional -from deprecated import sphinx - from ibapi import contract as ib_contract from ibpy_native import account as ib_account @@ -140,97 +138,6 @@ async def unsub_account_updates(self, #endregion - IB account related # Contracts - @sphinx.deprecated( - version='0.2.0', - reason="Function will be removed in the future if it's not compatible " - "with the updates. Suggest to retrieve the contracts by using " - "function `search_detailed_contract(contract)` instead." - ) - async def get_us_stock_contract(self, symbol: str) -> ib_contract.Contract: - """Resolve the IB US stock contract. - - Args: - symbol (:obj:`str`): Symbol of the target instrument. - - Returns: - ibapi.contract.Contract: Corresponding `Contract` object returned - from IB. - - Raises: - ibpy_native.error.IBError: If there is connection issue, or it - failed to get additional contract details for the specified - symbol. - """ - - contract = ib_contract.Contract() - contract.currency = 'USD' - contract.exchange = 'SMART' - contract.secType = 'STK' - contract.symbol = symbol - - try: - result = await self._client.resolve_contract( - req_id=self._wrapper.next_req_id, contract=contract - ) - except error.IBError as err: - raise err - - return result - - @sphinx.deprecated( - version='0.2.0', - reason="Function will be removed in the future if it's not compatible " - "with the updates. Suggest to retrieve the contracts by using " - "function `search_detailed_contract(contract)` instead." - ) - async def get_us_future_contract( - self, symbol: str, contract_month: Optional[str] = None - ) -> ib_contract.Contract: - """Search the US future contract from IB. - - Args: - symbol (:obj:`str`): Symbol of the target instrument. - contract_month (:obj:`str`, optional): Contract month for the - target future contract in format - "YYYYMM". Defaults to None. - - Returns: - ibapi.contract.Contract: Corresponding `Contract` object returned - from IB. The current on going contract will be returned if - `contract_month` is left as `None`. - - Raises: - ibpy_native.error.IBError: If there is connection related issue, - or it failed to get additional contract details for the - specified symbol. - """ - include_expired = False - - if contract_month is None: - contract_month = '' - else: - if len(contract_month) != 6 or not contract_month.isdecimal(): - raise ValueError( - "Value of argument `contract_month` should be in format of " - "'YYYYMM'" - ) - include_expired = True - - contract = ib_contract.Contract() - contract.currency = 'USD' - contract.secType = 'FUT' - contract.includeExpired = include_expired - contract.symbol = symbol - contract.lastTradeDateOrContractMonth = contract_month - - try: - result = await self._client.resolve_contract( - req_id=self._wrapper.next_req_id, contract=contract - ) - except error.IBError as err: - raise err - - return result - async def search_detailed_contracts(self, contract: ib_contract.Contract) \ -> List[ib_contract.ContractDetails]: """Search the contracts with complete details from IB's database. diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 6ecf2bc..361069d 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -7,8 +7,6 @@ import pytz -from ibapi import contract as ib_contract - import ibpy_native from ibpy_native import error from ibpy_native.interfaces import listeners @@ -134,23 +132,6 @@ async def test_account_updates(self): ) #endregion - IB account related - @utils.async_test - async def test_get_us_stock_contract(self): - """Test function `get_us_stock_contract`.""" - contract = await self._bridge.get_us_stock_contract(symbol='AAPL') - - self.assertIsInstance(contract, ib_contract.Contract) - - @utils.async_test - async def test_get_us_future_contract(self): - """Test function `get_us_future_contract`.""" - contract = await self._bridge.get_us_future_contract(symbol='MYM') - self.assertIsInstance(contract, ib_contract.Contract) - - with self.assertRaises(ValueError): - await self._bridge.get_us_future_contract(symbol='MYM', - contract_month='abcd') - @utils.async_test async def test_search_detailed_contracts(self): """Test function `search_detailed_contracts`.""" From ca90b422387ac2a37290877f82bfec0f0437b021 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 10:40:33 +0000 Subject: [PATCH 038/126] Clean up `bridge.py` --- ibpy_native/bridge.py | 184 ++++++++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 79 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index bdf9014..5fabb86 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -19,15 +19,31 @@ from ibpy_native.utils import datatype as dt class IBBridge: - """Public class to bridge between `ibpy-native` & IB API.""" - + """Public class to bridge between `ibpy-native` & IB API. + + Args: + host (str, optional): Hostname/IP address of IB Gateway. Defaults to + `127.0.0.1`. + port (int, optional): Port to connect to IB Gateway. Defaults to `4001`. + client_id (int, optional): Session ID which will be shown on IB Gateway + interface as `Client {client_id}`. Defaults to `1`. + auto_conn (bool, optional): `IBBridge` auto connects to IB Gateway on + initial. Defaults to `True`. + notification_listener (:obj:`ibpy_native.internfaces.listeners + .NotificationListener`, optional): Handler to receive system + notifications from IB Gateway. Defaults to `None`. + accounts_manager (:obj:`ibpy_native.account.AccountsManager`, optional): + Object to handle accounts related data. If omitted, an default + one will be created on initial of `IBBridge` (which should be + enough for most cases unless you have a customised one). Defaults + to `None`. + """ def __init__( - self, host: str = '127.0.0.1', port: int = 4001, - client_id: int = 1, auto_conn: bool = True, - notification_listener: \ - Optional[listeners.NotificationListener] = None, - accounts_manager: Optional[ib_account.AccountsManager] = None - ): + self, host: str="127.0.0.1", port: int=4001, + client_id: int=1, auto_conn: bool=True, + notification_listener:Optional[listeners.NotificationListener]=None, + accounts_manager: Optional[ib_account.AccountsManager]=None + ): self._host = host self._port = port self._client_id = client_id @@ -40,7 +56,8 @@ def __init__( notification_listener=notification_listener ) self._wrapper.set_account_management_delegate( - delegate=self._accounts_manager) + delegate=self._accounts_manager + ) self._client = ib_client._IBClient(wrapper=self._wrapper) @@ -50,12 +67,12 @@ def __init__( # Properties @property def accounts_manager(self) -> ib_account.AccountsManager: - """:obj:`account.AccountsManager`: Instance that stores & manages all IB - account(s) related data. + """:obj:`ibpy_native.account.AccountsManager`: Instance that stores & + manages all IB account(s) related data. """ return self._accounts_manager - # Setters + #region - Setters @staticmethod def set_timezone(tz: datetime.tzinfo): # pylint: disable=invalid-name @@ -76,12 +93,13 @@ def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. Args: - listener (listeners.NotificationListener): Listener for IB - notifications. + listener (:obj:`ibpy_native.interfaces.listeners + .NotificationListener`): Listener for IB notifications. """ self._wrapper.set_on_notify_listener(listener=listener) + #endregion - Setters - # Connections + #region - Connections def is_connected(self) -> bool: """Check if the bridge is connected to a running & logged in TWS/IB Gateway instance. @@ -92,7 +110,8 @@ def connect(self): """Connect the bridge to a running & logged in TWS/IB Gateway instance. """ if not self.is_connected(): - self._client.connect(self._host, self._port, self._client_id) + self._client.connect(host=self._host, port=self._port, + clientId=self._client_id) thread = threading.Thread(target=self._client.run) thread.start() @@ -103,8 +122,8 @@ def disconnect(self): """Disconnect the bridge from the connected TWS/IB Gateway instance. """ self._client.disconnect() + #endregion - Connections - #region - Interacts with IB APIs #region - IB account related def req_managed_accounts(self): """Fetch the accounts handle by the username logged in on IB Gateway.""" @@ -114,21 +133,23 @@ async def sub_account_updates(self, account: models.Account): """Subscribes to account updates from IB. Args: - account (:obj:`models.Account`): Account object retrieved from - `AccountsManager`. + account (:obj:`ibpy_native.models.Account`): Account object + retrieved from `AccountsManager`. """ - asyncio.create_task(self._accounts_manager.sub_account_updates( - account=account)) + asyncio.create_task( + self._accounts_manager.sub_account_updates(account=account) + ) self._client.reqAccountUpdates(subscribe=True, acctCode=account.account_id) - async def unsub_account_updates(self, - account: Optional[models.Account]=None): + async def unsub_account_updates( + self, account: Optional[models.Account]=None + ): """Stop receiving account updates from IB. Args: - account (:obj:`models.Account`, optional): Account that's currently - subscribed for account updates. + account (:obj:`ibpy_native.models.Account`, optional): + Account that's currently subscribed for account updates. """ self._client.reqAccountUpdates( subscribe=False, @@ -138,18 +159,18 @@ async def unsub_account_updates(self, #endregion - IB account related # Contracts - async def search_detailed_contracts(self, contract: ib_contract.Contract) \ - -> List[ib_contract.ContractDetails]: + async def search_detailed_contracts( + self, contract: ib_contract.Contract + ) -> List[ib_contract.ContractDetails]: """Search the contracts with complete details from IB's database. Args: - contract (:obj:`ibapi.contract.Contract): `Contract` object with - partially completed info - - e.g. symbol, currency, etc... + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + partially completed info (e.g. symbol, currency, etc...) Returns: - List[ibapi.contract.ContractDetails]: Fully fledged IB contract(s) - with detailed info. + :obj:`List[ibapi.contract.ContractDetails]`: Fully fledged IB + contract(s) with detailed info. Raises: ibpy_native.error.IBError: If @@ -157,32 +178,34 @@ async def search_detailed_contracts(self, contract: ib_contract.Contract) \ - there's any error returned from IB. """ try: - res: List[ib_contract.ContractDetails] = await self._client\ - .resolve_contracts(req_id=self._wrapper.next_req_id, - contract=contract) + res: List[ib_contract.ContractDetails] = ( + await self._client.resolve_contracts( + req_id=self._wrapper.next_req_id, contract=contract + ) + ) except error.IBError as err: raise err return res - # Historical data + #region - Historical data async def get_earliest_data_point( - self, contract: ib_contract.Contract, - data_type: Optional[dt.EarliestDataPoint] = \ - dt.EarliestDataPoint.TRADES - ) -> datetime: + self, contract: ib_contract.Contract, + data_type: dt.EarliestDataPoint=dt.EarliestDataPoint.TRADES + ) -> datetime: """Returns the earliest data point of specified contract. Args: contract (:obj:`ibapi.contract.Contract`): `Contract` object with sufficient info to identify the instrument. - data_type (Literal['BID_ASK', 'TRADES'], optional): - Type of data for earliest data point. Defaults to 'TRADES'. + data_type (:obj:`ibpy_native.utils.datatype.EarliestPoint`, + optional): Type of data for earliest data point. Defaults to + `EarliestPoint.TRADES`. Returns: - datetime.datetime: The earliest data point for the specified + :obj:`datetime.datetime`: The earliest data point for the specified contract in the timezone of whatever timezone set for this - `IBBridge`. + `IBBridge` instance. Raises: ibpy_native.error.IBError: If there is either connection related @@ -196,18 +219,20 @@ async def get_earliest_data_point( except error.IBError as err: raise err - data_point = datetime.datetime.fromtimestamp(result)\ - .astimezone(ib_client._IBClient.TZ) + data_point = datetime.datetime.fromtimestamp( + result + ).astimezone( + ib_client._IBClient.TZ + ) return data_point.replace(tzinfo=None) async def get_historical_ticks( - self, contract: ib_contract.Contract, - start: datetime.datetime = None, - end: Optional[datetime.datetime] = datetime.datetime.now(), - data_type: Optional[dt.HistoricalTicks] = dt.HistoricalTicks.TRADES, - attempts: Optional[int] = 1 - ) -> dt.HistoricalTicksResult: + self, contract: ib_contract.Contract, start: datetime.datetime=None, + end: datetime.datetime=datetime.datetime.now(), + data_type: dt.HistoricalTicks=dt.HistoricalTicks.TRADES, + attempts: int=1 + ) -> dt.HistoricalTicksResult: """Retrieve historical ticks data for specificed instrument/contract from IB. @@ -225,15 +250,17 @@ async def get_historical_ticks( earliest tick data to be included. Defaults to `None`. end (:obj:`datetime.datetime`, optional): The time for the latest tick data to be included. Defaults to now. - data_type (Literal['MIDPOINT', 'BID_ASK', 'TRADES'], optional): - Type of data for the ticks. Defaults to 'TRADES'. + data_type (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, + optional): Type of data for the ticks. Defaults to + `HistoricalTicks.TRADES`. attempts (int, optional): Attemp(s) to try requesting the historical ticks. Passing -1 into this argument will let the function retries for infinity times until all available ticks are received. Defaults to 1. Returns: - IBTicksResult: Ticks returned from IB and a boolean to indicate if - the returning object contains all available ticks. + :obj:`ibpy_native.utils.datatype.IBTicksResult`: Ticks returned + from IB and a boolean to indicate if the returning object + contains all available ticks. Raises: ValueError: If @@ -252,10 +279,8 @@ async def get_historical_ticks( next_end_time = ib_client._IBClient.TZ.localize(dt=end) # Error checking - if end.tzinfo is not None or ( - start is not None - and start.tzinfo is not None - ): + if end.tzinfo is not None or (start is not None and + start.tzinfo is not None): raise ValueError( "Timezone should not be specified in either `start` or `end`." ) @@ -264,9 +289,9 @@ async def get_historical_ticks( head_timestamp = datetime.datetime.fromtimestamp( await self._client.resolve_head_timestamp( req_id=self._wrapper.next_req_id, contract=contract, - show=dt.EarliestDataPoint.TRADES if \ - data_type is dt.HistoricalTicks.TRADES \ - else dt.EarliestDataPoint.BID + show=dt.EarliestDataPoint.TRADES if ( + data_type is dt.HistoricalTicks.TRADES + ) else dt.EarliestDataPoint.BID ) ).astimezone(tz=ib_client._IBClient.TZ) except error.IBError as err: @@ -311,16 +336,16 @@ async def get_historical_ticks( #  `ticks[1]` is a boolean represents if the data are all # fetched without timeout - if res['completed']: - res['ticks'].extend(all_ticks) + if res["completed"]: + res["ticks"].extend(all_ticks) return { - 'ticks': res['ticks'], - 'completed': True + "ticks": res["ticks"], + "completed": True, } - res['ticks'].extend(all_ticks) - all_ticks = res['ticks'] + res["ticks"].extend(all_ticks) + all_ticks = res["ticks"] next_end_time = datetime.datetime.fromtimestamp( res[0][0].time @@ -358,16 +383,17 @@ async def get_historical_ticks( raise err return { - 'ticks': all_ticks, - 'completed': False + "ticks": all_ticks, + "completed": False, } + #endregion - Historical data - # Live data + #region - Live data async def stream_live_ticks( - self, contract: ib_contract.Contract, - listener: listeners.LiveTicksListener, - tick_type: Optional[dt.LiveTicks] = dt.LiveTicks.LAST - ) -> int: + self, contract: ib_contract.Contract, + listener: listeners.LiveTicksListener, + tick_type: dt.LiveTicks=dt.LiveTicks.LAST + ) -> int: """Request to stream live tick data. Args: @@ -376,8 +402,8 @@ async def stream_live_ticks( listener (:obj:`ibpy_native.interfaces.listenersLiveTicksListener`): Callback listener for receiving ticks, finish signale, and error from IB API. - tick_type (:obj:`TickType`, optional): Type of ticks to be - requested. Defaults to `TickType.LAST`. + tick_type (:obj:`ibpy_native.utils.datatype.LiveTicks`, optional): + Type of ticks to be requested. Defaults to `LiveTicks.Last`. Returns: int: Request identifier. This will be needed to stop the stream @@ -408,4 +434,4 @@ def stop_live_ticks_stream(self, stream_id: int): self._client.cancel_live_ticks_stream(req_id=stream_id) except error.IBError as err: raise err - #endregion - Interacts with IB APIs + #endregion - Live data From c9f65ffb42cd4f2964e2578488de9b0c7d5e747e Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 10:42:59 +0000 Subject: [PATCH 039/126] Clean up `error.py` --- ibpy_native/error.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ibpy_native/error.py b/ibpy_native/error.py index 09196d7..6f0a72f 100644 --- a/ibpy_native/error.py +++ b/ibpy_native/error.py @@ -19,7 +19,7 @@ class IBError(Exception): """Error object to handle the error retruns from IB.""" def __init__(self, rid: int, err_code: int, err_str: str, - err_extra: Any = None): + err_extra: Any=None): self.rid = rid self.err_code = err_code self.err_str = err_str @@ -29,7 +29,7 @@ def __init__(self, rid: int, err_code: int, err_str: str, def __str__(self): # override method - error_msg = "IB error id %d errorcode %d string %s" \ - % (self.rid, self.err_code, self.err_str) + error_msg = ("IB error id %d errorcode %d string %s" + % (self.rid, self.err_code, self.err_str)) return error_msg From e0fa7b037f177df11b267898c7402af31192543a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 11:06:45 +0000 Subject: [PATCH 040/126] Added trailing commas --- ibpy_native/account.py | 4 ++-- ibpy_native/interfaces/listeners/live_ticks.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 756d0e1..281e3e0 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -92,11 +92,11 @@ async def sub_account_updates(self, account: models.Account): await self._prevent_multi_account_updates() last_elm: Optional[Union[models.RawAccountValueData, - models.RawPortfolioData]] = None + models.RawPortfolioData,]] = None async for elm in self._account_updates_queue.stream(): if isinstance(elm, (models.RawAccountValueData, - models.RawPortfolioData)): + models.RawPortfolioData,)): if elm.account != account.account_id: # Skip the current element incase the data received doesn't # belong to the account specified, which shouldn't happen diff --git a/ibpy_native/interfaces/listeners/live_ticks.py b/ibpy_native/interfaces/listeners/live_ticks.py index 708a67c..11506d7 100644 --- a/ibpy_native/interfaces/listeners/live_ticks.py +++ b/ibpy_native/interfaces/listeners/live_ticks.py @@ -12,7 +12,7 @@ class LiveTicksListener(base._BaseListener): @abc.abstractmethod def on_tick_receive(self, req_id: int, tick: Union[ wrapper.HistoricalTick, wrapper.HistoricalTickBidAsk, - wrapper.HistoricalTickLast + wrapper.HistoricalTickLast, ]): """Callback on receives new live tick records. From 7d81ac20d364f7119601083efea6110058b30fa5 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 17:34:56 +0000 Subject: [PATCH 041/126] Clean up `client.py` --- ibpy_native/internal/client.py | 149 +++++++++++++++++---------------- 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/ibpy_native/internal/client.py b/ibpy_native/internal/client.py index 089e150..bc0b434 100644 --- a/ibpy_native/internal/client.py +++ b/ibpy_native/internal/client.py @@ -1,7 +1,7 @@ """Code implementation for `EClient` related stuffs""" # pylint: disable=protected-access import datetime -from typing import Any, List, Optional, Union +from typing import Any, List, Union import pytz from typing_extensions import TypedDict @@ -21,7 +21,7 @@ class _ProcessHistoricalTicksResult(TypedDict): """Use for type hint the returns of `_IBClient.fetch_historical_ticks`.""" ticks: List[Union[ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]] + ib_wrapper.HistoricalTickLast,]] next_end_time: datetime.datetime class _IBClient(ib_client.EClient): @@ -31,32 +31,32 @@ class _IBClient(ib_client.EClient): Attributes: TZ: Class level timezone for all datetime related object. Timezone should be aligned with the timezone specified in TWS/IB Gateway - at login. Defaults to 'America/New_York'. - REQ_TIMEOUT (int): Constant uses as a default timeout value. + at login. Defaults to "America/New_York". + + Args: + wrapper (:obj:`ibpy_native.internal.wrapper._IBWrapper`): The wrapper + object to handle messages return from IB Gateway. """ # Static variable to define the timezone - TZ = pytz.timezone('America/New_York') - - # Default timeout time in second for requests - REQ_TIMEOUT = 10 + TZ = pytz.timezone("America/New_York") def __init__(self, wrapper: ibpy_wrapper._IBWrapper): self._wrapper = wrapper super().__init__(wrapper) + #region - Contract async def resolve_contract( - self, req_id: int, contract: ib_contract.Contract - ) -> ib_contract.Contract: + self, req_id: int, contract: ib_contract.Contract + ) -> ib_contract.Contract: """From a partially formed contract, returns a fully fledged version. Args: req_id (int): Request ID (ticker ID in IB API). - contract (:obj:`ibapi.contract.Contract`): - `Contract` object with partially completed info - - e.g. symbol, currency, etc... + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + partially completed info (e.g. symbol, currency, etc...) Returns: - ibapi.contract.Contract: Fully resolved IB contract. + :obj:`ibapi.contract.Contract`: Fully resolved IB contract. Raises: ibpy_native.error.IBError: If @@ -98,20 +98,19 @@ async def resolve_contract( ) async def resolve_contracts( - self, req_id: int, contract: ib_contract.Contract - ) -> List[ib_contract.ContractDetails]: + self, req_id: int, contract: ib_contract.Contract + ) -> List[ib_contract.ContractDetails]: """Search the fully fledged contracts with details from a partially formed `ibapi.contract.Contract` object. Args: req_id (int): Request ID (ticker ID in IB API). contract (:obj:`ibapi.contract.Contract`): `Contract` object with - partially completed info - - e.g. symbol, currency, etc... + partially completed info (e.g. symbol, currency, etc...) Returns: - List[ibapi.contract.ContractDetails]: Fully fledged IB contract(s) - with detailed info. + :obj:`list` of `ibapi.contract.ContractDetails`: Fully fledged IB + contract(s) with detailed info. Raises: ibpy_native.error.IBError: If @@ -129,8 +128,9 @@ async def resolve_contracts( self.reqContractDetails(reqId=req_id, contract=contract) - res: List[Union[ib_contract.ContractDetails, error.IBError]] = \ + res: List[Union[ib_contract.ContractDetails, error.IBError]] = ( await f_queue.get() + ) if res: if f_queue.status is fq._Status.ERROR: @@ -143,11 +143,12 @@ async def resolve_contracts( rid=req_id, err_code=error.IBErrorCode.RES_NO_CONTENT, err_str="Failed to get additional contract details" ) + #endregion - Contract async def resolve_head_timestamp( - self, req_id: int, contract: ib_contract.Contract, - show: Optional[dt.EarliestDataPoint] = dt.EarliestDataPoint.TRADES - ) -> int: + self, req_id: int, contract: ib_contract.Contract, + show: dt.EarliestDataPoint=dt.EarliestDataPoint.TRADES + ) -> int: """Fetch the earliest available data point for a given instrument from IB. @@ -155,8 +156,9 @@ async def resolve_head_timestamp( req_id (int): Request ID (ticker ID in IB API). contract (:obj:`ibapi.contract.Contract`): `Contract` object with sufficient info to identify the instrument. - show (Literal['BID', 'ASK', 'TRADES'], optional): - Type of data for head timestamp. Defaults to 'TRADES'. + show (:obj:`ibpy_native.utils.datatype.EarliestDataPoint`, + optional): Type of data for head timestamp. Defaults to + `EarliestDataPoint.TRADES`. Returns: int: Unix timestamp of the earliest available datapoint. @@ -174,7 +176,7 @@ async def resolve_head_timestamp( raise err print("Getting earliest available data point for the given " - "instrument from IB... ") + "instrument from IB...") self.reqHeadTimeStamp(reqId=req_id, contract=contract, whatToShow=show.value, useRTH=0, formatDate=2) @@ -206,12 +208,11 @@ async def resolve_head_timestamp( ) async def fetch_historical_ticks( - self, req_id: int, contract: ib_contract.Contract, - start: datetime.datetime, - end: Optional[datetime.datetime] = datetime.datetime.now()\ - .astimezone(TZ), - show: Optional[dt.HistoricalTicks] = dt.HistoricalTicks.TRADES - ) -> dt.HistoricalTicksResult: + self, req_id: int, contract: ib_contract.Contract, + start: datetime.datetime, + end: datetime.datetime=datetime.datetime.now().astimezone(TZ), + show: dt.HistoricalTicks=dt.HistoricalTicks.TRADES + ) -> dt.HistoricalTicksResult: """Fetch the historical ticks data for a given instrument from IB. Args: @@ -222,11 +223,12 @@ async def fetch_historical_ticks( data to be included. end (:obj:`datetime.datetime`, optional): The time for the latest tick data to be included. Defaults to now. - show (Literal['MIDPOINT', 'BID_ASK', 'TRADES'], optional): - Type of data requested. Defaults to 'TRADES'. + show (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, optional): + Type of data requested. Defaults to `HistoricalTicks.TRADES`. Returns: - Ticks data (fetched recursively to get around IB 1000 ticks limit) + :obj:`ibpy_native.utils.datatype.HistoricalTicksResult`: Ticks + data (fetched recursively to get around IB 1000 ticks limit) Raises: ValueError: If @@ -260,11 +262,13 @@ async def fetch_historical_ticks( all_ticks: list = [] - real_start_time = _IBClient.TZ.localize(start) if start.tzinfo is None \ - else start + real_start_time = ( + _IBClient.TZ.localize(start) if start.tzinfo is None else start + ) - next_end_time = _IBClient.TZ.localize(end) if end.tzinfo is None \ - else end + next_end_time = ( + _IBClient.TZ.localize(end) if end.tzinfo is None else end + ) finished = False @@ -281,15 +285,15 @@ async def fetch_historical_ticks( res: List[List[Union[ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]], - bool] = await f_queue.get() + ib_wrapper.HistoricalTickLast,]], + bool,] = await f_queue.get() if res and f_queue.status is fq._Status.ERROR: # Response received and internal queue reports error if isinstance(res[-1], error.IBError): if all_ticks: - if res[-1].err_code == error.IBErrorCode\ - .INVALID_CONTRACT: + if res[-1].err_code == (error.IBErrorCode + .INVALID_CONTRACT): # Continue if IB returns error `No security # definition has been found for the request` as # it's not possible that ticks can be fetched @@ -319,7 +323,7 @@ async def fetch_historical_ticks( raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_UNEXPECTED, err_str="[Abnormal] Incorrect number of items " - f"received: {len(res)}" + f"received: {len(res)}" ) # Process the data @@ -328,8 +332,8 @@ async def fetch_historical_ticks( start_time=real_start_time, end_time=next_end_time ) - all_ticks.extend(processed_result['ticks']) - next_end_time = processed_result['next_end_time'] + all_ticks.extend(processed_result["ticks"]) + next_end_time = processed_result["next_end_time"] print( f"{len(all_ticks)} ticks fetched (" @@ -362,26 +366,26 @@ async def fetch_historical_ticks( all_ticks.reverse() # return (all_ticks, finished) - return {'ticks': all_ticks, - 'completed': finished} + return {"ticks": all_ticks, + "completed": finished,} - # Stream live tick data + #region - Stream live tick data async def stream_live_ticks( - self, req_id: int, contract: ib_contract.Contract, - listener: listeners.LiveTicksListener, - tick_type: Optional[dt.LiveTicks] = dt.LiveTicks.LAST - ): + self, req_id: int, contract: ib_contract.Contract, + listener: listeners.LiveTicksListener, + tick_type: dt.LiveTicks=dt.LiveTicks.LAST + ): """Request to stream live tick data. Args: req_id (int): Request ID (ticker ID in IB API). contract (:obj:`ibapi.contract.Contract`): `Contract` object with sufficient info to identify the instrument. - listener (:obj:`ibpy_native.interfaces.listeners.LiveTicksListener`): - Callback listener for receiving ticks, finish signal, and error - from IB API. - tick_type (Literal['Last', 'AllLast', 'BidAsk', 'MidPoint'], - optional): Type of tick to be requested. Defaults to 'Last'. + listener (:obj:`ibpy_native.interfaces.listeners + .LiveTicksListener`): Callback listener for receiving ticks, + finish signal, and error from IB API. + tick_type (:obj:`ibpy_native.utils.datatype.LiveTicks`, optional): + Type of tick to be requested. Defaults to `LiveTicks.LAST`. Raises: ibpy_native.error.IBError: If queue associated with `req_id` is @@ -411,7 +415,7 @@ async def stream_live_ticks( async for elm in f_queue.stream(): if isinstance(elm, (ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickLast, - ib_wrapper.HistoricalTickBidAsk)): + ib_wrapper.HistoricalTickBidAsk,)): listener.on_tick_receive(req_id=req_id, tick=elm) elif isinstance(elm, error.IBError): listener.on_err(err=elm) @@ -439,14 +443,15 @@ def cancel_live_ticks_stream(self, req_id: int): rid=req_id, err_code=error.IBErrorCode.RES_NOT_FOUND, err_str=f"Task associated with request ID {req_id} not found" ) + #endregion - Stream live tick data - # Private functions + #region - Private functions def _process_historical_ticks( - self, ticks: List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]], - start_time: datetime.datetime, - end_time: datetime.datetime + self, ticks: List[Union[ib_wrapper.HistoricalTick, + ib_wrapper.HistoricalTickBidAsk, + ib_wrapper.HistoricalTickLast,]], + start_time: datetime.datetime, + end_time: datetime.datetime ) -> _ProcessHistoricalTicksResult: """Processes the tick data returned from IB in function `fetch_historical_ticks`. @@ -473,8 +478,9 @@ def _process_historical_ticks( # Updates the next end time to prepare to fetch more # data again from IB - end_time = datetime.datetime.fromtimestamp(ticks[-1].time)\ - .astimezone(end_time.tzinfo) + end_time = datetime.datetime.fromtimestamp( + ticks[-1].time + ).astimezone(end_time.tzinfo) else: # Ticks data received from IB but all records included in # response are earlier than the start time. @@ -492,8 +498,8 @@ def _process_historical_ticks( else: end_time = end_time - delta - return {'ticks': ticks, - 'next_end_time': end_time} + return {"ticks": ticks, + "next_end_time": end_time,} def _unknown_error(self, req_id: int, extra: Any = None): """Constructs `IBError` with error code `UNKNOWN` @@ -513,6 +519,7 @@ def _unknown_error(self, req_id: int, extra: Any = None): return error.IBError( rid=req_id, err_code=error.IBErrorCode.UNKNOWN, err_str="Unknown error: Internal queue reported error " - "status but no exception received", + "status but no exception received", err_extra=extra ) + #endregion - Private functions From 48c299fe9c5975860e498c9405f25c5bb443e531 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 3 Feb 2021 22:59:26 +0000 Subject: [PATCH 042/126] Clean up `wrapper.py` --- ibpy_native/internal/wrapper.py | 82 +++++++++++++++++---------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index fd3e49c..937501b 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -15,12 +15,16 @@ class _IBWrapper(wrapper.EWrapper): """The wrapper deals with the action coming back from the IB gateway or TWS instance. + + Args: + notification_listener (:obj:`ibpy_native.interfaces.listeners + .NotificationListener`, optional): Handler to receive system + notifications from IB Gateway. Defaults to `None`. """ def __init__( - self, - notification_listener: Optional[ - listeners.NotificationListener] = None - ): + self, + notification_listener: Optional[listeners.NotificationListener]=None + ): self._req_queue: Dict[int, fq._FinishableQueue] = {} self._ac_man_delegate: Optional[ @@ -55,6 +59,7 @@ def next_req_id(self) -> int: return usable_id + 1 + #region - Getters def get_request_queue(self, req_id: int) -> fq._FinishableQueue: """Initialise queue or returns the existing queue with ID `req_id`. @@ -63,9 +68,9 @@ def get_request_queue(self, req_id: int) -> fq._FinishableQueue: queue. Returns: - ibpy_native.utils.finishable_queue._FinishableQueue: The newly - initialised queue or the already existed queue associated to - the `req_id`. + :obj:`ibpy_native.utils.finishable_queue._FinishableQueue`: + The newly initialised queue or the already existed queue + associated to the `req_id`. Raises: ibpy_native.error.IBError: If `_FinishableQueue` associated with @@ -78,8 +83,9 @@ def get_request_queue(self, req_id: int) -> fq._FinishableQueue: return self._req_queue[req_id] - def get_request_queue_no_throw(self, req_id: int) -> \ - Optional[fq._FinishableQueue]: + def get_request_queue_no_throw(self, req_id: int) -> Optional[ + fq._FinishableQueue + ]: """Returns the existing queue with ID `req_id`. Args: @@ -87,17 +93,18 @@ def get_request_queue_no_throw(self, req_id: int) -> \ queue. Returns: - Optional[ibpy_native.utils.finishable_queue._FinishableQueue]: + :obj:`Optional[ibpy_native.utils.finishable_queue._FinishableQueue]`: The existing `_FinishableQueue` associated to the specified `req_id`. `None` if `req_id` doesn't match with any existing `_FinishableQueue` object. """ return self._req_queue[req_id] if req_id in self._req_queue else None + #endregion - Getters - # Setters - def set_account_management_delegate(self, - delegate: delegates - ._AccountManagementDelegate): + #region - Setters + def set_account_management_delegate( + self, delegate: delegates._AccountManagementDelegate + ): """Setter for optional `_AccountListDelegate`. Args: @@ -110,14 +117,15 @@ def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. Args: - listener (ibpy_native.interfaces.listeners.NotificationListener): - Listener for IB notifications. + listener (:obj:`ibpy_native.interfaces.listeners + .NotificationListener`): Listener for IB notifications. """ self._notification_listener = listener + #endregion - Setters + #region - Override functions from `wrapper.EWrapper` # Error handling def error(self, reqId, errorCode, errorString): - # override method err = error.IBError(rid=reqId, err_code=errorCode, err_str=errorString) # -1 indicates a notification and not true error condition @@ -130,14 +138,12 @@ def error(self, reqId, errorCode, errorString): msg=errorString ) - #region - Override functions from `wrapper.EWrapper` #region - Accounts & portfolio def managedAccounts(self, accountsList: str): - # override method # Trim the spaces in `accountsList` received trimmed = "".join(accountsList.split()) # Separate different account IDs into a list - account_list = trimmed.split(',') + account_list = trimmed.split(",") if self._ac_man_delegate is not None: self._ac_man_delegate.on_account_list_update( @@ -172,14 +178,13 @@ def updateAccountTime(self, timeStamp: str): #endregion - account updates #endregion - Accounts & portfolio - # Get contract details + #region - Get contract details def contractDetails(self, reqId, contractDetails): - # override method self._req_queue[reqId].put(element=contractDetails) def contractDetailsEnd(self, reqId): - # override method self._req_queue[reqId].put(element=fq._Status.FINISHED) + #endregion - Get contract details # Get earliest data point for a given instrument and data def headTimestamp(self, reqId: int, headTimestamp: str): @@ -187,32 +192,29 @@ def headTimestamp(self, reqId: int, headTimestamp: str): self._req_queue[reqId].put(element=headTimestamp) self._req_queue[reqId].put(element=fq._Status.FINISHED) - # Fetch historical ticks data + #region - Fetch historical tick data def historicalTicks(self, reqId: int, ticks: List[wrapper.HistoricalTick], done: bool): - # override method self._handle_historical_ticks_results(reqId, ticks, done) def historicalTicksBidAsk(self, reqId: int, ticks: List[wrapper.HistoricalTickBidAsk], done: bool): - # override method self._handle_historical_ticks_results(req_id=reqId, ticks=ticks, done=done) def historicalTicksLast(self, reqId: int, ticks: List[wrapper.HistoricalTickLast], done: bool): - # override method self._handle_historical_ticks_results(req_id=reqId, ticks=ticks, done=done) + #endregion - Fetch historical tick data - # Stream live tick data + #region - Stream live tick data def tickByTickAllLast(self, reqId: int, tickType: int, time: int, price: float, size: int, tickAttribLast: wrapper.TickAttribLast, exchange: str, specialConditions: str): - # override method record = wrapper.HistoricalTickLast() record.time = time record.price = price @@ -226,7 +228,6 @@ def tickByTickAllLast(self, reqId: int, tickType: int, time: int, def tickByTickBidAsk(self, reqId: int, time: int, bidPrice: float, askPrice: float, bidSize: int, askSize: int, tickAttribBidAsk: wrapper.TickAttribBidAsk): - # override method record = wrapper.HistoricalTickBidAsk() record.time = time record.priceBid = bidPrice @@ -238,15 +239,15 @@ def tickByTickBidAsk(self, reqId: int, time: int, bidPrice: float, self._handle_live_ticks(req_id=reqId, tick=record) def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): - # override method record = wrapper.HistoricalTick() record.time = time record.price = midPoint self._handle_live_ticks(req_id=reqId, tick=record) + #endregion - Stream live tick data #endregion - Override functions from `wrapper.EWrapper` - ## Private functions + #region - Private functions def __init_req_queue(self, req_id: int): """Initials a new `_FinishableQueue` if there's no object at `self.__req_queue[req_id]`; Resets the queue status to its' initial @@ -262,18 +263,18 @@ def __init_req_queue(self, req_id: int): else: raise error.IBError( rid=req_id, err_code=error.IBErrorCode.QUEUE_IN_USE, - err_str=f"Requested queue with ID {str(req_id)} is "\ - "currently in use" + err_str=f"Requested queue with ID {str(req_id)} is " + "currently in use" ) else: self._req_queue[req_id] = fq._FinishableQueue(queue.Queue()) def _handle_historical_ticks_results( - self, req_id: int, - ticks: Union[List[wrapper.HistoricalTick], - List[wrapper.HistoricalTickBidAsk], - List[wrapper.HistoricalTickLast]], - done: bool + self, req_id: int, + ticks: Union[List[wrapper.HistoricalTick], + List[wrapper.HistoricalTickBidAsk], + List[wrapper.HistoricalTickLast],], + done: bool ): """Handles results return from functions `historicalTicks`, `historicalTicksBidAsk`, and `historicalTicksLast` by putting the @@ -286,9 +287,10 @@ def _handle_historical_ticks_results( def _handle_live_ticks(self, req_id: int, tick: Union[wrapper.HistoricalTick, wrapper.HistoricalTickBidAsk, - wrapper.HistoricalTickLast]): + wrapper.HistoricalTickLast,]): """Handles live ticks passed to functions `tickByTickAllLast`, `tickByTickBidAsk`, and `tickByTickMidPoint` by putting the ticks received into corresponding queue. """ self._req_queue[req_id].put(element=tick) + #endregion - Private functions From 09b85605cef7dd257a0d07a1a2aab89f83deb5ea Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 4 Feb 2021 11:21:02 +0000 Subject: [PATCH 043/126] Clean up `models/account.py` --- ibpy_native/models/account.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ibpy_native/models/account.py b/ibpy_native/models/account.py index 37937dd..a60a62d 100644 --- a/ibpy_native/models/account.py +++ b/ibpy_native/models/account.py @@ -7,8 +7,11 @@ from ibpy_native.models import raw_data class Account: - """Model class for individual IB account.""" + """Model class for individual IB account. + Args: + account_id (str): Account ID received from IB Gateway. + """ def __init__(self, account_id: str): self._lock = threading.Lock() @@ -69,7 +72,7 @@ def destroy_flag(self) -> bool: """ return self._destroy_flag - def get_account_value(self, key: str, currency: str = "") -> Optional[str]: + def get_account_value(self, key: str, currency: str="") -> Optional[str]: """Returns the value of specified account's information. Args: @@ -89,7 +92,7 @@ def get_account_value(self, key: str, currency: str = "") -> Optional[str]: return None - def update_account_value(self, key: str, currency: str = "", val: str = ""): + def update_account_value(self, key: str, currency: str="", val: str=""): """Thread-safe setter function to update the account value. Args: From d78c3568881cfd6d25863a1387e49addb03f2654 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 4 Feb 2021 11:23:57 +0000 Subject: [PATCH 044/126] Disable rule `too-many-instance-attributes` --- .pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintrc b/.pylintrc index 83d3264..288b3eb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -141,6 +141,7 @@ disable=print-statement, comprehension-escape, too-many-arguments, too-many-branches, + too-many-instance-attributes, too-many-locals, no-self-use From feeae4eb644434ac7b7a1b75b1e28b8effc4b768 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 4 Feb 2021 11:35:13 +0000 Subject: [PATCH 045/126] Replace single quotes - Replaced `''` with `""`. --- ibpy_native/utils/const.py | 2 +- ibpy_native/utils/datatype.py | 20 ++++++++++---------- tests/internal/test_ib_client.py | 28 ++++++++++++++-------------- tests/internal/test_ib_wrapper.py | 18 +++++++++--------- tests/test_account.py | 8 ++++---- tests/test_ib_bridge.py | 20 ++++++++++---------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/ibpy_native/utils/const.py b/ibpy_native/utils/const.py index e0e0cd3..eab23a7 100644 --- a/ibpy_native/utils/const.py +++ b/ibpy_native/utils/const.py @@ -7,7 +7,7 @@ class _IB: """Constants use across the project.""" # Defined constants - TIME_FMT: Final[str] = '%Y%m%d %H:%M:%S' #IB time format + TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" #IB time format # Messages MSG_TIMEOUT: Final[str] = ( diff --git a/ibpy_native/utils/datatype.py b/ibpy_native/utils/datatype.py index 504abe6..a67efb2 100644 --- a/ibpy_native/utils/datatype.py +++ b/ibpy_native/utils/datatype.py @@ -8,16 +8,16 @@ @enum.unique class EarliestDataPoint(enum.Enum): """Data type options defined for earliest data point.""" - BID = 'BID' - ASK = 'ASK' - TRADES = 'TRADES' + BID = "BID" + ASK = "ASK" + TRADES = "TRADES" @enum.unique class HistoricalTicks(enum.Enum): """Data type options defined for fetching historical ticks.""" - BID_ASK = 'BID_ASK' - MIDPOINT = 'MIDPOINT' - TRADES = 'TRADES' + BID_ASK = "BID_ASK" + MIDPOINT = "MIDPOINT" + TRADES = "TRADES" class HistoricalTicksResult(TypedDict): """Use to type hint the returns of `IBBridge.get_historical_ticks`.""" @@ -31,7 +31,7 @@ class HistoricalTicksResult(TypedDict): @enum.unique class LiveTicks(enum.Enum): """Data types defined for live tick data.""" - ALL_LAST = 'AllLast' - BID_ASK = 'BidAsk' - MIDPOINT = 'MidPoint' - LAST = 'Last' + ALL_LAST = "AllLast" + BID_ASK = "BidAsk" + MIDPOINT = "MidPoint" + LAST = "Last" diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index 024bf51..9d0489d 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -39,14 +39,14 @@ class TestIBClient(unittest.TestCase): @classmethod def setUpClass(cls): - ibpy_client._IBClient.TZ = pytz.timezone('America/New_York') + ibpy_client._IBClient.TZ = pytz.timezone("America/New_York") cls._wrapper = ibpy_wrapper._IBWrapper() cls._client = ibpy_client._IBClient(cls._wrapper) cls._client.connect( - os.getenv('IB_HOST', '127.0.0.1'), - int(os.getenv('IB_PORT', '4002')), + os.getenv("IB_HOST", "127.0.0.1"), + int(os.getenv("IB_PORT", "4002")), 1001 ) @@ -70,7 +70,7 @@ async def test_resolve_contract(self): async def test_resolve_contracts(self): """Test function `resolve_contracts`.""" contract: ib_contract.Contract = sample_contracts.us_future() - contract.lastTradeDateOrContractMonth = '' + contract.lastTradeDateOrContractMonth = "" res: List[ib_contract.ContractDetails] = await self._client\ .resolve_contracts(req_id=Const.RID_RESOLVE_CONTRACTS.value, @@ -106,10 +106,10 @@ async def test_fetch_historical_ticks(self): show=dt.HistoricalTicks.MIDPOINT ) - self.assertIsInstance(data['ticks'], list) - self.assertTrue(data['completed']) - self.assertTrue(data['ticks']) - self.assertIsInstance(data['ticks'][0], ib_wrapper.HistoricalTick) + self.assertIsInstance(data["ticks"], list) + self.assertTrue(data["completed"]) + self.assertTrue(data["ticks"]) + self.assertIsInstance(data["ticks"][0], ib_wrapper.HistoricalTick) data = await self._client.fetch_historical_ticks( req_id=Const.RID_FETCH_HISTORICAL_TICKS.value, @@ -121,10 +121,10 @@ async def test_fetch_historical_ticks(self): show=dt.HistoricalTicks.BID_ASK ) - self.assertIsInstance(data['ticks'], list) - self.assertTrue(data['completed']) - self.assertTrue(data['ticks']) - self.assertIsInstance(data['ticks'][0], ib_wrapper.HistoricalTickBidAsk) + self.assertIsInstance(data["ticks"], list) + self.assertTrue(data["completed"]) + self.assertTrue(data["ticks"]) + self.assertIsInstance(data["ticks"][0], ib_wrapper.HistoricalTickBidAsk) @utils.async_test async def test_fetch_historical_ticks_err(self): @@ -135,9 +135,9 @@ async def test_fetch_historical_ticks_err(self): req_id=Const.RID_FETCH_HISTORICAL_TICKS_ERR.value, contract=sample_contracts.gbp_usd_fx(), start=datetime.datetime.now()\ - .astimezone(pytz.timezone('Asia/Hong_Kong')), + .astimezone(pytz.timezone("Asia/Hong_Kong")), end=datetime.datetime.now()\ - .astimezone(pytz.timezone('America/New_York')) + .astimezone(pytz.timezone("America/New_York")) ) # Invalid contract object diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 141c4f5..e93158c 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -36,8 +36,8 @@ def setUpClass(cls): cls._client = ibpy_client._IBClient(cls._wrapper) cls._client.connect( - os.getenv('IB_HOST', '127.0.0.1'), - int(os.getenv('IB_PORT', '4002')), + os.getenv("IB_HOST", "127.0.0.1"), + int(os.getenv("IB_PORT", "4002")), 1001 ) @@ -162,7 +162,7 @@ async def test_tick_by_tick_all_last(self): self._client.reqTickByTickData( reqId=Const.RID_REQ_TICK_BY_TICK_DATA_ALL_LAST.value, contract=sample_contracts.us_future(), - tickType='AllLast', + tickType="AllLast", numberOfTicks=0, ignoreSize=True ) @@ -190,7 +190,7 @@ async def test_tick_by_tick_last(self): self._client.reqTickByTickData( reqId=Const.RID_REQ_TICK_BY_TICK_DATA_LAST.value, contract=sample_contracts.us_future(), - tickType='Last', + tickType="Last", numberOfTicks=0, ignoreSize=True ) @@ -218,7 +218,7 @@ async def test_tick_by_tick_bid_ask(self): self._client.reqTickByTickData( reqId=Const.RID_REQ_TICK_BY_TICK_DATA_BIDASK.value, contract=sample_contracts.gbp_usd_fx(), - tickType='BidAsk', + tickType="BidAsk", numberOfTicks=0, ignoreSize=True ) @@ -245,7 +245,7 @@ async def test_tick_by_tick_mid_point(self): self._client.reqTickByTickData( reqId=Const.RID_REQ_TICK_BY_TICK_DATA_MIDPOINT.value, contract=sample_contracts.gbp_usd_fx(), - tickType='MidPoint', + tickType="MidPoint", numberOfTicks=0, ignoreSize=True ) @@ -278,8 +278,8 @@ def setUpClass(cls): cls.client = ibpy_client._IBClient(cls.wrapper) cls.client.connect( - os.getenv('IB_HOST', '127.0.0.1'), - int(os.getenv('IB_PORT', '4002')), + os.getenv("IB_HOST", "127.0.0.1"), + int(os.getenv("IB_PORT", "4002")), 1001 ) @@ -295,7 +295,7 @@ def setUp(self): def test_account_management_delegate(self): """Test `_AccountManagementDelegate` implementation.""" self.mock_delegate.on_account_list_update( - account_list=['DU0000140', 'DU0000141'] + account_list=["DU0000140", "DU0000141"] ) self.assertTrue(self.mock_delegate.accounts) diff --git a/tests/test_account.py b/tests/test_account.py index 30b5f8f..17ef3c2 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -12,10 +12,10 @@ from tests.toolkit import utils -_MOCK_AC_140: str = 'DU0000140' -_MOCK_AC_141: str = 'DU0000141' -_MOCK_AC_142: str = 'DU0000142' -_MOCK_AC_143: str = 'DU0000143' +_MOCK_AC_140: str = "DU0000140" +_MOCK_AC_141: str = "DU0000141" +_MOCK_AC_142: str = "DU0000142" +_MOCK_AC_143: str = "DU0000143" class TestAccountsManager(unittest.TestCase): """Unit tests for class `AccountsManager`.""" diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 361069d..12f00c2 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -16,8 +16,8 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils -TEST_HOST = os.getenv('IB_HOST', '127.0.0.1') -TEST_PORT = int(os.getenv('IB_PORT', '4002')) +TEST_HOST = os.getenv("IB_HOST", "127.0.0.1") +TEST_PORT = int(os.getenv("IB_PORT", "4002")) TEST_ID = 1001 class TestIBBridgeConn(unittest.TestCase): @@ -72,15 +72,15 @@ async def test_accounts_manager(self): def test_set_timezone(self): """Test function `set_timezone`.""" - ibpy_native.IBBridge.set_timezone(tz=pytz.timezone('Asia/Hong_Kong')) + ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("Asia/Hong_Kong")) self.assertEqual(ibpy_client._IBClient.TZ, - pytz.timezone('Asia/Hong_Kong')) + pytz.timezone("Asia/Hong_Kong")) # Reset timezone to New York - ibpy_native.IBBridge.set_timezone(tz=pytz.timezone('America/New_York')) + ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("America/New_York")) self.assertEqual(ibpy_client._IBClient.TZ, - pytz.timezone('America/New_York')) + pytz.timezone("America/New_York")) def test_set_on_notify_listener(self): """Test notification listener supports.""" @@ -136,7 +136,7 @@ async def test_account_updates(self): async def test_search_detailed_contracts(self): """Test function `search_detailed_contracts`.""" contract = sample_contracts.us_future() - contract.lastTradeDateOrContractMonth = '' + contract.lastTradeDateOrContractMonth = "" res = await self._bridge.search_detailed_contracts(contract=contract) self.assertGreater(len(res), 1) @@ -166,8 +166,8 @@ async def test_get_historical_ticks(self): end=datetime.datetime(2020, 3, 16, 9, 50) ) - self.assertTrue(result['ticks']) - self.assertTrue(result['completed']) + self.assertTrue(result["ticks"]) + self.assertTrue(result["completed"]) @utils.async_test async def test_get_historical_ticks_err(self): @@ -176,7 +176,7 @@ async def test_get_historical_ticks_err(self): with self.assertRaises(ValueError): await self._bridge.get_historical_ticks( contract=sample_contracts.us_stock(), - end=pytz.timezone('Asia/Hong_Kong').localize( + end=pytz.timezone("Asia/Hong_Kong").localize( datetime.datetime(2020, 4, 28) ) ) From fd3b59f6a58395c035903b237ec102baed09ace9 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 4 Feb 2021 14:43:03 +0000 Subject: [PATCH 046/126] Clean up `tests` - Housekeeping on files in folder `/tests/` --- tests/internal/test_ib_client.py | 92 ++++++++++++++++--------------- tests/internal/test_ib_wrapper.py | 66 +++++++++++----------- tests/test_account.py | 4 +- tests/test_ib_bridge.py | 6 ++ tests/toolkit/utils.py | 2 +- 5 files changed, 93 insertions(+), 77 deletions(-) diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index 9d0489d..24aa14b 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access import asyncio import datetime -import enum import os import threading import unittest @@ -22,17 +21,18 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils -class Const(enum.IntEnum): - """Predefined request IDs for tests in `TestIBClient`.""" - RID_RESOLVE_CONTRACT = 43 - RID_RESOLVE_CONTRACTS = 44 - RID_RESOLVE_HEAD_TIMESTAMP = 14001 - RID_RESOLVE_HEAD_TIMESTAMP_EPOCH = 14002 - RID_FETCH_HISTORICAL_TICKS = 18001 - RID_FETCH_HISTORICAL_TICKS_ERR = 18002 - RID_STREAM_LIVE_TICKS = 19001 - RID_CANCEL_LIVE_TICKS_STREAM = 19002 - RID_CANCEL_LIVE_TICKS_STREAM_ERR = 19003 +#region - Constants +# Predefined request IDs for tests in `TestIBClient`. +_RID_RESOLVE_CONTRACT = 43 +_RID_RESOLVE_CONTRACTS = 44 +_RID_RESOLVE_HEAD_TIMESTAMP = 14001 +_RID_RESOLVE_HEAD_TIMESTAMP_EPOCH = 14002 +_RID_FETCH_HISTORICAL_TICKS = 18001 +_RID_FETCH_HISTORICAL_TICKS_ERR = 18002 +_RID_STREAM_LIVE_TICKS = 19001 +_RID_CANCEL_LIVE_TICKS_STREAM = 19002 +_RID_CANCEL_LIVE_TICKS_STREAM_ERR = 19003 +#endregion - Constants class TestIBClient(unittest.TestCase): """Unit tests for class `_IBClient`.""" @@ -55,16 +55,16 @@ def setUpClass(cls): setattr(cls._client, "_thread", thread) + #region - Contract @utils.async_test async def test_resolve_contract(self): """Test function `resolve_contract`.""" resolved_contract = await self._client.resolve_contract( - req_id=Const.RID_RESOLVE_CONTRACT.value, + req_id=_RID_RESOLVE_CONTRACT, contract=sample_contracts.gbp_usd_fx() ) self.assertIsNotNone(resolved_contract) - print(resolved_contract) @utils.async_test async def test_resolve_contracts(self): @@ -72,37 +72,38 @@ async def test_resolve_contracts(self): contract: ib_contract.Contract = sample_contracts.us_future() contract.lastTradeDateOrContractMonth = "" - res: List[ib_contract.ContractDetails] = await self._client\ - .resolve_contracts(req_id=Const.RID_RESOLVE_CONTRACTS.value, - contract=contract) + res: List[ib_contract.ContractDetails] = ( + await self._client.resolve_contracts(req_id=_RID_RESOLVE_CONTRACTS, + contract=contract) + ) self.assertTrue(res) self.assertGreater(len(res), 1) + #endregion - Contract @utils.async_test async def test_resolve_head_timestamp(self): """Test function `resolve_head_timestamp`.""" head_timestamp = await self._client.resolve_head_timestamp( - req_id=Const.RID_RESOLVE_HEAD_TIMESTAMP.value, + req_id=_RID_RESOLVE_HEAD_TIMESTAMP, contract=sample_contracts.us_future(), show=dt.EarliestDataPoint.BID ) - print(head_timestamp) - self.assertIsNotNone(head_timestamp) self.assertIsInstance(head_timestamp, int) + #region - Historical ticks @utils.async_test async def test_fetch_historical_ticks(self): """Test function `fetch_historical_ticks`.""" data = await self._client.fetch_historical_ticks( - req_id=Const.RID_FETCH_HISTORICAL_TICKS.value, + req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client._IBClient.TZ.localize(datetime\ - .datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client._IBClient.TZ.localize(datetime\ - .datetime(2020, 4, 29, 10, 35, 0)), + start=ibpy_client._IBClient.TZ.localize( + datetime.datetime(2020, 4, 29, 10, 30, 0)), + end=ibpy_client._IBClient.TZ.localize( + datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.MIDPOINT ) @@ -112,12 +113,12 @@ async def test_fetch_historical_ticks(self): self.assertIsInstance(data["ticks"][0], ib_wrapper.HistoricalTick) data = await self._client.fetch_historical_ticks( - req_id=Const.RID_FETCH_HISTORICAL_TICKS.value, + req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client._IBClient.TZ.localize(datetime\ - .datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client._IBClient.TZ.localize(datetime\ - .datetime(2020, 4, 29, 10, 35, 0)), + start=ibpy_client._IBClient.TZ.localize( + datetime.datetime(2020, 4, 29, 10, 30, 0)), + end=ibpy_client._IBClient.TZ.localize( + datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.BID_ASK ) @@ -132,35 +133,37 @@ async def test_fetch_historical_ticks_err(self): # Timezone of start & end are not identical with self.assertRaises(ValueError): await self._client.fetch_historical_ticks( - req_id=Const.RID_FETCH_HISTORICAL_TICKS_ERR.value, + req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, contract=sample_contracts.gbp_usd_fx(), - start=datetime.datetime.now()\ - .astimezone(pytz.timezone("Asia/Hong_Kong")), - end=datetime.datetime.now()\ - .astimezone(pytz.timezone("America/New_York")) + start=datetime.datetime.now().astimezone( + pytz.timezone("Asia/Hong_Kong")), + end=datetime.datetime.now().astimezone( + pytz.timezone("America/New_York")) ) # Invalid contract object with self.assertRaises(error.IBError): await self._client.fetch_historical_ticks( - req_id=Const.RID_FETCH_HISTORICAL_TICKS_ERR.value, + req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, contract=ib_contract.Contract(), - start=datetime.datetime(2020, 5, 20, 3, 20, 0)\ - .astimezone(ibpy_client._IBClient.TZ), + start=datetime.datetime(2020, 5, 20, 3, 20, 0).astimezone( + ibpy_client._IBClient.TZ), end=datetime.datetime.now().astimezone(ibpy_client._IBClient.TZ) ) + #endregion - Historical ticks + #region - Live ticks @utils.async_test async def test_stream_live_ticks(self): """Test function `stream_live_ticks`.""" async def cancel_req(): await asyncio.sleep(5) self._client.cancelTickByTickData( - reqId=Const.RID_STREAM_LIVE_TICKS.value + reqId=_RID_STREAM_LIVE_TICKS ) queue = self._wrapper.get_request_queue_no_throw( - req_id=Const.RID_STREAM_LIVE_TICKS + req_id=_RID_STREAM_LIVE_TICKS ) queue.put(element=fq._Status.FINISHED) @@ -168,7 +171,7 @@ async def cancel_req(): stream = asyncio.create_task( self._client.stream_live_ticks( - req_id=Const.RID_STREAM_LIVE_TICKS.value, + req_id=_RID_STREAM_LIVE_TICKS, contract=sample_contracts.gbp_usd_fx(), listener=listener, tick_type=dt.LiveTicks.BID_ASK @@ -195,14 +198,14 @@ async def test_cancel_live_ticks_stream(self): async def cancel_req(): await asyncio.sleep(3) self._client.cancel_live_ticks_stream( - req_id=Const.RID_CANCEL_LIVE_TICKS_STREAM.value + req_id=_RID_CANCEL_LIVE_TICKS_STREAM ) listener = utils.MockLiveTicksListener() stream = asyncio.create_task( self._client.stream_live_ticks( - req_id=Const.RID_CANCEL_LIVE_TICKS_STREAM.value, + req_id=_RID_CANCEL_LIVE_TICKS_STREAM, contract=sample_contracts.gbp_usd_fx(), listener=listener, tick_type=dt.LiveTicks.BID_ASK @@ -229,8 +232,9 @@ async def test_cancel_live_ticks_stream_err(self): """ with self.assertRaises(error.IBError): self._client.cancel_live_ticks_stream( - req_id=Const.RID_CANCEL_LIVE_TICKS_STREAM_ERR.value + req_id=_RID_CANCEL_LIVE_TICKS_STREAM_ERR ) + #endregion - Live ticks @classmethod def tearDownClass(cls): diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index e93158c..e3d0e6d 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -1,7 +1,6 @@ """Unit tests for module `ibpy_native.wrapper`.""" # pylint: disable=protected-access import asyncio -import enum import os import threading import unittest @@ -17,15 +16,16 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils -class Const(enum.IntEnum): - """Predefined constants for `TestIBWrapper`.""" - RID_RESOLVE_CONTRACT = 43 - RID_FETCH_HISTORICAL_TICKS = 18001 - RID_REQ_TICK_BY_TICK_DATA_ALL_LAST = 19001 - RID_REQ_TICK_BY_TICK_DATA_LAST = 19002 - RID_REQ_TICK_BY_TICK_DATA_MIDPOINT = 19003 - RID_REQ_TICK_BY_TICK_DATA_BIDASK = 19004 - QUEUE_MAX_WAIT_SEC = 10 +#region - Constants +# Predefined constants for `TestIBWrapper`. +_RID_RESOLVE_CONTRACT = 43 +_RID_FETCH_HISTORICAL_TICKS = 18001 +_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST = 19001 +_RID_REQ_TICK_BY_TICK_DATA_LAST = 19002 +_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT = 19003 +_RID_REQ_TICK_BY_TICK_DATA_BIDASK = 19004 +_QUEUE_MAX_WAIT_SEC = 10 +#endregion - Constants class TestIBWrapper(unittest.TestCase): """Unit tests for class `_IBWrapper`.""" @@ -46,7 +46,7 @@ def setUpClass(cls): setattr(cls._client, "_thread", thread) - # _IBWrapper specifics + #region - _IBWrapper specifics @utils.async_test async def test_next_req_id(self): """Test retrieval of next usable request ID.""" @@ -81,19 +81,20 @@ def on_notify(self, msg_code: int, msg: str): self._wrapper.error(reqId=-1, errorCode=1100, errorString="MOCK MSG") self.assertTrue(mock_listener.triggered) + #endregion - _IBWrapper specifics - # Historical market data + #region - Historical ticks @utils.async_test async def test_historical_ticks(self): """Test overridden function `historicalTicks`.""" end_time = "20200327 16:30:00" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_FETCH_HISTORICAL_TICKS + req_id=_RID_FETCH_HISTORICAL_TICKS ) self._client.reqHistoricalTicks( - reqId=Const.RID_FETCH_HISTORICAL_TICKS.value, + reqId=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), startDateTime="", endDateTime=end_time, numberOfTicks=1000, whatToShow="MIDPOINT", useRth=1, @@ -112,11 +113,11 @@ async def test_historical_ticks_bid_ask(self): end_time = "20200327 16:30:00" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_FETCH_HISTORICAL_TICKS + req_id=_RID_FETCH_HISTORICAL_TICKS ) self._client.reqHistoricalTicks( - reqId=Const.RID_FETCH_HISTORICAL_TICKS.value, + reqId=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), startDateTime="", endDateTime=end_time, numberOfTicks=1000, whatToShow="BID_ASK", useRth=1, @@ -135,11 +136,11 @@ async def test_historical_ticks_last(self): end_time = "20200327 16:30:00" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_FETCH_HISTORICAL_TICKS + req_id=_RID_FETCH_HISTORICAL_TICKS ) self._client.reqHistoricalTicks( - reqId=Const.RID_FETCH_HISTORICAL_TICKS.value, + reqId=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), startDateTime="", endDateTime=end_time, numberOfTicks=1000, whatToShow="TRADES", useRth=1, @@ -151,16 +152,18 @@ async def test_historical_ticks_last(self): self.assertEqual(f_queue.status, fq._Status.FINISHED) self.assertEqual(len(result), 2) self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTickLast) + #endregion - Historical ticks + #region - Tick by tick data (Live ticks) @utils.async_test async def test_tick_by_tick_all_last(self): """Test overridden function `tickByTickAllLast`.""" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_REQ_TICK_BY_TICK_DATA_ALL_LAST + req_id=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST ) self._client.reqTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_ALL_LAST.value, + reqId=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST, contract=sample_contracts.us_future(), tickType="AllLast", numberOfTicks=0, @@ -174,7 +177,7 @@ async def test_tick_by_tick_all_last(self): if ele is not fq._Status.FINISHED: self._client.cancelTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_ALL_LAST.value + reqId=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST ) f_queue.put(element=fq._Status.FINISHED) @@ -184,11 +187,11 @@ async def test_tick_by_tick_last(self): """Test overridden function `tickByTickAllLast` with tick type `Last`. """ f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_REQ_TICK_BY_TICK_DATA_LAST + req_id=_RID_REQ_TICK_BY_TICK_DATA_LAST ) self._client.reqTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_LAST.value, + reqId=_RID_REQ_TICK_BY_TICK_DATA_LAST, contract=sample_contracts.us_future(), tickType="Last", numberOfTicks=0, @@ -202,7 +205,7 @@ async def test_tick_by_tick_last(self): if ele is not fq._Status.FINISHED: self._client.cancelTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_LAST.value + reqId=_RID_REQ_TICK_BY_TICK_DATA_LAST ) f_queue.put(element=fq._Status.FINISHED) @@ -212,11 +215,11 @@ async def test_tick_by_tick_last(self): async def test_tick_by_tick_bid_ask(self): """Test overridden function `tickByTickBidAsk`.""" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_REQ_TICK_BY_TICK_DATA_BIDASK + req_id=_RID_REQ_TICK_BY_TICK_DATA_BIDASK ) self._client.reqTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_BIDASK.value, + reqId=_RID_REQ_TICK_BY_TICK_DATA_BIDASK, contract=sample_contracts.gbp_usd_fx(), tickType="BidAsk", numberOfTicks=0, @@ -230,7 +233,7 @@ async def test_tick_by_tick_bid_ask(self): if ele is not fq._Status.FINISHED: self._client.cancelTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_BIDASK.value + reqId=_RID_REQ_TICK_BY_TICK_DATA_BIDASK ) f_queue.put(element=fq._Status.FINISHED) @@ -239,11 +242,11 @@ async def test_tick_by_tick_bid_ask(self): async def test_tick_by_tick_mid_point(self): """Test overridden function `tickByTickMidPoint`.""" f_queue = self._wrapper.get_request_queue( - req_id=Const.RID_REQ_TICK_BY_TICK_DATA_MIDPOINT + req_id=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT ) self._client.reqTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_MIDPOINT.value, + reqId=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT, contract=sample_contracts.gbp_usd_fx(), tickType="MidPoint", numberOfTicks=0, @@ -257,10 +260,11 @@ async def test_tick_by_tick_mid_point(self): if ele is not fq._Status.FINISHED: self._client.cancelTickByTickData( - reqId=Const.RID_REQ_TICK_BY_TICK_DATA_MIDPOINT.value + reqId=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT ) f_queue.put(element=fq._Status.FINISHED) + #endregion - Tick by tick data (Live ticks) @classmethod def tearDownClass(cls): @@ -360,4 +364,4 @@ async def _cancel_account_updates(self): acctCode=os.getenv("IB_ACC_ID", "")) await asyncio.sleep(0.5) self.mock_delegate.account_updates_queue.put(fq._Status.FINISHED) - #endregion + #endregion - Private functions diff --git a/tests/test_account.py b/tests/test_account.py index 17ef3c2..b25966c 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -12,10 +12,12 @@ from tests.toolkit import utils +#region - Constants _MOCK_AC_140: str = "DU0000140" _MOCK_AC_141: str = "DU0000141" _MOCK_AC_142: str = "DU0000142" _MOCK_AC_143: str = "DU0000143" +#endregion - Constants class TestAccountsManager(unittest.TestCase): """Unit tests for class `AccountsManager`.""" @@ -43,7 +45,7 @@ def test_on_account_list_update_existing_list(self): self._manager = account.AccountsManager( accounts={_MOCK_AC_140: models.Account(account_id=_MOCK_AC_140), _MOCK_AC_142: models.Account(account_id=_MOCK_AC_142), - _MOCK_AC_143: models.Account(account_id=_MOCK_AC_143)} + _MOCK_AC_143: models.Account(account_id=_MOCK_AC_143),} ) self._manager.on_account_list_update( diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 12f00c2..992dea3 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -16,9 +16,11 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils +#region - Constants TEST_HOST = os.getenv("IB_HOST", "127.0.0.1") TEST_PORT = int(os.getenv("IB_PORT", "4002")) TEST_ID = 1001 +#endregion - Constants class TestIBBridgeConn(unittest.TestCase): """Test cases for connection related functions in `IBBridge`.""" @@ -157,6 +159,7 @@ async def test_get_earliest_data_point(self): ) self.assertEqual(datetime.datetime(2008, 12, 29, 7, 0), head_bid) + #region - Historical ticks @utils.async_test async def test_get_historical_ticks(self): """Test function `get_historical_ticks`.""" @@ -201,7 +204,9 @@ async def test_get_historical_ticks_err(self): await self._bridge.get_historical_ticks( contract=sample_contracts.us_stock(), attempts=0 ) + #endregion - Historical ticks + #region - Live ticks @utils.async_test async def test_stream_live_ticks(self): """Test function `stream_live_ticks`.""" @@ -238,6 +243,7 @@ def test_stop_live_ticks_stream_err(self): """Test functions `stop_live_ticks_stream` for the error cases.""" with self.assertRaises(error.IBError): self._bridge.stop_live_ticks_stream(stream_id=9999999) + #endregion - Live ticks @classmethod def tearDownClass(cls): diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 4fc819f..cbfd7d7 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -59,7 +59,7 @@ class MockLiveTicksListener(listeners.LiveTicksListener): def on_tick_receive(self, req_id: int, tick: Union[ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]): + ib_wrapper.HistoricalTickLast,]): print(tick) self.ticks.append(tick) From 44e628656f363a8b818aaece9ab0eca6c0f178cd Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 7 Feb 2021 20:21:18 +0000 Subject: [PATCH 047/126] Public `finishable_queue` - Refactored classnames of classes in module `utils/finishable_queue` to make them public. --- ibpy_native/account.py | 10 ++--- ibpy_native/interfaces/delegates/account.py | 5 +-- ibpy_native/internal/client.py | 16 ++++---- ibpy_native/internal/wrapper.py | 32 +++++++-------- ibpy_native/utils/finishable_queue.py | 36 ++++++++--------- tests/internal/test_ib_client.py | 4 +- tests/internal/test_ib_wrapper.py | 44 ++++++++++----------- tests/test_account.py | 2 +- tests/test_ib_bridge.py | 2 +- tests/toolkit/utils.py | 4 +- 10 files changed, 77 insertions(+), 78 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 281e3e0..9acd8a0 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -23,7 +23,7 @@ class AccountsManager(delegates._AccountManagementDelegate): def __init__(self, accounts: Optional[Dict[str, models.Account]]=None): self._accounts: Dict[str, models.Account] = ({} if accounts is None else accounts) - self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( + self._account_updates_queue: fq.FinishableQueue = fq.FinishableQueue( queue_to_finish=queue.Queue() ) @@ -37,8 +37,8 @@ def accounts(self) -> Dict[str, models.Account]: return self._accounts @property - def account_updates_queue(self) -> fq._FinishableQueue: - """":obj:`ibpy_native.utils.finishable_queue._FinishableQueue`: + def account_updates_queue(self) -> fq.FinishableQueue: + """":obj:`ibpy_native.utils.finishable_queue.FinishableQueue`: The queue that stores account updates data from IB Gateway. """ return self._account_updates_queue @@ -137,7 +137,7 @@ async def sub_account_updates(self, account: models.Account): async def unsub_account_updates(self): """Unsubscribes to account updates.""" - self._account_updates_queue.put(fq._Status.FINISHED) + self._account_updates_queue.put(fq.Status.FINISHED) #region - Private functions async def _prevent_multi_account_updates(self): @@ -146,7 +146,7 @@ async def _prevent_multi_account_updates(self): `ibapi.EClient.reqAccountUpdates` is designed as only one account at a time can be subscribed at a time. """ - if self._account_updates_queue.status is fq._Status.INIT: + if self._account_updates_queue.status is fq.Status.INIT: # Returns as no account updates request has been made before. return diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index d0587bd..55f822c 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -1,5 +1,4 @@ """Internal delegate module for accounts & portfolio related features.""" -# pylint: disable=protected-access import abc from typing import Dict, List @@ -19,11 +18,11 @@ def accounts(self) -> Dict[str, models.Account]: @property @abc.abstractmethod - def account_updates_queue(self) -> fq._FinishableQueue: + def account_updates_queue(self) -> fq.FinishableQueue: """Abstract getter of the queue designed to handle account updates data from IB gateway. - This property should be implemented to return the `_FinishableQueue` + This property should be implemented to return the `FinishableQueue` object. """ return NotImplemented diff --git a/ibpy_native/internal/client.py b/ibpy_native/internal/client.py index bc0b434..0c304f0 100644 --- a/ibpy_native/internal/client.py +++ b/ibpy_native/internal/client.py @@ -79,7 +79,7 @@ async def resolve_contract( res = await f_queue.get() if res: - if f_queue.status is fq._Status.ERROR: + if f_queue.status is fq.Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] @@ -133,7 +133,7 @@ async def resolve_contracts( ) if res: - if f_queue.status is fq._Status.ERROR: + if f_queue.status is fq.Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] @@ -188,7 +188,7 @@ async def resolve_head_timestamp( self.cancelHeadTimeStamp(reqId=req_id) if res: - if f_queue.status is fq._Status.ERROR: + if f_queue.status is fq.Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] @@ -288,7 +288,7 @@ async def fetch_historical_ticks( ib_wrapper.HistoricalTickLast,]], bool,] = await f_queue.get() - if res and f_queue.status is fq._Status.ERROR: + if res and f_queue.status is fq.Status.ERROR: # Response received and internal queue reports error if isinstance(res[-1], error.IBError): if all_ticks: @@ -419,7 +419,7 @@ async def stream_live_ticks( listener.on_tick_receive(req_id=req_id, tick=elm) elif isinstance(elm, error.IBError): listener.on_err(err=elm) - elif elm is fq._Status.FINISHED: + elif elm is fq.Status.FINISHED: listener.on_finish(req_id=req_id) def cancel_live_ticks_stream(self, req_id: int): @@ -429,7 +429,7 @@ def cancel_live_ticks_stream(self, req_id: int): req_id (int): Request ID (ticker ID in IB API). Raises: - ibpy_native.error.IBError: If there's no `_FinishableQueue` object + ibpy_native.error.IBError: If there's no `FinishableQueue` object associated with the specified `req_id` found in the internal `_IBWrapper` object. """ @@ -437,7 +437,7 @@ def cancel_live_ticks_stream(self, req_id: int): if f_queue is not None: self.cancelTickByTickData(reqId=req_id) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) else: raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_NOT_FOUND, @@ -504,7 +504,7 @@ def _process_historical_ticks( def _unknown_error(self, req_id: int, extra: Any = None): """Constructs `IBError` with error code `UNKNOWN` - For siturations which internal `_FinishableQueue` reports error status + For siturations which internal `FinishableQueue` reports error status but not exception received. Args: diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 937501b..2203b49 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -25,7 +25,7 @@ def __init__( self, notification_listener: Optional[listeners.NotificationListener]=None ): - self._req_queue: Dict[int, fq._FinishableQueue] = {} + self._req_queue: Dict[int, fq.FinishableQueue] = {} self._ac_man_delegate: Optional[ delegates._AccountManagementDelegate] = None @@ -40,8 +40,8 @@ def next_req_id(self) -> int: """The next usable request ID (ticker ID in IB API). Finds the next available request ID by looking up if there's any - finished `_FinishableQueue` in internal queue dictionary `__req_queue`. - If so, returns the ID of the first finished `_FinishableQueue` found. + finished `FinishableQueue` in internal queue dictionary `__req_queue`. + If so, returns the ID of the first finished `FinishableQueue` found. Returns the last ID in `__req_queue` + 1 if otherwise. Returns: @@ -60,7 +60,7 @@ def next_req_id(self) -> int: return usable_id + 1 #region - Getters - def get_request_queue(self, req_id: int) -> fq._FinishableQueue: + def get_request_queue(self, req_id: int) -> fq.FinishableQueue: """Initialise queue or returns the existing queue with ID `req_id`. Args: @@ -68,12 +68,12 @@ def get_request_queue(self, req_id: int) -> fq._FinishableQueue: queue. Returns: - :obj:`ibpy_native.utils.finishable_queue._FinishableQueue`: + :obj:`ibpy_native.utils.finishable_queue.FinishableQueue`: The newly initialised queue or the already existed queue associated to the `req_id`. Raises: - ibpy_native.error.IBError: If `_FinishableQueue` associated with + ibpy_native.error.IBError: If `FinishableQueue` associated with `req_id` is being used by other tasks. """ try: @@ -84,7 +84,7 @@ def get_request_queue(self, req_id: int) -> fq._FinishableQueue: return self._req_queue[req_id] def get_request_queue_no_throw(self, req_id: int) -> Optional[ - fq._FinishableQueue + fq.FinishableQueue ]: """Returns the existing queue with ID `req_id`. @@ -93,10 +93,10 @@ def get_request_queue_no_throw(self, req_id: int) -> Optional[ queue. Returns: - :obj:`Optional[ibpy_native.utils.finishable_queue._FinishableQueue]`: - The existing `_FinishableQueue` associated to the specified + :obj:`Optional[ibpy_native.utils.finishable_queue.FinishableQueue]`: + The existing `FinishableQueue` associated to the specified `req_id`. `None` if `req_id` doesn't match with any existing - `_FinishableQueue` object. + `FinishableQueue` object. """ return self._req_queue[req_id] if req_id in self._req_queue else None #endregion - Getters @@ -183,14 +183,14 @@ def contractDetails(self, reqId, contractDetails): self._req_queue[reqId].put(element=contractDetails) def contractDetailsEnd(self, reqId): - self._req_queue[reqId].put(element=fq._Status.FINISHED) + self._req_queue[reqId].put(element=fq.Status.FINISHED) #endregion - Get contract details # Get earliest data point for a given instrument and data def headTimestamp(self, reqId: int, headTimestamp: str): # override method self._req_queue[reqId].put(element=headTimestamp) - self._req_queue[reqId].put(element=fq._Status.FINISHED) + self._req_queue[reqId].put(element=fq.Status.FINISHED) #region - Fetch historical tick data def historicalTicks(self, reqId: int, @@ -249,12 +249,12 @@ def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): #region - Private functions def __init_req_queue(self, req_id: int): - """Initials a new `_FinishableQueue` if there's no object at + """Initials a new `FinishableQueue` if there's no object at `self.__req_queue[req_id]`; Resets the queue status to its' initial status. Raises: - ibpy_native.error.IBError: If a `_FinishableQueue` already exists at + ibpy_native.error.IBError: If a `FinishableQueue` already exists at `self.__req_queue[req_id]` and it's not finished. """ if req_id in self._req_queue: @@ -267,7 +267,7 @@ def __init_req_queue(self, req_id: int): "currently in use" ) else: - self._req_queue[req_id] = fq._FinishableQueue(queue.Queue()) + self._req_queue[req_id] = fq.FinishableQueue(queue.Queue()) def _handle_historical_ticks_results( self, req_id: int, @@ -282,7 +282,7 @@ def _handle_historical_ticks_results( """ self._req_queue[req_id].put(element=ticks) self._req_queue[req_id].put(element=done) - self._req_queue[req_id].put(element=fq._Status.FINISHED) + self._req_queue[req_id].put(element=fq.Status.FINISHED) def _handle_live_ticks(self, req_id: int, tick: Union[wrapper.HistoricalTick, diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index 4643ef5..1e20956 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -1,4 +1,4 @@ -"""Code implementation for custom `_FinishableQueue`.""" +"""Code implementation for custom `FinishableQueue`.""" import asyncio import enum import queue @@ -7,15 +7,15 @@ from typing import Iterator, Any # Queue status -class _Status(enum.Enum): - """Status codes for `_FinishableQueue`""" +class Status(enum.Enum): + """Status codes for `FinishableQueue`""" INIT = 0 READY = 103 ERROR = 500 FINISHED = 200 TIMEOUT = 408 -class _FinishableQueue(): +class FinishableQueue(): """Thread-safe class that takes a built-in `queue.Queue` object to handle the async tasks by managing its' status based on elements retrieve from the `Queue` object. @@ -27,11 +27,11 @@ class _FinishableQueue(): def __init__(self, queue_to_finish: queue.Queue): self._lock = threading.Lock() self._queue = queue_to_finish - self._status = _Status.INIT + self._status = Status.INIT @property - def status(self) -> _Status: - """:obj:`ibpy_native.utils.finishable_queue._Status`: Status represents + def status(self) -> Status: + """:obj:`ibpy_native.utils.finishable_queue.Status`: Status represents wether the queue is newly initialised, ready for use, finished, timeout, or encountered error. """ @@ -45,22 +45,22 @@ def finished(self) -> bool: Returns: bool: True is task last associated is finished, False otherwise. """ - return (self._status is _Status.TIMEOUT - or self._status is _Status.FINISHED - or self._status is _Status.ERROR) + return (self._status is Status.TIMEOUT + or self._status is Status.FINISHED + or self._status is Status.ERROR) def reset(self): """Reset the status to `STARTED` for reusing the queue if the status is marked as either `TIMEOUT` or `FINISHED` """ if self.finished: - self._status = _Status.READY + self._status = Status.READY def put(self, element: Any): """Setter to put element to internal synchronised queue.""" - if self._status is _Status.INIT: + if self._status is Status.INIT: with self._lock: - self._status = _Status.READY + self._status = Status.READY self._queue.put(element) @@ -79,13 +79,13 @@ async def get(self) -> list: None, self._queue.get ) - if current_element is _Status.FINISHED: + if current_element is Status.FINISHED: with self._lock: - self._status = _Status.FINISHED + self._status = Status.FINISHED else: if isinstance(current_element, BaseException): with self._lock: - self._status = _Status.ERROR + self._status = Status.ERROR contents_of_queue.append(current_element) @@ -102,11 +102,11 @@ async def stream(self) -> Iterator[Any]: None, self._queue.get ) - if current_element is _Status.FINISHED: + if current_element is Status.FINISHED: with self._lock: self._status = current_element elif isinstance(current_element, BaseException): with self._lock: - self._status = _Status.ERROR + self._status = Status.ERROR yield current_element diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index 24aa14b..b3db5c4 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -165,7 +165,7 @@ async def cancel_req(): queue = self._wrapper.get_request_queue_no_throw( req_id=_RID_STREAM_LIVE_TICKS ) - queue.put(element=fq._Status.FINISHED) + queue.put(element=fq.Status.FINISHED) listener = utils.MockLiveTicksListener() @@ -228,7 +228,7 @@ async def cancel_req(): @utils.async_test async def test_cancel_live_ticks_stream_err(self): """Test function `cancel_live_ticks_stream` with request ID that has no - `_FinishableQueue` associated with. + `FinishableQueue` associated with. """ with self.assertRaises(error.IBError): self._client.cancel_live_ticks_stream( diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index e3d0e6d..c8ada7a 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -50,7 +50,7 @@ def setUpClass(cls): @utils.async_test async def test_next_req_id(self): """Test retrieval of next usable request ID.""" - # Prepare the `_FinishableQueue` objects in internal `__req_queue` + # Prepare the `FinishableQueue` objects in internal `__req_queue` self._wrapper._req_queue.clear() _ = self._wrapper.get_request_queue(req_id=0) f_queue = self._wrapper.get_request_queue(req_id=1) @@ -58,7 +58,7 @@ async def test_next_req_id(self): self.assertEqual(self._wrapper.next_req_id, 11) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) await f_queue.get() self.assertEqual(self._wrapper.next_req_id, 1) @@ -103,7 +103,7 @@ async def test_historical_ticks(self): result = await f_queue.get() - self.assertEqual(f_queue.status, fq._Status.FINISHED) + self.assertEqual(f_queue.status, fq.Status.FINISHED) self.assertEqual(len(result), 2) self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTick) @@ -126,7 +126,7 @@ async def test_historical_ticks_bid_ask(self): result = await f_queue.get() - self.assertEqual(f_queue.status, fq._Status.FINISHED) + self.assertEqual(f_queue.status, fq.Status.FINISHED) self.assertEqual(len(result), 2) self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTickBidAsk) @@ -149,7 +149,7 @@ async def test_historical_ticks_last(self): result = await f_queue.get() - self.assertEqual(f_queue.status, fq._Status.FINISHED) + self.assertEqual(f_queue.status, fq.Status.FINISHED) self.assertEqual(len(result), 2) self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTickLast) #endregion - Historical ticks @@ -172,15 +172,15 @@ async def test_tick_by_tick_all_last(self): async for ele in f_queue.stream(): self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickLast, fq._Status)) - self.assertIsNot(ele, fq._Status.ERROR) + (ib_wrapper.HistoricalTickLast, fq.Status)) + self.assertIsNot(ele, fq.Status.ERROR) - if ele is not fq._Status.FINISHED: + if ele is not fq.Status.FINISHED: self._client.cancelTickByTickData( reqId=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST ) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) @utils.async_test async def test_tick_by_tick_last(self): @@ -200,15 +200,15 @@ async def test_tick_by_tick_last(self): async for ele in f_queue.stream(): self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickLast, fq._Status)) - self.assertIsNot(ele, fq._Status.ERROR) + (ib_wrapper.HistoricalTickLast, fq.Status)) + self.assertIsNot(ele, fq.Status.ERROR) - if ele is not fq._Status.FINISHED: + if ele is not fq.Status.FINISHED: self._client.cancelTickByTickData( reqId=_RID_REQ_TICK_BY_TICK_DATA_LAST ) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) @utils.async_test @@ -228,15 +228,15 @@ async def test_tick_by_tick_bid_ask(self): async for ele in f_queue.stream(): self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickBidAsk, fq._Status)) - self.assertIsNot(ele, fq._Status.ERROR) + (ib_wrapper.HistoricalTickBidAsk, fq.Status)) + self.assertIsNot(ele, fq.Status.ERROR) - if ele is not fq._Status.FINISHED: + if ele is not fq.Status.FINISHED: self._client.cancelTickByTickData( reqId=_RID_REQ_TICK_BY_TICK_DATA_BIDASK ) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) @utils.async_test async def test_tick_by_tick_mid_point(self): @@ -255,15 +255,15 @@ async def test_tick_by_tick_mid_point(self): async for ele in f_queue.stream(): self.assertIsInstance(ele, - (ib_wrapper.HistoricalTick, fq._Status)) - self.assertIsNot(ele, fq._Status.ERROR) + (ib_wrapper.HistoricalTick, fq.Status)) + self.assertIsNot(ele, fq.Status.ERROR) - if ele is not fq._Status.FINISHED: + if ele is not fq.Status.FINISHED: self._client.cancelTickByTickData( reqId=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT ) - f_queue.put(element=fq._Status.FINISHED) + f_queue.put(element=fq.Status.FINISHED) #endregion - Tick by tick data (Live ticks) @classmethod @@ -363,5 +363,5 @@ async def _cancel_account_updates(self): self.client.reqAccountUpdates(subscribe=False, acctCode=os.getenv("IB_ACC_ID", "")) await asyncio.sleep(0.5) - self.mock_delegate.account_updates_queue.put(fq._Status.FINISHED) + self.mock_delegate.account_updates_queue.put(fq.Status.FINISHED) #endregion - Private functions diff --git a/tests/test_account.py b/tests/test_account.py index b25966c..8e4c174 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -132,5 +132,5 @@ async def _simulate_account_updates(self, account_id: str): ) self._manager.account_updates_queue.put("10:11") - self._manager.account_updates_queue.put(fq._Status.FINISHED) + self._manager.account_updates_queue.put(fq.Status.FINISHED) #endregion - Private functions diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 992dea3..898e21a 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -130,7 +130,7 @@ async def test_account_updates(self): self.assertTrue(account.account_ready) self.assertEqual( self._bridge.accounts_manager.account_updates_queue.status, - fq._Status.FINISHED + fq.Status.FINISHED ) #endregion - IB account related diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index cbfd7d7..f7589a2 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -26,7 +26,7 @@ class MockAccountManagementDelegate(delegates._AccountManagementDelegate): """Mock accounts delegate""" def __init__(self): self._account_list: Dict[str, models.Account] = [] - self._account_updates_queue: fq._FinishableQueue = fq._FinishableQueue( + self._account_updates_queue: fq.FinishableQueue = fq.FinishableQueue( queue_to_finish=queue.Queue() ) @@ -35,7 +35,7 @@ def accounts(self) -> Dict[str, models.Account]: return self._account_list @property - def account_updates_queue(self) -> fq._FinishableQueue: + def account_updates_queue(self) -> fq.FinishableQueue: return self._account_updates_queue def on_account_list_update(self, account_list: List[str]): From a7ae0a2bd32590ee35da65c70657b3376738891b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 16:52:28 +0000 Subject: [PATCH 048/126] Replace test contract - Use FX instead of future contract. --- tests/internal/test_ib_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index b3db5c4..bef1997 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -86,7 +86,7 @@ async def test_resolve_head_timestamp(self): """Test function `resolve_head_timestamp`.""" head_timestamp = await self._client.resolve_head_timestamp( req_id=_RID_RESOLVE_HEAD_TIMESTAMP, - contract=sample_contracts.us_future(), + contract=sample_contracts.gbp_usd_fx(), show=dt.EarliestDataPoint.BID ) From 66a5fd48d4706a0e1109b1dc612245eef0cfe039 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 16:52:51 +0000 Subject: [PATCH 049/126] Update to current contract - Pointed to the current future contract. --- tests/toolkit/sample_contracts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/toolkit/sample_contracts.py b/tests/toolkit/sample_contracts.py index b9f6474..3404d84 100644 --- a/tests/toolkit/sample_contracts.py +++ b/tests/toolkit/sample_contracts.py @@ -28,7 +28,7 @@ def us_future() -> ib_contract.Contract: contract.secType = "FUT" contract.exchange = "ECBOT" contract.currency = "USD" - contract.lastTradeDateOrContractMonth = "202009" + contract.lastTradeDateOrContractMonth = "202103" return contract From 7a1139b189ffcf4eaf82ee587e8750d7ec942da8 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 17:02:49 +0000 Subject: [PATCH 050/126] Public `AccountsManagementDelegate` - Made protected class `_AccountManagementDelegate` public and refactored its' classname. --- ibpy_native/account.py | 2 +- ibpy_native/interfaces/delegates/__init__.py | 2 +- ibpy_native/interfaces/delegates/account.py | 2 +- ibpy_native/internal/wrapper.py | 4 ++-- tests/toolkit/utils.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 9acd8a0..ac79c42 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -11,7 +11,7 @@ from ibpy_native.internal import client as ib_client from ibpy_native.utils import finishable_queue as fq -class AccountsManager(delegates._AccountManagementDelegate): +class AccountsManager(delegates.AccountsManagementDelegate): """Class to manage all IB accounts under the same username logged-in on IB Gateway. diff --git a/ibpy_native/interfaces/delegates/__init__.py b/ibpy_native/interfaces/delegates/__init__.py index 060c54b..ab30362 100644 --- a/ibpy_native/interfaces/delegates/__init__.py +++ b/ibpy_native/interfaces/delegates/__init__.py @@ -1,2 +1,2 @@ """Interfaces of delegates.""" -from .account import _AccountManagementDelegate +from .account import AccountsManagementDelegate diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 55f822c..5ec4f51 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -5,7 +5,7 @@ from ibpy_native import models from ibpy_native.utils import finishable_queue as fq -class _AccountManagementDelegate(metaclass=abc.ABCMeta): +class AccountsManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for accounts & portfolio related features.""" @property @abc.abstractmethod diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/internal/wrapper.py index 2203b49..c2e77c9 100644 --- a/ibpy_native/internal/wrapper.py +++ b/ibpy_native/internal/wrapper.py @@ -28,7 +28,7 @@ def __init__( self._req_queue: Dict[int, fq.FinishableQueue] = {} self._ac_man_delegate: Optional[ - delegates._AccountManagementDelegate] = None + delegates.AccountsManagementDelegate] = None self._notification_listener: Optional[ listeners.NotificationListener] = notification_listener @@ -103,7 +103,7 @@ def get_request_queue_no_throw(self, req_id: int) -> Optional[ #region - Setters def set_account_management_delegate( - self, delegate: delegates._AccountManagementDelegate + self, delegate: delegates.AccountsManagementDelegate ): """Setter for optional `_AccountListDelegate`. diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index f7589a2..3eb09e9 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -22,7 +22,7 @@ def wrapper(*args, **kwargs): return wrapper -class MockAccountManagementDelegate(delegates._AccountManagementDelegate): +class MockAccountManagementDelegate(delegates.AccountsManagementDelegate): """Mock accounts delegate""" def __init__(self): self._account_list: Dict[str, models.Account] = [] From 3f7b8f798b7cd52948f2d9083c410cfddfca7493 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 17:09:13 +0000 Subject: [PATCH 051/126] Public `BaseListener` - Made `BaseListener` public to better align with the access scope (not important in Python tho). --- ibpy_native/interfaces/listeners/base.py | 2 +- ibpy_native/interfaces/listeners/live_ticks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/interfaces/listeners/base.py b/ibpy_native/interfaces/listeners/base.py index 82c9fc3..c77884b 100644 --- a/ibpy_native/interfaces/listeners/base.py +++ b/ibpy_native/interfaces/listeners/base.py @@ -3,7 +3,7 @@ from ibpy_native import error -class _BaseListener(metaclass=abc.ABCMeta): +class BaseListener(metaclass=abc.ABCMeta): """Interface of listener for general purposes.""" @abc.abstractmethod def on_err(self, err: error.IBError): diff --git a/ibpy_native/interfaces/listeners/live_ticks.py b/ibpy_native/interfaces/listeners/live_ticks.py index 11506d7..acc8ff9 100644 --- a/ibpy_native/interfaces/listeners/live_ticks.py +++ b/ibpy_native/interfaces/listeners/live_ticks.py @@ -7,7 +7,7 @@ from ibpy_native.interfaces.listeners import base -class LiveTicksListener(base._BaseListener): +class LiveTicksListener(base.BaseListener): """Interface of listener for "Tick-by-Tick Data" related functions.""" @abc.abstractmethod def on_tick_receive(self, req_id: int, tick: Union[ From 06ec0234c289bacff96627923c2b181afd580c3b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 17:20:51 +0000 Subject: [PATCH 052/126] Protect module `_internal` - Renamed module `internal` to `_internal` to indicate it's module private. --- ibpy_native/{internal => _internal}/__init__.py | 0 ibpy_native/{internal => _internal}/client.py | 4 ++-- ibpy_native/{internal => _internal}/wrapper.py | 0 ibpy_native/account.py | 2 +- ibpy_native/bridge.py | 4 ++-- ibpy_native/models/raw_data.py | 4 ++-- tests/internal/test_ib_client.py | 4 ++-- tests/internal/test_ib_wrapper.py | 6 +++--- tests/test_account.py | 2 +- tests/test_ib_bridge.py | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) rename ibpy_native/{internal => _internal}/__init__.py (100%) rename ibpy_native/{internal => _internal}/client.py (99%) rename ibpy_native/{internal => _internal}/wrapper.py (100%) diff --git a/ibpy_native/internal/__init__.py b/ibpy_native/_internal/__init__.py similarity index 100% rename from ibpy_native/internal/__init__.py rename to ibpy_native/_internal/__init__.py diff --git a/ibpy_native/internal/client.py b/ibpy_native/_internal/client.py similarity index 99% rename from ibpy_native/internal/client.py rename to ibpy_native/_internal/client.py index 0c304f0..479d094 100644 --- a/ibpy_native/internal/client.py +++ b/ibpy_native/_internal/client.py @@ -11,8 +11,8 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error +from ibpy_native._internal import wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners -from ibpy_native.internal import wrapper as ibpy_wrapper from ibpy_native.utils import const from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -34,7 +34,7 @@ class _IBClient(ib_client.EClient): at login. Defaults to "America/New_York". Args: - wrapper (:obj:`ibpy_native.internal.wrapper._IBWrapper`): The wrapper + wrapper (:obj:`ibpy_native._internal.wrapper._IBWrapper`): The wrapper object to handle messages return from IB Gateway. """ # Static variable to define the timezone diff --git a/ibpy_native/internal/wrapper.py b/ibpy_native/_internal/wrapper.py similarity index 100% rename from ibpy_native/internal/wrapper.py rename to ibpy_native/_internal/wrapper.py diff --git a/ibpy_native/account.py b/ibpy_native/account.py index ac79c42..2fc14cb 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -7,8 +7,8 @@ from typing import Dict, List, Optional, Union from ibpy_native import models +from ibpy_native._internal import client as ib_client from ibpy_native.interfaces import delegates -from ibpy_native.internal import client as ib_client from ibpy_native.utils import finishable_queue as fq class AccountsManager(delegates.AccountsManagementDelegate): diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 5fabb86..65384eb 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,9 +12,9 @@ from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import client as ib_client +from ibpy_native._internal import wrapper as ib_wrapper from ibpy_native.interfaces import listeners -from ibpy_native.internal import client as ib_client -from ibpy_native.internal import wrapper as ib_wrapper from ibpy_native.utils import const from ibpy_native.utils import datatype as dt diff --git a/ibpy_native/models/raw_data.py b/ibpy_native/models/raw_data.py index c030fb3..67f3d5e 100644 --- a/ibpy_native/models/raw_data.py +++ b/ibpy_native/models/raw_data.py @@ -9,7 +9,7 @@ @dataclasses.dataclass class RawAccountValueData: """Model class for account value updates received in callback - `ibpy_native.internal.wrapper.IBWrapper.updateAccountValue`. + `ibpy_native._internal.wrapper.IBWrapper.updateAccountValue`. Attributes: account (str): Account ID that the data belongs to. @@ -30,7 +30,7 @@ class RawAccountValueData: @dataclasses.dataclass class RawPortfolioData: """Model class for portfolio updates received in callback - `ibpy_native.internal.wrapper.IBWrapper.updatePortfolio`. + `ibpy_native._internal.wrapper.IBWrapper.updatePortfolio`. Attributes: account (str): Account ID that the data belongs to. diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index bef1997..172bfcf 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -13,8 +13,8 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error -from ibpy_native.internal import client as ibpy_client -from ibpy_native.internal import wrapper as ibpy_wrapper +from ibpy_native._internal import client as ibpy_client +from ibpy_native._internal import wrapper as ibpy_wrapper from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index c8ada7a..b01162b 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -8,9 +8,9 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import models +from ibpy_native._internal import client as ibpy_client +from ibpy_native._internal import wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners -from ibpy_native.internal import client as ibpy_client -from ibpy_native.internal import wrapper as ibpy_wrapper from ibpy_native.utils import finishable_queue as fq from tests.toolkit import sample_contracts @@ -272,7 +272,7 @@ def tearDownClass(cls): class TestAccountAndPortfolioData(unittest.TestCase): """Unit tests for account and portfolio data related callbacks & functions - in class `ibpy_native.internal.wrapper._IBWrapper`. + in class `ibpy_native._internal.wrapper._IBWrapper`. Connection with IB Gateway is required for this test suit. """ diff --git a/tests/test_account.py b/tests/test_account.py index 8e4c174..1455048 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -7,7 +7,7 @@ from ibapi import contract as ib_contract from ibpy_native import account from ibpy_native import models -from ibpy_native.internal import client as ib_client +from ibpy_native._internal import client as ib_client from ibpy_native.utils import finishable_queue as fq from tests.toolkit import utils diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 898e21a..390e6f3 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -9,8 +9,8 @@ import ibpy_native from ibpy_native import error +from ibpy_native._internal import client as ibpy_client from ibpy_native.interfaces import listeners -from ibpy_native.internal import client as ibpy_client from ibpy_native.utils import datatype as dt, finishable_queue as fq from tests.toolkit import sample_contracts From c107f68ee31a9695ffff01230062b7df8285cf68 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 17:39:16 +0000 Subject: [PATCH 053/126] Protect module `_internal._client`. - Renamed module `_intenal.client` to `_internal._client` to indicates it's module private, and refactored the classname of `_IBClient` to `IBClient` as it isn't file private. --- .../_internal/{client.py => _client.py} | 8 ++++---- ibpy_native/account.py | 4 ++-- ibpy_native/bridge.py | 18 ++++++++--------- tests/internal/test_ib_client.py | 20 +++++++++---------- tests/internal/test_ib_wrapper.py | 6 +++--- tests/test_account.py | 8 ++++---- tests/test_ib_bridge.py | 8 ++++---- 7 files changed, 36 insertions(+), 36 deletions(-) rename ibpy_native/_internal/{client.py => _client.py} (98%) diff --git a/ibpy_native/_internal/client.py b/ibpy_native/_internal/_client.py similarity index 98% rename from ibpy_native/_internal/client.py rename to ibpy_native/_internal/_client.py index 479d094..13730c8 100644 --- a/ibpy_native/_internal/client.py +++ b/ibpy_native/_internal/_client.py @@ -18,13 +18,13 @@ from ibpy_native.utils import finishable_queue as fq class _ProcessHistoricalTicksResult(TypedDict): - """Use for type hint the returns of `_IBClient.fetch_historical_ticks`.""" + """Use for type hint the returns of `IBClient.fetch_historical_ticks`.""" ticks: List[Union[ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickBidAsk, ib_wrapper.HistoricalTickLast,]] next_end_time: datetime.datetime -class _IBClient(ib_client.EClient): +class IBClient(ib_client.EClient): """The client calls the native methods from _IBWrapper instead of overriding native methods. @@ -263,11 +263,11 @@ async def fetch_historical_ticks( all_ticks: list = [] real_start_time = ( - _IBClient.TZ.localize(start) if start.tzinfo is None else start + IBClient.TZ.localize(start) if start.tzinfo is None else start ) next_end_time = ( - _IBClient.TZ.localize(end) if end.tzinfo is None else end + IBClient.TZ.localize(end) if end.tzinfo is None else end ) finished = False diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 2fc14cb..9b06531 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union from ibpy_native import models -from ibpy_native._internal import client as ib_client +from ibpy_native._internal import _client as ib_client from ibpy_native.interfaces import delegates from ibpy_native.utils import finishable_queue as fq @@ -116,7 +116,7 @@ async def sub_account_updates(self, account: models.Account): if re.fullmatch(r"\d{2}:\d{2}", elm): time = datetime.datetime.strptime(elm, "%H:%M").time() - time = time.replace(tzinfo=ib_client._IBClient.TZ) + time = time.replace(tzinfo=ib_client.IBClient.TZ) if isinstance(last_elm, (str, models.RawAccountValueData)): # This timestamp represents the last update system time diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 65384eb..6629b84 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,7 +12,7 @@ from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models -from ibpy_native._internal import client as ib_client +from ibpy_native._internal import _client as ib_client from ibpy_native._internal import wrapper as ib_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import const @@ -59,7 +59,7 @@ def __init__( delegate=self._accounts_manager ) - self._client = ib_client._IBClient(wrapper=self._wrapper) + self._client = ib_client.IBClient(wrapper=self._wrapper) if auto_conn: self.connect() @@ -87,7 +87,7 @@ def set_timezone(tz: datetime.tzinfo): tz (datetime.tzinfo): Timezone. Recommend to set this value via `pytz.timezone(zone: str)`. """ - ib_client._IBClient.TZ = tz + ib_client.IBClient.TZ = tz def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. @@ -222,7 +222,7 @@ async def get_earliest_data_point( data_point = datetime.datetime.fromtimestamp( result ).astimezone( - ib_client._IBClient.TZ + ib_client.IBClient.TZ ) return data_point.replace(tzinfo=None) @@ -276,7 +276,7 @@ async def get_historical_ticks( attempt(s). """ all_ticks = [] - next_end_time = ib_client._IBClient.TZ.localize(dt=end) + next_end_time = ib_client.IBClient.TZ.localize(dt=end) # Error checking if end.tzinfo is not None or (start is not None and @@ -293,7 +293,7 @@ async def get_historical_ticks( data_type is dt.HistoricalTicks.TRADES ) else dt.EarliestDataPoint.BID ) - ).astimezone(tz=ib_client._IBClient.TZ) + ).astimezone(tz=ib_client.IBClient.TZ) except error.IBError as err: raise err @@ -309,7 +309,7 @@ async def get_historical_ticks( "Specificed end time cannot be earlier than start time" ) - start = ib_client._IBClient.TZ.localize(dt=start) + start = ib_client.IBClient.TZ.localize(dt=start) else: start = head_timestamp @@ -349,7 +349,7 @@ async def get_historical_ticks( next_end_time = datetime.datetime.fromtimestamp( res[0][0].time - ).astimezone(ib_client._IBClient.TZ) + ).astimezone(ib_client.IBClient.TZ) except ValueError as err: raise err except error.IBError as err: @@ -371,7 +371,7 @@ async def get_historical_ticks( # Updates the end time for next attempt next_end_time = datetime.datetime.fromtimestamp( all_ticks[0].time - ).astimezone(ib_client._IBClient.TZ) + ).astimezone(ib_client.IBClient.TZ) continue diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index 172bfcf..a6291ed 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -13,7 +13,7 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error -from ibpy_native._internal import client as ibpy_client +from ibpy_native._internal import _client as ibpy_client from ibpy_native._internal import wrapper as ibpy_wrapper from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -35,14 +35,14 @@ #endregion - Constants class TestIBClient(unittest.TestCase): - """Unit tests for class `_IBClient`.""" + """Unit tests for class `IBClient`.""" @classmethod def setUpClass(cls): - ibpy_client._IBClient.TZ = pytz.timezone("America/New_York") + ibpy_client.IBClient.TZ = pytz.timezone("America/New_York") cls._wrapper = ibpy_wrapper._IBWrapper() - cls._client = ibpy_client._IBClient(cls._wrapper) + cls._client = ibpy_client.IBClient(cls._wrapper) cls._client.connect( os.getenv("IB_HOST", "127.0.0.1"), @@ -100,9 +100,9 @@ async def test_fetch_historical_ticks(self): data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client._IBClient.TZ.localize( + start=ibpy_client.IBClient.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client._IBClient.TZ.localize( + end=ibpy_client.IBClient.TZ.localize( datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.MIDPOINT ) @@ -115,9 +115,9 @@ async def test_fetch_historical_ticks(self): data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client._IBClient.TZ.localize( + start=ibpy_client.IBClient.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client._IBClient.TZ.localize( + end=ibpy_client.IBClient.TZ.localize( datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.BID_ASK ) @@ -147,8 +147,8 @@ async def test_fetch_historical_ticks_err(self): req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, contract=ib_contract.Contract(), start=datetime.datetime(2020, 5, 20, 3, 20, 0).astimezone( - ibpy_client._IBClient.TZ), - end=datetime.datetime.now().astimezone(ibpy_client._IBClient.TZ) + ibpy_client.IBClient.TZ), + end=datetime.datetime.now().astimezone(ibpy_client.IBClient.TZ) ) #endregion - Historical ticks diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index b01162b..063e038 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -8,7 +8,7 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import models -from ibpy_native._internal import client as ibpy_client +from ibpy_native._internal import _client as ibpy_client from ibpy_native._internal import wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -33,7 +33,7 @@ class TestIBWrapper(unittest.TestCase): @classmethod def setUpClass(cls): cls._wrapper = ibpy_wrapper._IBWrapper() - cls._client = ibpy_client._IBClient(cls._wrapper) + cls._client = ibpy_client.IBClient(cls._wrapper) cls._client.connect( os.getenv("IB_HOST", "127.0.0.1"), @@ -279,7 +279,7 @@ class TestAccountAndPortfolioData(unittest.TestCase): @classmethod def setUpClass(cls): cls.wrapper = ibpy_wrapper._IBWrapper() - cls.client = ibpy_client._IBClient(cls.wrapper) + cls.client = ibpy_client.IBClient(cls.wrapper) cls.client.connect( os.getenv("IB_HOST", "127.0.0.1"), diff --git a/tests/test_account.py b/tests/test_account.py index 1455048..3552ce5 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -7,7 +7,7 @@ from ibapi import contract as ib_contract from ibpy_native import account from ibpy_native import models -from ibpy_native._internal import client as ib_client +from ibpy_native._internal import _client as ib_client from ibpy_native.utils import finishable_queue as fq from tests.toolkit import utils @@ -79,21 +79,21 @@ async def test_sub_account_updates(self): currency="BASE") ) self.assertEqual(datetime.time(hour=10, minute=10, - tzinfo=ib_client._IBClient.TZ), + tzinfo=ib_client.IBClient.TZ), self._manager.accounts[_MOCK_AC_140].last_update_time) # Assert portfolio data self.assertEqual( 8, self._manager.accounts[_MOCK_AC_140].positions[0].avg_cost) self.assertEqual( - datetime.time(hour=10, minute=7, tzinfo=ib_client._IBClient.TZ), + datetime.time(hour=10, minute=7, tzinfo=ib_client.IBClient.TZ), self._manager.accounts[_MOCK_AC_140].positions[0].last_update_time ) self.assertEqual(20689, (self._manager.accounts[_MOCK_AC_140] .positions[412888950].market_price)) self.assertEqual(datetime.time(hour=10, minute=11, - tzinfo=ib_client._IBClient.TZ), + tzinfo=ib_client.IBClient.TZ), (self._manager.accounts[_MOCK_AC_140] .positions[412888950].last_update_time)) diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 390e6f3..8566e34 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -9,7 +9,7 @@ import ibpy_native from ibpy_native import error -from ibpy_native._internal import client as ibpy_client +from ibpy_native._internal import _client as ibpy_client from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt, finishable_queue as fq @@ -76,12 +76,12 @@ def test_set_timezone(self): """Test function `set_timezone`.""" ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("Asia/Hong_Kong")) - self.assertEqual(ibpy_client._IBClient.TZ, + self.assertEqual(ibpy_client.IBClient.TZ, pytz.timezone("Asia/Hong_Kong")) # Reset timezone to New York ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("America/New_York")) - self.assertEqual(ibpy_client._IBClient.TZ, + self.assertEqual(ibpy_client.IBClient.TZ, pytz.timezone("America/New_York")) def test_set_on_notify_listener(self): @@ -210,7 +210,7 @@ async def test_get_historical_ticks_err(self): @utils.async_test async def test_stream_live_ticks(self): """Test function `stream_live_ticks`.""" - client: ibpy_client._IBClient = self._bridge._client + client: ibpy_client.IBClient = self._bridge._client listener = utils.MockLiveTicksListener() req_id = await self._bridge.stream_live_ticks( From fe4434b7b2f99acab9af865349fcd4ee659560da Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 18:55:42 +0000 Subject: [PATCH 054/126] Protect module `_internal._wrapper` - Renamed module `_internal.wrapper` to `_internal._wrapper` to indicates its' module private, and refactored the classname of `_IBWrapper` to `IBWrapper` as it isn't file private. --- ibpy_native/_internal/_client.py | 10 +++++----- ibpy_native/_internal/{wrapper.py => _wrapper.py} | 2 +- ibpy_native/account.py | 2 +- ibpy_native/bridge.py | 4 ++-- ibpy_native/interfaces/delegates/account.py | 2 +- ibpy_native/models/raw_data.py | 4 ++-- tests/internal/test_ib_client.py | 4 ++-- tests/internal/test_ib_wrapper.py | 14 +++++++------- 8 files changed, 21 insertions(+), 21 deletions(-) rename ibpy_native/_internal/{wrapper.py => _wrapper.py} (99%) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 13730c8..cdfc033 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -11,7 +11,7 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error -from ibpy_native._internal import wrapper as ibpy_wrapper +from ibpy_native._internal import _wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import const from ibpy_native.utils import datatype as dt @@ -25,7 +25,7 @@ class _ProcessHistoricalTicksResult(TypedDict): next_end_time: datetime.datetime class IBClient(ib_client.EClient): - """The client calls the native methods from _IBWrapper instead of + """The client calls the native methods from IBWrapper instead of overriding native methods. Attributes: @@ -34,13 +34,13 @@ class IBClient(ib_client.EClient): at login. Defaults to "America/New_York". Args: - wrapper (:obj:`ibpy_native._internal.wrapper._IBWrapper`): The wrapper + wrapper (:obj:`ibpy_native._internal._wrapper.IBWrapper`): The wrapper object to handle messages return from IB Gateway. """ # Static variable to define the timezone TZ = pytz.timezone("America/New_York") - def __init__(self, wrapper: ibpy_wrapper._IBWrapper): + def __init__(self, wrapper: ibpy_wrapper.IBWrapper): self._wrapper = wrapper super().__init__(wrapper) @@ -431,7 +431,7 @@ def cancel_live_ticks_stream(self, req_id: int): Raises: ibpy_native.error.IBError: If there's no `FinishableQueue` object associated with the specified `req_id` found in the internal - `_IBWrapper` object. + `IBWrapper` object. """ f_queue = self._wrapper.get_request_queue_no_throw(req_id=req_id) diff --git a/ibpy_native/_internal/wrapper.py b/ibpy_native/_internal/_wrapper.py similarity index 99% rename from ibpy_native/_internal/wrapper.py rename to ibpy_native/_internal/_wrapper.py index c2e77c9..40e682d 100644 --- a/ibpy_native/_internal/wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -12,7 +12,7 @@ from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq -class _IBWrapper(wrapper.EWrapper): +class IBWrapper(wrapper.EWrapper): """The wrapper deals with the action coming back from the IB gateway or TWS instance. diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 9b06531..fa6bf1c 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -45,7 +45,7 @@ def account_updates_queue(self) -> fq.FinishableQueue: def on_account_list_update(self, account_list: List[str]): """Callback function for internal API callback - `_IBWrapper.managedAccounts`. + `IBWrapper.managedAccounts`. Checks the existing account list for update(s) to the list. Terminates action(s) or subscription(s) on account(s) which is/are no longer diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 6629b84..9932922 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -13,7 +13,7 @@ from ibpy_native import error from ibpy_native import models from ibpy_native._internal import _client as ib_client -from ibpy_native._internal import wrapper as ib_wrapper +from ibpy_native._internal import _wrapper as ib_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import const from ibpy_native.utils import datatype as dt @@ -52,7 +52,7 @@ def __init__( else accounts_manager ) - self._wrapper = ib_wrapper._IBWrapper( + self._wrapper = ib_wrapper.IBWrapper( notification_listener=notification_listener ) self._wrapper.set_account_management_delegate( diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 5ec4f51..8c35eb0 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -29,7 +29,7 @@ def account_updates_queue(self) -> fq.FinishableQueue: @abc.abstractmethod def on_account_list_update(self, account_list: List[str]): - """Callback on `_IBWrapper.managedAccounts` is triggered by IB API. + """Callback on `IBWrapper.managedAccounts` is triggered by IB API. Args: account_list (:obj:`List[str]`): List of proceeded account IDs diff --git a/ibpy_native/models/raw_data.py b/ibpy_native/models/raw_data.py index 67f3d5e..c18ebaf 100644 --- a/ibpy_native/models/raw_data.py +++ b/ibpy_native/models/raw_data.py @@ -9,7 +9,7 @@ @dataclasses.dataclass class RawAccountValueData: """Model class for account value updates received in callback - `ibpy_native._internal.wrapper.IBWrapper.updateAccountValue`. + `ibpy_native._internal._wrapper.IBWrapper.updateAccountValue`. Attributes: account (str): Account ID that the data belongs to. @@ -30,7 +30,7 @@ class RawAccountValueData: @dataclasses.dataclass class RawPortfolioData: """Model class for portfolio updates received in callback - `ibpy_native._internal.wrapper.IBWrapper.updatePortfolio`. + `ibpy_native._internal._wrapper.IBWrapper.updatePortfolio`. Attributes: account (str): Account ID that the data belongs to. diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index a6291ed..f064dcc 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -14,7 +14,7 @@ from ibpy_native import error from ibpy_native._internal import _client as ibpy_client -from ibpy_native._internal import wrapper as ibpy_wrapper +from ibpy_native._internal import _wrapper as ibpy_wrapper from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -41,7 +41,7 @@ class TestIBClient(unittest.TestCase): def setUpClass(cls): ibpy_client.IBClient.TZ = pytz.timezone("America/New_York") - cls._wrapper = ibpy_wrapper._IBWrapper() + cls._wrapper = ibpy_wrapper.IBWrapper() cls._client = ibpy_client.IBClient(cls._wrapper) cls._client.connect( diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 063e038..cb8e77a 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -9,7 +9,7 @@ from ibpy_native import models from ibpy_native._internal import _client as ibpy_client -from ibpy_native._internal import wrapper as ibpy_wrapper +from ibpy_native._internal import _wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -28,11 +28,11 @@ #endregion - Constants class TestIBWrapper(unittest.TestCase): - """Unit tests for class `_IBWrapper`.""" + """Unit tests for class `IBWrapper`.""" @classmethod def setUpClass(cls): - cls._wrapper = ibpy_wrapper._IBWrapper() + cls._wrapper = ibpy_wrapper.IBWrapper() cls._client = ibpy_client.IBClient(cls._wrapper) cls._client.connect( @@ -46,7 +46,7 @@ def setUpClass(cls): setattr(cls._client, "_thread", thread) - #region - _IBWrapper specifics + #region - IBWrapper specifics @utils.async_test async def test_next_req_id(self): """Test retrieval of next usable request ID.""" @@ -81,7 +81,7 @@ def on_notify(self, msg_code: int, msg: str): self._wrapper.error(reqId=-1, errorCode=1100, errorString="MOCK MSG") self.assertTrue(mock_listener.triggered) - #endregion - _IBWrapper specifics + #endregion - IBWrapper specifics #region - Historical ticks @utils.async_test @@ -272,13 +272,13 @@ def tearDownClass(cls): class TestAccountAndPortfolioData(unittest.TestCase): """Unit tests for account and portfolio data related callbacks & functions - in class `ibpy_native._internal.wrapper._IBWrapper`. + in class `ibpy_native._internal._wrapper.IBWrapper`. Connection with IB Gateway is required for this test suit. """ @classmethod def setUpClass(cls): - cls.wrapper = ibpy_wrapper._IBWrapper() + cls.wrapper = ibpy_wrapper.IBWrapper() cls.client = ibpy_client.IBClient(cls.wrapper) cls.client.connect( From 5b22a292484297ccf80ddbf1e571d85ee824adda Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 19:04:30 +0000 Subject: [PATCH 055/126] Refactor function name - Replace double underscores with single to align the code style. --- ibpy_native/_internal/_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 40e682d..5a72e4c 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -77,7 +77,7 @@ def get_request_queue(self, req_id: int) -> fq.FinishableQueue: `req_id` is being used by other tasks. """ try: - self.__init_req_queue(req_id=req_id) + self._init_req_queue(req_id=req_id) except error.IBError as err: raise err @@ -248,7 +248,7 @@ def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): #endregion - Override functions from `wrapper.EWrapper` #region - Private functions - def __init_req_queue(self, req_id: int): + def _init_req_queue(self, req_id: int): """Initials a new `FinishableQueue` if there's no object at `self.__req_queue[req_id]`; Resets the queue status to its' initial status. From e6564b789d0acc0af880c9fab8a915582be18b07 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 19:37:29 +0000 Subject: [PATCH 056/126] Relocate module `_const` - Relocated module `utils.const` as `_internal._const`. - Made module an internal module. - Removed class `_const._IB`. --- ibpy_native/_internal/_client.py | 6 +++--- ibpy_native/_internal/_const.py | 4 ++++ ibpy_native/bridge.py | 6 +++--- ibpy_native/utils/const.py | 15 --------------- 4 files changed, 10 insertions(+), 21 deletions(-) create mode 100644 ibpy_native/_internal/_const.py delete mode 100644 ibpy_native/utils/const.py diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index cdfc033..96a709b 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -11,9 +11,9 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error +from ibpy_native._internal import _const from ibpy_native._internal import _wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners -from ibpy_native.utils import const from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -278,7 +278,7 @@ async def fetch_historical_ticks( while not finished: self.reqHistoricalTicks( reqId=req_id, contract=contract, startDateTime="", - endDateTime=next_end_time.strftime(const._IB.TIME_FMT), + endDateTime=next_end_time.strftime(_const.TIME_FMT), numberOfTicks=1000, whatToShow=show.value, useRth=0, ignoreSize=False, miscOptions=[] ) @@ -338,7 +338,7 @@ async def fetch_historical_ticks( print( f"{len(all_ticks)} ticks fetched (" f"{len(processed_result['ticks'])} new ticks); Next end " - f"time - {next_end_time.strftime(const._IB.TIME_FMT)}" + f"time - {next_end_time.strftime(_const.TIME_FMT)}" ) if next_end_time.timestamp() <= real_start_time.timestamp(): diff --git a/ibpy_native/_internal/_const.py b/ibpy_native/_internal/_const.py new file mode 100644 index 0000000..9301cda --- /dev/null +++ b/ibpy_native/_internal/_const.py @@ -0,0 +1,4 @@ +"""Constants use across the project.""" +from typing_extensions import Final + +TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" #IB time format diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 9932922..45443e0 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,10 +12,10 @@ from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import _const from ibpy_native._internal import _client as ib_client from ibpy_native._internal import _wrapper as ib_wrapper from ibpy_native.interfaces import listeners -from ibpy_native.utils import const from ibpy_native.utils import datatype as dt class IBBridge: @@ -302,7 +302,7 @@ async def get_historical_ticks( raise ValueError( "Specificed start time is earlier than the earliest " "available datapoint - " - f"{head_timestamp.strftime(const._IB.TIME_FMT)}" + f"{head_timestamp.strftime(_const.TIME_FMT)}" ) if end.timestamp() < start.timestamp(): raise ValueError( @@ -316,7 +316,7 @@ async def get_historical_ticks( if next_end_time.timestamp() < head_timestamp.timestamp(): raise ValueError( "Specificed end time is earlier than the earliest available " - f"datapoint - {head_timestamp.strftime(const._IB.TIME_FMT)}" + f"datapoint - {head_timestamp.strftime(_const.TIME_FMT)}" ) if attempts < 1 and attempts != -1: diff --git a/ibpy_native/utils/const.py b/ibpy_native/utils/const.py deleted file mode 100644 index eab23a7..0000000 --- a/ibpy_native/utils/const.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Constants use across the project.""" -from typing_extensions import final, Final - -@final -class _IB: - # pylint: disable=too-few-public-methods - """Constants use across the project.""" - - # Defined constants - TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" #IB time format - - # Messages - MSG_TIMEOUT: Final[str] = ( - "Exceed maximum wait for wrapper to confirm finished" - ) From b7c39132d1e516ccf0555c16e3bdd17754336f59 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 19:59:59 +0000 Subject: [PATCH 057/126] Relocate var `TZ` - Reanmed module `_internal._const` to `_internal._global` as it stores all global items which are used across the project instead of just constants. - Moved static var `IBClient.TZ` to module `_internal._global` as a global variable. --- ibpy_native/_internal/_client.py | 21 ++++++--------------- ibpy_native/_internal/_const.py | 4 ---- ibpy_native/_internal/_global.py | 10 ++++++++++ ibpy_native/account.py | 4 ++-- ibpy_native/bridge.py | 20 ++++++++++---------- tests/test_account.py | 8 ++++---- tests/test_ib_bridge.py | 5 +++-- 7 files changed, 35 insertions(+), 37 deletions(-) delete mode 100644 ibpy_native/_internal/_const.py create mode 100644 ibpy_native/_internal/_global.py diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 96a709b..4470bb1 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -3,7 +3,6 @@ import datetime from typing import Any, List, Union -import pytz from typing_extensions import TypedDict from ibapi import client as ib_client @@ -11,7 +10,7 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error -from ibpy_native._internal import _const +from ibpy_native._internal import _global from ibpy_native._internal import _wrapper as ibpy_wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt @@ -28,18 +27,10 @@ class IBClient(ib_client.EClient): """The client calls the native methods from IBWrapper instead of overriding native methods. - Attributes: - TZ: Class level timezone for all datetime related object. Timezone - should be aligned with the timezone specified in TWS/IB Gateway - at login. Defaults to "America/New_York". - Args: wrapper (:obj:`ibpy_native._internal._wrapper.IBWrapper`): The wrapper object to handle messages return from IB Gateway. """ - # Static variable to define the timezone - TZ = pytz.timezone("America/New_York") - def __init__(self, wrapper: ibpy_wrapper.IBWrapper): self._wrapper = wrapper super().__init__(wrapper) @@ -210,7 +201,7 @@ async def resolve_head_timestamp( async def fetch_historical_ticks( self, req_id: int, contract: ib_contract.Contract, start: datetime.datetime, - end: datetime.datetime=datetime.datetime.now().astimezone(TZ), + end: datetime.datetime=datetime.datetime.now().astimezone(_global.TZ), show: dt.HistoricalTicks=dt.HistoricalTicks.TRADES ) -> dt.HistoricalTicksResult: """Fetch the historical ticks data for a given instrument from IB. @@ -263,11 +254,11 @@ async def fetch_historical_ticks( all_ticks: list = [] real_start_time = ( - IBClient.TZ.localize(start) if start.tzinfo is None else start + _global.TZ.localize(start) if start.tzinfo is None else start ) next_end_time = ( - IBClient.TZ.localize(end) if end.tzinfo is None else end + _global.TZ.localize(end) if end.tzinfo is None else end ) finished = False @@ -278,7 +269,7 @@ async def fetch_historical_ticks( while not finished: self.reqHistoricalTicks( reqId=req_id, contract=contract, startDateTime="", - endDateTime=next_end_time.strftime(_const.TIME_FMT), + endDateTime=next_end_time.strftime(_global.TIME_FMT), numberOfTicks=1000, whatToShow=show.value, useRth=0, ignoreSize=False, miscOptions=[] ) @@ -338,7 +329,7 @@ async def fetch_historical_ticks( print( f"{len(all_ticks)} ticks fetched (" f"{len(processed_result['ticks'])} new ticks); Next end " - f"time - {next_end_time.strftime(_const.TIME_FMT)}" + f"time - {next_end_time.strftime(_global.TIME_FMT)}" ) if next_end_time.timestamp() <= real_start_time.timestamp(): diff --git a/ibpy_native/_internal/_const.py b/ibpy_native/_internal/_const.py deleted file mode 100644 index 9301cda..0000000 --- a/ibpy_native/_internal/_const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants use across the project.""" -from typing_extensions import Final - -TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" #IB time format diff --git a/ibpy_native/_internal/_global.py b/ibpy_native/_internal/_global.py new file mode 100644 index 0000000..11cde86 --- /dev/null +++ b/ibpy_native/_internal/_global.py @@ -0,0 +1,10 @@ +"""Global items use across the project.""" +import datetime + +import pytz +from typing_extensions import Final + +# IB time format +TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" +# Timezone to match the one set in IB Gateway/TWS at login +TZ: datetime.tzinfo = pytz.timezone("America/New_York") diff --git a/ibpy_native/account.py b/ibpy_native/account.py index fa6bf1c..088cdae 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union from ibpy_native import models -from ibpy_native._internal import _client as ib_client +from ibpy_native._internal import _global from ibpy_native.interfaces import delegates from ibpy_native.utils import finishable_queue as fq @@ -116,7 +116,7 @@ async def sub_account_updates(self, account: models.Account): if re.fullmatch(r"\d{2}:\d{2}", elm): time = datetime.datetime.strptime(elm, "%H:%M").time() - time = time.replace(tzinfo=ib_client.IBClient.TZ) + time = time.replace(tzinfo=_global.TZ) if isinstance(last_elm, (str, models.RawAccountValueData)): # This timestamp represents the last update system time diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 45443e0..ca3f7d6 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,7 +12,7 @@ from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models -from ibpy_native._internal import _const +from ibpy_native._internal import _global from ibpy_native._internal import _client as ib_client from ibpy_native._internal import _wrapper as ib_wrapper from ibpy_native.interfaces import listeners @@ -87,7 +87,7 @@ def set_timezone(tz: datetime.tzinfo): tz (datetime.tzinfo): Timezone. Recommend to set this value via `pytz.timezone(zone: str)`. """ - ib_client.IBClient.TZ = tz + _global.TZ = tz def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. @@ -222,7 +222,7 @@ async def get_earliest_data_point( data_point = datetime.datetime.fromtimestamp( result ).astimezone( - ib_client.IBClient.TZ + _global.TZ ) return data_point.replace(tzinfo=None) @@ -276,7 +276,7 @@ async def get_historical_ticks( attempt(s). """ all_ticks = [] - next_end_time = ib_client.IBClient.TZ.localize(dt=end) + next_end_time = _global.TZ.localize(dt=end) # Error checking if end.tzinfo is not None or (start is not None and @@ -293,7 +293,7 @@ async def get_historical_ticks( data_type is dt.HistoricalTicks.TRADES ) else dt.EarliestDataPoint.BID ) - ).astimezone(tz=ib_client.IBClient.TZ) + ).astimezone(tz=_global.TZ) except error.IBError as err: raise err @@ -302,21 +302,21 @@ async def get_historical_ticks( raise ValueError( "Specificed start time is earlier than the earliest " "available datapoint - " - f"{head_timestamp.strftime(_const.TIME_FMT)}" + f"{head_timestamp.strftime(_global.TIME_FMT)}" ) if end.timestamp() < start.timestamp(): raise ValueError( "Specificed end time cannot be earlier than start time" ) - start = ib_client.IBClient.TZ.localize(dt=start) + start = _global.TZ.localize(dt=start) else: start = head_timestamp if next_end_time.timestamp() < head_timestamp.timestamp(): raise ValueError( "Specificed end time is earlier than the earliest available " - f"datapoint - {head_timestamp.strftime(_const.TIME_FMT)}" + f"datapoint - {head_timestamp.strftime(_global.TIME_FMT)}" ) if attempts < 1 and attempts != -1: @@ -349,7 +349,7 @@ async def get_historical_ticks( next_end_time = datetime.datetime.fromtimestamp( res[0][0].time - ).astimezone(ib_client.IBClient.TZ) + ).astimezone(_global.TZ) except ValueError as err: raise err except error.IBError as err: @@ -371,7 +371,7 @@ async def get_historical_ticks( # Updates the end time for next attempt next_end_time = datetime.datetime.fromtimestamp( all_ticks[0].time - ).astimezone(ib_client.IBClient.TZ) + ).astimezone(_global.TZ) continue diff --git a/tests/test_account.py b/tests/test_account.py index 3552ce5..f7e12ed 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -7,7 +7,7 @@ from ibapi import contract as ib_contract from ibpy_native import account from ibpy_native import models -from ibpy_native._internal import _client as ib_client +from ibpy_native._internal import _global from ibpy_native.utils import finishable_queue as fq from tests.toolkit import utils @@ -79,21 +79,21 @@ async def test_sub_account_updates(self): currency="BASE") ) self.assertEqual(datetime.time(hour=10, minute=10, - tzinfo=ib_client.IBClient.TZ), + tzinfo=_global.TZ), self._manager.accounts[_MOCK_AC_140].last_update_time) # Assert portfolio data self.assertEqual( 8, self._manager.accounts[_MOCK_AC_140].positions[0].avg_cost) self.assertEqual( - datetime.time(hour=10, minute=7, tzinfo=ib_client.IBClient.TZ), + datetime.time(hour=10, minute=7, tzinfo=_global.TZ), self._manager.accounts[_MOCK_AC_140].positions[0].last_update_time ) self.assertEqual(20689, (self._manager.accounts[_MOCK_AC_140] .positions[412888950].market_price)) self.assertEqual(datetime.time(hour=10, minute=11, - tzinfo=ib_client.IBClient.TZ), + tzinfo=_global.TZ), (self._manager.accounts[_MOCK_AC_140] .positions[412888950].last_update_time)) diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 8566e34..50555bc 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -10,6 +10,7 @@ import ibpy_native from ibpy_native import error from ibpy_native._internal import _client as ibpy_client +from ibpy_native._internal import _global from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt, finishable_queue as fq @@ -76,12 +77,12 @@ def test_set_timezone(self): """Test function `set_timezone`.""" ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("Asia/Hong_Kong")) - self.assertEqual(ibpy_client.IBClient.TZ, + self.assertEqual(_global.TZ, pytz.timezone("Asia/Hong_Kong")) # Reset timezone to New York ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("America/New_York")) - self.assertEqual(ibpy_client.IBClient.TZ, + self.assertEqual(_global.TZ, pytz.timezone("America/New_York")) def test_set_on_notify_listener(self): From cbd69cff73089c6821608fec53c9367d2fb3846a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 20:22:29 +0000 Subject: [PATCH 058/126] No import as for modules in `_internal` - Cleaned-up the statements `from ibpy_native._internal import ... as ...`. - Removed the `as` statements at the end as they're no longer needed. - Fixed the `TZ` pointing to old path in `test_ib_client`. --- ibpy_native/_internal/_client.py | 4 ++-- ibpy_native/bridge.py | 8 ++++---- tests/internal/test_ib_client.py | 23 ++++++++++++----------- tests/internal/test_ib_wrapper.py | 12 ++++++------ tests/test_ib_bridge.py | 4 ++-- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 4470bb1..8cf7504 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -11,7 +11,7 @@ from ibpy_native import error from ibpy_native._internal import _global -from ibpy_native._internal import _wrapper as ibpy_wrapper +from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -31,7 +31,7 @@ class IBClient(ib_client.EClient): wrapper (:obj:`ibpy_native._internal._wrapper.IBWrapper`): The wrapper object to handle messages return from IB Gateway. """ - def __init__(self, wrapper: ibpy_wrapper.IBWrapper): + def __init__(self, wrapper: _wrapper.IBWrapper): self._wrapper = wrapper super().__init__(wrapper) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index ca3f7d6..509b1da 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,9 +12,9 @@ from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import _client from ibpy_native._internal import _global -from ibpy_native._internal import _client as ib_client -from ibpy_native._internal import _wrapper as ib_wrapper +from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt @@ -52,14 +52,14 @@ def __init__( else accounts_manager ) - self._wrapper = ib_wrapper.IBWrapper( + self._wrapper = _wrapper.IBWrapper( notification_listener=notification_listener ) self._wrapper.set_account_management_delegate( delegate=self._accounts_manager ) - self._client = ib_client.IBClient(wrapper=self._wrapper) + self._client = _client.IBClient(wrapper=self._wrapper) if auto_conn: self.connect() diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index f064dcc..f9de65d 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -13,8 +13,9 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import error -from ibpy_native._internal import _client as ibpy_client -from ibpy_native._internal import _wrapper as ibpy_wrapper +from ibpy_native._internal import _client +from ibpy_native._internal import _global +from ibpy_native._internal import _wrapper from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq @@ -39,10 +40,10 @@ class TestIBClient(unittest.TestCase): @classmethod def setUpClass(cls): - ibpy_client.IBClient.TZ = pytz.timezone("America/New_York") + _global.TZ = pytz.timezone("America/New_York") - cls._wrapper = ibpy_wrapper.IBWrapper() - cls._client = ibpy_client.IBClient(cls._wrapper) + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) cls._client.connect( os.getenv("IB_HOST", "127.0.0.1"), @@ -100,9 +101,9 @@ async def test_fetch_historical_ticks(self): data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client.IBClient.TZ.localize( + start=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client.IBClient.TZ.localize( + end=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.MIDPOINT ) @@ -115,9 +116,9 @@ async def test_fetch_historical_ticks(self): data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, contract=sample_contracts.gbp_usd_fx(), - start=ibpy_client.IBClient.TZ.localize( + start=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=ibpy_client.IBClient.TZ.localize( + end=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 35, 0)), show=dt.HistoricalTicks.BID_ASK ) @@ -147,8 +148,8 @@ async def test_fetch_historical_ticks_err(self): req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, contract=ib_contract.Contract(), start=datetime.datetime(2020, 5, 20, 3, 20, 0).astimezone( - ibpy_client.IBClient.TZ), - end=datetime.datetime.now().astimezone(ibpy_client.IBClient.TZ) + _global.TZ), + end=datetime.datetime.now().astimezone(_global.TZ) ) #endregion - Historical ticks diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index cb8e77a..54ba2c5 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -8,8 +8,8 @@ from ibapi import wrapper as ib_wrapper from ibpy_native import models -from ibpy_native._internal import _client as ibpy_client -from ibpy_native._internal import _wrapper as ibpy_wrapper +from ibpy_native._internal import _client +from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -32,8 +32,8 @@ class TestIBWrapper(unittest.TestCase): @classmethod def setUpClass(cls): - cls._wrapper = ibpy_wrapper.IBWrapper() - cls._client = ibpy_client.IBClient(cls._wrapper) + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) cls._client.connect( os.getenv("IB_HOST", "127.0.0.1"), @@ -278,8 +278,8 @@ class TestAccountAndPortfolioData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls.wrapper = ibpy_wrapper.IBWrapper() - cls.client = ibpy_client.IBClient(cls.wrapper) + cls.wrapper = _wrapper.IBWrapper() + cls.client = _client.IBClient(cls.wrapper) cls.client.connect( os.getenv("IB_HOST", "127.0.0.1"), diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 50555bc..74fc60b 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -9,7 +9,7 @@ import ibpy_native from ibpy_native import error -from ibpy_native._internal import _client as ibpy_client +from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt, finishable_queue as fq @@ -211,7 +211,7 @@ async def test_get_historical_ticks_err(self): @utils.async_test async def test_stream_live_ticks(self): """Test function `stream_live_ticks`.""" - client: ibpy_client.IBClient = self._bridge._client + client: _client.IBClient = self._bridge._client listener = utils.MockLiveTicksListener() req_id = await self._bridge.stream_live_ticks( From 700e74d6041ce2500a3830a8ad83519e361a8418 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 8 Feb 2021 21:03:11 +0000 Subject: [PATCH 059/126] Target future contracts dynamically --- tests/toolkit/sample_contracts.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/toolkit/sample_contracts.py b/tests/toolkit/sample_contracts.py index 3404d84..2483a75 100644 --- a/tests/toolkit/sample_contracts.py +++ b/tests/toolkit/sample_contracts.py @@ -1,4 +1,6 @@ """Predefined contracts for unittest.""" +import datetime + from ibapi import contract as ib_contract def gbp_usd_fx() -> ib_contract.Contract: @@ -23,23 +25,32 @@ def us_stock() -> ib_contract.Contract: def us_future() -> ib_contract.Contract: """US future - YM""" + # Generate contract month dynamically + now = datetime.datetime.now() + month = now.month + for i in [3, 6, 9, 12]: + if month < i: + month = i + break + contract = ib_contract.Contract() contract.symbol = "YM" contract.secType = "FUT" contract.exchange = "ECBOT" contract.currency = "USD" - contract.lastTradeDateOrContractMonth = "202103" + contract.lastTradeDateOrContractMonth = f"{now.year}{month:02d}" return contract def us_future_expired() -> ib_contract.Contract: - """Expired US future - YM 2020.06""" + """Expired US future""" contract = ib_contract.Contract() contract.symbol = "YM" contract.secType = "FUT" contract.exchange = "ECBOT" contract.currency = "USD" - contract.lastTradeDateOrContractMonth = "202006" + # Targets the latest contract of last year + contract.lastTradeDateOrContractMonth = f"{datetime.datetime.now().year}12" contract.includeExpired = True return contract From 052bb1da0fd3bc34cb967307f564f51415ff4764 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 9 Feb 2021 12:23:29 +0000 Subject: [PATCH 060/126] Fix historical ticks test - Fetch stock data instead of FX data. Not sure why there's no data for FX returned. --- tests/internal/test_ib_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index f9de65d..c988ca8 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -100,7 +100,7 @@ async def test_fetch_historical_ticks(self): """Test function `fetch_historical_ticks`.""" data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.gbp_usd_fx(), + contract=sample_contracts.us_stock(), start=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), end=_global.TZ.localize( @@ -115,7 +115,7 @@ async def test_fetch_historical_ticks(self): data = await self._client.fetch_historical_ticks( req_id=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.gbp_usd_fx(), + contract=sample_contracts.us_stock(), start=_global.TZ.localize( datetime.datetime(2020, 4, 29, 10, 30, 0)), end=_global.TZ.localize( From 401df0034d0baa56e8e97c019a0b0d113b7f6683 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 9 Feb 2021 17:10:20 +0000 Subject: [PATCH 061/126] Organise const for tests --- tests/internal/test_ib_client.py | 7 +------ tests/internal/test_ib_wrapper.py | 17 ++++------------- tests/test_ib_bridge.py | 27 +++++++++++---------------- tests/toolkit/utils.py | 11 +++++++++++ 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index c988ca8..365efb1 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access import asyncio import datetime -import os import threading import unittest from typing import List @@ -45,11 +44,7 @@ def setUpClass(cls): cls._wrapper = _wrapper.IBWrapper() cls._client = _client.IBClient(cls._wrapper) - cls._client.connect( - os.getenv("IB_HOST", "127.0.0.1"), - int(os.getenv("IB_PORT", "4002")), - 1001 - ) + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) thread = threading.Thread(target=cls._client.run) thread.start() diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 54ba2c5..5e84c93 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -1,7 +1,6 @@ """Unit tests for module `ibpy_native.wrapper`.""" # pylint: disable=protected-access import asyncio -import os import threading import unittest @@ -35,11 +34,7 @@ def setUpClass(cls): cls._wrapper = _wrapper.IBWrapper() cls._client = _client.IBClient(cls._wrapper) - cls._client.connect( - os.getenv("IB_HOST", "127.0.0.1"), - int(os.getenv("IB_PORT", "4002")), - 1001 - ) + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) thread = threading.Thread(target=cls._client.run) thread.start() @@ -281,11 +276,7 @@ def setUpClass(cls): cls.wrapper = _wrapper.IBWrapper() cls.client = _client.IBClient(cls.wrapper) - cls.client.connect( - os.getenv("IB_HOST", "127.0.0.1"), - int(os.getenv("IB_PORT", "4002")), - 1001 - ) + cls.client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) thread = threading.Thread(target=cls.client.run) thread.start() @@ -356,12 +347,12 @@ def tearDownClass(cls): #region - Private functions async def _start_account_updates(self): self.client.reqAccountUpdates(subscribe=True, - acctCode=os.getenv("IB_ACC_ID", "")) + acctCode=utils.IB_ACC_ID) await asyncio.sleep(1) async def _cancel_account_updates(self): self.client.reqAccountUpdates(subscribe=False, - acctCode=os.getenv("IB_ACC_ID", "")) + acctCode=utils.IB_ACC_ID) await asyncio.sleep(0.5) self.mock_delegate.account_updates_queue.put(fq.Status.FINISHED) #endregion - Private functions diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py index 74fc60b..c231d5f 100644 --- a/tests/test_ib_bridge.py +++ b/tests/test_ib_bridge.py @@ -17,20 +17,13 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils -#region - Constants -TEST_HOST = os.getenv("IB_HOST", "127.0.0.1") -TEST_PORT = int(os.getenv("IB_PORT", "4002")) -TEST_ID = 1001 -#endregion - Constants - class TestIBBridgeConn(unittest.TestCase): """Test cases for connection related functions in `IBBridge`.""" def test_init_auto_connect(self): """Test initialise `IBBridge` with `auto_conn=True`.""" - bridge = ibpy_native.IBBridge( - host=TEST_HOST, port=TEST_PORT, client_id=TEST_ID - ) + bridge = ibpy_native.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) self.assertTrue(bridge.is_connected()) @@ -38,9 +31,10 @@ def test_init_auto_connect(self): def test_init_manual_connect(self): """Test initialise `IBBridge` with `auto_conn=False`.""" - bridge = ibpy_native.IBBridge( - host=TEST_HOST, port=TEST_PORT, client_id=TEST_ID, auto_conn=False - ) + bridge = ibpy_native.IBBridge(host=utils.IB_HOST, + port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) bridge.connect() self.assertTrue(bridge.is_connected()) @@ -49,9 +43,10 @@ def test_init_manual_connect(self): def test_disconnect_without_connection(self): """Test function `disconnect` without an established connection.""" - bridge = ibpy_native.IBBridge( - host=TEST_HOST, port=TEST_PORT, client_id=TEST_ID, auto_conn=False - ) + bridge = ibpy_native.IBBridge(host=utils.IB_HOST, + port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) bridge.disconnect() self.assertFalse(bridge.is_connected()) @@ -62,7 +57,7 @@ class TestIBBridge(unittest.TestCase): @classmethod def setUpClass(cls): cls._bridge = ibpy_native.IBBridge( - host=TEST_HOST, port=TEST_PORT, client_id=TEST_ID + host=utils.IB_HOST, port=utils.IB_PORT, client_id=utils.IB_CLIENT_ID ) @utils.async_test diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 3eb09e9..c0da64a 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -1,6 +1,7 @@ """Utilities for making unittests easier to write.""" # pylint: disable=protected-access import asyncio +import os import queue from typing import Dict, List, Union @@ -12,6 +13,7 @@ from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq +#region - General utils def async_test(fn): # pylint: disable=invalid-name """Decorator for testing the async functions.""" @@ -21,6 +23,14 @@ def wrapper(*args, **kwargs): return loop.run_until_complete(fn(*args, **kwargs)) return wrapper +#endregion - General utils + +#region - ibpy_native specific +# Constants +IB_HOST: str = os.getenv("IB_HOST", "127.0.0.1") +IB_PORT: int = int(os.getenv("IB_PORT", "4002")) +IB_CLIENT_ID: int = int(os.getenv("IB_CLIENT_ID", "1001")) +IB_ACC_ID: str = os.getenv("IB_ACC_ID", "") class MockAccountManagementDelegate(delegates.AccountsManagementDelegate): """Mock accounts delegate""" @@ -68,3 +78,4 @@ def on_finish(self, req_id: int): def on_err(self, err: error.IBError): raise err +#endregion - ibpy_native specific From 2fdf326be31db7f0810cd95d65281dd70f657be5 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 9 Feb 2021 17:44:02 +0000 Subject: [PATCH 062/126] Remove `setattr` - Removed useless `setattr` statements for thread created on connected. --- ibpy_native/bridge.py | 2 -- tests/internal/test_ib_client.py | 2 -- tests/internal/test_ib_wrapper.py | 4 ---- 3 files changed, 8 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 509b1da..8164de2 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -116,8 +116,6 @@ def connect(self): thread = threading.Thread(target=self._client.run) thread.start() - setattr(self._client, "_thread", thread) - def disconnect(self): """Disconnect the bridge from the connected TWS/IB Gateway instance. """ diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py index 365efb1..7583e20 100644 --- a/tests/internal/test_ib_client.py +++ b/tests/internal/test_ib_client.py @@ -49,8 +49,6 @@ def setUpClass(cls): thread = threading.Thread(target=cls._client.run) thread.start() - setattr(cls._client, "_thread", thread) - #region - Contract @utils.async_test async def test_resolve_contract(self): diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py index 5e84c93..2a46a42 100644 --- a/tests/internal/test_ib_wrapper.py +++ b/tests/internal/test_ib_wrapper.py @@ -39,8 +39,6 @@ def setUpClass(cls): thread = threading.Thread(target=cls._client.run) thread.start() - setattr(cls._client, "_thread", thread) - #region - IBWrapper specifics @utils.async_test async def test_next_req_id(self): @@ -281,8 +279,6 @@ def setUpClass(cls): thread = threading.Thread(target=cls.client.run) thread.start() - setattr(cls.client, "_thread", thread) - def setUp(self): self.mock_delegate = utils.MockAccountManagementDelegate() self.wrapper.set_account_management_delegate(self.mock_delegate) From e24f063ba311250ce85f4760aa418de210e8a741 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 01:06:30 +0000 Subject: [PATCH 063/126] Safe guard func `error` - Check the queue with key `reqId` exists before access. --- ibpy_native/_internal/_wrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 5a72e4c..f0ca219 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -130,7 +130,8 @@ def error(self, reqId, errorCode, errorString): # -1 indicates a notification and not true error condition if reqId is not -1: - self._req_queue[reqId].put(element=err) + if reqId in self._req_queue: + self._req_queue[reqId].put(element=err) else: if self._notification_listener is not None: self._notification_listener.on_notify( From aac99c82881ba4870dcf88f20ea8e0d9742110b4 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 01:26:47 +0000 Subject: [PATCH 064/126] Rename to match name - Renamed to match the name of `ibpy_native.interfaces.delegate._AccountsManagementDelegate`. --- ibpy_native/_internal/_wrapper.py | 9 +++++---- ibpy_native/bridge.py | 2 +- tests/toolkit/utils.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index f0ca219..3c642c4 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -102,14 +102,15 @@ def get_request_queue_no_throw(self, req_id: int) -> Optional[ #endregion - Getters #region - Setters - def set_account_management_delegate( + def set_accounts_management_delegate( self, delegate: delegates.AccountsManagementDelegate ): - """Setter for optional `_AccountListDelegate`. + """Setter for optional `_AccountsManagementDelegate`. Args: - delegate (ibpy_native.interfaces.delegates._AccountListDelegate): - Delegate for managing IB account list. + delegate (ibpy_native.interfaces.delegates + ._AccountsManagementDelegate): Delegate for managing IB + account list. """ self._ac_man_delegate = delegate diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 8164de2..f846956 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -55,7 +55,7 @@ def __init__( self._wrapper = _wrapper.IBWrapper( notification_listener=notification_listener ) - self._wrapper.set_account_management_delegate( + self._wrapper.set_accounts_management_delegate( delegate=self._accounts_manager ) diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index c0da64a..2c75f87 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -32,7 +32,7 @@ def wrapper(*args, **kwargs): IB_CLIENT_ID: int = int(os.getenv("IB_CLIENT_ID", "1001")) IB_ACC_ID: str = os.getenv("IB_ACC_ID", "") -class MockAccountManagementDelegate(delegates.AccountsManagementDelegate): +class MockAccountsManagementDelegate(delegates.AccountsManagementDelegate): """Mock accounts delegate""" def __init__(self): self._account_list: Dict[str, models.Account] = [] From c002c0a283b230ce3d5f79850e8602bdba73c708 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 01:51:11 +0000 Subject: [PATCH 065/126] Fix `MockAccountsManagementDelegate` --- tests/toolkit/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 2c75f87..8fe6377 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -35,7 +35,7 @@ def wrapper(*args, **kwargs): class MockAccountsManagementDelegate(delegates.AccountsManagementDelegate): """Mock accounts delegate""" def __init__(self): - self._account_list: Dict[str, models.Account] = [] + self._account_list: Dict[str, models.Account] = {} self._account_updates_queue: fq.FinishableQueue = fq.FinishableQueue( queue_to_finish=queue.Queue() ) @@ -50,7 +50,7 @@ def account_updates_queue(self) -> fq.FinishableQueue: def on_account_list_update(self, account_list: List[str]): for account_id in account_list: - self._account_list.append(models.Account(account_id)) + self._account_list[account_id] = models.Account(account_id) async def sub_account_updates(self, account: models.Account): pass From 80438d74fc5bae793f08a9b130560430e7ecfa99 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 10:46:22 +0000 Subject: [PATCH 066/126] Mod queue finish condition - Only `Status.FINISHED` is considered as finish now. - Removed `Status.TIMEOUT` as it has no use. --- ibpy_native/utils/finishable_queue.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index 1e20956..b8540fe 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -13,7 +13,6 @@ class Status(enum.Enum): READY = 103 ERROR = 500 FINISHED = 200 - TIMEOUT = 408 class FinishableQueue(): """Thread-safe class that takes a built-in `queue.Queue` object to handle @@ -45,9 +44,7 @@ def finished(self) -> bool: Returns: bool: True is task last associated is finished, False otherwise. """ - return (self._status is Status.TIMEOUT - or self._status is Status.FINISHED - or self._status is Status.ERROR) + return self._status is Status.FINISHED def reset(self): """Reset the status to `STARTED` for reusing the queue if the From 973dd2158b26c0d1e8c2fab840762587808685c8 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 14:07:22 +0000 Subject: [PATCH 067/126] Rewrite unittest for module `_wrapper` --- tests/internal/test_ib_wrapper.py | 354 ---------------------- tests/internal/test_wrapper.py | 472 ++++++++++++++++++++++++++++++ tests/toolkit/utils.py | 11 + 3 files changed, 483 insertions(+), 354 deletions(-) delete mode 100644 tests/internal/test_ib_wrapper.py create mode 100644 tests/internal/test_wrapper.py diff --git a/tests/internal/test_ib_wrapper.py b/tests/internal/test_ib_wrapper.py deleted file mode 100644 index 2a46a42..0000000 --- a/tests/internal/test_ib_wrapper.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Unit tests for module `ibpy_native.wrapper`.""" -# pylint: disable=protected-access -import asyncio -import threading -import unittest - -from ibapi import wrapper as ib_wrapper - -from ibpy_native import models -from ibpy_native._internal import _client -from ibpy_native._internal import _wrapper -from ibpy_native.interfaces import listeners -from ibpy_native.utils import finishable_queue as fq - -from tests.toolkit import sample_contracts -from tests.toolkit import utils - -#region - Constants -# Predefined constants for `TestIBWrapper`. -_RID_RESOLVE_CONTRACT = 43 -_RID_FETCH_HISTORICAL_TICKS = 18001 -_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST = 19001 -_RID_REQ_TICK_BY_TICK_DATA_LAST = 19002 -_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT = 19003 -_RID_REQ_TICK_BY_TICK_DATA_BIDASK = 19004 -_QUEUE_MAX_WAIT_SEC = 10 -#endregion - Constants - -class TestIBWrapper(unittest.TestCase): - """Unit tests for class `IBWrapper`.""" - - @classmethod - def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() - cls._client = _client.IBClient(cls._wrapper) - - cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) - - thread = threading.Thread(target=cls._client.run) - thread.start() - - #region - IBWrapper specifics - @utils.async_test - async def test_next_req_id(self): - """Test retrieval of next usable request ID.""" - # Prepare the `FinishableQueue` objects in internal `__req_queue` - self._wrapper._req_queue.clear() - _ = self._wrapper.get_request_queue(req_id=0) - f_queue = self._wrapper.get_request_queue(req_id=1) - _ = self._wrapper.get_request_queue(req_id=10) - - self.assertEqual(self._wrapper.next_req_id, 11) - - f_queue.put(element=fq.Status.FINISHED) - await f_queue.get() - self.assertEqual(self._wrapper.next_req_id, 1) - - def test_notification_listener(self): - """Test notification listener approach.""" - class MockListener(listeners.NotificationListener): - """Mock notification listener.""" - triggered = False - - def on_notify(self, msg_code: int, msg: str): - """Mock callback implementation - """ - print(f"{msg_code} - {msg}") - - self.triggered = True - - mock_listener = MockListener() - - self._wrapper.set_on_notify_listener(listener=mock_listener) - self._wrapper.error(reqId=-1, errorCode=1100, errorString="MOCK MSG") - - self.assertTrue(mock_listener.triggered) - #endregion - IBWrapper specifics - - #region - Historical ticks - @utils.async_test - async def test_historical_ticks(self): - """Test overridden function `historicalTicks`.""" - end_time = "20200327 16:30:00" - - f_queue = self._wrapper.get_request_queue( - req_id=_RID_FETCH_HISTORICAL_TICKS - ) - - self._client.reqHistoricalTicks( - reqId=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.gbp_usd_fx(), - startDateTime="", endDateTime=end_time, - numberOfTicks=1000, whatToShow="MIDPOINT", useRth=1, - ignoreSize=False, miscOptions=[] - ) - - result = await f_queue.get() - - self.assertEqual(f_queue.status, fq.Status.FINISHED) - self.assertEqual(len(result), 2) - self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTick) - - @utils.async_test - async def test_historical_ticks_bid_ask(self): - """Test overridden function `historicalTicksBidAsk`.""" - end_time = "20200327 16:30:00" - - f_queue = self._wrapper.get_request_queue( - req_id=_RID_FETCH_HISTORICAL_TICKS - ) - - self._client.reqHistoricalTicks( - reqId=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.gbp_usd_fx(), - startDateTime="", endDateTime=end_time, - numberOfTicks=1000, whatToShow="BID_ASK", useRth=1, - ignoreSize=False, miscOptions=[] - ) - - result = await f_queue.get() - - self.assertEqual(f_queue.status, fq.Status.FINISHED) - self.assertEqual(len(result), 2) - self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTickBidAsk) - - @utils.async_test - async def test_historical_ticks_last(self): - """Test overridden function `historicalTicksLast`.""" - end_time = "20200327 16:30:00" - - f_queue = self._wrapper.get_request_queue( - req_id=_RID_FETCH_HISTORICAL_TICKS - ) - - self._client.reqHistoricalTicks( - reqId=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.gbp_usd_fx(), - startDateTime="", endDateTime=end_time, - numberOfTicks=1000, whatToShow="TRADES", useRth=1, - ignoreSize=False, miscOptions=[] - ) - - result = await f_queue.get() - - self.assertEqual(f_queue.status, fq.Status.FINISHED) - self.assertEqual(len(result), 2) - self.assertIsInstance(result[0], ib_wrapper.ListOfHistoricalTickLast) - #endregion - Historical ticks - - #region - Tick by tick data (Live ticks) - @utils.async_test - async def test_tick_by_tick_all_last(self): - """Test overridden function `tickByTickAllLast`.""" - f_queue = self._wrapper.get_request_queue( - req_id=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST - ) - - self._client.reqTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST, - contract=sample_contracts.us_future(), - tickType="AllLast", - numberOfTicks=0, - ignoreSize=True - ) - - async for ele in f_queue.stream(): - self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickLast, fq.Status)) - self.assertIsNot(ele, fq.Status.ERROR) - - if ele is not fq.Status.FINISHED: - self._client.cancelTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_ALL_LAST - ) - - f_queue.put(element=fq.Status.FINISHED) - - @utils.async_test - async def test_tick_by_tick_last(self): - """Test overridden function `tickByTickAllLast` with tick type `Last`. - """ - f_queue = self._wrapper.get_request_queue( - req_id=_RID_REQ_TICK_BY_TICK_DATA_LAST - ) - - self._client.reqTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_LAST, - contract=sample_contracts.us_future(), - tickType="Last", - numberOfTicks=0, - ignoreSize=True - ) - - async for ele in f_queue.stream(): - self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickLast, fq.Status)) - self.assertIsNot(ele, fq.Status.ERROR) - - if ele is not fq.Status.FINISHED: - self._client.cancelTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_LAST - ) - - f_queue.put(element=fq.Status.FINISHED) - - - @utils.async_test - async def test_tick_by_tick_bid_ask(self): - """Test overridden function `tickByTickBidAsk`.""" - f_queue = self._wrapper.get_request_queue( - req_id=_RID_REQ_TICK_BY_TICK_DATA_BIDASK - ) - - self._client.reqTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_BIDASK, - contract=sample_contracts.gbp_usd_fx(), - tickType="BidAsk", - numberOfTicks=0, - ignoreSize=True - ) - - async for ele in f_queue.stream(): - self.assertIsInstance(ele, - (ib_wrapper.HistoricalTickBidAsk, fq.Status)) - self.assertIsNot(ele, fq.Status.ERROR) - - if ele is not fq.Status.FINISHED: - self._client.cancelTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_BIDASK - ) - - f_queue.put(element=fq.Status.FINISHED) - - @utils.async_test - async def test_tick_by_tick_mid_point(self): - """Test overridden function `tickByTickMidPoint`.""" - f_queue = self._wrapper.get_request_queue( - req_id=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT - ) - - self._client.reqTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT, - contract=sample_contracts.gbp_usd_fx(), - tickType="MidPoint", - numberOfTicks=0, - ignoreSize=True - ) - - async for ele in f_queue.stream(): - self.assertIsInstance(ele, - (ib_wrapper.HistoricalTick, fq.Status)) - self.assertIsNot(ele, fq.Status.ERROR) - - if ele is not fq.Status.FINISHED: - self._client.cancelTickByTickData( - reqId=_RID_REQ_TICK_BY_TICK_DATA_MIDPOINT - ) - - f_queue.put(element=fq.Status.FINISHED) - #endregion - Tick by tick data (Live ticks) - - @classmethod - def tearDownClass(cls): - cls._client.disconnect() - -class TestAccountAndPortfolioData(unittest.TestCase): - """Unit tests for account and portfolio data related callbacks & functions - in class `ibpy_native._internal._wrapper.IBWrapper`. - - Connection with IB Gateway is required for this test suit. - """ - @classmethod - def setUpClass(cls): - cls.wrapper = _wrapper.IBWrapper() - cls.client = _client.IBClient(cls.wrapper) - - cls.client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) - - thread = threading.Thread(target=cls.client.run) - thread.start() - - def setUp(self): - self.mock_delegate = utils.MockAccountManagementDelegate() - self.wrapper.set_account_management_delegate(self.mock_delegate) - - def test_account_management_delegate(self): - """Test `_AccountManagementDelegate` implementation.""" - self.mock_delegate.on_account_list_update( - account_list=["DU0000140", "DU0000141"] - ) - - self.assertTrue(self.mock_delegate.accounts) - - @utils.async_test - async def test_managed_accounts(self): - """Test overridden function `managedAccounts`.""" - self.client.reqManagedAccts() - - await asyncio.sleep(1) - - self.assertTrue(self.mock_delegate.accounts) - - #region - account updates - @utils.async_test - async def test_update_account_value(self): - """Test overridden function `updateAccountValue`.""" - await self._start_account_updates() - await self._cancel_account_updates() - - result: list = await self.mock_delegate.account_updates_queue.get() - self.assertTrue( - any(isinstance(data, models.RawAccountValueData) for data in result) - ) - - @utils.async_test - async def test_update_portfolio(self): - """Test overridden function `updatePortfolio`.""" - await self._start_account_updates() - await self._cancel_account_updates() - - result: list = await self.mock_delegate.account_updates_queue.get() - self.assertTrue( - any(isinstance(data, models.RawPortfolioData) for data in result) - ) - - @utils.async_test - async def test_update_account_time(self): - """Test overridden function `updateAccountTime`.""" - await self._start_account_updates() - await self._cancel_account_updates() - - result: list = await self.mock_delegate.account_updates_queue.get() - self.assertRegex( - next(data for data in result if isinstance(data, str)), - r"\d{2}:\d{2}" - ) - #endregion - account updates - - @classmethod - def tearDownClass(cls): - cls.client.disconnect() - - #region - Private functions - async def _start_account_updates(self): - self.client.reqAccountUpdates(subscribe=True, - acctCode=utils.IB_ACC_ID) - await asyncio.sleep(1) - - async def _cancel_account_updates(self): - self.client.reqAccountUpdates(subscribe=False, - acctCode=utils.IB_ACC_ID) - await asyncio.sleep(0.5) - self.mock_delegate.account_updates_queue.put(fq.Status.FINISHED) - #endregion - Private functions diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py new file mode 100644 index 0000000..bd02153 --- /dev/null +++ b/tests/internal/test_wrapper.py @@ -0,0 +1,472 @@ +"""Unit tests for module `ibpy_native._internal._wrapper`.""" +# pylint: disable=protected-access +import asyncio +import datetime +import threading +import unittest + +from ibapi import contract +from ibapi import wrapper + +from ibpy_native import error +from ibpy_native import models +from ibpy_native._internal import _client +from ibpy_native._internal import _global +from ibpy_native._internal import _wrapper +from ibpy_native.utils import datatype +from ibpy_native.utils import finishable_queue as fq + +from tests.toolkit import sample_contracts +from tests.toolkit import utils + +class TestGeneral(unittest.TestCase): + """Unit tests for general/uncategorised things in `IBWrapper`. + + Connection with IB is NOT required. + """ + def setUp(self): + self._wrapper = _wrapper.IBWrapper() + + def test_set_on_notify_listener(self): + """Test setter `set_on_notify_listener` & overridden function `error` + for the delivery of IB system notification. + + * Expect to receive IB's system notification (`reqId` -1) once the + notification listener is set. + """ + code = 1001 + msg = "MOCK MSG" + listener = utils.MockNotificationListener() + + self._wrapper.set_on_notify_listener(listener) + # Mock IB system notification + self._wrapper.error(reqId=-1, errorCode=code, errorString=msg) + # Notification should have received by the notification listener + self.assertEqual(listener.msg_code, code) + self.assertEqual(listener.msg, msg) + + @utils.async_test + async def test_error(self): + """Test overridden function `error`.""" + req_id = 1 + code = 404 + msg = "ERROR" + # Prepare request queue + queue = self._wrapper.get_request_queue(req_id) + # Mock IB error + self._wrapper.error(reqId=req_id, errorCode=code, errorString=msg) + queue.put(element=fq.Status.FINISHED) + result = await queue.get() + # Expect exception of `IBError` to be sent to corresponding request + # queue. + self.assertIsInstance(result[0], error.IBError) + self.assertEqual(result[0].err_code, code) + self.assertEqual(result[0].err_str, msg) + +class TestReqQueue(unittest.TestCase): + """Unit tests for `_req_queue` related mechanicisms in `IBWrapper`. + + Connection with IB is NOT required. + """ + def setUp(self): + self._wrapper = _wrapper.IBWrapper() + + def test_next_req_id_0(self): + """Test property `next_req_id` for retrieval of next usable + request ID. + + * No ID has been occupied yet. + """ + # 1st available request ID should always be 1 + self.assertEqual(self._wrapper.next_req_id, 1) + + def test_next_req_id_1(self): + """Test property `next_req_id` for retrieval of next usable + request ID. + + * Request ID 1 has already been occupied. + """ + self._wrapper.get_request_queue(req_id=1) # Occupy request ID 1 + # Next available request ID should be 2 + self.assertEqual(self._wrapper.next_req_id, 2) + + @utils.async_test + async def test_next_req_id_2(self): + """Test property `next_req_id` for retrieval of next usable + request ID. + + * Request ID 1 was occupied but released for reuse. + """ + # Occupy request ID 1 + queue = self._wrapper.get_request_queue(req_id=1) + # Release request ID 1 by marking the queue associated as FINISHED + queue.put(element=fq.Status.FINISHED) + await queue.get() + # Next available request ID should reuse 1 + self.assertEqual(self._wrapper.next_req_id, 1) + + def test_get_request_queue_0(self): + """Test getter `get_request_queue`.""" + try: + self._wrapper.get_request_queue(req_id=1) + except error.IBError: + self.fail("IBError raised unexpectedly.") + + @utils.async_test + async def test_get_request_queue_1(self): + """Test getter `get_request_queue`. + + * Queue associated with ID 1 has already been initialised and available + for reuse. Should return the same `FinishableQueue` instance. + """ + # Prepare queue with request ID 1 + queue = self._wrapper.get_request_queue(req_id=1) + queue.put(element=fq.Status.FINISHED) + await queue.get() + # Should return the same `FinishableQueue` instance for reuse + self.assertEqual(self._wrapper.get_request_queue(req_id=1), queue) + + def test_get_request_queue_err(self): + """Test getter `get_request_queue` for error case. + + * Queue associated with ID 1 has already been initialised and NOT + ready for reuse. Should raise exception `IBError`. + """ + # Prepare queue with request ID 1 + self._wrapper.get_request_queue(req_id=1) + # Expect exception `IBError` + with self.assertRaises(error.IBError): + self._wrapper.get_request_queue(req_id=1) + + def test_get_request_queue_no_throw_0(self): + """Test getter `get_request_queue_no_throw`. + + * No queue has been initialised before. Should return `None`. + """ + self.assertEqual(self._wrapper.get_request_queue_no_throw(req_id=1), + None) + + def test_get_request_queue_no_throw_1(self): + """Test getter `get_request_queue_no_throw`. + + * Queue associated with ID 1 has already been initialised. Should + return the same `FinishableQueue` instance even if it's not ready for + reuse yet. + """ + # Prepare queue with request ID 1 + queue = self._wrapper.get_request_queue(req_id=1) + # Expect the same `FinishableQueue` instance. + self.assertEqual(self._wrapper.get_request_queue_no_throw(req_id=1), + queue) + +class TestAccountAndPortfolio(unittest.TestCase): + """Unit tests for account and portfolio data related functions in + `IBWrapper`. + + Connection with IB is NOT required. + """ + def setUp(self): + self._wrapper = _wrapper.IBWrapper() + + self._delegate = utils.MockAccountsManagementDelegate() + self._wrapper.set_accounts_management_delegate(delegate=self._delegate) + + def test_managed_accounts(self): + """Test overridden function `managedAccounts`.""" + # Mock accounts list received from IB + acc_1 = "DU0000140" + acc_2 = "DU0000141" + # IB accounts list format "DU0000140,DU0000141,..." + self._wrapper.managedAccounts(accountsList=f"{acc_1},{acc_2}") + # Expect instances of model `Accounts` for `acc_1` & `acc_2` + # to be stored in the `AccountsManagementDelegate` instance. + self.assertTrue(self._delegate.accounts) + self.assertTrue(acc_1 in self._delegate.accounts) + self.assertTrue(acc_2 in self._delegate.accounts) + + @utils.async_test + async def test_update_account_value(self): + """Test overridden function `updateAccountValue`.""" + # Mock account value data received from IB + self._wrapper.updateAccountValue( + key="AvailableFunds", val="890622.47", + currency="USD", accountName="DU0000140" + ) + self._delegate.account_updates_queue.put(element=fq.Status.FINISHED) + result = await self._delegate.account_updates_queue.get() + # Expect instance of `RawAccountValueData` in `account_updates_queue` + self.assertIsInstance(result[0], models.RawAccountValueData) + + @utils.async_test + async def test_update_portfolio(self): + """Test overridden function `updatePortfolio`.""" + # Mock portfolio data received from IB + self._wrapper.updatePortfolio( + contract=sample_contracts.gbp_usd_fx(), position=1000, + marketPrice=1.38220, marketValue=1382.2, averageCost=1.33327, + unrealizedPNL=48.93, realizedPNL=0, accountName="DU0000140" + ) + self._delegate.account_updates_queue.put(element=fq.Status.FINISHED) + results = await self._delegate.account_updates_queue.get() + # Expect instance of `RawPortfolioData` in `account_updates_queue` + self.assertIsInstance(results[0], models.RawPortfolioData) + + @utils.async_test + async def test_update_account_time(self): + """Test overridden function `updateAccountTime`.""" + # Mock last update system time received from IB + time = "09:30" + self._wrapper.updateAccountTime(timeStamp=time) + self._delegate.account_updates_queue.put(element=fq.Status.FINISHED) + results = await self._delegate.account_updates_queue.get() + # Expect data stored as-is in `account_updates_queue` + self.assertEqual(results[0], time) + +class TestContract(unittest.TestCase): + """Unit tests for IB contract related functions in `IBWrapper`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + @utils.async_test + async def test_contract_details(self): + """Test overridden function `contractDetails`. + + * `contractDetailsEnd` will be invoked after `contractDetails`. + """ + req_id = self._wrapper.next_req_id + queue = self._wrapper.get_request_queue(req_id) + + self._client.reqContractDetails(reqId=req_id, + contract=sample_contracts.gbp_usd_fx()) + await asyncio.sleep(0.5) + result = await queue.get() + + self.assertTrue(result) # Expect item from queue + # Expect the resolved `ContractDetails` object to be returned + self.assertIsInstance(result[0], contract.ContractDetails) + + @utils.async_test + async def test_contract_details_end(self): + """Test overridden function `contractDetailsEnd`.""" + req_id = self._wrapper.next_req_id + queue = self._wrapper.get_request_queue(req_id) + + self._wrapper.contractDetailsEnd(reqId=req_id) + await queue.get() + + self.assertTrue(queue.finished) # Expect the queue to be marked as FINISHED + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + +class TestHistoricalData(unittest.TestCase): + """Unit tests for historical market data related functions in `IBWrapper`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def setUp(self): + self._req_id = self._wrapper.next_req_id + self._queue = self._wrapper.get_request_queue(req_id=self._req_id) + + @utils.async_test + async def test_head_timestamp(self): + """Test overridden function `headTimestamp`.""" + timestamp = "1110342600" # Unix timestamp + # Mock timestamp received from IB + self._wrapper.headTimestamp(reqId=self._req_id, headTimestamp=timestamp) + result = await self._queue.get() + + self.assertTrue(result) # Expect item from queue + self.assertEqual(result[0], timestamp) # Expect data received as-is + # Expect queue to be marked as FINISHED + self.assertTrue(self._queue.finished) + + @utils.async_test + async def test_historical_ticks(self): + """Test overridden function `historicalTicks`.""" + end = (datetime.datetime.now().astimezone(_global.TZ) + .strftime(_global.TIME_FMT)) + + self._client.reqHistoricalTicks( + reqId=self._req_id, contract=sample_contracts.gbp_usd_fx(), + startDateTime="", endDateTime=end, numberOfTicks=1000, + whatToShow=datatype.HistoricalTicks.MIDPOINT.value, useRth=1, + ignoreSize=False, miscOptions=[] + ) + result = await self._queue.get() + + self.assertTrue(result) # Expect item from queue + # Expect `ListOfHistoricalTick` to be sent to the queue + self.assertIsInstance(result[0], wrapper.ListOfHistoricalTick) + # Expect queue to be marked as FINISHED + self.assertTrue(self._queue.finished) + + @utils.async_test + async def test_historical_ticks_bid_ask(self): + """Test overridden function `historicalTicksBidAsk`.""" + end = (datetime.datetime.now().astimezone(_global.TZ) + .strftime(_global.TIME_FMT)) + + self._client.reqHistoricalTicks( + reqId=self._req_id, contract=sample_contracts.gbp_usd_fx(), + startDateTime="", endDateTime=end, numberOfTicks=1000, + whatToShow=datatype.HistoricalTicks.BID_ASK.value, useRth=1, + ignoreSize=False, miscOptions=[] + ) + result = await self._queue.get() + + self.assertTrue(result) # Expect item from queue + # Expect `ListOfHistoricalTick` to be sent to the queue + self.assertIsInstance(result[0], wrapper.ListOfHistoricalTickBidAsk) + # Expect queue to be marked as FINISHED + self.assertTrue(self._queue.finished) + + @utils.async_test + async def test_historical_ticks_last(self): + """Test overridden function `historicalTicksLast`.""" + end = (datetime.datetime.now().astimezone(_global.TZ) + .strftime(_global.TIME_FMT)) + + self._client.reqHistoricalTicks( + reqId=self._req_id, contract=sample_contracts.gbp_usd_fx(), + startDateTime="", endDateTime=end, numberOfTicks=1000, + whatToShow=datatype.HistoricalTicks.TRADES.value, useRth=1, + ignoreSize=False, miscOptions=[] + ) + result = await self._queue.get() + + self.assertTrue(result) # Expect item from queue + # Expect `ListOfHistoricalTick` to be sent to the queue + self.assertIsInstance(result[0], wrapper.ListOfHistoricalTickLast) + # Expect queue to be marked as FINISHED + self.assertTrue(self._queue.finished) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + +class TestTickByTickData(unittest.TestCase): + """Unit tests for Tick-by-Tick data related functions in `IBWrapper`. + + Connection with IB is REQUIRED. + + * Tests in this suit will hang up when the market is closed. + * Subscription of US Futures market data is REQUIRED for some tests. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def setUp(self): + self._received = False # Indicates if tick received + self._req_id = self._wrapper.next_req_id + self._queue = self._wrapper.get_request_queue(req_id=self._req_id) + + @utils.async_test + async def test_tick_by_tick_all_last_0(self): + """Test overridden function `tickByTickAllLast` with tick type `Last`. + """ + self._client.reqTickByTickData( + reqId=self._req_id, contract=sample_contracts.us_future(), + tickType=datatype.LiveTicks.LAST.value, numberOfTicks=0, + ignoreSize=True + ) + + async for elem in self._queue.stream(): + if elem is fq.Status.FINISHED: + continue # Let the async task finish + if not self._received: + # Expect `HistoricalTickLast` to be sent to queue + self.assertIsInstance(elem, wrapper.HistoricalTickLast) + await self._stop_streaming(req_id=self._req_id) + + @utils.async_test + async def test_tick_by_tick_all_last_1(self): + """Test overridden function `tickByTickAllLast` with tick type + `AllLast`. + """ + self._client.reqTickByTickData( + reqId=self._req_id, contract=sample_contracts.us_future(), + tickType=datatype.LiveTicks.ALL_LAST.value, numberOfTicks=0, + ignoreSize=True + ) + + async for elem in self._queue.stream(): + if elem is fq.Status.FINISHED: + continue # Let the async task finish + if not self._received: + # Expect `HistoricalTickLast` to be sent to queue + self.assertIsInstance(elem, wrapper.HistoricalTickLast) + await self._stop_streaming(req_id=self._req_id) + + + @utils.async_test + async def test_tick_by_tick_bid_ask(self): + """Test overridden function `tickByTickBidAsk`.""" + self._client.reqTickByTickData( + reqId=self._req_id, contract=sample_contracts.gbp_usd_fx(), + tickType=datatype.LiveTicks.BID_ASK.value, numberOfTicks=0, + ignoreSize=True + ) + + async for elem in self._queue.stream(): + if elem is fq.Status.FINISHED: + continue # Let the async task finish + if not self._received: + # Expect `HistoricalTickLast` to be sent to queue + self.assertIsInstance(elem, wrapper.HistoricalTickBidAsk) + await self._stop_streaming(req_id=self._req_id) + + @utils.async_test + async def test_tick_by_tick_mid_point(self): + """Test overridden function `tickByTickMidPoint`.""" + self._client.reqTickByTickData( + reqId=self._req_id, contract=sample_contracts.gbp_usd_fx(), + tickType=datatype.LiveTicks.MIDPOINT.value, numberOfTicks=0, + ignoreSize=True + ) + + async for elem in self._queue.stream(): + if elem is fq.Status.FINISHED: + continue # Let the async task finish + if not self._received: + # Expect `HistoricalTickLast` to be sent to queue + self.assertIsInstance(elem, wrapper.HistoricalTick) + await self._stop_streaming(req_id=self._req_id) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + + async def _stop_streaming(self, req_id: int): + self._received = True + self._client.cancelTickByTickData(reqId=req_id) + await asyncio.sleep(2) + self._queue.put(element=fq.Status.FINISHED) diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 8fe6377..a66d9d5 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -32,6 +32,17 @@ def wrapper(*args, **kwargs): IB_CLIENT_ID: int = int(os.getenv("IB_CLIENT_ID", "1001")) IB_ACC_ID: str = os.getenv("IB_ACC_ID", "") +class MockNotificationListener(listeners.NotificationListener): + """Mock notification listener.""" + def __init__(self): + self.msg_code = -1 + self.msg = "" + + def on_notify(self, msg_code: int, msg: str): + """Mock callback implementation.""" + self.msg_code = msg_code + self.msg = msg + class MockAccountsManagementDelegate(delegates.AccountsManagementDelegate): """Mock accounts delegate""" def __init__(self): From a8cac35512ba0479abae894ef0a569c4ecbb6583 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 17:54:52 +0000 Subject: [PATCH 068/126] Fix `FinishableQueue` - Fixed the while loop condition to exit on error as well. --- ibpy_native/utils/finishable_queue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index b8540fe..c8e2924 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -71,7 +71,7 @@ async def get(self) -> list: contents_of_queue = [] loop = asyncio.get_event_loop() - while not self.finished: + while not self.finished and self.status is not Status.ERROR: current_element = await loop.run_in_executor( None, self._queue.get ) @@ -94,7 +94,7 @@ async def stream(self) -> Iterator[Any]: """ loop = asyncio.get_event_loop() - while not self.finished: + while not self.finished and self.status is not Status.ERROR: current_element = await loop.run_in_executor( None, self._queue.get ) From 0663e77273737cafd6d6315349efe36096364c54 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 10 Feb 2021 18:12:35 +0000 Subject: [PATCH 069/126] Fix expireed sample contract year --- tests/toolkit/sample_contracts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/toolkit/sample_contracts.py b/tests/toolkit/sample_contracts.py index 2483a75..17678dc 100644 --- a/tests/toolkit/sample_contracts.py +++ b/tests/toolkit/sample_contracts.py @@ -50,7 +50,8 @@ def us_future_expired() -> ib_contract.Contract: contract.exchange = "ECBOT" contract.currency = "USD" # Targets the latest contract of last year - contract.lastTradeDateOrContractMonth = f"{datetime.datetime.now().year}12" + contract.lastTradeDateOrContractMonth = ( + f"{datetime.datetime.now().year-1}12") contract.includeExpired = True return contract From 14fd2b9bef7a00c2d00a27e159b5e4bf97e59fc3 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 14 Feb 2021 14:20:14 +0000 Subject: [PATCH 070/126] Rewrite unittest for module `_client` --- ibpy_native/_internal/_client.py | 7 - tests/internal/test_client.py | 373 +++++++++++++++++++++++++++++++ tests/internal/test_ib_client.py | 235 ------------------- 3 files changed, 373 insertions(+), 242 deletions(-) create mode 100644 tests/internal/test_client.py delete mode 100644 tests/internal/test_ib_client.py diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 8cf7504..54b4e3c 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -158,8 +158,6 @@ async def resolve_head_timestamp( ibpy_native.error.IBError: If - queue associated with `req_id` is being used by other tasks; - there's any error returned from IB; - - no element found in received result; - - multiple elements found in received result. """ try: f_queue = self._wrapper.get_request_queue(req_id=req_id) @@ -229,10 +227,6 @@ async def fetch_historical_ticks( - queue associated with `req_id` is being used by other tasks; - there's any error returned from IB before any tick data is fetched successfully; - - no result received from IB with no tick fetched in pervious - request(s); - - incorrect number of items (!= 2) found in the result received - from IB with no tick fetched in pervious request(s). """ # Pre-process & error checking if type(start.tzinfo) is not type(end.tzinfo): @@ -356,7 +350,6 @@ async def fetch_historical_ticks( all_ticks.reverse() - # return (all_ticks, finished) return {"ticks": all_ticks, "completed": finished,} diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py new file mode 100644 index 0000000..b92a161 --- /dev/null +++ b/tests/internal/test_client.py @@ -0,0 +1,373 @@ +"""Unit tests for module `ibpy_native._internal._client`.""" +# pylint: disable=protected-access +import asyncio +import datetime +import threading +import unittest + +import pytz + +from ibapi import contract +from ibapi import wrapper + +from ibpy_native import error +from ibpy_native._internal import _client +from ibpy_native._internal import _global +from ibpy_native._internal import _wrapper +from ibpy_native.utils import datatype +from ibpy_native.utils import finishable_queue as fq + +from tests.toolkit import sample_contracts +from tests.toolkit import utils + +class TestContract(unittest.TestCase): + """Unit tests for IB contract related functions in `IBClient`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def setUp(self): + self._req_id = self._wrapper.next_req_id + + @utils.async_test + async def test_resolve_contracts(self): + """Test function `resolve_contracts`.""" + result = await self._client.resolve_contracts( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx()) + + self.assertTrue(result) # Expect item returned from request + # Expect a propulated `ContractDetails` received from IB + self.assertIsInstance(result[0], contract.ContractDetails) + self.assertNotEqual(result[0].contract.conId, 0) + + @utils.async_test + async def test_resolve_contracts_err_0(self): + """Test function `resolve_contracts`. + + * Queue associated with `_req_id` is being occupied. + """ + # Mock queue occupation + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): # Expect `IBError` + await self._client.resolve_contracts( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx()) + + @utils.async_test + async def test_resolve_contracts_err_1(self): + """Test function `resolve_contracts`. + + * Error returned from IB for an non-resolvable `Contract`. + """ + with self.assertRaises(error.IBError): # Expect `IBError` + await self._client.resolve_contracts( + req_id=self._req_id, contract=contract.Contract()) + + @utils.async_test + async def test_resolve_contracts_err_2(self): + """Test function `resolve_contracts`. + + * No result received from IB. + """ + invalid_contract = sample_contracts.us_future_expired() + invalid_contract.includeExpired = False + + with self.assertRaises(error.IBError): # Expect `IBError` + await self._client.resolve_contracts( + req_id=self._req_id, contract=invalid_contract) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + +class TestHistoricalData(unittest.TestCase): + """Unit tests for historical market data related function in `IBClient`. + + * Connection with IB is REQUIRED. + * Subscription of US Futures market data is REQUIRED for some tests. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def setUp(self): + self._req_id = self._wrapper.next_req_id + self._start = (datetime.datetime.now().astimezone(_global.TZ) - + datetime.timedelta(minutes=5)) + self._end = datetime.datetime.now().astimezone(_global.TZ) + + @utils.async_test + async def test_resolve_head_timestamp(self): + """Test function `resolve_head_timestamp`.""" + result = await self._client.resolve_head_timestamp( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + show=datatype.EarliestDataPoint.ASK + ) + + self.assertIsInstance(result, int) # Expect epoch returned as `int` + self.assertGreater(result, 0) # Expect a valid epoch value + + @utils.async_test + async def test_resolve_head_timestamp_err_0(self): + """Test function `resolve_head_timestamp`. + + * Queue associated with `_req_id` is being occupied. + """ + # Mock queue occupation + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): + await self._client.resolve_head_timestamp( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + show=datatype.EarliestDataPoint.ASK + ) + + @utils.async_test + async def test_resolve_head_timestamp_err_1(self): + """Test function `resolve_head_timestamp`. + + * Error returned from IB for non-resolvable `Contract`. + """ + with self.assertRaises(error.IBError): + await self._client.resolve_head_timestamp( + req_id=self._req_id, contract=contract.Contract(), + show=datatype.EarliestDataPoint.ASK + ) + + @utils.async_test + async def test_fetch_historical_ticks_0(self): + """Test function `fetch_historical_ticks`. + + * Request tick data for `BidAsk`. + """ + result = await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=self._end, + show=datatype.HistoricalTicks.BID_ASK + ) + + self.assertTrue(result) # Expect data returned from IB + self.assertIsInstance(result, dict) # Expect data returned as `dict` + # Expect ticks return in a list + self.assertIsInstance(result["ticks"], list) + # Expect `True` as a prove of all data within requested period are + # received + self.assertTrue(result["completed"]) + # Expect received tick type is `HistoricalTickBidAsk` + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickBidAsk) + + @utils.async_test + async def test_fetch_historical_ticks_1(self): + """Test function `fetch_historical_ticks`. + + * Request tick data for `MidPoint`. + """ + result = await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=self._end, + show=datatype.HistoricalTicks.MIDPOINT + ) + + self.assertTrue(result) # Expect data returned from IB + self.assertIsInstance(result, dict) # Expect data returned as `dict` + # Expect ticks return in a list + self.assertIsInstance(result["ticks"], list) + # Expect `True` as a prove of all data within requested period are + # received + self.assertTrue(result["completed"]) + # Expect received tick type is `HistoricalTickBidAsk` + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTick) + + @utils.async_test + async def test_fetch_historical_ticks_2(self): + """Test function `fetch_historical_ticks`. + + * Request tick data for `Last`. + """ + result = await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.us_future(), + start=self._start, end=self._end, + show=datatype.HistoricalTicks.TRADES + ) + + self.assertTrue(result) # Expect data returned from IB + self.assertIsInstance(result, dict) # Expect data returned as `dict` + # Expect ticks return in a list + self.assertIsInstance(result["ticks"], list) + # Expect `True` as a prove of all data within requested period are + # received + self.assertTrue(result["completed"]) + # Expect received tick type is `HistoricalTickBidAsk` + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickLast) + + @utils.async_test + async def test_fetch_historical_ticks_err_0(self): + """Test function `fetch_historical_ticks`. + + * `ValueError` raised due to inconsistences timezone set for `start` + and `end` time. + """ + with self.assertRaises(ValueError): + await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start=datetime.datetime.now().astimezone( + pytz.timezone("Asia/Hong_Kong") + ), + end=self._end, show=datatype.HistoricalTicks.BID_ASK + ) + + @utils.async_test + async def test_fetch_historical_ticks_err_1(self): + """Test function `fetch_historical_ticks`. + + * `ValueError` raised due to value of `start` > `end`. + """ + with self.assertRaises(ValueError): + await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start=self._end, end=self._start, + show=datatype.HistoricalTicks.BID_ASK + ) + + @utils.async_test + async def test_fetch_historical_ticks_err_2(self): + """Test function `fetch_historical_ticks`. + + * `IBError` raised due to `req_id` is being occupied by other task. + """ + # Mock queue occupation + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): + await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=self._end, + show=datatype.HistoricalTicks.BID_ASK + ) + + @utils.async_test + async def test_fetch_historical_ticks_err_3(self): + """Test function `fetch_historical_ticks`. + + * Error returned from IB for invalid contract. + """ + with self.assertRaises(error.IBError): + await self._client.fetch_historical_ticks( + req_id=self._req_id, contract=contract.Contract(), + start=self._start, end=self._end, + show=datatype.HistoricalTicks.TRADES + ) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + +class TestLiveData(unittest.TestCase): + """Unit tests for live market data related functions in `IBClient`. + + Connection with IB is REQUIRED. + + * Tests in this suit will hang up when the market is closed. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def setUp(self): + self._req_id = self._wrapper.next_req_id + self._listener = utils.MockLiveTicksListener() + + @utils.async_test + async def test_stream_live_ticks(self): + """Test function `stream_live_ticks`.""" + # Prepare function & tasks for the test + async def cancel_req(): + while not self._listener.ticks: # Wait until a tick received + await asyncio.sleep(0.1) + await self._stop_streaming(req_id=self._req_id) + + cancel_task = asyncio.create_task(cancel_req()) + + try: + await self._start_streaming(req_id=self._req_id) + await cancel_task + except error.IBError as err: + for task in asyncio.all_tasks(): + task.cancel() + self.fail(err.err_str) + + self.assertIsInstance(self._listener.ticks[0], + wrapper.HistoricalTickBidAsk) + + @utils.async_test + async def test_stream_live_ticks_err(self): + """Test function `stream_live_ticks`. + + * Should raise `IBError` as queue associated with `_req_id` being + occupied. + """ + # Mock queue occupation + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): + await self._client.stream_live_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + listener=self._listener, tick_type=datatype.LiveTicks.BID_ASK + ) + + @utils.async_test + async def test_cancel_live_ticks_stream(self): + """Test function `cancel_live_ticks_stream`.""" + try: + await self._start_streaming(req_id=self._req_id) + await asyncio.sleep(0.5) + self._client.cancel_live_ticks_stream(req_id=self._req_id) + except error.IBError as err: + for task in asyncio.all_tasks(): + task.cancel() + self.fail(err.err_str) + + await asyncio.sleep(0.5) + self.assertTrue(self._listener.finished) + + @utils.async_test + async def test_cancel_live_ticks_stream_err(self): + """Test function `cancel_live_ticks_stream`. + + * Should raise `IBError` as no queue is associated with `_req_id`. + """ + with self.assertRaises(error.IBError): + self._client.cancel_live_ticks_stream(req_id=self._req_id) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + + async def _start_streaming(self, req_id: int) -> asyncio.Task: + return asyncio.create_task(self._client.stream_live_ticks( + req_id, contract=sample_contracts.gbp_usd_fx(), + listener=self._listener, tick_type=datatype.LiveTicks.BID_ASK + ) + ) + + async def _stop_streaming(self, req_id: int): + self._client.cancelTickByTickData(reqId=req_id) + await asyncio.sleep(2) + self._wrapper.get_request_queue_no_throw(req_id).put(fq.Status.FINISHED) diff --git a/tests/internal/test_ib_client.py b/tests/internal/test_ib_client.py deleted file mode 100644 index 7583e20..0000000 --- a/tests/internal/test_ib_client.py +++ /dev/null @@ -1,235 +0,0 @@ -"""Unit tests for module `ibpy_native.client`.""" -# pylint: disable=protected-access -import asyncio -import datetime -import threading -import unittest -from typing import List - -import pytz - -from ibapi import contract as ib_contract -from ibapi import wrapper as ib_wrapper - -from ibpy_native import error -from ibpy_native._internal import _client -from ibpy_native._internal import _global -from ibpy_native._internal import _wrapper -from ibpy_native.utils import datatype as dt -from ibpy_native.utils import finishable_queue as fq - -from tests.toolkit import sample_contracts -from tests.toolkit import utils - -#region - Constants -# Predefined request IDs for tests in `TestIBClient`. -_RID_RESOLVE_CONTRACT = 43 -_RID_RESOLVE_CONTRACTS = 44 -_RID_RESOLVE_HEAD_TIMESTAMP = 14001 -_RID_RESOLVE_HEAD_TIMESTAMP_EPOCH = 14002 -_RID_FETCH_HISTORICAL_TICKS = 18001 -_RID_FETCH_HISTORICAL_TICKS_ERR = 18002 -_RID_STREAM_LIVE_TICKS = 19001 -_RID_CANCEL_LIVE_TICKS_STREAM = 19002 -_RID_CANCEL_LIVE_TICKS_STREAM_ERR = 19003 -#endregion - Constants - -class TestIBClient(unittest.TestCase): - """Unit tests for class `IBClient`.""" - - @classmethod - def setUpClass(cls): - _global.TZ = pytz.timezone("America/New_York") - - cls._wrapper = _wrapper.IBWrapper() - cls._client = _client.IBClient(cls._wrapper) - - cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) - - thread = threading.Thread(target=cls._client.run) - thread.start() - - #region - Contract - @utils.async_test - async def test_resolve_contract(self): - """Test function `resolve_contract`.""" - resolved_contract = await self._client.resolve_contract( - req_id=_RID_RESOLVE_CONTRACT, - contract=sample_contracts.gbp_usd_fx() - ) - - self.assertIsNotNone(resolved_contract) - - @utils.async_test - async def test_resolve_contracts(self): - """Test function `resolve_contracts`.""" - contract: ib_contract.Contract = sample_contracts.us_future() - contract.lastTradeDateOrContractMonth = "" - - res: List[ib_contract.ContractDetails] = ( - await self._client.resolve_contracts(req_id=_RID_RESOLVE_CONTRACTS, - contract=contract) - ) - - self.assertTrue(res) - self.assertGreater(len(res), 1) - #endregion - Contract - - @utils.async_test - async def test_resolve_head_timestamp(self): - """Test function `resolve_head_timestamp`.""" - head_timestamp = await self._client.resolve_head_timestamp( - req_id=_RID_RESOLVE_HEAD_TIMESTAMP, - contract=sample_contracts.gbp_usd_fx(), - show=dt.EarliestDataPoint.BID - ) - - self.assertIsNotNone(head_timestamp) - self.assertIsInstance(head_timestamp, int) - - #region - Historical ticks - @utils.async_test - async def test_fetch_historical_ticks(self): - """Test function `fetch_historical_ticks`.""" - data = await self._client.fetch_historical_ticks( - req_id=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.us_stock(), - start=_global.TZ.localize( - datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=_global.TZ.localize( - datetime.datetime(2020, 4, 29, 10, 35, 0)), - show=dt.HistoricalTicks.MIDPOINT - ) - - self.assertIsInstance(data["ticks"], list) - self.assertTrue(data["completed"]) - self.assertTrue(data["ticks"]) - self.assertIsInstance(data["ticks"][0], ib_wrapper.HistoricalTick) - - data = await self._client.fetch_historical_ticks( - req_id=_RID_FETCH_HISTORICAL_TICKS, - contract=sample_contracts.us_stock(), - start=_global.TZ.localize( - datetime.datetime(2020, 4, 29, 10, 30, 0)), - end=_global.TZ.localize( - datetime.datetime(2020, 4, 29, 10, 35, 0)), - show=dt.HistoricalTicks.BID_ASK - ) - - self.assertIsInstance(data["ticks"], list) - self.assertTrue(data["completed"]) - self.assertTrue(data["ticks"]) - self.assertIsInstance(data["ticks"][0], ib_wrapper.HistoricalTickBidAsk) - - @utils.async_test - async def test_fetch_historical_ticks_err(self): - """Test function `fetch_historical_ticks` for the error cases.""" - # Timezone of start & end are not identical - with self.assertRaises(ValueError): - await self._client.fetch_historical_ticks( - req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, - contract=sample_contracts.gbp_usd_fx(), - start=datetime.datetime.now().astimezone( - pytz.timezone("Asia/Hong_Kong")), - end=datetime.datetime.now().astimezone( - pytz.timezone("America/New_York")) - ) - - # Invalid contract object - with self.assertRaises(error.IBError): - await self._client.fetch_historical_ticks( - req_id=_RID_FETCH_HISTORICAL_TICKS_ERR, - contract=ib_contract.Contract(), - start=datetime.datetime(2020, 5, 20, 3, 20, 0).astimezone( - _global.TZ), - end=datetime.datetime.now().astimezone(_global.TZ) - ) - #endregion - Historical ticks - - #region - Live ticks - @utils.async_test - async def test_stream_live_ticks(self): - """Test function `stream_live_ticks`.""" - async def cancel_req(): - await asyncio.sleep(5) - self._client.cancelTickByTickData( - reqId=_RID_STREAM_LIVE_TICKS - ) - - queue = self._wrapper.get_request_queue_no_throw( - req_id=_RID_STREAM_LIVE_TICKS - ) - queue.put(element=fq.Status.FINISHED) - - listener = utils.MockLiveTicksListener() - - stream = asyncio.create_task( - self._client.stream_live_ticks( - req_id=_RID_STREAM_LIVE_TICKS, - contract=sample_contracts.gbp_usd_fx(), - listener=listener, - tick_type=dt.LiveTicks.BID_ASK - ) - ) - cancel = asyncio.create_task(cancel_req()) - - try: - await stream - await cancel - except error.IBError as err: - tasks = asyncio.all_tasks() - for task in tasks: - task.cancel() - - raise err - - self.assertTrue(listener.ticks) - self.assertTrue(listener.finished) - - @utils.async_test - async def test_cancel_live_ticks_stream(self): - """Test function `cancel_live_ticks_stream`.""" - async def cancel_req(): - await asyncio.sleep(3) - self._client.cancel_live_ticks_stream( - req_id=_RID_CANCEL_LIVE_TICKS_STREAM - ) - - listener = utils.MockLiveTicksListener() - - stream = asyncio.create_task( - self._client.stream_live_ticks( - req_id=_RID_CANCEL_LIVE_TICKS_STREAM, - contract=sample_contracts.gbp_usd_fx(), - listener=listener, - tick_type=dt.LiveTicks.BID_ASK - ) - ) - cancel = asyncio.create_task(cancel_req()) - - try: - await stream - await cancel - except error.IBError as err: - tasks = asyncio.all_tasks() - for task in tasks: - task.cancel() - - raise err - - self.assertTrue(listener.finished) - - @utils.async_test - async def test_cancel_live_ticks_stream_err(self): - """Test function `cancel_live_ticks_stream` with request ID that has no - `FinishableQueue` associated with. - """ - with self.assertRaises(error.IBError): - self._client.cancel_live_ticks_stream( - req_id=_RID_CANCEL_LIVE_TICKS_STREAM_ERR - ) - #endregion - Live ticks - - @classmethod - def tearDownClass(cls): - cls._client.disconnect() From c1f5839703e1b476f63da37e314c10fb7115dc45 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 14 Feb 2021 16:11:46 +0000 Subject: [PATCH 071/126] Change `IBBridge.is_connected` to property --- ibpy_native/bridge.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index f846956..73a8f4b 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -65,6 +65,13 @@ def __init__( self.connect() # Properties + @property + def is_connected(self) -> bool: + """Check if the bridge is connected to a running & logged in TWS/IB + Gateway instance. + """ + return self._client.isConnected() + @property def accounts_manager(self) -> ib_account.AccountsManager: """:obj:`ibpy_native.account.AccountsManager`: Instance that stores & @@ -100,16 +107,10 @@ def set_on_notify_listener(self, listener: listeners.NotificationListener): #endregion - Setters #region - Connections - def is_connected(self) -> bool: - """Check if the bridge is connected to a running & logged in TWS/IB - Gateway instance. - """ - return self._client.isConnected() - def connect(self): """Connect the bridge to a running & logged in TWS/IB Gateway instance. """ - if not self.is_connected(): + if not self.is_connected: self._client.connect(host=self._host, port=self._port, clientId=self._client_id) From 9d151437375460dd6e7ab448faff497439c15249 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 04:54:32 +0000 Subject: [PATCH 072/126] Use last Friday for historical ticks tests --- tests/internal/test_client.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index b92a161..5b1407a 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -4,6 +4,7 @@ import datetime import threading import unittest +from dateutil import relativedelta import pytz @@ -106,9 +107,12 @@ def setUpClass(cls): def setUp(self): self._req_id = self._wrapper.next_req_id - self._start = (datetime.datetime.now().astimezone(_global.TZ) - - datetime.timedelta(minutes=5)) - self._end = datetime.datetime.now().astimezone(_global.TZ) + self._end = (datetime.datetime.now() + relativedelta.relativedelta( + weekday=relativedelta.FR(-1)) + ).replace(hour=12, minute=0).astimezone(_global.TZ) + self._start = (self._end - datetime.timedelta(minutes=5)).astimezone( + _global.TZ + ) @utils.async_test async def test_resolve_head_timestamp(self): @@ -351,10 +355,10 @@ async def test_cancel_live_ticks_stream(self): async def test_cancel_live_ticks_stream_err(self): """Test function `cancel_live_ticks_stream`. - * Should raise `IBError` as no queue is associated with `_req_id`. + * Should raise `IBError` as no queue is associated with ID `0`. """ with self.assertRaises(error.IBError): - self._client.cancel_live_ticks_stream(req_id=self._req_id) + self._client.cancel_live_ticks_stream(req_id=0) @classmethod def tearDownClass(cls): From ad529f23fca601c38377b36ba0492dc80ca6eec6 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 04:55:14 +0000 Subject: [PATCH 073/126] Fix class members scope - Define class members scope in `__init__` to avoid data persists between tests. --- tests/toolkit/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index a66d9d5..925aa98 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -71,11 +71,11 @@ async def unsub_account_updates(self): class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" - ticks: List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]] = [] - - finished: bool = False + def __init__(self): + self.ticks: List[Union[ib_wrapper.HistoricalTick, + ib_wrapper.HistoricalTickBidAsk, + ib_wrapper.HistoricalTickLast]] = [] + self.finished = False def on_tick_receive(self, req_id: int, tick: Union[ib_wrapper.HistoricalTick, From e478c85f3aa8c65f5709a9e89f59b798f32b2261 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 04:59:30 +0000 Subject: [PATCH 074/126] Add missing setter - Added setter for property `contract`. --- ibpy_native/models/portfolio.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ibpy_native/models/portfolio.py b/ibpy_native/models/portfolio.py index c2249a8..ede92e5 100644 --- a/ibpy_native/models/portfolio.py +++ b/ibpy_native/models/portfolio.py @@ -43,6 +43,11 @@ def contract(self) -> ib_contract.Contract: """ return self._contract + @contract.setter + def contract(self, val: contract): + with self._lock: + self._contract = val + @property def position(self) -> float: """float: The number of positions held.""" From 7e489d9e65a9448c26fa3101e168fa2d6dd15859 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 05:01:49 +0000 Subject: [PATCH 075/126] Rewrite unittest for module `bridge` --- ibpy_native/bridge.py | 11 +- tests/test_bridge.py | 496 ++++++++++++++++++++++++++++++++++++++++ tests/test_ib_bridge.py | 246 -------------------- 3 files changed, 497 insertions(+), 256 deletions(-) create mode 100644 tests/test_bridge.py delete mode 100644 tests/test_ib_bridge.py diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 73a8f4b..d697c5a 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -218,9 +218,7 @@ async def get_earliest_data_point( except error.IBError as err: raise err - data_point = datetime.datetime.fromtimestamp( - result - ).astimezone( + data_point = datetime.datetime.fromtimestamp(result).astimezone( _global.TZ ) @@ -235,13 +233,6 @@ async def get_historical_ticks( """Retrieve historical ticks data for specificed instrument/contract from IB. - Note: - Multiple attempts is recommended for requesting long period of - data as the request may timeout due to IB delays the responds to - protect their service over a long session. - Longer timeout value is also recommended for the same reason. Around - 30 to 100 seconds should be reasonable. - Args: contract (:obj:`ibapi.contract.Contract`): `Contract` object with sufficient info to identify the instrument. diff --git a/tests/test_bridge.py b/tests/test_bridge.py new file mode 100644 index 0000000..6f2c303 --- /dev/null +++ b/tests/test_bridge.py @@ -0,0 +1,496 @@ +"""Unit tests for module `ibpy_native._internal._bridge`.""" +# pylint: disable=protected-access +import asyncio +import datetime +import unittest +from dateutil import relativedelta + +import pytz + +from ibapi import contract +from ibapi import wrapper + +from ibpy_native import bridge +from ibpy_native import error +from ibpy_native._internal import _global +from ibpy_native.utils import datatype +from ibpy_native.utils import finishable_queue as fq + +from tests.toolkit import sample_contracts +from tests.toolkit import utils + +class TestGeneral(unittest.TestCase): + """Unit tests for general/uncategorised things in `IBBridge`. + + Connection with IB is NOT required. + """ + def setUp(self): + self._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) + + def test_set_timezone(self): + """Test function `set_timezone`.""" + self._bridge.set_timezone(tz=pytz.timezone("Asia/Hong_Kong")) + self.assertEqual(_global.TZ, pytz.timezone("Asia/Hong_Kong")) + + # Reset timezone to New York + self._bridge.set_timezone(tz=pytz.timezone("America/New_York")) + self.assertEqual(_global.TZ, pytz.timezone("America/New_York")) + + def test_set_on_notify_listener(self): + """Test setter function `set_on_notify_listener`.""" + listener = utils.MockNotificationListener() + code = 404 + msg = "MOCK_MSG" + + self._bridge.set_on_notify_listener(listener) + self._bridge._wrapper.error(reqId=-1, errorCode=code, errorString=msg) + + self.assertEqual(listener.msg_code, code) + self.assertEqual(listener.msg, msg) + +class TestConnection(unittest.TestCase): + """Unit tests for IB TWS/Gateway connection related functions in `IBBridge`. + + * Connection with IB is REQUIRED. + """ + def test_init_0(self): + """Test initialisation of `IBBridge`. + + * With auto connect enabled. + """ + ib_bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=True) + + self.assertTrue(ib_bridge.is_connected) + ib_bridge.disconnect() + + def test_init_1(self): + """Test initialisation of `IBBridge`. + + * With auto connect disabled. + """ + ib_bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) + + self.assertFalse(ib_bridge.is_connected) + + def test_connect(self): + """Test function `connect`.""" + ib_bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) + ib_bridge.connect() + + self.assertTrue(ib_bridge.is_connected) + ib_bridge.disconnect() + + def test_disconnect(self): + """Test function `disconnect`.""" + ib_bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID, + auto_conn=False) + ib_bridge.connect() + ib_bridge.disconnect() + + self.assertFalse(ib_bridge.is_connected) + +class TestAccount(unittest.TestCase): + """Unit tests for IB account related functions in `IBBridge`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) + + @utils.async_test + async def test_accounts_manager(self): + """Test property `accounts_manager`. + + * A default `AccountsManager` should be set up and account(s) should + be returned from IB once connected. + """ + await asyncio.sleep(0.5) # Wait for IB to return the account ID(s) + self.assertTrue(self._bridge.accounts_manager.accounts) + + @utils.async_test + async def test_req_managed_accounts(self): + """Test function `req_managed_accouts`.""" + # Wait for IB to finish its' data return on connection + await asyncio.sleep(0.5) + # Clean up the already filled accounts dict + self._bridge.accounts_manager.accounts.clear() + + self._bridge.req_managed_accounts() + await asyncio.sleep(0.5) # Wait for IB to return the account ID(s) + self.assertTrue(self._bridge.accounts_manager.accounts) + + @utils.async_test + async def test_sub_account_updates(self): + """Test function `sub_account_updates`.""" + # Wait for IB to finish its' data return on connection + await asyncio.sleep(0.5) + account = self._bridge.accounts_manager.accounts[utils.IB_ACC_ID] + await self._bridge.sub_account_updates(account) + + timeout_counter = 0 + while not account.account_ready: + if timeout_counter == 5: + self.fail("Test timeout as account ready status hasn't been " + "updated to `True` within the permitted time.") + await asyncio.sleep(1) + + self.assertTrue( + account.get_account_value(key="CashBalance", currency="BASE") + ) + + self._bridge._client.reqAccountUpdates(subscribe=False, + acctCode=utils.IB_ACC_ID) + self._bridge.accounts_manager.account_updates_queue.put( + element=fq.Status.FINISHED) + await asyncio.sleep(0.5) # Wait async tasks to finish + + @utils.async_test + async def test_unsub_account_updates(self): + """Test function `unsub_account_updates`.""" + # Wait for IB to finish its' data return on connection + await asyncio.sleep(0.5) + account = self._bridge.accounts_manager.accounts[utils.IB_ACC_ID] + await self._bridge.unsub_account_updates(account) + + self.assertTrue( + self._bridge.accounts_manager.account_updates_queue.finished) + + @classmethod + def tearDownClass(cls): + cls._bridge.disconnect() + +class TestContract(unittest.TestCase): + """Unit tests for IB contract related functions in `IBBridge`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) + + @utils.async_test + async def test_search_detailed_contracts(self): + """Test function `search_detailed_contracts`.""" + result = await self._bridge.search_detailed_contracts( + contract = sample_contracts.gbp_usd_fx() + ) + + self.assertTrue(result) + self.assertNotEqual(result[0].contract.conId, 0) + + @utils.async_test + async def test_search_detailed_contracts_err(self): + """Test function `search_detailed_contracts`. + + * Should raise `IBError` for non-resolveable `Contract + """ + with self.assertRaises(error.IBError): + await self._bridge.search_detailed_contracts( + contract = contract.Contract() + ) + + @classmethod + def tearDownClass(cls): + cls._bridge.disconnect() + +class TestHistoricalData(unittest.TestCase): + """Unit tests for historical market data related functions in `IBBridge`. + + Connection with IB is REQUIRED. + + * Tests in this suit will hang up when the market is closed. + * Subscription of US Futures market data is REQUIRED for some tests. + """ + @classmethod + def setUpClass(cls): + cls._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) + + def setUp(self): + self._end = (datetime.datetime.now() + relativedelta.relativedelta( + weekday=relativedelta.FR(-1)) + ).replace(hour=12, minute=0) + self._start = self._end - datetime.timedelta(minutes=5) + + @utils.async_test + async def test_get_earliest_data_point(self): + """Test function `get_earliest_data_point`.""" + try: + await self._bridge.get_earliest_data_point( + contract=sample_contracts.gbp_usd_fx(), + data_type=datatype.EarliestDataPoint.BID + ) + except error.IBError: + self.fail("Test fail as unexpected `IBError` raised.") + + @utils.async_test + async def test_get_earliest_data_point_err(self): + """Test function `get_earliest_data_point`. + + * Should raise `IBError` for invalid `Contract`. + """ + with self.assertRaises(error.IBError): + await self._bridge.get_earliest_data_point( + contract=contract.Contract(), + data_type=datatype.EarliestDataPoint.BID + ) + + @utils.async_test + async def test_get_historical_ticks_0(self): + """Test function `get_historical_ticks`. + + * Request tick data for `BID_ASK`. + """ + result = await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), start=self._start, + end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, + attempts=1 + ) + + self.assertTrue(result["ticks"]) + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickBidAsk) + self.assertTrue(result["completed"]) + + @utils.async_test + async def test_get_historical_ticks_1(self): + """Test function `get_historical_ticks`. + + * Request tick data for `MIDPOINT`. + """ + result = await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), start=self._start, + end=self._end, data_type=datatype.HistoricalTicks.MIDPOINT, + attempts=1 + ) + + self.assertTrue(result["ticks"]) + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTick) + self.assertTrue(result["completed"]) + + @utils.async_test + async def test_get_historical_ticks_2(self): + """Test function `get_historical_ticks`. + + * Request tick data for `TRADES`. + """ + result = await self._bridge.get_historical_ticks( + contract=sample_contracts.us_future(), start=self._start, + end=self._end, data_type=datatype.HistoricalTicks.TRADES, + attempts=1 + ) + + self.assertTrue(result["ticks"]) + self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickLast) + self.assertTrue(result["completed"]) + + @utils.async_test + async def test_get_historical_ticks_err_0(self): + """Test function `get_historical_ticks`. + + * Expect `ValueError` as argument `start` or `end` contains timezone + info. + """ + with self.assertRaises(ValueError): + await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=_global.TZ.localize(dt=self._start), + end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, + attempts=1 + ) + + with self.assertRaises(ValueError): + await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=_global.TZ.localize(dt=self._end), + data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 + ) + + @utils.async_test + async def test_get_historical_ticks_err_1(self): + """Test function `get_historical_ticks`. + + * Expect `ValueError` as value of `start` is earlier than the earliest + available datapoint. + """ + with self.assertRaises(ValueError): + await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=datetime.datetime(year=1990, month=1, day=1), + end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, + attempts=1 + ) + + @utils.async_test + async def test_get_historical_ticks_err_2(self): + """Test function `get_historical_ticks`. + + * Expect `ValueError` as value of `end` is earlier than `start` + """ + with self.assertRaises(ValueError): + await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=self._end, end=self._start, + data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 + ) + + @utils.async_test + async def test_get_historical_ticks_err_3(self): + """Test function `get_historical_ticks`. + + * Expect `ValueError` as value of `attempts` < 1 and != -1. + """ + with self.assertRaises(ValueError): + await self._bridge.get_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=self._end, + data_type=datatype.HistoricalTicks.BID_ASK, attempts=-2 + ) + + @utils.async_test + async def test_get_historical_ticks_err_4(self): + """Test function `get_historical_ticks`. + + * Expect `IBError` due to invalid `Contract` passed in. + """ + with self.assertRaises(error.IBError): + await self._bridge.get_historical_ticks( + contract=contract.Contract(), start=self._start, end=self._end, + data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 + ) + + @classmethod + def tearDownClass(cls): + cls._bridge.disconnect() + +class TestLiveData(unittest.TestCase): + """Unit tests for live market data related functions in `IBBridge`. + + Connection with IB is REQUIRED. + + * Tests in this suit will hang up when the market is closed. + * Subscription of US Futures market data is REQUIRED for some tests. + """ + @classmethod + def setUpClass(cls): + cls._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) + + def setUp(self): + self._listener = utils.MockLiveTicksListener() + + @utils.async_test + async def test_stream_live_ticks_0(self): + """Test function `stream_live_ticks`. + + * Request tick data for `BID_ASK`. + """ + req_id = await self._bridge.stream_live_ticks( + contract=sample_contracts.gbp_usd_fx(), listener=self._listener, + tick_type=datatype.LiveTicks.BID_ASK + ) + + while not self._listener.ticks: + await asyncio.sleep(0.5) + + self.assertIsInstance(self._listener.ticks[0], + wrapper.HistoricalTickBidAsk) + + self._bridge._client.cancel_live_ticks_stream(req_id) + await asyncio.sleep(0.5) + + @utils.async_test + async def test_stream_live_ticks_1(self): + """Test function `stream_live_ticks`. + + * Request tick data for `MIDPOINT`. + """ + req_id = await self._bridge.stream_live_ticks( + contract=sample_contracts.gbp_usd_fx(), listener=self._listener, + tick_type=datatype.LiveTicks.MIDPOINT + ) + + while not self._listener.ticks: + await asyncio.sleep(0.5) + + self.assertIsInstance(self._listener.ticks[0], wrapper.HistoricalTick) + + self._bridge._client.cancel_live_ticks_stream(req_id) + await asyncio.sleep(0.5) + + @utils.async_test + async def test_stream_live_ticks_2(self): + """Test function `stream_live_ticks`. + + * Request tick data for `ALL_LAST`. + """ + req_id = await self._bridge.stream_live_ticks( + contract=sample_contracts.us_future(), listener=self._listener, + tick_type=datatype.LiveTicks.ALL_LAST + ) + + while not self._listener.ticks: + await asyncio.sleep(0.5) + + self.assertIsInstance(self._listener.ticks[0], + wrapper.HistoricalTickLast) + + self._bridge._client.cancel_live_ticks_stream(req_id) + await asyncio.sleep(0.5) + + @utils.async_test + async def test_stream_live_ticks_3(self): + """Test function `stream_live_ticks`. + + * Request tick data for `LAST`. + """ + req_id = await self._bridge.stream_live_ticks( + contract=sample_contracts.us_future(), listener=self._listener, + tick_type=datatype.LiveTicks.LAST + ) + + while not self._listener.ticks: + await asyncio.sleep(0.5) + + self.assertIsInstance(self._listener.ticks[0], + wrapper.HistoricalTickLast) + + self._bridge._client.cancel_live_ticks_stream(req_id) + await asyncio.sleep(0.5) + + @utils.async_test + async def test_stop_live_ticks_stream(self): + """Test function `stop_live_ticks_stream`.""" + req_id = await self._bridge.stream_live_ticks( + contract=sample_contracts.gbp_usd_fx(), listener=self._listener, + tick_type=datatype.LiveTicks.BID_ASK + ) + await asyncio.sleep(0.5) + + self._bridge.stop_live_ticks_stream(stream_id=req_id) + await asyncio.sleep(0.5) + self.assertTrue(self._listener.finished) + + def test_stop_live_ticks_stream_err(self): + """Test function `stop_live_ticks_stream`. + + * Should raise `IBError` as stream ID 0 has no stream associated with + it. + """ + with self.assertRaises(error.IBError): + self._bridge.stop_live_ticks_stream(stream_id=0) + + @classmethod + def tearDownClass(cls): + cls._bridge.disconnect() diff --git a/tests/test_ib_bridge.py b/tests/test_ib_bridge.py deleted file mode 100644 index c231d5f..0000000 --- a/tests/test_ib_bridge.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Unit tests for module `ibpy_native.bridge`.""" -# pylint: disable=protected-access -import asyncio -import datetime -import os -import unittest - -import pytz - -import ibpy_native -from ibpy_native import error -from ibpy_native._internal import _client -from ibpy_native._internal import _global -from ibpy_native.interfaces import listeners -from ibpy_native.utils import datatype as dt, finishable_queue as fq - -from tests.toolkit import sample_contracts -from tests.toolkit import utils - -class TestIBBridgeConn(unittest.TestCase): - """Test cases for connection related functions in `IBBridge`.""" - - def test_init_auto_connect(self): - """Test initialise `IBBridge` with `auto_conn=True`.""" - bridge = ibpy_native.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, - client_id=utils.IB_CLIENT_ID) - - self.assertTrue(bridge.is_connected()) - - bridge.disconnect() - - def test_init_manual_connect(self): - """Test initialise `IBBridge` with `auto_conn=False`.""" - bridge = ibpy_native.IBBridge(host=utils.IB_HOST, - port=utils.IB_PORT, - client_id=utils.IB_CLIENT_ID, - auto_conn=False) - bridge.connect() - - self.assertTrue(bridge.is_connected()) - - bridge.disconnect() - - def test_disconnect_without_connection(self): - """Test function `disconnect` without an established connection.""" - bridge = ibpy_native.IBBridge(host=utils.IB_HOST, - port=utils.IB_PORT, - client_id=utils.IB_CLIENT_ID, - auto_conn=False) - bridge.disconnect() - - self.assertFalse(bridge.is_connected()) - -class TestIBBridge(unittest.TestCase): - """Unit tests for class `IBBridge`.""" - - @classmethod - def setUpClass(cls): - cls._bridge = ibpy_native.IBBridge( - host=utils.IB_HOST, port=utils.IB_PORT, client_id=utils.IB_CLIENT_ID - ) - - @utils.async_test - async def test_accounts_manager(self): - """Test if the `AccountsManager` is properly set up and has the - account(s) received from IB Gateway once logged in. - """ - await asyncio.sleep(0.5) # Wait for the Gateway to return account ID(s). - self.assertTrue(self._bridge.accounts_manager.accounts) - - def test_set_timezone(self): - """Test function `set_timezone`.""" - ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("Asia/Hong_Kong")) - - self.assertEqual(_global.TZ, - pytz.timezone("Asia/Hong_Kong")) - - # Reset timezone to New York - ibpy_native.IBBridge.set_timezone(tz=pytz.timezone("America/New_York")) - self.assertEqual(_global.TZ, - pytz.timezone("America/New_York")) - - def test_set_on_notify_listener(self): - """Test notification listener supports.""" - class MockListener(listeners.NotificationListener): - """Mock notification listener""" - triggered = False - - def on_notify(self, msg_code: int, msg: str): - """Mock callback implementation""" - print(f"{msg_code} - {msg}") - - self.triggered = True - - mock_listener = MockListener() - - self._bridge.set_on_notify_listener(listener=mock_listener) - self._bridge._wrapper.error( - reqId=-1, errorCode=1100, errorString="MOCK MSG" - ) - - #region - IB account related - @utils.async_test - async def test_req_managed_accounts(self): - """Test function `req_managed_accounts`.""" - await asyncio.sleep(0.5) - # Clean up the already filled dict. - self._bridge.accounts_manager.accounts.clear() - - self._bridge.req_managed_accounts() - - await asyncio.sleep(0.5) - self.assertTrue(self._bridge.accounts_manager.accounts) - - @utils.async_test - async def test_account_updates(self): - """Test functions `sub_acccount_updates` & `unsub_account_updates`.""" - await asyncio.sleep(0.5) - account = self._bridge.accounts_manager.accounts[os.getenv("IB_ACC_ID")] - - await self._bridge.sub_account_updates(account=account) - await asyncio.sleep(0.5) - await self._bridge.unsub_account_updates(account=account) - await asyncio.sleep(0.5) - - self.assertTrue(account.account_ready) - self.assertEqual( - self._bridge.accounts_manager.account_updates_queue.status, - fq.Status.FINISHED - ) - #endregion - IB account related - - @utils.async_test - async def test_search_detailed_contracts(self): - """Test function `search_detailed_contracts`.""" - contract = sample_contracts.us_future() - contract.lastTradeDateOrContractMonth = "" - - res = await self._bridge.search_detailed_contracts(contract=contract) - self.assertGreater(len(res), 1) - for item in res: - print(item.contract) - - @utils.async_test - async def test_get_earliest_data_point(self): - """Test function `get_earliest_data_point`.""" - head_trade = await self._bridge.get_earliest_data_point( - contract=sample_contracts.us_stock() - ) - self.assertEqual(datetime.datetime(1980, 12, 12, 9, 30), head_trade) - - head_bid = await self._bridge.get_earliest_data_point( - contract=sample_contracts.us_stock(), - data_type=dt.EarliestDataPoint.BID - ) - self.assertEqual(datetime.datetime(2008, 12, 29, 7, 0), head_bid) - - #region - Historical ticks - @utils.async_test - async def test_get_historical_ticks(self): - """Test function `get_historical_ticks`.""" - result = await self._bridge.get_historical_ticks( - contract=sample_contracts.us_stock(), - start=datetime.datetime(2020, 3, 16, 9, 30), - end=datetime.datetime(2020, 3, 16, 9, 50) - ) - - self.assertTrue(result["ticks"]) - self.assertTrue(result["completed"]) - - @utils.async_test - async def test_get_historical_ticks_err(self): - """Test function `get_historical_ticks` for the error cases.""" - # start/end should not contains timezone info - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.us_stock(), - end=pytz.timezone("Asia/Hong_Kong").localize( - datetime.datetime(2020, 4, 28) - ) - ) - - # `start` is earlier than earliest available data point - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.us_stock(), - start=datetime.datetime(1972, 12, 12) - ) - - # `end` is earlier than `start` - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.us_stock(), - start=datetime.datetime(2020, 4, 29, 9, 30), - end=datetime.datetime(2020, 4, 28, 9, 30) - ) - - # Invalid `attempts` value - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.us_stock(), attempts=0 - ) - #endregion - Historical ticks - - #region - Live ticks - @utils.async_test - async def test_stream_live_ticks(self): - """Test function `stream_live_ticks`.""" - client: _client.IBClient = self._bridge._client - listener = utils.MockLiveTicksListener() - - req_id = await self._bridge.stream_live_ticks( - contract=sample_contracts.gbp_usd_fx(), - listener=listener, tick_type=dt.LiveTicks.BID_ASK - ) - self.assertIsNotNone(req_id) - - await asyncio.sleep(5) - self.assertTrue(listener.ticks) - client.cancel_live_ticks_stream(req_id=req_id) - await asyncio.sleep(0.5) - - @utils.async_test - async def test_stop_live_ticks_stream(self): - """Test functions `stop_live_ticks_stream`.""" - listener = utils.MockLiveTicksListener() - - stream_id = await self._bridge.stream_live_ticks( - contract=sample_contracts.gbp_usd_fx(), - listener=listener, tick_type=dt.LiveTicks.BID_ASK - ) - - await asyncio.sleep(2) - self._bridge.stop_live_ticks_stream(stream_id=stream_id) - await asyncio.sleep(0.5) - self.assertTrue(listener.finished) - - def test_stop_live_ticks_stream_err(self): - """Test functions `stop_live_ticks_stream` for the error cases.""" - with self.assertRaises(error.IBError): - self._bridge.stop_live_ticks_stream(stream_id=9999999) - #endregion - Live ticks - - @classmethod - def tearDownClass(cls): - cls._bridge.disconnect() From 621fe958c6ad1f04b75f9fafc93cb757d68d1fc9 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 18:04:20 +0000 Subject: [PATCH 076/126] Deprecate `get_historical_ticks` --- ibpy_native/bridge.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index d697c5a..1f4e5a5 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -7,6 +7,8 @@ import threading from typing import List, Optional +from deprecated import sphinx + from ibapi import contract as ib_contract from ibpy_native import account as ib_account @@ -224,6 +226,11 @@ async def get_earliest_data_point( return data_point.replace(tzinfo=None) + @sphinx.deprecated( + version="1.0.0", + reason="Function will be removed in next release. Use alternative " + "function `get_historical_ticks_v2` instead." + ) async def get_historical_ticks( self, contract: ib_contract.Contract, start: datetime.datetime=None, end: datetime.datetime=datetime.datetime.now(), From 947c5fed54903a0475cc6ec0c8c17e21db32af12 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 23:36:53 +0000 Subject: [PATCH 077/126] Add internal module `_typing` - Added internal moduie `_typing` for internal type aliases. --- ibpy_native/_internal/_client.py | 8 +++----- ibpy_native/_internal/_typing.py | 16 ++++++++++++++++ ibpy_native/_internal/_wrapper.py | 13 ++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 ibpy_native/_internal/_typing.py diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 54b4e3c..3c4b1c2 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -11,6 +11,7 @@ from ibpy_native import error from ibpy_native._internal import _global +from ibpy_native._internal import _typing from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype as dt @@ -431,11 +432,8 @@ def cancel_live_ticks_stream(self, req_id: int): #region - Private functions def _process_historical_ticks( - self, ticks: List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast,]], - start_time: datetime.datetime, - end_time: datetime.datetime + self, ticks: _typing.ResHistoricalTicks, + start_time: datetime.datetime, end_time: datetime.datetime ) -> _ProcessHistoricalTicksResult: """Processes the tick data returned from IB in function `fetch_historical_ticks`. diff --git a/ibpy_native/_internal/_typing.py b/ibpy_native/_internal/_typing.py new file mode 100644 index 0000000..f4dccc0 --- /dev/null +++ b/ibpy_native/_internal/_typing.py @@ -0,0 +1,16 @@ +"""Internal type aliases for cleaner code.""" +from typing import List, Union + +from ibapi import wrapper + +from ibpy_native import error + +HistoricalTickTypes = Union[wrapper.HistoricalTick, + wrapper.HistoricalTickBidAsk, + wrapper.HistoricalTickLast,] + +ResHistoricalTicks = Union[List[wrapper.HistoricalTick], + List[wrapper.HistoricalTickBidAsk], + List[wrapper.HistoricalTickLast],] + +WrapperResHistoricalTicks = List[Union[ResHistoricalTicks, bool, error.IBError]] diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 3c642c4..28008a6 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -1,13 +1,14 @@ """Code implementation of IB API resposes handling.""" # pylint: disable=protected-access import queue -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional from ibapi import contract as ib_contract from ibapi import wrapper from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import _typing from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners from ibpy_native.utils import finishable_queue as fq @@ -272,11 +273,7 @@ def _init_req_queue(self, req_id: int): self._req_queue[req_id] = fq.FinishableQueue(queue.Queue()) def _handle_historical_ticks_results( - self, req_id: int, - ticks: Union[List[wrapper.HistoricalTick], - List[wrapper.HistoricalTickBidAsk], - List[wrapper.HistoricalTickLast],], - done: bool + self, req_id: int, ticks: _typing.WrapperResHistoricalTicks, done: bool ): """Handles results return from functions `historicalTicks`, `historicalTicksBidAsk`, and `historicalTicksLast` by putting the @@ -287,9 +284,7 @@ def _handle_historical_ticks_results( self._req_queue[req_id].put(element=fq.Status.FINISHED) def _handle_live_ticks(self, req_id: int, - tick: Union[wrapper.HistoricalTick, - wrapper.HistoricalTickBidAsk, - wrapper.HistoricalTickLast,]): + tick: _typing.HistoricalTickTypes): """Handles live ticks passed to functions `tickByTickAllLast`, `tickByTickBidAsk`, and `tickByTickMidPoint` by putting the ticks received into corresponding queue. From d6b02220c74efa09a8f326cba41cd1c69596cd37 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 23:43:10 +0000 Subject: [PATCH 078/126] New client req historical ticks func - Implemented function `IBClient.req_historical_ticks` as a new way to request historical ticks from IB. --- ibpy_native/_internal/_client.py | 70 +++++++++++++++++++++++++ tests/internal/test_client.py | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 3c4b1c2..c14dbc8 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -197,6 +197,76 @@ async def resolve_head_timestamp( err_str="Failed to get the earliest available data point" ) + async def req_historical_ticks( + self, req_id: int, contract: ib_contract.Contract, + start_date_time: datetime.datetime, + show: dt.HistoricalTicks=dt.HistoricalTicks.TRADES + ) -> _typing.ResHistoricalTicks: + """Request historical tick data of the given instrument from IB. + + Args: + req_id (int): Request ID (ticker ID in IB API). + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + sufficient info to identify the instrument. + start_date_time (:obj:`datetime.datetime`): Time for the earliest + tick data to be included. + show (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, optional): + Type of data to be requested. Defaults to + `HistoricalTicks.TRADES`. + + Returns: + :obj:`ibpy_native._internal._typing.ResHistoricalTicks`: Tick data + returned from IB. + + Raises: + ValueError: If argument `start_date_time` is an aware `datetime` + object. + ibpy_native.error.IBError: If + - queue associated with argument `req_id` is being used by other + task; + - there's any error returned from IB; + - Data received from IB is indicated as incomplete. + + Notes: + Around 1000 ticks will be returned from IB. Ticks returned will + always cover a full second as describled in IB API document. + """ + # Error checking + if start_date_time.tzinfo is not None: + raise ValueError("Value of argument `start_date_time` must not " + "be an aware `datetime` object.") + # Pre-processing + try: + f_queue = self._wrapper.get_request_queue(req_id) + except error.IBError as err: + raise err + + converted_start_time = _global.TZ.localize(start_date_time) + + self.reqHistoricalTicks( + reqId=req_id, contract=contract, + startDateTime=converted_start_time.strftime(_global.TIME_FMT), + endDateTime="", numberOfTicks=1000, whatToShow=show.value, + useRth=0, ignoreSize=False, miscOptions=[] + ) + + result: _typing.WrapperResHistoricalTicks = await f_queue.get() + + if result: + if f_queue.status is fq.Status.ERROR: + # Handle error returned from IB + if isinstance(result[-1], error.IBError): + raise result[-1] + + if not result[1]: + raise error.IBError( + rid=req_id, err_code=error.IBErrorCode.RES_UNEXPECTED, + err_str="Not all historical tick data has been received " + "for this request. Please retry." + ) + + return result[0] + async def fetch_historical_ticks( self, req_id: int, contract: ib_contract.Contract, start: datetime.datetime, diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 5b1407a..d30e165 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -151,6 +151,95 @@ async def test_resolve_head_timestamp_err_1(self): show=datatype.EarliestDataPoint.ASK ) + @utils.async_test + async def test_req_historical_ticks_0(self): + """Test function `req_historical_ticks`. + + * Request tick data for `BidAsk`. + """ + result = await self._client.req_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start_date_time=self._start.replace(tzinfo=None), + show=datatype.HistoricalTicks.BID_ASK + ) + + self.assertTrue(result) + self.assertIsInstance(result, list) + self.assertIsInstance(result[0], wrapper.HistoricalTickBidAsk) + + @utils.async_test + async def test_req_historical_ticks_1(self): + """Test function `req_historical_ticks`. + + * Request tick data for `MidPoint`. + """ + result = await self._client.req_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start_date_time=self._start.replace(tzinfo=None), + show=datatype.HistoricalTicks.MIDPOINT + ) + + self.assertTrue(result) + self.assertIsInstance(result, list) + self.assertIsInstance(result[0], wrapper.HistoricalTick) + + @utils.async_test + async def test_req_historical_ticks_2(self): + """Test function `req_historical_ticks`. + + * Request tick data for `MidPoint`. + """ + result = await self._client.req_historical_ticks( + req_id=self._req_id, contract=sample_contracts.us_future(), + start_date_time=self._start.replace(tzinfo=None), + show=datatype.HistoricalTicks.TRADES + ) + + self.assertTrue(result) + self.assertIsInstance(result, list) + self.assertIsInstance(result[0], wrapper.HistoricalTickLast) + + @utils.async_test + async def test_req_historical_ticks_err_0(self): + """Test function `req_historical_ticks`. + + * Expect `ValueError` for aware datetime object passed in. + """ + with self.assertRaises(ValueError): + await self._client.req_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start_date_time=self._start, + show=datatype.HistoricalTicks.BID_ASK + ) + + @utils.async_test + async def test_req_historical_ticks_err_1(self): + """Test function `req_historical_ticks`. + + * `IBError` raised due to `req_id` is being occupied by other task + """ + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): + await self._client.req_historical_ticks( + req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), + start_date_time=self._start.replace(tzinfo=None), + show=datatype.HistoricalTicks.BID_ASK + ) + + @utils.async_test + async def test_req_historical_ticks_err_2(self): + """Test function `req_historical_ticks`. + + * `IBError` raised due to unresolvable IB `Contract` + """ + self._wrapper.get_request_queue(req_id=self._req_id) + with self.assertRaises(error.IBError): + await self._client.req_historical_ticks( + req_id=self._req_id, contract=contract.Contract(), + start_date_time=self._start.replace(tzinfo=None), + show=datatype.HistoricalTicks.BID_ASK + ) + @utils.async_test async def test_fetch_historical_ticks_0(self): """Test function `fetch_historical_ticks`. From b2a35a9d749244f1080fad547b287c347a4a940a Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Mon, 15 Feb 2021 23:53:44 +0000 Subject: [PATCH 079/126] Deprecated `IBClient.fetch_historical_ticks` --- ibpy_native/_internal/_client.py | 6 ++++++ tests/internal/test_client.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index c14dbc8..6f3e524 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -3,6 +3,7 @@ import datetime from typing import Any, List, Union +from deprecated import sphinx from typing_extensions import TypedDict from ibapi import client as ib_client @@ -267,6 +268,11 @@ async def req_historical_ticks( return result[0] + @sphinx.deprecated( + version="1.0.0", + reason="Function will be removed in next release. Use alternative " + "function `req_historical_ticks` instead." + ) async def fetch_historical_ticks( self, req_id: int, contract: ib_contract.Contract, start: datetime.datetime, diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index d30e165..a139f20 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -240,6 +240,7 @@ async def test_req_historical_ticks_err_2(self): show=datatype.HistoricalTicks.BID_ASK ) + # region - Deprecated @utils.async_test async def test_fetch_historical_ticks_0(self): """Test function `fetch_historical_ticks`. @@ -362,6 +363,7 @@ async def test_fetch_historical_ticks_err_3(self): start=self._start, end=self._end, show=datatype.HistoricalTicks.TRADES ) + #endregion - Deprecated @classmethod def tearDownClass(cls): From 7621b6d759a558ef319e04eff34eb64835bdb331 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 16 Feb 2021 11:47:19 +0000 Subject: [PATCH 080/126] Add `IBBridge.req_historical_ticks` - Added function `req_historical_ticks` and corresponding unit tests. - Added data type `datatype.ResHistoricalTicks`. --- ibpy_native/bridge.py | 100 ++++++++++++++++++++++++++++++++- ibpy_native/utils/datatype.py | 29 +++++++--- tests/test_bridge.py | 103 +++++++++++++++++++++++++++++++++- 3 files changed, 222 insertions(+), 10 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 1f4e5a5..64c7029 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -5,7 +5,7 @@ import asyncio import datetime import threading -from typing import List, Optional +from typing import Iterator, List, Optional from deprecated import sphinx @@ -226,6 +226,104 @@ async def get_earliest_data_point( return data_point.replace(tzinfo=None) + async def req_historical_ticks( + self, contract: ib_contract.Contract, + start: Optional[datetime.datetime]=None, + end: Optional[datetime.datetime]=None, + tick_type: dt.HistoricalTicks=dt.HistoricalTicks.TRADES, + retry: int=0 + ) -> Iterator[dt.ResHistoricalTicks]: + """Retrieve historical tick data for specificed instrument/contract + from IB. + + Args: + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + sufficient info to identify the instrument. + start (:obj:`datetime.datetime`, optional): Datetime for the + earliest tick data to be included. If is `None`, the start time + will be set as the earliest data point. Defaults to `None`. + end (:obj:`datetime.datetime`, optional): Datetime for the latest + tick data to be included. If is `None`, the end time will be + set as now. Defaults to `None`. + tick_type (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, + optional): Type of tick data. Defaults to + `HistoricalTicks.TRADES`. + retry (int): Max retry attempts if error occur before terminating + the task and rasing the error. + + Yields: + :obj:ibpy_native.utils.datatype.ResHistoricalTicks`: Tick data + received from IB. Attribute `completed` indicates if all ticks + within the specified time period are received. + + Raises: + ValueError: If either argument `start` or `end` is not an native + `datetime` object; + ibpy_native.error.IBError: If + - `contract` passed in is unresolvable; + - there is any issue raised from the request function while + excuteing the task, and max retry attemps has been reached. + """ + # Error checking + if start.tzinfo is not None or end.tzinfo is not None: + raise ValueError("Value of argument `start` & `end` must be an " + "native `datetime` object.") + # Prep start and end time + try: + if tick_type is dt.HistoricalTicks.TRADES: + head_time = await self.get_earliest_data_point(contract) + else: + head_time_ask = await self.get_earliest_data_point( + contract, data_type=dt.EarliestDataPoint.ASK) + head_time_bid = await self.get_earliest_data_point( + contract, data_type=dt.EarliestDataPoint.BID) + head_time = (head_time_ask if head_time_ask < head_time_bid + else head_time_bid) + except error.IBError as err: + raise err + + start_date_time = head_time if head_time > start else start + end_date_time = datetime.datetime.now() if end is None else end + + # Request tick data + finished = False + retry_attemps = 0 + + while not finished: + try: + ticks = await self._client.req_historical_ticks( + req_id=self._wrapper.next_req_id, contract=contract, + start_date_time=start_date_time, show=tick_type + ) + except error.IBError as err: + if retry_attemps < retry: + continue + raise err + + if ticks: + # Drop the 1st tick as tick time of it is `start_date_time` + # - 1 second + del ticks[0] + # Determine if it should fetch next batch of data + last_tick_time = datetime.datetime.fromtimestamp( + timestamp=ticks[-1].time, tz=_global.TZ).replace(tzinfo=None) + if last_tick_time >= end_date_time: + # All ticks within the specified time period are received + finished = True + for i in range(len(ticks) - 1, -1, -1): + data_time = datetime.datetime.fromtimestamp( + timestamp=ticks[i].time, tz=_global.TZ).replace(tzinfo=None) + if data_time > end_date_time: + del ticks[i] + else: + break + else: + # Ready for next request + start_date_time = (last_tick_time + + datetime.timedelta(seconds=1)) + # Yield the result + yield dt.ResHistoricalTicks(ticks=ticks, completed=finished) + @sphinx.deprecated( version="1.0.0", reason="Function will be removed in next release. Use alternative " diff --git a/ibpy_native/utils/datatype.py b/ibpy_native/utils/datatype.py index a67efb2..3bca347 100644 --- a/ibpy_native/utils/datatype.py +++ b/ibpy_native/utils/datatype.py @@ -1,10 +1,11 @@ """Enums/Types for parameters or return objects.""" import enum -from typing import List, Union +from typing import List, NamedTuple, Union from typing_extensions import TypedDict from ibapi import wrapper +#region - Argument options @enum.unique class EarliestDataPoint(enum.Enum): """Data type options defined for earliest data point.""" @@ -19,6 +20,16 @@ class HistoricalTicks(enum.Enum): MIDPOINT = "MIDPOINT" TRADES = "TRADES" +@enum.unique +class LiveTicks(enum.Enum): + """Data types defined for live tick data.""" + ALL_LAST = "AllLast" + BID_ASK = "BidAsk" + MIDPOINT = "MidPoint" + LAST = "Last" +#endregion - Argument options + +#region - Return type class HistoricalTicksResult(TypedDict): """Use to type hint the returns of `IBBridge.get_historical_ticks`.""" ticks: List[Union[ @@ -28,10 +39,12 @@ class HistoricalTicksResult(TypedDict): ]] completed: bool -@enum.unique -class LiveTicks(enum.Enum): - """Data types defined for live tick data.""" - ALL_LAST = "AllLast" - BID_ASK = "BidAsk" - MIDPOINT = "MidPoint" - LAST = "Last" +class ResHistoricalTicks(NamedTuple): + """Return type of function `bridge.IBBridge.get_historical_ticks_v2`.""" + ticks: List[Union[ + wrapper.HistoricalTick, + wrapper.HistoricalTickBidAsk, + wrapper.HistoricalTickLast + ]] + completed: bool +#endregion - Return type diff --git a/tests/test_bridge.py b/tests/test_bridge.py index 6f2c303..fe87cb7 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -221,7 +221,7 @@ def setUpClass(cls): def setUp(self): self._end = (datetime.datetime.now() + relativedelta.relativedelta( weekday=relativedelta.FR(-1)) - ).replace(hour=12, minute=0) + ).replace(hour=12, minute=0, second=0, microsecond=0) self._start = self._end - datetime.timedelta(minutes=5) @utils.async_test @@ -247,6 +247,106 @@ async def test_get_earliest_data_point_err(self): data_type=datatype.EarliestDataPoint.BID ) + @utils.async_test + async def test_req_historical_ticks_0(self): + """Test function `req_historical_ticks`. + + * Reqest tick data for `BID_ASK`. + """ + async for result in self._bridge.req_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), start=self._start, + end=self._end, tick_type=datatype.HistoricalTicks.BID_ASK, + retry=0 + ): + self.assertTrue(result.ticks) + self.assertIsInstance(result.ticks[0], wrapper.HistoricalTickBidAsk) + + @utils.async_test + async def test_req_historical_ticks_1(self): + """Test function `req_historical_ticks`. + + * Reqest tick data for `MIDPOINT`. + """ + async for result in self._bridge.req_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), start=self._start, + end=self._end, tick_type=datatype.HistoricalTicks.MIDPOINT, + retry=0 + ): + self.assertTrue(result.ticks) + self.assertIsInstance(result.ticks[0], wrapper.HistoricalTick) + + @utils.async_test + async def test_req_historical_ticks_2(self): + """Test function `req_historical_ticks`. + + * Reqest tick data for `TRADES`. + """ + async for result in self._bridge.req_historical_ticks( + contract=sample_contracts.us_future(), start=self._start, + end=self._end, tick_type=datatype.HistoricalTicks.TRADES, + retry=0 + ): + self.assertTrue(result.ticks) + self.assertIsInstance(result.ticks[0], wrapper.HistoricalTickLast) + + @utils.async_test + async def test_req_historical_ticks_err_0(self): + """Test function `req_historical_ticks`. + + * Expect `ValueError` due to value of `start` is an aware datetime + object. + """ + with self.assertRaises(ValueError): + async for _ in self._bridge.req_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=_global.TZ.localize(self._start), end=self._end, + tick_type=datatype.HistoricalTicks.BID_ASK, retry=0 + ): + pass + + @utils.async_test + async def test_req_historical_ticks_err_1(self): + """Test function `req_historical_ticks`. + + * Expect `ValueError` due to value of `end` is an aware datetime + object. + """ + with self.assertRaises(ValueError): + async for _ in self._bridge.req_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), + start=self._start, end=_global.TZ.localize(self._end), + tick_type=datatype.HistoricalTicks.BID_ASK, retry=0 + ): + pass + + @utils.async_test + async def test_req_historical_ticks_err_2(self): + """Test function `req_historical_ticks`. + + * Expect `IBError` due to unresolvable `Contract`. + """ + with self.assertRaises(error.IBError): + async for _ in self._bridge.req_historical_ticks( + contract=contract.Contract(), start=self._start, end=self._end, + tick_type=datatype.HistoricalTicks.BID_ASK, retry=0 + ): + pass + + @utils.async_test + async def test_req_historical_ticks_err_3(self): + """Test function `req_historical_ticks`. + + * Expect `IBError` due to IB returning an error + """ + with self.assertRaises(error.IBError): + async for _ in self._bridge.req_historical_ticks( + contract=sample_contracts.gbp_usd_fx(), start=self._start, + end=self._end, tick_type=datatype.HistoricalTicks.TRADES, + retry=0 + ): + pass + + #region - Deprecated @utils.async_test async def test_get_historical_ticks_0(self): """Test function `get_historical_ticks`. @@ -369,6 +469,7 @@ async def test_get_historical_ticks_err_4(self): contract=contract.Contract(), start=self._start, end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 ) + #endregion - Deprecated @classmethod def tearDownClass(cls): From 3cdcd1c0baccef52ac7c91a3e60af43dcd60f660 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 16 Feb 2021 12:06:43 +0000 Subject: [PATCH 081/126] Remove deprecated - Removed old deprecated functions and related things for fetching historical ticks. --- ibpy_native/_internal/_client.py | 224 ------------------------------- ibpy_native/bridge.py | 160 ---------------------- ibpy_native/utils/datatype.py | 10 -- tests/internal/test_client.py | 125 ----------------- tests/test_bridge.py | 125 ----------------- 5 files changed, 644 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 6f3e524..0d82e7b 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -3,9 +3,6 @@ import datetime from typing import Any, List, Union -from deprecated import sphinx -from typing_extensions import TypedDict - from ibapi import client as ib_client from ibapi import contract as ib_contract from ibapi import wrapper as ib_wrapper @@ -18,13 +15,6 @@ from ibpy_native.utils import datatype as dt from ibpy_native.utils import finishable_queue as fq -class _ProcessHistoricalTicksResult(TypedDict): - """Use for type hint the returns of `IBClient.fetch_historical_ticks`.""" - ticks: List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast,]] - next_end_time: datetime.datetime - class IBClient(ib_client.EClient): """The client calls the native methods from IBWrapper instead of overriding native methods. @@ -268,168 +258,6 @@ async def req_historical_ticks( return result[0] - @sphinx.deprecated( - version="1.0.0", - reason="Function will be removed in next release. Use alternative " - "function `req_historical_ticks` instead." - ) - async def fetch_historical_ticks( - self, req_id: int, contract: ib_contract.Contract, - start: datetime.datetime, - end: datetime.datetime=datetime.datetime.now().astimezone(_global.TZ), - show: dt.HistoricalTicks=dt.HistoricalTicks.TRADES - ) -> dt.HistoricalTicksResult: - """Fetch the historical ticks data for a given instrument from IB. - - Args: - req_id (int): Request ID (ticker ID in IB API). - contract (:obj:`ibapi.contract.Contract`): `Contract` object with - sufficient info to identify the instrument. - start (:obj:`datetime.datetime`): The time for the earliest tick - data to be included. - end (:obj:`datetime.datetime`, optional): The time for the latest - tick data to be included. Defaults to now. - show (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, optional): - Type of data requested. Defaults to `HistoricalTicks.TRADES`. - - Returns: - :obj:`ibpy_native.utils.datatype.HistoricalTicksResult`: Ticks - data (fetched recursively to get around IB 1000 ticks limit) - - Raises: - ValueError: If - - `tzinfo` of `start` & `end` do not align; - - Value of start` > `end`. - ibpy_native.error.IBError: If - - queue associated with `req_id` is being used by other tasks; - - there's any error returned from IB before any tick data is - fetched successfully; - """ - # Pre-process & error checking - if type(start.tzinfo) is not type(end.tzinfo): - raise ValueError( - "Timezone of the start time and end time must be the same" - ) - - if start.timestamp() > end.timestamp(): - raise ValueError( - "Specificed start time cannot be later than end time" - ) - - # Time to fetch the ticks - try: - f_queue = self._wrapper.get_request_queue(req_id=req_id) - except error.IBError as err: - raise err - - all_ticks: list = [] - - real_start_time = ( - _global.TZ.localize(start) if start.tzinfo is None else start - ) - - next_end_time = ( - _global.TZ.localize(end) if end.tzinfo is None else end - ) - - finished = False - - print(f"Getting historical ticks data [{show}] for the given" - " instrument from IB...") - - while not finished: - self.reqHistoricalTicks( - reqId=req_id, contract=contract, startDateTime="", - endDateTime=next_end_time.strftime(_global.TIME_FMT), - numberOfTicks=1000, whatToShow=show.value, useRth=0, - ignoreSize=False, miscOptions=[] - ) - - res: List[List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast,]], - bool,] = await f_queue.get() - - if res and f_queue.status is fq.Status.ERROR: - # Response received and internal queue reports error - if isinstance(res[-1], error.IBError): - if all_ticks: - if res[-1].err_code == (error.IBErrorCode - .INVALID_CONTRACT): - # Continue if IB returns error `No security - # definition has been found for the request` as - # it's not possible that ticks can be fetched - # on pervious attempts for an invalid contract. - f_queue.reset() - continue - - # Encounters error. Returns ticks fetched in - # pervious loop(s). - break - - res[-1].err_extra = next_end_time - raise res[-1] - - raise self._unknown_error(req_id=req_id, extra=next_end_time) - - if res: - if len(res) != 2: - # The result should be a list that contains 2 items: - # [ticks: ListOfHistoricalTick(BidAsk/Last), done: bool] - if all_ticks: - print("Abnormal result received while fetching the " - f"remaining ticks: returning {len(all_ticks)} " - "ticks fetched") - break - - raise error.IBError( - rid=req_id, err_code=error.IBErrorCode.RES_UNEXPECTED, - err_str="[Abnormal] Incorrect number of items " - f"received: {len(res)}" - ) - - # Process the data - processed_result = self._process_historical_ticks( - ticks=res[0], - start_time=real_start_time, - end_time=next_end_time - ) - all_ticks.extend(processed_result["ticks"]) - next_end_time = processed_result["next_end_time"] - - print( - f"{len(all_ticks)} ticks fetched (" - f"{len(processed_result['ticks'])} new ticks); Next end " - f"time - {next_end_time.strftime(_global.TIME_FMT)}" - ) - - if next_end_time.timestamp() <= real_start_time.timestamp(): - # All tick data within the specificed range has been - # fetched from IB. Finishes the while loop. - finished = True - - break - - # Resets the queue for next historical ticks request - f_queue.reset() - - else: - if all_ticks: - print("Request failed while fetching the remaining ticks: " - f"returning {len(all_ticks)} ticks fetched") - - break - - raise error.IBError( - rid=req_id, err_code=error.IBErrorCode.RES_NO_CONTENT, - err_str="Failed to get historical ticks data" - ) - - all_ticks.reverse() - - return {"ticks": all_ticks, - "completed": finished,} - #region - Stream live tick data async def stream_live_ticks( self, req_id: int, contract: ib_contract.Contract, @@ -507,58 +335,6 @@ def cancel_live_ticks_stream(self, req_id: int): #endregion - Stream live tick data #region - Private functions - def _process_historical_ticks( - self, ticks: _typing.ResHistoricalTicks, - start_time: datetime.datetime, end_time: datetime.datetime - ) -> _ProcessHistoricalTicksResult: - """Processes the tick data returned from IB in function - `fetch_historical_ticks`. - """ - if ticks: - # Exclude record(s) which are earlier than specified start time. - exclude_to_idx = -1 - - for idx, tick in enumerate(ticks): - if tick.time >= start_time.timestamp(): - exclude_to_idx = idx - del idx, tick - - break - - if exclude_to_idx > -1: - if exclude_to_idx > 0: - ticks = ticks[exclude_to_idx:] - - # Reverses the list of tick data as the data are fetched - # reversely from end time. Thus, reverses the list `ticks` - # to append the tick data to `all_ticks` more efficient. - ticks.reverse() - - # Updates the next end time to prepare to fetch more - # data again from IB - end_time = datetime.datetime.fromtimestamp( - ticks[-1].time - ).astimezone(end_time.tzinfo) - else: - # Ticks data received from IB but all records included in - # response are earlier than the start time. - ticks = [] - end_time = start_time - else: - # Floor the `end_time` to pervious 30 minutes point to avoid IB - # cutting off the data at the date start point for the instrument. - # e.g. - delta = datetime.timedelta(minutes=end_time.minute % 30, - seconds=end_time.second) - - if delta.total_seconds() == 0: - end_time = end_time - datetime.timedelta(minutes=30) - else: - end_time = end_time - delta - - return {"ticks": ticks, - "next_end_time": end_time,} - def _unknown_error(self, req_id: int, extra: Any = None): """Constructs `IBError` with error code `UNKNOWN` diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 64c7029..52d56c1 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -7,8 +7,6 @@ import threading from typing import Iterator, List, Optional -from deprecated import sphinx - from ibapi import contract as ib_contract from ibpy_native import account as ib_account @@ -323,164 +321,6 @@ async def req_historical_ticks( datetime.timedelta(seconds=1)) # Yield the result yield dt.ResHistoricalTicks(ticks=ticks, completed=finished) - - @sphinx.deprecated( - version="1.0.0", - reason="Function will be removed in next release. Use alternative " - "function `get_historical_ticks_v2` instead." - ) - async def get_historical_ticks( - self, contract: ib_contract.Contract, start: datetime.datetime=None, - end: datetime.datetime=datetime.datetime.now(), - data_type: dt.HistoricalTicks=dt.HistoricalTicks.TRADES, - attempts: int=1 - ) -> dt.HistoricalTicksResult: - """Retrieve historical ticks data for specificed instrument/contract - from IB. - - Args: - contract (:obj:`ibapi.contract.Contract`): `Contract` object with - sufficient info to identify the instrument. - start (:obj:`datetime.datetime`, optional): The time for the - earliest tick data to be included. Defaults to `None`. - end (:obj:`datetime.datetime`, optional): The time for the latest - tick data to be included. Defaults to now. - data_type (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, - optional): Type of data for the ticks. Defaults to - `HistoricalTicks.TRADES`. - attempts (int, optional): Attemp(s) to try requesting the historical - ticks. Passing -1 into this argument will let the function - retries for infinity times until all available ticks are received. Defaults to 1. - - Returns: - :obj:`ibpy_native.utils.datatype.IBTicksResult`: Ticks returned - from IB and a boolean to indicate if the returning object - contains all available ticks. - - Raises: - ValueError: If - - argument `start` or `end` contains the timezone info; - - timestamp of `start` is earlier than the earliest available - datapoint. - - timestamp of `end` is earlier than `start` or earliest - available datapoint; - - value of `attempts` < 1 and != -1. - ibpy_native.error.IBError: If there is any issue raised from the - request function while excuteing the task, with `attempts - reduced to 0 and no tick fetched successfully in pervious - attempt(s). - """ - all_ticks = [] - next_end_time = _global.TZ.localize(dt=end) - - # Error checking - if end.tzinfo is not None or (start is not None and - start.tzinfo is not None): - raise ValueError( - "Timezone should not be specified in either `start` or `end`." - ) - - try: - head_timestamp = datetime.datetime.fromtimestamp( - await self._client.resolve_head_timestamp( - req_id=self._wrapper.next_req_id, contract=contract, - show=dt.EarliestDataPoint.TRADES if ( - data_type is dt.HistoricalTicks.TRADES - ) else dt.EarliestDataPoint.BID - ) - ).astimezone(tz=_global.TZ) - except error.IBError as err: - raise err - - if start is not None: - if start.timestamp() < head_timestamp.timestamp(): - raise ValueError( - "Specificed start time is earlier than the earliest " - "available datapoint - " - f"{head_timestamp.strftime(_global.TIME_FMT)}" - ) - if end.timestamp() < start.timestamp(): - raise ValueError( - "Specificed end time cannot be earlier than start time" - ) - - start = _global.TZ.localize(dt=start) - else: - start = head_timestamp - - if next_end_time.timestamp() < head_timestamp.timestamp(): - raise ValueError( - "Specificed end time is earlier than the earliest available " - f"datapoint - {head_timestamp.strftime(_global.TIME_FMT)}" - ) - - if attempts < 1 and attempts != -1: - raise ValueError( - "Value of argument `attempts` must be positive integer or -1" - ) - - # Process the request - while attempts > 0 or attempts == -1: - attempts = attempts - 1 if attempts != -1 else attempts - - try: - res = await self._client.fetch_historical_ticks( - req_id=self._wrapper.next_req_id, contract=contract, - start=start, end=next_end_time, show=data_type - ) - - #  `ticks[1]` is a boolean represents if the data are all - # fetched without timeout - if res["completed"]: - res["ticks"].extend(all_ticks) - - return { - "ticks": res["ticks"], - "completed": True, - } - - res["ticks"].extend(all_ticks) - all_ticks = res["ticks"] - - next_end_time = datetime.datetime.fromtimestamp( - res[0][0].time - ).astimezone(_global.TZ) - except ValueError as err: - raise err - except error.IBError as err: - if err.err_code == error.IBErrorCode.DUPLICATE_TICKER_ID: - # Restore the attempts count for error `Duplicate ticker ID` - # as it seems like sometimes IB cannot release the ID used - # as soon as it has responded the request while the - # reverse historical ticks request approaching the start - # time with all available ticks fetched and throws - # the duplicate ticker ID error. - attempts = attempts + 1 if attempts != -1 else attempts - - next_end_time: datetime = err.err_extra - - continue - - if attempts > 0: - if all_ticks: - # Updates the end time for next attempt - next_end_time = datetime.datetime.fromtimestamp( - all_ticks[0].time - ).astimezone(_global.TZ) - - continue - - if attempts == 0 and all_ticks: - print("Reached maximum attempts. Ending...") - break - - if attempts == 0 and not all_ticks: - raise err - - return { - "ticks": all_ticks, - "completed": False, - } #endregion - Historical data #region - Live data diff --git a/ibpy_native/utils/datatype.py b/ibpy_native/utils/datatype.py index 3bca347..1f58940 100644 --- a/ibpy_native/utils/datatype.py +++ b/ibpy_native/utils/datatype.py @@ -1,7 +1,6 @@ """Enums/Types for parameters or return objects.""" import enum from typing import List, NamedTuple, Union -from typing_extensions import TypedDict from ibapi import wrapper @@ -30,15 +29,6 @@ class LiveTicks(enum.Enum): #endregion - Argument options #region - Return type -class HistoricalTicksResult(TypedDict): - """Use to type hint the returns of `IBBridge.get_historical_ticks`.""" - ticks: List[Union[ - wrapper.HistoricalTick, - wrapper.HistoricalTickBidAsk, - wrapper.HistoricalTickLast - ]] - completed: bool - class ResHistoricalTicks(NamedTuple): """Return type of function `bridge.IBBridge.get_historical_ticks_v2`.""" ticks: List[Union[ diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index a139f20..51a4d68 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -240,131 +240,6 @@ async def test_req_historical_ticks_err_2(self): show=datatype.HistoricalTicks.BID_ASK ) - # region - Deprecated - @utils.async_test - async def test_fetch_historical_ticks_0(self): - """Test function `fetch_historical_ticks`. - - * Request tick data for `BidAsk`. - """ - result = await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), - start=self._start, end=self._end, - show=datatype.HistoricalTicks.BID_ASK - ) - - self.assertTrue(result) # Expect data returned from IB - self.assertIsInstance(result, dict) # Expect data returned as `dict` - # Expect ticks return in a list - self.assertIsInstance(result["ticks"], list) - # Expect `True` as a prove of all data within requested period are - # received - self.assertTrue(result["completed"]) - # Expect received tick type is `HistoricalTickBidAsk` - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickBidAsk) - - @utils.async_test - async def test_fetch_historical_ticks_1(self): - """Test function `fetch_historical_ticks`. - - * Request tick data for `MidPoint`. - """ - result = await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), - start=self._start, end=self._end, - show=datatype.HistoricalTicks.MIDPOINT - ) - - self.assertTrue(result) # Expect data returned from IB - self.assertIsInstance(result, dict) # Expect data returned as `dict` - # Expect ticks return in a list - self.assertIsInstance(result["ticks"], list) - # Expect `True` as a prove of all data within requested period are - # received - self.assertTrue(result["completed"]) - # Expect received tick type is `HistoricalTickBidAsk` - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTick) - - @utils.async_test - async def test_fetch_historical_ticks_2(self): - """Test function `fetch_historical_ticks`. - - * Request tick data for `Last`. - """ - result = await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.us_future(), - start=self._start, end=self._end, - show=datatype.HistoricalTicks.TRADES - ) - - self.assertTrue(result) # Expect data returned from IB - self.assertIsInstance(result, dict) # Expect data returned as `dict` - # Expect ticks return in a list - self.assertIsInstance(result["ticks"], list) - # Expect `True` as a prove of all data within requested period are - # received - self.assertTrue(result["completed"]) - # Expect received tick type is `HistoricalTickBidAsk` - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickLast) - - @utils.async_test - async def test_fetch_historical_ticks_err_0(self): - """Test function `fetch_historical_ticks`. - - * `ValueError` raised due to inconsistences timezone set for `start` - and `end` time. - """ - with self.assertRaises(ValueError): - await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), - start=datetime.datetime.now().astimezone( - pytz.timezone("Asia/Hong_Kong") - ), - end=self._end, show=datatype.HistoricalTicks.BID_ASK - ) - - @utils.async_test - async def test_fetch_historical_ticks_err_1(self): - """Test function `fetch_historical_ticks`. - - * `ValueError` raised due to value of `start` > `end`. - """ - with self.assertRaises(ValueError): - await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), - start=self._end, end=self._start, - show=datatype.HistoricalTicks.BID_ASK - ) - - @utils.async_test - async def test_fetch_historical_ticks_err_2(self): - """Test function `fetch_historical_ticks`. - - * `IBError` raised due to `req_id` is being occupied by other task. - """ - # Mock queue occupation - self._wrapper.get_request_queue(req_id=self._req_id) - with self.assertRaises(error.IBError): - await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=sample_contracts.gbp_usd_fx(), - start=self._start, end=self._end, - show=datatype.HistoricalTicks.BID_ASK - ) - - @utils.async_test - async def test_fetch_historical_ticks_err_3(self): - """Test function `fetch_historical_ticks`. - - * Error returned from IB for invalid contract. - """ - with self.assertRaises(error.IBError): - await self._client.fetch_historical_ticks( - req_id=self._req_id, contract=contract.Contract(), - start=self._start, end=self._end, - show=datatype.HistoricalTicks.TRADES - ) - #endregion - Deprecated - @classmethod def tearDownClass(cls): cls._client.disconnect() diff --git a/tests/test_bridge.py b/tests/test_bridge.py index fe87cb7..943c6fa 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -346,131 +346,6 @@ async def test_req_historical_ticks_err_3(self): ): pass - #region - Deprecated - @utils.async_test - async def test_get_historical_ticks_0(self): - """Test function `get_historical_ticks`. - - * Request tick data for `BID_ASK`. - """ - result = await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), start=self._start, - end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, - attempts=1 - ) - - self.assertTrue(result["ticks"]) - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickBidAsk) - self.assertTrue(result["completed"]) - - @utils.async_test - async def test_get_historical_ticks_1(self): - """Test function `get_historical_ticks`. - - * Request tick data for `MIDPOINT`. - """ - result = await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), start=self._start, - end=self._end, data_type=datatype.HistoricalTicks.MIDPOINT, - attempts=1 - ) - - self.assertTrue(result["ticks"]) - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTick) - self.assertTrue(result["completed"]) - - @utils.async_test - async def test_get_historical_ticks_2(self): - """Test function `get_historical_ticks`. - - * Request tick data for `TRADES`. - """ - result = await self._bridge.get_historical_ticks( - contract=sample_contracts.us_future(), start=self._start, - end=self._end, data_type=datatype.HistoricalTicks.TRADES, - attempts=1 - ) - - self.assertTrue(result["ticks"]) - self.assertIsInstance(result["ticks"][0], wrapper.HistoricalTickLast) - self.assertTrue(result["completed"]) - - @utils.async_test - async def test_get_historical_ticks_err_0(self): - """Test function `get_historical_ticks`. - - * Expect `ValueError` as argument `start` or `end` contains timezone - info. - """ - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), - start=_global.TZ.localize(dt=self._start), - end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, - attempts=1 - ) - - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), - start=self._start, end=_global.TZ.localize(dt=self._end), - data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 - ) - - @utils.async_test - async def test_get_historical_ticks_err_1(self): - """Test function `get_historical_ticks`. - - * Expect `ValueError` as value of `start` is earlier than the earliest - available datapoint. - """ - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), - start=datetime.datetime(year=1990, month=1, day=1), - end=self._end, data_type=datatype.HistoricalTicks.BID_ASK, - attempts=1 - ) - - @utils.async_test - async def test_get_historical_ticks_err_2(self): - """Test function `get_historical_ticks`. - - * Expect `ValueError` as value of `end` is earlier than `start` - """ - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), - start=self._end, end=self._start, - data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 - ) - - @utils.async_test - async def test_get_historical_ticks_err_3(self): - """Test function `get_historical_ticks`. - - * Expect `ValueError` as value of `attempts` < 1 and != -1. - """ - with self.assertRaises(ValueError): - await self._bridge.get_historical_ticks( - contract=sample_contracts.gbp_usd_fx(), - start=self._start, end=self._end, - data_type=datatype.HistoricalTicks.BID_ASK, attempts=-2 - ) - - @utils.async_test - async def test_get_historical_ticks_err_4(self): - """Test function `get_historical_ticks`. - - * Expect `IBError` due to invalid `Contract` passed in. - """ - with self.assertRaises(error.IBError): - await self._bridge.get_historical_ticks( - contract=contract.Contract(), start=self._start, end=self._end, - data_type=datatype.HistoricalTicks.BID_ASK, attempts=1 - ) - #endregion - Deprecated - @classmethod def tearDownClass(cls): cls._bridge.disconnect() From 756b02c5825bb54d3b45551d177f88343026cb36 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 16 Feb 2021 12:15:46 +0000 Subject: [PATCH 082/126] No more `datatype as dt` --- ibpy_native/_internal/_client.py | 8 ++++---- ibpy_native/bridge.py | 18 +++++++++--------- tests/internal/test_client.py | 2 -- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 0d82e7b..a425f55 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -12,7 +12,7 @@ from ibpy_native._internal import _typing from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners -from ibpy_native.utils import datatype as dt +from ibpy_native.utils import datatype from ibpy_native.utils import finishable_queue as fq class IBClient(ib_client.EClient): @@ -130,7 +130,7 @@ async def resolve_contracts( async def resolve_head_timestamp( self, req_id: int, contract: ib_contract.Contract, - show: dt.EarliestDataPoint=dt.EarliestDataPoint.TRADES + show: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES ) -> int: """Fetch the earliest available data point for a given instrument from IB. @@ -191,7 +191,7 @@ async def resolve_head_timestamp( async def req_historical_ticks( self, req_id: int, contract: ib_contract.Contract, start_date_time: datetime.datetime, - show: dt.HistoricalTicks=dt.HistoricalTicks.TRADES + show: datatype.HistoricalTicks=datatype.HistoricalTicks.TRADES ) -> _typing.ResHistoricalTicks: """Request historical tick data of the given instrument from IB. @@ -262,7 +262,7 @@ async def req_historical_ticks( async def stream_live_ticks( self, req_id: int, contract: ib_contract.Contract, listener: listeners.LiveTicksListener, - tick_type: dt.LiveTicks=dt.LiveTicks.LAST + tick_type: datatype.LiveTicks=datatype.LiveTicks.LAST ): """Request to stream live tick data. diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 52d56c1..3859950 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -16,7 +16,7 @@ from ibpy_native._internal import _global from ibpy_native._internal import _wrapper from ibpy_native.interfaces import listeners -from ibpy_native.utils import datatype as dt +from ibpy_native.utils import datatype class IBBridge: """Public class to bridge between `ibpy-native` & IB API. @@ -190,7 +190,7 @@ async def search_detailed_contracts( #region - Historical data async def get_earliest_data_point( self, contract: ib_contract.Contract, - data_type: dt.EarliestDataPoint=dt.EarliestDataPoint.TRADES + data_type: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES ) -> datetime: """Returns the earliest data point of specified contract. @@ -228,9 +228,9 @@ async def req_historical_ticks( self, contract: ib_contract.Contract, start: Optional[datetime.datetime]=None, end: Optional[datetime.datetime]=None, - tick_type: dt.HistoricalTicks=dt.HistoricalTicks.TRADES, + tick_type: datatype.HistoricalTicks=datatype.HistoricalTicks.TRADES, retry: int=0 - ) -> Iterator[dt.ResHistoricalTicks]: + ) -> Iterator[datatype.ResHistoricalTicks]: """Retrieve historical tick data for specificed instrument/contract from IB. @@ -268,13 +268,13 @@ async def req_historical_ticks( "native `datetime` object.") # Prep start and end time try: - if tick_type is dt.HistoricalTicks.TRADES: + if tick_type is datatype.HistoricalTicks.TRADES: head_time = await self.get_earliest_data_point(contract) else: head_time_ask = await self.get_earliest_data_point( - contract, data_type=dt.EarliestDataPoint.ASK) + contract, data_type=datatype.EarliestDataPoint.ASK) head_time_bid = await self.get_earliest_data_point( - contract, data_type=dt.EarliestDataPoint.BID) + contract, data_type=datatype.EarliestDataPoint.BID) head_time = (head_time_ask if head_time_ask < head_time_bid else head_time_bid) except error.IBError as err: @@ -320,14 +320,14 @@ async def req_historical_ticks( start_date_time = (last_tick_time + datetime.timedelta(seconds=1)) # Yield the result - yield dt.ResHistoricalTicks(ticks=ticks, completed=finished) + yield datatype.ResHistoricalTicks(ticks=ticks, completed=finished) #endregion - Historical data #region - Live data async def stream_live_ticks( self, contract: ib_contract.Contract, listener: listeners.LiveTicksListener, - tick_type: dt.LiveTicks=dt.LiveTicks.LAST + tick_type: datatype.LiveTicks=datatype.LiveTicks.LAST ) -> int: """Request to stream live tick data. diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 51a4d68..eb750c5 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -6,8 +6,6 @@ import unittest from dateutil import relativedelta -import pytz - from ibapi import contract from ibapi import wrapper From 62ad010d771723c1cecd073405d1c74fbec03ae3 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 16 Feb 2021 12:26:21 +0000 Subject: [PATCH 083/126] Housekeeping - Format code. - Removed all `print` statements. --- ibpy_native/_internal/_client.py | 15 ++------------- ibpy_native/_internal/_wrapper.py | 5 +---- ibpy_native/bridge.py | 12 +++++++----- tests/toolkit/utils.py | 1 - 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index a425f55..4858375 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -54,8 +54,6 @@ async def resolve_contract( except error.IBError as err: raise err - print("Getting full contract details from IB...") - self.reqContractDetails(reqId=req_id, contract=contract) # Run until we get a valid contract(s) @@ -68,9 +66,6 @@ async def resolve_contract( raise self._unknown_error(req_id=req_id) - if len(res) > 1: - print("Multiple contracts found: returning 1st contract") - resolved_contract = res[0].contract return resolved_contract @@ -107,8 +102,6 @@ async def resolve_contracts( except error.IBError as err: raise err - print("Searching contracts with details from IB...") - self.reqContractDetails(reqId=req_id, contract=contract) res: List[Union[ib_contract.ContractDetails, error.IBError]] = ( @@ -128,6 +121,7 @@ async def resolve_contracts( ) #endregion - Contract + #region - Historical data async def resolve_head_timestamp( self, req_id: int, contract: ib_contract.Contract, show: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES @@ -156,9 +150,6 @@ async def resolve_head_timestamp( except error.IBError as err: raise err - print("Getting earliest available data point for the given " - "instrument from IB...") - self.reqHeadTimeStamp(reqId=req_id, contract=contract, whatToShow=show.value, useRTH=0, formatDate=2) @@ -257,6 +248,7 @@ async def req_historical_ticks( ) return result[0] + #endregion - Historical data #region - Stream live tick data async def stream_live_ticks( @@ -293,9 +285,6 @@ async def stream_live_ticks( except error.IBError as err: raise err - print(f"Streaming live ticks [{tick_type}] for the given instrument " - "instrument from IB...") - self.reqTickByTickData( reqId=req_id, contract=contract, tickType=tick_type.value, numberOfTicks=0, ignoreSize=True diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 28008a6..3bc1380 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -27,12 +27,9 @@ def __init__( notification_listener: Optional[listeners.NotificationListener]=None ): self._req_queue: Dict[int, fq.FinishableQueue] = {} - self._ac_man_delegate: Optional[ delegates.AccountsManagementDelegate] = None - - self._notification_listener: Optional[ - listeners.NotificationListener] = notification_listener + self._notification_listener = notification_listener super().__init__() diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 3859950..f117cbe 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -218,9 +218,8 @@ async def get_earliest_data_point( except error.IBError as err: raise err - data_point = datetime.datetime.fromtimestamp(result).astimezone( - _global.TZ - ) + data_point = datetime.datetime.fromtimestamp( + timestamp=result, tz=_global.TZ) return data_point.replace(tzinfo=None) @@ -304,13 +303,16 @@ async def req_historical_ticks( del ticks[0] # Determine if it should fetch next batch of data last_tick_time = datetime.datetime.fromtimestamp( - timestamp=ticks[-1].time, tz=_global.TZ).replace(tzinfo=None) + timestamp=ticks[-1].time, tz=_global.TZ + ).replace(tzinfo=None) + if last_tick_time >= end_date_time: # All ticks within the specified time period are received finished = True for i in range(len(ticks) - 1, -1, -1): data_time = datetime.datetime.fromtimestamp( - timestamp=ticks[i].time, tz=_global.TZ).replace(tzinfo=None) + timestamp=ticks[i].time, tz=_global.TZ + ).replace(tzinfo=None) if data_time > end_date_time: del ticks[i] else: diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index 925aa98..b4b0d9e 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -81,7 +81,6 @@ def on_tick_receive(self, req_id: int, tick: Union[ib_wrapper.HistoricalTick, ib_wrapper.HistoricalTickBidAsk, ib_wrapper.HistoricalTickLast,]): - print(tick) self.ticks.append(tick) def on_finish(self, req_id: int): From eb9e33fed33d146c5b3647ad7d1316b6edcf2907 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 16 Feb 2021 18:48:54 +0000 Subject: [PATCH 084/126] Store next order ID - Added thread-safe property `IBWrapper.next_order_id` & corresponding setter. - Overrode function `IBWrapper.nextValidId` to update value of `next_order_id`. - Added corresponding unit test case. --- ibpy_native/_internal/_wrapper.py | 22 ++++++++++++++++++++++ tests/internal/test_wrapper.py | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 3bc1380..827fdf0 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -1,5 +1,6 @@ """Code implementation of IB API resposes handling.""" # pylint: disable=protected-access +import threading import queue from typing import Dict, List, Optional @@ -14,6 +15,7 @@ from ibpy_native.utils import finishable_queue as fq class IBWrapper(wrapper.EWrapper): + # pylint: disable=too-many-public-methods """The wrapper deals with the action coming back from the IB gateway or TWS instance. @@ -26,10 +28,13 @@ def __init__( self, notification_listener: Optional[listeners.NotificationListener]=None ): + self._lock = threading.Lock() + self._req_queue: Dict[int, fq.FinishableQueue] = {} self._ac_man_delegate: Optional[ delegates.AccountsManagementDelegate] = None self._notification_listener = notification_listener + self._next_order_id = 0 # Next available order ID super().__init__() @@ -57,6 +62,17 @@ def next_req_id(self) -> int: return usable_id + 1 + @property + def next_order_id(self) -> int: + """int: Next valid order ID. If is `0`, it means the connection with IB + has not been established yet.""" + return self._next_order_id + + @next_order_id.setter + def next_order_id(self, val: int): + with self._lock: + self._next_order_id = val + #region - Getters def get_request_queue(self, req_id: int) -> fq.FinishableQueue: """Initialise queue or returns the existing queue with ID `req_id`. @@ -178,6 +194,12 @@ def updateAccountTime(self, timeStamp: str): #endregion - account updates #endregion - Accounts & portfolio + #region - Orders + def nextValidId(self, orderId: int): + # Next valid order ID returned from IB + self.next_order_id = orderId + #endregion - Orders + #region - Get contract details def contractDetails(self, reqId, contractDetails): self._req_queue[reqId].put(element=contractDetails) diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index bd02153..7534c29 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -222,6 +222,30 @@ async def test_update_account_time(self): # Expect data stored as-is in `account_updates_queue` self.assertEqual(results[0], time) +class TestOrder(unittest.TestCase): + """Unit tests for IB order related functions & properties in `IBWrapper`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + def test_next_valid_id(self): + """Test overridden function `nextValidId`.""" + self._wrapper.nextValidId(orderId=10) + self.assertEqual(self._wrapper.next_order_id, 10) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + class TestContract(unittest.TestCase): """Unit tests for IB contract related functions in `IBWrapper`. From 9198ef8a56ce22075e0db062336f393f1c471b92 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 17 Feb 2021 14:49:23 +0000 Subject: [PATCH 085/126] Request next order ID - Implemented client function to request next order ID from IB. - Modified the `reset` function of `FinisihableQueue` to take effect on `INIT` queue as well. --- ibpy_native/_internal/_client.py | 23 +++++++++++++++++++++++ ibpy_native/_internal/_wrapper.py | 11 ++++++++++- ibpy_native/utils/finishable_queue.py | 6 +++--- tests/internal/test_client.py | 26 ++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 4858375..12b5d8e 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -27,6 +27,29 @@ def __init__(self, wrapper: _wrapper.IBWrapper): self._wrapper = wrapper super().__init__(wrapper) + #region - Orders + async def req_next_order_id(self) -> int: + """Request the next valid order ID from IB. + + Returns: + int: The next valid order ID returned from IB. + + Raises: + ibpy_native.error.IBError: If queue associated with `req_id` -1 is + being used by other task. + """ + try: + f_queue = self._wrapper.get_request_queue(req_id=-1) + except error.IBError as err: + raise err + # Request next valid order ID + self.reqIds(numIds=-1) # `numIds` has deprecated + await f_queue.get() + + return self._wrapper.next_order_id + + #endregion - Orders + #region - Contract async def resolve_contract( self, req_id: int, contract: ib_contract.Contract diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 827fdf0..20901f3 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -34,6 +34,9 @@ def __init__( self._ac_man_delegate: Optional[ delegates.AccountsManagementDelegate] = None self._notification_listener = notification_listener + + # Queue with ID -1 is always reserved for next order ID + self._req_queue[-1] = fq.FinishableQueue(queue.Queue()) self._next_order_id = 0 # Next available order ID super().__init__() @@ -198,6 +201,10 @@ def updateAccountTime(self, timeStamp: str): def nextValidId(self, orderId: int): # Next valid order ID returned from IB self.next_order_id = orderId + # To finish waiting on IBClient.req_next_order_id + if (self._req_queue[-1].status is not + (fq.Status.INIT or fq.Status.FINISHED)): + self._req_queue[-1].put(element=fq.Status.FINISHED) #endregion - Orders #region - Get contract details @@ -280,7 +287,9 @@ def _init_req_queue(self, req_id: int): `self.__req_queue[req_id]` and it's not finished. """ if req_id in self._req_queue: - if self._req_queue[req_id].finished: + if self._req_queue[req_id].finished or ( + req_id == -1 and self._req_queue[-1].status is fq.Status.INIT + ): self._req_queue[req_id].reset() else: raise error.IBError( diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index c8e2924..a37b7dc 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -47,10 +47,10 @@ def finished(self) -> bool: return self._status is Status.FINISHED def reset(self): - """Reset the status to `STARTED` for reusing the queue if the - status is marked as either `TIMEOUT` or `FINISHED` + """Reset the status to `READY` for reusing the queue if the + status is marked as either `INIT` or `FINISHED` """ - if self.finished: + if self.finished or self._status is Status.INIT: self._status = Status.READY def put(self, element: Any): diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index eb750c5..30a03d9 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -19,6 +19,32 @@ from tests.toolkit import sample_contracts from tests.toolkit import utils +class TestOrder(unittest.TestCase): + """Unit tests for IB order related functions & properties in `IBWrapper`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._wrapper = _wrapper.IBWrapper() + cls._client = _client.IBClient(cls._wrapper) + + cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) + + thread = threading.Thread(target=cls._client.run) + thread.start() + + @utils.async_test + async def test_req_next_order_id(self): + """Test function `req_next_order_id`.""" + next_order_id = await self._client.req_next_order_id() + print(next_order_id) + self.assertGreater(next_order_id, 0) + + @classmethod + def tearDownClass(cls): + cls._client.disconnect() + class TestContract(unittest.TestCase): """Unit tests for IB contract related functions in `IBClient`. From 79cd09e9850c1f554263d5617054d70b45e40477 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 18 Feb 2021 03:29:29 +0000 Subject: [PATCH 086/126] Create `OrdersManager` - Created protocol `OrdersManagementDelegate` & its' implementataion `OrdersManager`. - Modified the constructor of `IBWrapper` to initialise with an instance of `OrdersManagementDelegate`. - Added property `orders_manager` in `IBWrapper`. - Moved the `next_order_id` property from `IBWrapper` to `OrdersManager`. - Updated function `IBClient.req_next_order_id` to return the order ID from `OrdersManager` instead of `IBWrapper`. - Updated relative test cases and init statements of `IBWrapper`. --- ibpy_native/__init__.py | 1 + ibpy_native/_internal/_client.py | 2 +- ibpy_native/_internal/_wrapper.py | 23 ++++++++++--------- ibpy_native/bridge.py | 11 +++++++++ ibpy_native/interfaces/delegates/__init__.py | 1 + ibpy_native/interfaces/delegates/order.py | 22 ++++++++++++++++++ ibpy_native/order.py | 24 ++++++++++++++++++++ tests/internal/test_client.py | 10 ++++---- tests/internal/test_wrapper.py | 20 +++++++--------- 9 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 ibpy_native/interfaces/delegates/order.py create mode 100644 ibpy_native/order.py diff --git a/ibpy_native/__init__.py b/ibpy_native/__init__.py index 4b424ab..85263c8 100644 --- a/ibpy_native/__init__.py +++ b/ibpy_native/__init__.py @@ -1,3 +1,4 @@ """Public classes & functions of `ibpy_native`.""" from .bridge import IBBridge +from .order import OrdersManager from .utils import datatype diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 12b5d8e..3fdf049 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -46,7 +46,7 @@ async def req_next_order_id(self) -> int: self.reqIds(numIds=-1) # `numIds` has deprecated await f_queue.get() - return self._wrapper.next_order_id + return self._wrapper.orders_manager.next_order_id #endregion - Orders diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 20901f3..80b84d5 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -20,12 +20,16 @@ class IBWrapper(wrapper.EWrapper): TWS instance. Args: + orders_manager (:obj:`ibpy_native.interfaces.delgates.order + .OrdersManagementDelegate`): Manager to handler orders related + events. notification_listener (:obj:`ibpy_native.interfaces.listeners .NotificationListener`, optional): Handler to receive system notifications from IB Gateway. Defaults to `None`. """ def __init__( self, + orders_manager: delegates.OrdersManagementDelegate, notification_listener: Optional[listeners.NotificationListener]=None ): self._lock = threading.Lock() @@ -33,11 +37,12 @@ def __init__( self._req_queue: Dict[int, fq.FinishableQueue] = {} self._ac_man_delegate: Optional[ delegates.AccountsManagementDelegate] = None + + self._orders_manager = orders_manager self._notification_listener = notification_listener # Queue with ID -1 is always reserved for next order ID self._req_queue[-1] = fq.FinishableQueue(queue.Queue()) - self._next_order_id = 0 # Next available order ID super().__init__() @@ -66,15 +71,11 @@ def next_req_id(self) -> int: return usable_id + 1 @property - def next_order_id(self) -> int: - """int: Next valid order ID. If is `0`, it means the connection with IB - has not been established yet.""" - return self._next_order_id - - @next_order_id.setter - def next_order_id(self, val: int): - with self._lock: - self._next_order_id = val + def orders_manager(self) -> delegates.OrdersManagementDelegate: + """:obj:`ibpy_native.interfaces.delegates.order + .OrdersManagementDelegate`: The internal orders manager. + """ + return self._orders_manager #region - Getters def get_request_queue(self, req_id: int) -> fq.FinishableQueue: @@ -200,7 +201,7 @@ def updateAccountTime(self, timeStamp: str): #region - Orders def nextValidId(self, orderId: int): # Next valid order ID returned from IB - self.next_order_id = orderId + self._orders_manager.update_next_order_id(order_id=orderId) # To finish waiting on IBClient.req_next_order_id if (self._req_queue[-1].status is not (fq.Status.INIT or fq.Status.FINISHED)): diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index f117cbe..d2ef70b 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -9,12 +9,14 @@ from ibapi import contract as ib_contract +import ibpy_native from ibpy_native import account as ib_account from ibpy_native import error from ibpy_native import models from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native._internal import _wrapper +from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype @@ -51,8 +53,10 @@ def __init__( ib_account.AccountsManager() if accounts_manager is None else accounts_manager ) + self._orders_manager = ibpy_native.OrdersManager() self._wrapper = _wrapper.IBWrapper( + orders_manager=self._orders_manager, notification_listener=notification_listener ) self._wrapper.set_accounts_management_delegate( @@ -79,6 +83,13 @@ def accounts_manager(self) -> ib_account.AccountsManager: """ return self._accounts_manager + @property + def orders_manager(self) -> delegates.OrdersManagementDelegate: + """:obj:`ibpy_native.order.OrdersManager`: Instance that handles order + related events. + """ + return self._orders_manager + #region - Setters @staticmethod def set_timezone(tz: datetime.tzinfo): diff --git a/ibpy_native/interfaces/delegates/__init__.py b/ibpy_native/interfaces/delegates/__init__.py index ab30362..91ce60f 100644 --- a/ibpy_native/interfaces/delegates/__init__.py +++ b/ibpy_native/interfaces/delegates/__init__.py @@ -1,2 +1,3 @@ """Interfaces of delegates.""" +from .order import OrdersManagementDelegate from .account import AccountsManagementDelegate diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py new file mode 100644 index 0000000..f687476 --- /dev/null +++ b/ibpy_native/interfaces/delegates/order.py @@ -0,0 +1,22 @@ +"""Internal delegate module for orders related features.""" +import abc + +class OrdersManagementDelegate(metaclass=abc.ABCMeta): + """Internal delegate protocol for handling orders.""" + @property + @abc.abstractmethod + def next_order_id(self) -> int: + """int: Next valid order ID. If is `0`, it means the connection + with IB has not been established yet. + """ + return NotImplemented + + @abc.abstractmethod + def update_next_order_id(self, order_id: int): + """Internal function to update the next order ID stored. Not expected + to be invoked by the user. + + Args: + order_id (int): The updated order identifier. + """ + return NotImplemented diff --git a/ibpy_native/order.py b/ibpy_native/order.py new file mode 100644 index 0000000..3b8f249 --- /dev/null +++ b/ibpy_native/order.py @@ -0,0 +1,24 @@ +"""IB order related resources.""" +import threading + +from ibpy_native.interfaces import delegates + +class OrdersManager(delegates.OrdersManagementDelegate): + """Class to handle orders related events. + + Args: + account_manager: The accounts manager. + """ + def __init__(self): + # Internal members + self._lock = threading.Lock() + # Property + self._next_order_id = 0 + + @property + def next_order_id(self) -> int: + return self._next_order_id + + def update_next_order_id(self, order_id: int): + with self._lock: + self._next_order_id = order_id diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 30a03d9..95f34e9 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -10,6 +10,7 @@ from ibapi import wrapper from ibpy_native import error +from ibpy_native import order from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native._internal import _wrapper @@ -26,7 +27,7 @@ class TestOrder(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -38,7 +39,6 @@ def setUpClass(cls): async def test_req_next_order_id(self): """Test function `req_next_order_id`.""" next_order_id = await self._client.req_next_order_id() - print(next_order_id) self.assertGreater(next_order_id, 0) @classmethod @@ -52,7 +52,7 @@ class TestContract(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -121,7 +121,7 @@ class TestHistoricalData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -277,7 +277,7 @@ class TestLiveData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index 7534c29..95efc9b 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -10,6 +10,7 @@ from ibpy_native import error from ibpy_native import models +from ibpy_native import order from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native._internal import _wrapper @@ -25,7 +26,7 @@ class TestGeneral(unittest.TestCase): Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper() + self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) def test_set_on_notify_listener(self): """Test setter `set_on_notify_listener` & overridden function `error` @@ -69,7 +70,7 @@ class TestReqQueue(unittest.TestCase): Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper() + self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) def test_next_req_id_0(self): """Test property `next_req_id` for retrieval of next usable @@ -166,7 +167,7 @@ class TestAccountAndPortfolio(unittest.TestCase): Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper() + self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) self._delegate = utils.MockAccountsManagementDelegate() self._wrapper.set_accounts_management_delegate(delegate=self._delegate) @@ -229,7 +230,7 @@ class TestOrder(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -237,11 +238,6 @@ def setUpClass(cls): thread = threading.Thread(target=cls._client.run) thread.start() - def test_next_valid_id(self): - """Test overridden function `nextValidId`.""" - self._wrapper.nextValidId(orderId=10) - self.assertEqual(self._wrapper.next_order_id, 10) - @classmethod def tearDownClass(cls): cls._client.disconnect() @@ -253,7 +249,7 @@ class TestContract(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -301,7 +297,7 @@ class TestHistoricalData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -400,7 +396,7 @@ class TestTickByTickData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper() + cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) From a9c5e8b0f6a5a07ee57c132ffbb253c70359b37b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 18 Feb 2021 22:40:03 +0000 Subject: [PATCH 087/126] Identify & dirvert order error - Added abstract function `OrdersManagermentDelegate.is_pending_order` to determine if the error receive from `IBWrapper.error` is an order error. - Pass the order error to the orders maanger. --- ibpy_native/_internal/_wrapper.py | 4 +++- ibpy_native/interfaces/delegates/order.py | 20 ++++++++++++++++++++ ibpy_native/order.py | 8 ++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 80b84d5..6dd2e05 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -149,7 +149,9 @@ def error(self, reqId, errorCode, errorString): # -1 indicates a notification and not true error condition if reqId is not -1: - if reqId in self._req_queue: + if self._orders_manager.is_pending_order(val=reqId): + self._orders_manager.order_error(err) + elif reqId in self._req_queue: self._req_queue[reqId].put(element=err) else: if self._notification_listener is not None: diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index f687476..7d0bb6b 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -1,6 +1,8 @@ """Internal delegate module for orders related features.""" import abc +from ibpy_native import error + class OrdersManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for handling orders.""" @property @@ -20,3 +22,21 @@ def update_next_order_id(self, order_id: int): order_id (int): The updated order identifier. """ return NotImplemented + + @abc.abstractmethod + def is_pending_order(self, val: int) -> bool: + """Check if a identifier matches with an existing order in pending. + + Args: + val (int): The value to validate. + + Returns: + bool: `True` if `val` matches with the order identifier of an + pending order. `False` if otherwise. + """ + return NotImplemented + + @abc.abstractmethod + def order_error(self, err: error.IBError): + """Handles the error return from IB for the order submiteted.""" + return NotImplemented diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 3b8f249..916726e 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -1,6 +1,7 @@ """IB order related resources.""" import threading +from ibpy_native import error from ibpy_native.interfaces import delegates class OrdersManager(delegates.OrdersManagementDelegate): @@ -22,3 +23,10 @@ def next_order_id(self) -> int: def update_next_order_id(self, order_id: int): with self._lock: self._next_order_id = order_id + + def is_pending_order(self, val: int) -> bool: + return False + + def order_error(self, err: error.IBError): + if err.err_code == 399: # Warning message only + return From 427e8e25a7692285817ccf3a0fe8b350d0dbbf57 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 23 Feb 2021 04:29:48 +0000 Subject: [PATCH 088/126] Create enums for order - Added enum `OrderAction` & `OrderStatus` in module `utils.datatype` for easier option input of order related resources. --- ibpy_native/utils/datatype.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ibpy_native/utils/datatype.py b/ibpy_native/utils/datatype.py index 1f58940..055160a 100644 --- a/ibpy_native/utils/datatype.py +++ b/ibpy_native/utils/datatype.py @@ -38,3 +38,31 @@ class ResHistoricalTicks(NamedTuple): ]] completed: bool #endregion - Return type + +#region - Order related +@enum.unique +class OrderAction(enum.Enum): + """Action type of the order. Either BUY or SELL.""" + BUY = "BUY" + SELL = "SELL" + +@enum.unique +class OrderStatus(enum.Enum): + """Status of the order after submission to TWS/Gateway. + + For the definition of each status, please refer to `Possible Order States`_ + from TWS API document. + + .. _Possible Order States: + https://interactivebrokers.github.io/tws-api/order_submission.html#order_status + """ + API_PENDING = "ApiPending" + PENDING_SUBMIT = "PendingSubmit" + PENDING_CANCEL = "PendingCancel" + PRE_SUBMITTED = "PreSubmitted" + SUBMITTED = "Submitted" + API_CANCELLED = "ApiCancelled" + CANCELLED = "Cancelled" + FILLED = "Filled" + INACTIVE = "Inactive" +#endregion - Order related From 7fb4fbc70fdcfaea6bf0b168c054fdcda8e8ee63 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 23 Feb 2021 04:53:09 +0000 Subject: [PATCH 089/126] Create module `sample_orders` --- tests/toolkit/sample_orders.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/toolkit/sample_orders.py diff --git a/tests/toolkit/sample_orders.py b/tests/toolkit/sample_orders.py new file mode 100644 index 0000000..d21982e --- /dev/null +++ b/tests/toolkit/sample_orders.py @@ -0,0 +1,28 @@ +"""Predefined orders for unittest.""" +from ibapi import order as ib_order + +from ibpy_native.utils import datatype + +def _base_order(order_id: int, action: datatype.OrderAction) -> ib_order.Order: + order = ib_order.Order() + order.orderId = order_id + order.action = action.value + order.totalQuantity = 100 + + return order + +def lmt(order_id: int, action: datatype.OrderAction, + price: float) -> ib_order.Order: + """Limit order""" + order = _base_order(order_id, action) + order.orderType = "LMT" + order.lmtPrice = price + + return order + +def mkt(order_id: int, action: datatype.OrderAction) -> ib_order.Order: + """Market order""" + order = _base_order(order_id, action) + order.orderType = "MKT" + + return order From 5ae6f2a722a98f0931344eac79a957da6b4ce672 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Tue, 23 Feb 2021 11:43:11 +0000 Subject: [PATCH 090/126] Implement `openOrder` & `orderStatus` callbacks - Overridden callback function `openOrder` & `orderStatus` in `IBWrapper` to forward the order data return from IB. - Added corresponding handler functions `on_open_order_updated` & `on_order_status_updated`, also the related property `open_orders` in `OrdersManager` & `OrdersManagementDelegate`. - Created model class `OpenOrder` in new module `models.order`. - Added `NamedTuple` `OrderExecRec` in module `utils.datatype`. - Added corresponding unit tests for `openOrder` & `orderStatus` callbacks. --- ibpy_native/_internal/_wrapper.py | 18 +++ ibpy_native/interfaces/delegates/order.py | 45 ++++++ ibpy_native/models/__init__.py | 1 + ibpy_native/models/order.py | 174 ++++++++++++++++++++++ ibpy_native/order.py | 39 ++++- ibpy_native/utils/datatype.py | 6 + tests/internal/test_wrapper.py | 30 ++++ 7 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 ibpy_native/models/order.py diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 6dd2e05..1606910 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -5,6 +5,8 @@ from typing import Dict, List, Optional from ibapi import contract as ib_contract +from ibapi import order as ib_order +from ibapi import order_state from ibapi import wrapper from ibpy_native import error @@ -208,6 +210,22 @@ def nextValidId(self, orderId: int): if (self._req_queue[-1].status is not (fq.Status.INIT or fq.Status.FINISHED)): self._req_queue[-1].put(element=fq.Status.FINISHED) + + def openOrder(self, orderId: int, contract: ib_contract.Contract, + order: ib_order.Order, orderState: order_state.OrderState): + self._orders_manager.on_open_order_updated( + contract=contract, order=order, order_state=orderState + ) + + def orderStatus(self, orderId: int, status: str, filled: float, + remaining: float, avgFillPrice: float, permId: int, + parentId: int, lastFillPrice: float, clientId: int, + whyHeld: str, mktCapPrice: float): + self._orders_manager.on_order_status_updated( + order_id=orderId, filled=filled, remaining=remaining, + avg_fill_price=avgFillPrice, last_fill_price=lastFillPrice, + mkt_cap_price=mktCapPrice + ) #endregion - Orders #region - Get contract details diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index 7d0bb6b..02425c5 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -1,7 +1,13 @@ """Internal delegate module for orders related features.""" import abc +from typing import Dict + +from ibapi import contract as ib_contract +from ibapi import order as ib_order +from ibapi import order_state as ib_order_state from ibpy_native import error +from ibpy_native import models class OrdersManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for handling orders.""" @@ -13,6 +19,14 @@ def next_order_id(self) -> int: """ return NotImplemented + @property + @abc.abstractmethod + def open_orders(self) -> Dict[int, models.OpenOrder]: + """:obj:`Dict[int, models.OpenOrder]`: Open orders returned from IB + during this session. + """ + return NotImplemented + @abc.abstractmethod def update_next_order_id(self, order_id: int): """Internal function to update the next order ID stored. Not expected @@ -40,3 +54,34 @@ def is_pending_order(self, val: int) -> bool: def order_error(self, err: error.IBError): """Handles the error return from IB for the order submiteted.""" return NotImplemented + + @abc.abstractmethod + def on_open_order_updated( + self, contract: ib_contract.Contract, order: ib_order.Order, + order_state: ib_order_state.OrderState + ): + """INTERNAL FUNCTION! Handles the open order returned from IB + after an order is submitted to TWS/Gateway. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + order (:obj:`ibapi.order.Order`): The current active order returned + from IB. + order_state (:obj:`ibapi.order_state.OrderState`): Order states/ + status returned from IB. + """ + return NotImplemented + + @abc.abstractmethod + def on_order_status_updated( + self, order_id: int, filled: float, remaining: float, + avg_fill_price: float, last_fill_price: float, mkt_cap_price: float + ): + """INTERNAL FUNCTION! Handles the `orderStatus` callback from IB. + + Args: + order_id (int): The order's identifier on TWS/Gateway. + filled (float): Number of filled positions. + remaining (float): The remnant positions. + """ + return NotImplemented diff --git a/ibpy_native/models/__init__.py b/ibpy_native/models/__init__.py index b0557e0..6da3c27 100644 --- a/ibpy_native/models/__init__.py +++ b/ibpy_native/models/__init__.py @@ -1,5 +1,6 @@ """Expose models on package level.""" from .account import Account +from .order import OpenOrder from .portfolio import Position from .raw_data import RawAccountValueData from .raw_data import RawPortfolioData diff --git a/ibpy_native/models/order.py b/ibpy_native/models/order.py new file mode 100644 index 0000000..4ef0e62 --- /dev/null +++ b/ibpy_native/models/order.py @@ -0,0 +1,174 @@ +"""Model classes for order related data.""" +import threading +from typing import List + +from ibapi import contract as ib_contract +from ibapi import order as ib_order +from ibapi import order_state as ib_order_state + +from ibpy_native.utils import datatype + +class OpenOrder: + """Provides info of an active open order. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + order (:obj:`ibapi.order.Order`): The current active order returned + from IB. + order_state (:obj:`ibapi.order_state.OrderState`): Order states/status + returned from IB. + """ + def __init__(self, contract: ib_contract.Contract, order: ib_order.Order, + order_state: ib_order_state.OrderState): + self._lock = threading.Lock() + # From `openOrder` callback + self._contract = contract + self._order = order + self._order_state = order_state + # From `orderStatus` callback + self._avg_fill_price = 0.0 + self._mkt_cap_price = 0.0 + self._exec_rec: List[datatype.OrderExecRec] = [] + + @property + def contract(self) -> ib_contract.Contract: + """:obj:`ibapi.contract.Contract`: The order's contract. DO NOT modify + the `Contract` returned! + """ + return self._contract + + @property + def order(self) -> ib_order.Order: + """:obj:`ibapi.order.Order`: The order returned from IB. DO NOT modify + the `Order` returned! + + Note: + You should never monitor the value of the object returned from this + property directly, as the underlying object will be replaced with + the one returned from IB callback. + """ + return self._order + + @property + def order_state(self) -> ib_order_state.OrderState: + """:obj:`ibapi.order_state.OrderState`: The order's states/status + returned from IB. DO NOT modify the `OrderState` returned! + + Note: + You should never monitor the value of the object returned from this + property directly, as the underlying object will be replaced with + the one returned from IB callback. + """ + return self._order_state + + #region - From `Order` & `OrderState` + @property + def action(self) -> datatype.OrderAction: + """:obj:`ibpy_native.utils.datatype.OrderAction`: Order's action. + Either "BUY" or "SELL". + """ + return datatype.OrderAction(self._order.action) + + @property + def status(self) -> datatype.OrderStatus: + """:obj:`ibpy_native.utils.datatype.OrderStatus`: The order's current + status. + """ + return datatype.OrderStatus(self._order_state.status) + + @property + def filled(self) -> int: + """int: The number of positions bought/sold.""" + return self._order.filledQuantity + + @property + def quantity(self) -> int: + """int: The number of positions being bought/sold.""" + return self._order.totalQuantity + + @property + def commission(self) -> float: + """float: The order's generated commission.""" + return self._order_state.commission + #endregion - From `Order` & `OrderState` + + #region - From `orderStatus` + @property + def avg_fill_price(self) -> float: + """float: Average filling price of the order. + + Note: + Since IB does not guarante to return every change in order status, + the value stored in this property might not be 100% accurate and + should be used as reference only. + """ + return self._avg_fill_price + + @property + def mkt_cap_price(self) -> float: + """float: Indicaes the current capped price if the order has been + capped. Returns `0` if it isn't capped. + + Note: + Since IB does not guarante to return every change in order status, + the value stored in this property might not be 100% accurate and + should be used as reference only. + """ + return self._mkt_cap_price + + @property + def exec_rec(self): + """:obj:`tuple(ibpy_native.utils.datatype.OrderExecRec)`: The order + execution record(s) returned from IB. + + Note: + IB is not guaranteed to return every change in order status via the + `orderStatus` callback. Therefore, DO NOT take this as an absolute + reference for anything. + """ + return tuple(self._exec_rec) + #endregion - From `orderStatus` + + def order_update(self, order: ib_order.Order, + order_state: ib_order_state.OrderState): + """INTERNAL FUNCTION! Thread-safe function to handle the order and + order state updates from IB. DO NOT use this function in your own + code unless you're testing something specifically. + + Args: + order (:obj:`ibapi.order.Order`): The order returned from IB. + order_state (:obj:`ibapi.order_state.OrderState`): The order + current state returned from IB. + """ + with self._lock: + self._order = order + self._order_state = order_state + + def order_status_update(self, filled: float, remaining: float, + avg_fill_price: float, last_fill_price: float, + mkt_cap_price: float): + """INTERNAL FUNCTION! Thread-safe function to handle the changes on + order status updates from IB. DO NOT use this function in your own code + unless you're testing something specifically. + + Args: + filled (float): Number of filled positions. + remaining (float): The remnant positions. + avg_fill_price (float): Average filling price. + last_fill_price (float): Price at which the last positions were + filled. + mkt_cap_price (float): If an order has been capped, this indicates + the current capped price. + """ + with self._lock: + if self._exec_rec: + if self._exec_rec[-1].remaining == remaining: + # Filter out the duplicate messages returned from IB + return + + self._exec_rec.append( + datatype.OrderExecRec(filled=filled, remaining=remaining, + last_fill_price=last_fill_price) + ) + self._avg_fill_price = avg_fill_price + self._mkt_cap_price = mkt_cap_price diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 916726e..9305ff1 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -1,25 +1,32 @@ """IB order related resources.""" import threading +from typing import Dict + +from ibapi import contract as ib_contract +from ibapi import order as ib_order +from ibapi import order_state as ib_order_state from ibpy_native import error +from ibpy_native import models from ibpy_native.interfaces import delegates class OrdersManager(delegates.OrdersManagementDelegate): - """Class to handle orders related events. - - Args: - account_manager: The accounts manager. - """ + """Class to handle orders related events.""" def __init__(self): # Internal members self._lock = threading.Lock() # Property self._next_order_id = 0 + self._open_orders: Dict[int, models.OpenOrder] = {} @property def next_order_id(self) -> int: return self._next_order_id + @property + def open_orders(self) -> Dict[int, models.OpenOrder]: + return self._open_orders + def update_next_order_id(self, order_id: int): with self._lock: self._next_order_id = order_id @@ -30,3 +37,25 @@ def is_pending_order(self, val: int) -> bool: def order_error(self, err: error.IBError): if err.err_code == 399: # Warning message only return + + def on_open_order_updated( + self, contract: ib_contract.Contract, order: ib_order.Order, + order_state: ib_order_state.OrderState + ): + if order.orderId in self._open_orders: + self._open_orders[order.orderId].order_update(order, order_state) + else: + self._open_orders[order.orderId] = models.OpenOrder( + contract, order, order_state + ) + + def on_order_status_updated( + self, order_id: int, filled: float, remaining: float, + avg_fill_price: float, last_fill_price: float, mkt_cap_price: float + ): + if order_id in self._open_orders and filled != 0: + # Filter out the message(s) with no actual trade + self._open_orders[order_id].order_status_update( + filled, remaining, avg_fill_price, + last_fill_price, mkt_cap_price + ) diff --git a/ibpy_native/utils/datatype.py b/ibpy_native/utils/datatype.py index 055160a..c5ce3e5 100644 --- a/ibpy_native/utils/datatype.py +++ b/ibpy_native/utils/datatype.py @@ -65,4 +65,10 @@ class OrderStatus(enum.Enum): CANCELLED = "Cancelled" FILLED = "Filled" INACTIVE = "Inactive" + +class OrderExecRec(NamedTuple): + """Named tuple for order information returned from IB on changes.""" + filled: float + remaining: float + last_fill_price: float #endregion - Order related diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index 95efc9b..d50770b 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -18,6 +18,7 @@ from ibpy_native.utils import finishable_queue as fq from tests.toolkit import sample_contracts +from tests.toolkit import sample_orders from tests.toolkit import utils class TestGeneral(unittest.TestCase): @@ -238,6 +239,35 @@ def setUpClass(cls): thread = threading.Thread(target=cls._client.run) thread.start() + def setUp(self): + self._orders_manager = self._wrapper.orders_manager + + @utils.async_test + async def test_open_order(self): + """Test overridden function `openOrder`.""" + order_id = await self._client.req_next_order_id() + self._client.placeOrder( + orderId=order_id, contract=sample_contracts.gbp_usd_fx(), + order=sample_orders.mkt(order_id=order_id, + action=datatype.OrderAction.BUY) + ) + await asyncio.sleep(1) + + self.assertTrue(order_id in self._orders_manager.open_orders) + + @utils.async_test + async def test_order_status(self): + """Test overridden function `orderStatus`.""" + order_id = await self._client.req_next_order_id() + self._client.placeOrder( + orderId=order_id, contract=sample_contracts.gbp_usd_fx(), + order=sample_orders.mkt(order_id=order_id, + action=datatype.OrderAction.SELL) + ) + await asyncio.sleep(1) + + self.assertTrue(self._orders_manager.open_orders[order_id].exec_rec) + @classmethod def tearDownClass(cls): cls._client.disconnect() From f28af2d7006a5be469986177507afd427abe2b1b Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 04:53:14 +0000 Subject: [PATCH 091/126] Save error code - duplicate order ID --- ibpy_native/error.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ibpy_native/error.py b/ibpy_native/error.py index 6f0a72f..a3e7d51 100644 --- a/ibpy_native/error.py +++ b/ibpy_native/error.py @@ -6,6 +6,7 @@ class IBErrorCode(enum.IntEnum): """Error codes.""" # Error codes defined by IB DUPLICATE_TICKER_ID = 102 + DUPLICATE_ORDER_ID = 103 INVALID_CONTRACT = 200 # Self-defined error codes REQ_TIMEOUT = 50504 From c2f469e9259b704cc3b4e251790ddb407b2fc442 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 05:10:53 +0000 Subject: [PATCH 092/126] Add event `on_order_submission` - Created and implemented internal event function `on_order_submission` for setting up a way to monitor and return the completeion status of order submission tasks. --- ibpy_native/interfaces/delegates/order.py | 33 +++++++++++++++++++- ibpy_native/order.py | 38 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index 02425c5..a3489e4 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -1,6 +1,6 @@ """Internal delegate module for orders related features.""" import abc -from typing import Dict +from typing import Dict, Optional from ibapi import contract as ib_contract from ibapi import order as ib_order @@ -8,6 +8,7 @@ from ibpy_native import error from ibpy_native import models +from ibpy_native.utils import finishable_queue as fq class OrdersManagementDelegate(metaclass=abc.ABCMeta): """Internal delegate protocol for handling orders.""" @@ -50,11 +51,39 @@ def is_pending_order(self, val: int) -> bool: """ return NotImplemented + #region - Internal functions + @abc.abstractmethod + def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: + """INTERNAL FUNCTION! Retrieve the queue for order submission task + completeion status. + + Args: + order_id (int): The order's identifier on TWS/Gateway. + + Returns: + :obj:`Optional[ibpy_native.utils.finishable_queue.FinishableQueue]`: + Queue to monitor for the completeion signal of the order + submission task. `None` should be return if the `order_id` + passed in does not match with any queue stored. + """ + return NotImplemented + + #region - Order events @abc.abstractmethod def order_error(self, err: error.IBError): """Handles the error return from IB for the order submiteted.""" return NotImplemented + @abc.abstractmethod + def on_order_submission(self, order_id: int): + """INTERNAL FUNCTION! Triggers while invoking the internal order + submission function. + + Args: + order_id (int): The order's identifier on TWS/Gateway. + """ + return NotImplemented + @abc.abstractmethod def on_open_order_updated( self, contract: ib_contract.Contract, order: ib_order.Order, @@ -85,3 +114,5 @@ def on_order_status_updated( remaining (float): The remnant positions. """ return NotImplemented + #endregion - Order events + #endregion - Internal functions diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 9305ff1..c45614c 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -1,6 +1,7 @@ """IB order related resources.""" import threading -from typing import Dict +import queue +from typing import Dict, Optional from ibapi import contract as ib_contract from ibapi import order as ib_order @@ -9,6 +10,7 @@ from ibpy_native import error from ibpy_native import models from ibpy_native.interfaces import delegates +from ibpy_native.utils import finishable_queue as fq class OrdersManager(delegates.OrdersManagementDelegate): """Class to handle orders related events.""" @@ -18,6 +20,7 @@ def __init__(self): # Property self._next_order_id = 0 self._open_orders: Dict[int, models.OpenOrder] = {} + self._pending_queues: Dict[int, fq.FinishableQueue] = {} @property def next_order_id(self) -> int: @@ -34,10 +37,41 @@ def update_next_order_id(self, order_id: int): def is_pending_order(self, val: int) -> bool: return False + #region - Internal functions + def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: + if order_id in self._pending_queues: + return self._pending_queues[order_id] + + return None + + #region - Order events def order_error(self, err: error.IBError): if err.err_code == 399: # Warning message only return + def on_order_submission(self, order_id: int): + """INTERNAL FUNCTION! Creates a new `FinishableQueue` with `order_id` + as key in `_pending_queues` for order submission task completeion + status monitoring. + + Args: + order_id (int): The order's identifier on TWS/Gateway. + + Raises: + ibpy_native.error.IBError: If existing `FinishableQueue` assigned + for the `order_id` specificed is found. + """ + if order_id not in self._pending_queues: + self._pending_queues[order_id] = fq.FinishableQueue( + queue_to_finish=queue.Queue() + ) + else: + raise error.IBError( + rid=order_id, err_code=error.IBErrorCode.DUPLICATE_ORDER_ID, + err_str=f"Existing queue assigned for order ID {order_id} " + "found. Possiblely duplicate order ID is being used." + ) + def on_open_order_updated( self, contract: ib_contract.Contract, order: ib_order.Order, order_state: ib_order_state.OrderState @@ -59,3 +93,5 @@ def on_order_status_updated( filled, remaining, avg_fill_price, last_fill_price, mkt_cap_price ) + #endregion - Order events + #endregion - Internal functions From 61768062c3a9245dcb4090fae27e0311b6ac4392 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 08:29:39 +0000 Subject: [PATCH 093/126] Impelement `is_pending_order` --- ibpy_native/interfaces/delegates/order.py | 2 +- ibpy_native/order.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index a3489e4..da2e4c7 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -43,7 +43,7 @@ def is_pending_order(self, val: int) -> bool: """Check if a identifier matches with an existing order in pending. Args: - val (int): The value to validate. + val (int): The identifier to validate. Returns: bool: `True` if `val` matches with the order identifier of an diff --git a/ibpy_native/order.py b/ibpy_native/order.py index c45614c..4cf5484 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -35,7 +35,7 @@ def update_next_order_id(self, order_id: int): self._next_order_id = order_id def is_pending_order(self, val: int) -> bool: - return False + return val in self._pending_queues #region - Internal functions def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: From 3c1b568e27d569f692afa4d177716f1fd4ca1b15 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 17:34:37 +0000 Subject: [PATCH 094/126] Implement order submission - Added function `IBClient.submit_order`. - Implemented order submission completion signaling mechanism. - Added corresponding unit tests. --- ibpy_native/_internal/_client.py | 23 +++++++++++++++++++++++ ibpy_native/order.py | 7 +++++++ tests/internal/test_client.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 3fdf049..2637e71 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -5,6 +5,7 @@ from ibapi import client as ib_client from ibapi import contract as ib_contract +from ibapi import order as ib_order from ibapi import wrapper as ib_wrapper from ibpy_native import error @@ -48,6 +49,28 @@ async def req_next_order_id(self) -> int: return self._wrapper.orders_manager.next_order_id + async def submit_order(self, contract: ib_contract.Contract, + order: ib_order.Order): + """Send the order to IB TWS/Gateway for submission. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + order (:obj:`ibapi.order.Order`): Order to be submitted. + + Raises: + ibpy_native.error.IBError: If order error is returned from IB after + the order is sent to TWS/Gateway. + """ + self.placeOrder(orderId=order.orderId, contract=contract, order=order) + self._wrapper.orders_manager.on_order_submission(order_id=order.orderId) + + queue = self._wrapper.orders_manager.get_pending_queue( + order_id=order.orderId) + result = await queue.get() # Wait for completeion signal + + if queue.status is fq.Status.ERROR: + if isinstance(result[-1], error.IBError): + raise result[-1] #endregion - Orders #region - Contract diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 4cf5484..782eeae 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -48,6 +48,10 @@ def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: def order_error(self, err: error.IBError): if err.err_code == 399: # Warning message only return + if err.rid in self._pending_queues: + # Signals the order submission error + if self._pending_queues[err.rid].status is not fq.Status.FINISHED: + self._pending_queues[err.rid].put(element=err) def on_order_submission(self, order_id: int): """INTERNAL FUNCTION! Creates a new `FinishableQueue` with `order_id` @@ -82,6 +86,9 @@ def on_open_order_updated( self._open_orders[order.orderId] = models.OpenOrder( contract, order, order_state ) + if order.orderId in self._pending_queues: + self._pending_queues[order.orderId].put( + element=fq.Status.FINISHED) def on_order_status_updated( self, order_id: int, filled: float, remaining: float, diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 95f34e9..d26db8c 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -18,6 +18,7 @@ from ibpy_native.utils import finishable_queue as fq from tests.toolkit import sample_contracts +from tests.toolkit import sample_orders from tests.toolkit import utils class TestOrder(unittest.TestCase): @@ -35,12 +36,40 @@ def setUpClass(cls): thread = threading.Thread(target=cls._client.run) thread.start() + def setUp(self): + self._orders_manager = self._wrapper.orders_manager + @utils.async_test async def test_req_next_order_id(self): """Test function `req_next_order_id`.""" next_order_id = await self._client.req_next_order_id() self.assertGreater(next_order_id, 0) + @utils.async_test + async def test_submit_order(self): + """Test function `submit_order`.""" + order_id = await self._client.req_next_order_id() + + await self._client.submit_order( + contract=sample_contracts.gbp_usd_fx(), + order=sample_orders.mkt(order_id=order_id, + action=datatype.OrderAction.BUY) + ) + self.assertTrue(order_id in self._orders_manager.open_orders) + + @utils.async_test + async def test_submit_order_err(self): + """Test function `submit_order`. + + * Error returned from IB for duplicated order ID. + """ + with self.assertRaises(error.IBError): + await self._client.submit_order( + contract=sample_contracts.gbp_usd_fx(), + order=sample_orders.mkt(order_id=1, + action=datatype.OrderAction.BUY) + ) + @classmethod def tearDownClass(cls): cls._client.disconnect() From a11962377b8b316678d6584e01941c86772fe9b7 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 18:23:49 +0000 Subject: [PATCH 095/126] Reorder functions --- ibpy_native/_internal/_client.py | 90 +++++++++++++++---------------- ibpy_native/_internal/_wrapper.py | 16 +++--- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 2637e71..6f99728 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -28,51 +28,6 @@ def __init__(self, wrapper: _wrapper.IBWrapper): self._wrapper = wrapper super().__init__(wrapper) - #region - Orders - async def req_next_order_id(self) -> int: - """Request the next valid order ID from IB. - - Returns: - int: The next valid order ID returned from IB. - - Raises: - ibpy_native.error.IBError: If queue associated with `req_id` -1 is - being used by other task. - """ - try: - f_queue = self._wrapper.get_request_queue(req_id=-1) - except error.IBError as err: - raise err - # Request next valid order ID - self.reqIds(numIds=-1) # `numIds` has deprecated - await f_queue.get() - - return self._wrapper.orders_manager.next_order_id - - async def submit_order(self, contract: ib_contract.Contract, - order: ib_order.Order): - """Send the order to IB TWS/Gateway for submission. - - Args: - contract (:obj:`ibapi.contract.Contract`): The order's contract. - order (:obj:`ibapi.order.Order`): Order to be submitted. - - Raises: - ibpy_native.error.IBError: If order error is returned from IB after - the order is sent to TWS/Gateway. - """ - self.placeOrder(orderId=order.orderId, contract=contract, order=order) - self._wrapper.orders_manager.on_order_submission(order_id=order.orderId) - - queue = self._wrapper.orders_manager.get_pending_queue( - order_id=order.orderId) - result = await queue.get() # Wait for completeion signal - - if queue.status is fq.Status.ERROR: - if isinstance(result[-1], error.IBError): - raise result[-1] - #endregion - Orders - #region - Contract async def resolve_contract( self, req_id: int, contract: ib_contract.Contract @@ -167,6 +122,51 @@ async def resolve_contracts( ) #endregion - Contract + #region - Orders + async def req_next_order_id(self) -> int: + """Request the next valid order ID from IB. + + Returns: + int: The next valid order ID returned from IB. + + Raises: + ibpy_native.error.IBError: If queue associated with `req_id` -1 is + being used by other task. + """ + try: + f_queue = self._wrapper.get_request_queue(req_id=-1) + except error.IBError as err: + raise err + # Request next valid order ID + self.reqIds(numIds=-1) # `numIds` has deprecated + await f_queue.get() + + return self._wrapper.orders_manager.next_order_id + + async def submit_order(self, contract: ib_contract.Contract, + order: ib_order.Order): + """Send the order to IB TWS/Gateway for submission. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + order (:obj:`ibapi.order.Order`): Order to be submitted. + + Raises: + ibpy_native.error.IBError: If order error is returned from IB after + the order is sent to TWS/Gateway. + """ + self.placeOrder(orderId=order.orderId, contract=contract, order=order) + self._wrapper.orders_manager.on_order_submission(order_id=order.orderId) + + queue = self._wrapper.orders_manager.get_pending_queue( + order_id=order.orderId) + result = await queue.get() # Wait for completeion signal + + if queue.status is fq.Status.ERROR: + if isinstance(result[-1], error.IBError): + raise result[-1] + #endregion - Orders + #region - Historical data async def resolve_head_timestamp( self, req_id: int, contract: ib_contract.Contract, diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 1606910..3ba1ec7 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -202,6 +202,14 @@ def updateAccountTime(self, timeStamp: str): #endregion - account updates #endregion - Accounts & portfolio + #region - Get contract details + def contractDetails(self, reqId, contractDetails): + self._req_queue[reqId].put(element=contractDetails) + + def contractDetailsEnd(self, reqId): + self._req_queue[reqId].put(element=fq.Status.FINISHED) + #endregion - Get contract details + #region - Orders def nextValidId(self, orderId: int): # Next valid order ID returned from IB @@ -228,14 +236,6 @@ def orderStatus(self, orderId: int, status: str, filled: float, ) #endregion - Orders - #region - Get contract details - def contractDetails(self, reqId, contractDetails): - self._req_queue[reqId].put(element=contractDetails) - - def contractDetailsEnd(self, reqId): - self._req_queue[reqId].put(element=fq.Status.FINISHED) - #endregion - Get contract details - # Get earliest data point for a given instrument and data def headTimestamp(self, reqId: int, headTimestamp: str): # override method From 05e4e544061feb8f7970a740a2fb61fd5930a186 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 21:02:29 +0000 Subject: [PATCH 096/126] Implement order cancellation - Added function `IBClient.cancel_order`. - Implemented corresponding unit test. - Added helper function `sample_orders.no_transmit`. --- ibpy_native/_internal/_client.py | 13 +++++++++++++ tests/internal/test_client.py | 15 +++++++++++++++ tests/toolkit/sample_orders.py | 8 ++++++++ 3 files changed, 36 insertions(+) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 6f99728..012a69b 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -165,6 +165,19 @@ async def submit_order(self, contract: ib_contract.Contract, if queue.status is fq.Status.ERROR: if isinstance(result[-1], error.IBError): raise result[-1] + + def cancel_order(self, order_id: int): + """Cancel an order submitted. + + Args: + order_id (int): The order's identifer. + """ + self.cancelOrder(orderId=order_id) + if self._wrapper.orders_manager.is_pending_order(val=order_id): + # Send finish signal to the pending order + queue = self._wrapper.orders_manager.get_pending_queue(order_id) + if queue.status is not (fq.Status.FINISHED or fq.Status.ERROR): + queue.put(element=fq.Status.FINISHED) #endregion - Orders #region - Historical data diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index d26db8c..7d60f9e 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -70,6 +70,21 @@ async def test_submit_order_err(self): action=datatype.OrderAction.BUY) ) + @utils.async_test + async def test_cancel_order(self): + """Test function `cancel_order`.""" + order_id = await self._client.req_next_order_id() + + order_submit_task = asyncio.create_task(self._client.submit_order( + contract=sample_contracts.gbp_usd_fx(), + order=sample_orders.no_transmit(order_id) + )) + await asyncio.sleep(0.5) # Give the order time to arrive at TWS/Gateway + self._client.cancel_order(order_id) + await order_submit_task + # Nothing to assert in this test. + # The function is good as long as there's no error thrown. + @classmethod def tearDownClass(cls): cls._client.disconnect() diff --git a/tests/toolkit/sample_orders.py b/tests/toolkit/sample_orders.py index d21982e..a86319f 100644 --- a/tests/toolkit/sample_orders.py +++ b/tests/toolkit/sample_orders.py @@ -26,3 +26,11 @@ def mkt(order_id: int, action: datatype.OrderAction) -> ib_order.Order: order.orderType = "MKT" return order + +def no_transmit(order_id: int) -> ib_order.Order: + """Market order with `transmit` set to `False`.""" + order = _base_order(order_id, datatype.OrderAction.BUY) + order.orderType = "MKT" + order.transmit = False + + return order From c3f2653a59c4a065f0cb97689574354abb53bfb0 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 21:16:37 +0000 Subject: [PATCH 097/126] Modify `IBClient.submit_order` - Invokes `OrdersManager.on_order_submission` before sending the order to IB via `EClient.placeOrder` function and wrap it in a try/catch block. --- ibpy_native/_internal/_client.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 012a69b..5f11352 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -152,11 +152,17 @@ async def submit_order(self, contract: ib_contract.Contract, order (:obj:`ibapi.order.Order`): Order to be submitted. Raises: - ibpy_native.error.IBError: If order error is returned from IB after - the order is sent to TWS/Gateway. + ibpy_native.error.IBError: If + - pending order with order ID same as the order passed in; + - order error is returned from IB after the order is sent to + TWS/Gateway. """ + try: + self._wrapper.orders_manager.on_order_submission( + order_id=order.orderId) + except error.IBError as err: + raise err self.placeOrder(orderId=order.orderId, contract=contract, order=order) - self._wrapper.orders_manager.on_order_submission(order_id=order.orderId) queue = self._wrapper.orders_manager.get_pending_queue( order_id=order.orderId) From c01ffa9463b4fe179dc2ad832bab353959b6eb74 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Wed, 24 Feb 2021 21:30:29 +0000 Subject: [PATCH 098/126] Modify `OrdersManager.is_pending_order` - Renamed argument `val` to `order_id`. - Updated the condition of true/false return value. --- ibpy_native/_internal/_client.py | 2 +- ibpy_native/_internal/_wrapper.py | 2 +- ibpy_native/interfaces/delegates/order.py | 4 ++-- ibpy_native/order.py | 8 ++++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 5f11352..e09f998 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -179,7 +179,7 @@ def cancel_order(self, order_id: int): order_id (int): The order's identifer. """ self.cancelOrder(orderId=order_id) - if self._wrapper.orders_manager.is_pending_order(val=order_id): + if self._wrapper.orders_manager.is_pending_order(order_id): # Send finish signal to the pending order queue = self._wrapper.orders_manager.get_pending_queue(order_id) if queue.status is not (fq.Status.FINISHED or fq.Status.ERROR): diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 3ba1ec7..7d48ccf 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -151,7 +151,7 @@ def error(self, reqId, errorCode, errorString): # -1 indicates a notification and not true error condition if reqId is not -1: - if self._orders_manager.is_pending_order(val=reqId): + if self._orders_manager.is_pending_order(order_id=reqId): self._orders_manager.order_error(err) elif reqId in self._req_queue: self._req_queue[reqId].put(element=err) diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index da2e4c7..5bf61f6 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -39,11 +39,11 @@ def update_next_order_id(self, order_id: int): return NotImplemented @abc.abstractmethod - def is_pending_order(self, val: int) -> bool: + def is_pending_order(self, order_id: int) -> bool: """Check if a identifier matches with an existing order in pending. Args: - val (int): The identifier to validate. + order_id (int): The order identifier to validate. Returns: bool: `True` if `val` matches with the order identifier of an diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 782eeae..2a213dc 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -34,8 +34,12 @@ def update_next_order_id(self, order_id: int): with self._lock: self._next_order_id = order_id - def is_pending_order(self, val: int) -> bool: - return val in self._pending_queues + def is_pending_order(self, order_id: int) -> bool: + if order_id in self._pending_queues: + if self._pending_queues[order_id].status is fq.Status.INIT: + return True + + return False #region - Internal functions def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: From b346721603f165f2fc0d07c2a4f862a7da111945 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 02:12:40 +0000 Subject: [PATCH 099/126] `IBBridge` order placing - Added functions `next_order_id` & `place_orders` in `IBBridge` for order placing. - Implemented corresponding unit tests. --- ibpy_native/bridge.py | 45 +++++++++++++++++++++++++++++- tests/test_bridge.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index d2ef70b..3497227 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -5,9 +5,10 @@ import asyncio import datetime import threading -from typing import Iterator, List, Optional +from typing import Awaitable, Iterator, List, Optional from ibapi import contract as ib_contract +from ibapi import order as ib_order import ibpy_native from ibpy_native import account as ib_account @@ -198,6 +199,48 @@ async def search_detailed_contracts( return res + #region - Orders + async def next_order_id(self) -> int: + """Get next valid order ID. + + Returns: + int: The next valid order ID. + """ + return await self._client.req_next_order_id() + + async def place_orders(self, contract: ib_contract.Contract, + orders: List[ib_order.Order]): + """Place order(s) to IB. + + Note: + Order IDs must be unique for each order. Arguments `orders` can be + used to place child order(s) together with the parent order but you + should make sure orders passing in are all valid. All of the orders + passed in will be cancelled if there's error casuse by any of the + order in the list. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + orders (:obj:`List[ibapi.order.Order]`): Order(s) to be submitted. + + Raises: + ibpy_native.error.IBError: If any order error returned from IB or + lower level internal processes. + """ + coroutines: List[Awaitable[None]] = [] + + for order in orders: + coroutines.append(self._client.submit_order(contract, order)) + + try: + await asyncio.gather(*coroutines) + except error.IBError as err: + for order in orders: + self._client.cancel_order(order_id=order.orderId) + + raise err + #endregion - Orders + #region - Historical data async def get_earliest_data_point( self, contract: ib_contract.Contract, diff --git a/tests/test_bridge.py b/tests/test_bridge.py index 943c6fa..56e049e 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -17,6 +17,7 @@ from ibpy_native.utils import finishable_queue as fq from tests.toolkit import sample_contracts +from tests.toolkit import sample_orders from tests.toolkit import utils class TestGeneral(unittest.TestCase): @@ -205,6 +206,69 @@ async def test_search_detailed_contracts_err(self): def tearDownClass(cls): cls._bridge.disconnect() +class TestOrder(unittest.TestCase): + """Unit tests for IB order related functions in `IBBridge`. + + Connection with IB is REQUIRED. + """ + @classmethod + def setUpClass(cls): + cls._bridge = bridge.IBBridge(host=utils.IB_HOST, port=utils.IB_PORT, + client_id=utils.IB_CLIENT_ID) + + def setUp(self): + self._orders_manager = self._bridge.orders_manager + + @utils.async_test + async def test_next_order_id(self): + """Test function `next_order_id`.""" + old_order_id = self._orders_manager.next_order_id + next_order_id = await self._bridge.next_order_id() + + self.assertGreater(next_order_id, old_order_id) + + @utils.async_test + async def test_place_orders(self): + """Test function `place_orders`.""" + # Prepare orders + order1 = sample_orders.mkt(order_id=await self._bridge.next_order_id(), + action=datatype.OrderAction.BUY) + order2 = sample_orders.mkt(order_id=order1.orderId + 1, + action=datatype.OrderAction.SELL) + + await self._bridge.place_orders(contract=sample_contracts.gbp_usd_fx(), + orders=[order1, order2]) + self.assertTrue(order1.orderId in self._orders_manager.open_orders) + self.assertTrue(order2.orderId in self._orders_manager.open_orders) + self.assertFalse( + self._orders_manager.is_pending_order(order_id=order1.orderId)) + self.assertFalse( + self._orders_manager.is_pending_order(order_id=order2.orderId)) + + @utils.async_test + async def test_place_orders_err(self): + """Test function `place_orders`. + + * Error expected due to duplicate order ID. + """ + # Prepare orders + order1 = sample_orders.mkt(order_id=await self._bridge.next_order_id(), + action=datatype.OrderAction.BUY) + order2 = sample_orders.mkt(order_id=order1.orderId, + action=datatype.OrderAction.SELL) + + with self.assertRaises(error.IBError): + await self._bridge.place_orders( + contract=sample_contracts.gbp_usd_fx(), + orders=[order1, order2] + ) + self.assertFalse( + self._orders_manager.is_pending_order(order_id=order1.orderId)) + + @classmethod + def tearDownClass(cls): + cls._bridge.disconnect() + class TestHistoricalData(unittest.TestCase): """Unit tests for historical market data related functions in `IBBridge`. From 62ff594e55e6aa43c1bfe057b24bbd6fa645a0fc Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 04:25:49 +0000 Subject: [PATCH 100/126] Fix order status - Modified functions and properties related to order status information to take the status returned from `IBWrapper.orderStatus` callback as its' value. --- ibpy_native/_internal/_wrapper.py | 12 ++++++------ ibpy_native/interfaces/delegates/order.py | 8 +++++++- ibpy_native/models/order.py | 11 +++++++---- ibpy_native/order.py | 11 ++++++----- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 7d48ccf..d544553 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -150,11 +150,11 @@ def error(self, reqId, errorCode, errorString): err = error.IBError(rid=reqId, err_code=errorCode, err_str=errorString) # -1 indicates a notification and not true error condition - if reqId is not -1: - if self._orders_manager.is_pending_order(order_id=reqId): - self._orders_manager.order_error(err) - elif reqId in self._req_queue: - self._req_queue[reqId].put(element=err) + if reqId is not -1 and self._orders_manager.is_pending_order( + order_id=reqId): + self._orders_manager.order_error(err) + elif reqId is not -1 and reqId in self._req_queue: + self._req_queue[reqId].put(element=err) else: if self._notification_listener is not None: self._notification_listener.on_notify( @@ -230,7 +230,7 @@ def orderStatus(self, orderId: int, status: str, filled: float, parentId: int, lastFillPrice: float, clientId: int, whyHeld: str, mktCapPrice: float): self._orders_manager.on_order_status_updated( - order_id=orderId, filled=filled, remaining=remaining, + order_id=orderId, status=status, filled=filled, remaining=remaining, avg_fill_price=avgFillPrice, last_fill_price=lastFillPrice, mkt_cap_price=mktCapPrice ) diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index 5bf61f6..ab43a71 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -103,15 +103,21 @@ def on_open_order_updated( @abc.abstractmethod def on_order_status_updated( - self, order_id: int, filled: float, remaining: float, + self, order_id: int, status: str, filled: float, remaining: float, avg_fill_price: float, last_fill_price: float, mkt_cap_price: float ): """INTERNAL FUNCTION! Handles the `orderStatus` callback from IB. Args: order_id (int): The order's identifier on TWS/Gateway. + status (str): The current status of the order. filled (float): Number of filled positions. remaining (float): The remnant positions. + avg_fill_price (float): Average filling price. + last_fill_price (float): Price at which the last positions were + filled. + mkt_cap_price (float): If an order has been capped, this indicates + the current capped price. """ return NotImplemented #endregion - Order events diff --git a/ibpy_native/models/order.py b/ibpy_native/models/order.py index 4ef0e62..dd32786 100644 --- a/ibpy_native/models/order.py +++ b/ibpy_native/models/order.py @@ -26,6 +26,7 @@ def __init__(self, contract: ib_contract.Contract, order: ib_order.Order, self._order = order self._order_state = order_state # From `orderStatus` callback + self._status = datatype.OrderStatus(order_state.status) self._avg_fill_price = 0.0 self._mkt_cap_price = 0.0 self._exec_rec: List[datatype.OrderExecRec] = [] @@ -74,7 +75,7 @@ def status(self) -> datatype.OrderStatus: """:obj:`ibpy_native.utils.datatype.OrderStatus`: The order's current status. """ - return datatype.OrderStatus(self._order_state.status) + return self._status @property def filled(self) -> int: @@ -144,9 +145,9 @@ def order_update(self, order: ib_order.Order, self._order = order self._order_state = order_state - def order_status_update(self, filled: float, remaining: float, - avg_fill_price: float, last_fill_price: float, - mkt_cap_price: float): + def order_status_update(self, status: datatype.OrderStatus, filled: float, + remaining: float, avg_fill_price: float, + last_fill_price: float, mkt_cap_price: float): """INTERNAL FUNCTION! Thread-safe function to handle the changes on order status updates from IB. DO NOT use this function in your own code unless you're testing something specifically. @@ -161,6 +162,8 @@ def order_status_update(self, filled: float, remaining: float, the current capped price. """ with self._lock: + self._status = status # Update order status no matter what + if self._exec_rec: if self._exec_rec[-1].remaining == remaining: # Filter out the duplicate messages returned from IB diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 2a213dc..777474e 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -10,6 +10,7 @@ from ibpy_native import error from ibpy_native import models from ibpy_native.interfaces import delegates +from ibpy_native.utils import datatype from ibpy_native.utils import finishable_queue as fq class OrdersManager(delegates.OrdersManagementDelegate): @@ -95,14 +96,14 @@ def on_open_order_updated( element=fq.Status.FINISHED) def on_order_status_updated( - self, order_id: int, filled: float, remaining: float, + self, order_id: int, status: str, filled: float, remaining: float, avg_fill_price: float, last_fill_price: float, mkt_cap_price: float ): - if order_id in self._open_orders and filled != 0: - # Filter out the message(s) with no actual trade + if order_id in self._open_orders: self._open_orders[order_id].order_status_update( - filled, remaining, avg_fill_price, - last_fill_price, mkt_cap_price + status=datatype.OrderStatus(status), filled=filled, + remaining=remaining, avg_fill_price=avg_fill_price, + last_fill_price=last_fill_price, mkt_cap_price=mkt_cap_price ) #endregion - Order events #endregion - Internal functions From a6df5ad9b38371f29385ad7ae477d6d1493a6f59 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 04:31:26 +0000 Subject: [PATCH 101/126] `IBBridge` order cancelation - Added function `cancel_order` in `IBBridge`. - Implemented corresponding unit test. --- ibpy_native/bridge.py | 16 ++++++++++++++++ tests/test_bridge.py | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 3497227..dad3026 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -239,6 +239,22 @@ async def place_orders(self, contract: ib_contract.Contract, self._client.cancel_order(order_id=order.orderId) raise err + + def cancel_order(self, order_id: int): + """Cancel a submitted order. + + Note: + No error will be raise even if you pass in an ID which doesn't + match any existing open order. A warning message will be returned + via the `NotificationListener` supplied instead. + + A message will be returned to the `NotificationListener` supplied + once the order is cancelled. + + Args: + order_id (int): The order's identifier. + """ + self._client.cancel_order(order_id) #endregion - Orders #region - Historical data diff --git a/tests/test_bridge.py b/tests/test_bridge.py index 56e049e..a903b5c 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -265,6 +265,19 @@ async def test_place_orders_err(self): self.assertFalse( self._orders_manager.is_pending_order(order_id=order1.orderId)) + @utils.async_test + async def test_cancel_order(self): + """Test function `cancel_order`.""" + order = sample_orders.lmt(order_id=await self._bridge.next_order_id(), + action=datatype.OrderAction.SELL, + price=3) + await self._bridge.place_orders(contract=sample_contracts.gbp_usd_fx(), + orders=[order]) + self._bridge.cancel_order(order_id=order.orderId) + await asyncio.sleep(0.5) # Give time the cancel request to arrive IB + self.assertEqual(self._orders_manager.open_orders[order.orderId].status, + datatype.OrderStatus.CANCELLED) + @classmethod def tearDownClass(cls): cls._bridge.disconnect() From e3a5acb5348ec54763fcf5e5e6a66d772123815c Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 13:10:09 +0000 Subject: [PATCH 102/126] Tidy up - Reordered functions. - Edited docstring for the functions. --- ibpy_native/interfaces/delegates/order.py | 26 +++++++++++++---------- ibpy_native/models/order.py | 2 ++ ibpy_native/order.py | 8 +++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index ab43a71..ad7a37e 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -28,16 +28,6 @@ def open_orders(self) -> Dict[int, models.OpenOrder]: """ return NotImplemented - @abc.abstractmethod - def update_next_order_id(self, order_id: int): - """Internal function to update the next order ID stored. Not expected - to be invoked by the user. - - Args: - order_id (int): The updated order identifier. - """ - return NotImplemented - @abc.abstractmethod def is_pending_order(self, order_id: int) -> bool: """Check if a identifier matches with an existing order in pending. @@ -52,6 +42,15 @@ def is_pending_order(self, order_id: int) -> bool: return NotImplemented #region - Internal functions + @abc.abstractmethod + def update_next_order_id(self, order_id: int): + """INTERNAL FUNCTION! Update the next order ID stored. + + Args: + order_id (int): The updated order identifier. + """ + return NotImplemented + @abc.abstractmethod def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: """INTERNAL FUNCTION! Retrieve the queue for order submission task @@ -71,7 +70,12 @@ def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: #region - Order events @abc.abstractmethod def order_error(self, err: error.IBError): - """Handles the error return from IB for the order submiteted.""" + """INTERNAL FUNCTION! Handles the error return from IB for the order + submiteted. + + Args: + err (:obj:`ibpy_native.error.IBError`): Error returned from IB. + """ return NotImplemented @abc.abstractmethod diff --git a/ibpy_native/models/order.py b/ibpy_native/models/order.py index dd32786..75a82cd 100644 --- a/ibpy_native/models/order.py +++ b/ibpy_native/models/order.py @@ -153,6 +153,8 @@ def order_status_update(self, status: datatype.OrderStatus, filled: float, unless you're testing something specifically. Args: + status (:obj:`ibpy_native.utils.datatype.OrderStatus`): The order's + current status. filled (float): Number of filled positions. remaining (float): The remnant positions. avg_fill_price (float): Average filling price. diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 777474e..a8dd9d4 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -31,10 +31,6 @@ def next_order_id(self) -> int: def open_orders(self) -> Dict[int, models.OpenOrder]: return self._open_orders - def update_next_order_id(self, order_id: int): - with self._lock: - self._next_order_id = order_id - def is_pending_order(self, order_id: int) -> bool: if order_id in self._pending_queues: if self._pending_queues[order_id].status is fq.Status.INIT: @@ -43,6 +39,10 @@ def is_pending_order(self, order_id: int) -> bool: return False #region - Internal functions + def update_next_order_id(self, order_id: int): + with self._lock: + self._next_order_id = order_id + def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: if order_id in self._pending_queues: return self._pending_queues[order_id] From 1eb8099823c5cf323c7f7e5360b80147f95e8552 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 14:46:52 +0000 Subject: [PATCH 103/126] Order events listener - Created listener interface `OrderEventsListener`. - Modified `OrdersManager` to trigger event callbacks using the `OrderEventsListener`. --- ibpy_native/interfaces/listeners/__init__.py | 1 + ibpy_native/interfaces/listeners/order.py | 35 ++++++++++++++++++++ ibpy_native/order.py | 20 ++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 ibpy_native/interfaces/listeners/order.py diff --git a/ibpy_native/interfaces/listeners/__init__.py b/ibpy_native/interfaces/listeners/__init__.py index 0edeb5c..2f41682 100644 --- a/ibpy_native/interfaces/listeners/__init__.py +++ b/ibpy_native/interfaces/listeners/__init__.py @@ -1,3 +1,4 @@ """Interfaces of event listeners.""" from .live_ticks import LiveTicksListener from .notification import NotificationListener +from .order import OrderEventsListener diff --git a/ibpy_native/interfaces/listeners/order.py b/ibpy_native/interfaces/listeners/order.py new file mode 100644 index 0000000..62b51af --- /dev/null +++ b/ibpy_native/interfaces/listeners/order.py @@ -0,0 +1,35 @@ +"""Listener interfaces for order related functions.""" +import abc + +from ibpy_native import models +from ibpy_native.interfaces.listeners import base + +class OrderEventsListener(base.BaseListener): + """Interface of listener for order related events.""" + @abc.abstractmethod + def on_warning(self, order_id: int, msg: str): + """Callback on event of order warning message received from IB. + + Args: + order_id (int): The order's client identifier. + msg (str): The warning message. + """ + return NotImplemented + + @abc.abstractmethod + def on_cancelled(self, order: models.OpenOrder): + """Callback on event of order cancelation confirmed by IB. + + Args: + order (:obj:`ibpy_native.models.OpenOrder`): The cancelled order. + """ + return NotImplemented + + @abc.abstractmethod + def on_filled(self, order: models.OpenOrder): + """Callback on event of order has been completely filled. + + Args: + order (:obj:`ibpy_native.models.OpenOrder`): The completed order. + """ + return NotImplemented diff --git a/ibpy_native/order.py b/ibpy_native/order.py index a8dd9d4..2731e0f 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -3,6 +3,7 @@ import queue from typing import Dict, Optional +from ibapi import common from ibapi import contract as ib_contract from ibapi import order as ib_order from ibapi import order_state as ib_order_state @@ -10,14 +11,17 @@ from ibpy_native import error from ibpy_native import models from ibpy_native.interfaces import delegates +from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype from ibpy_native.utils import finishable_queue as fq class OrdersManager(delegates.OrdersManagementDelegate): """Class to handle orders related events.""" - def __init__(self): + def __init__(self, + event_listener: Optional[listeners.OrderEventsListener]=None): # Internal members self._lock = threading.Lock() + self._listener = event_listener # Property self._next_order_id = 0 self._open_orders: Dict[int, models.OpenOrder] = {} @@ -52,8 +56,13 @@ def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: #region - Order events def order_error(self, err: error.IBError): if err.err_code == 399: # Warning message only + if self._listener: + self._listener.on_warning(order_id=err.rid, msg=err.msg) return if err.rid in self._pending_queues: + if self._listener: + self._listener.on_err(err) + # Signals the order submission error if self._pending_queues[err.rid].status is not fq.Status.FINISHED: self._pending_queues[err.rid].put(element=err) @@ -87,6 +96,12 @@ def on_open_order_updated( ): if order.orderId in self._open_orders: self._open_orders[order.orderId].order_update(order, order_state) + if (self._listener + and order_state.status == "Filled" + and order_state.commission != common.UNSET_DOUBLE): + # Commission validation is to filter out the 1st incomplete + # order filled status update. + self._listener.on_filled(order=self._open_orders[order.orderId]) else: self._open_orders[order.orderId] = models.OpenOrder( contract, order, order_state @@ -105,5 +120,8 @@ def on_order_status_updated( remaining=remaining, avg_fill_price=avg_fill_price, last_fill_price=last_fill_price, mkt_cap_price=mkt_cap_price ) + + if self._listener and status == "Cancelled": + self._listener.on_cancelled(order=self._open_orders[order_id]) #endregion - Order events #endregion - Internal functions From c1f3bfad48ce199a76f951334989384fdc9ed822 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 15:07:54 +0000 Subject: [PATCH 104/126] Order rejection event - Handled the event of order rejection. --- ibpy_native/_internal/_wrapper.py | 5 ++++- ibpy_native/interfaces/delegates/order.py | 11 +++++++++++ ibpy_native/interfaces/listeners/order.py | 10 ++++++++++ ibpy_native/order.py | 5 +++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index d544553..c1c0205 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -151,8 +151,11 @@ def error(self, reqId, errorCode, errorString): # -1 indicates a notification and not true error condition if reqId is not -1 and self._orders_manager.is_pending_order( - order_id=reqId): + order_id=reqId): # Is an order error self._orders_manager.order_error(err) + elif reqId is not -1 and errorCode == 201: # 201 == order rejected + self._orders_manager.on_order_rejected(order_id=reqId, + reason=errorString) elif reqId is not -1 and reqId in self._req_queue: self._req_queue[reqId].put(element=err) else: diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index ad7a37e..b5ebe5b 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -124,5 +124,16 @@ def on_order_status_updated( the current capped price. """ return NotImplemented + + @abc.abstractmethod + def on_order_rejected(self, order_id: int, reason: str): + """INTERNAL FUNCTION! Handles the order rejection error and message + received in `error` callback from IB. + + Args: + order_id (int): The order's client identifier. + reason (str): Reason of order rejection. + """ + return NotImplemented #endregion - Order events #endregion - Internal functions diff --git a/ibpy_native/interfaces/listeners/order.py b/ibpy_native/interfaces/listeners/order.py index 62b51af..3b3db56 100644 --- a/ibpy_native/interfaces/listeners/order.py +++ b/ibpy_native/interfaces/listeners/order.py @@ -25,6 +25,16 @@ def on_cancelled(self, order: models.OpenOrder): """ return NotImplemented + @abc.abstractmethod + def on_rejected(self, order: models.OpenOrder, reason: str): + """Callback on event of order rejected by IB. + + Args: + order (:obj:`ibpy_native.models.OpenOrder`): The rejected order. + reason (str): Reason of order rejection. + """ + return NotImplemented + @abc.abstractmethod def on_filled(self, order: models.OpenOrder): """Callback on event of order has been completely filled. diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 2731e0f..3d00a8a 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -123,5 +123,10 @@ def on_order_status_updated( if self._listener and status == "Cancelled": self._listener.on_cancelled(order=self._open_orders[order_id]) + + def on_order_rejected(self, order_id: int, reason: str): + if self._listener and order_id in self._open_orders: + self._listener.on_rejected(order=self._open_orders[order_id], + reason=reason) #endregion - Order events #endregion - Internal functions From 735042206bcd58925b528a0eab3ef15a2d61964f Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 15:17:56 +0000 Subject: [PATCH 105/126] `IBBridge` takes `OrderEventsListener` - Modified constructor of `IBBridge` to take the user-defined `OrderEventsListener` and initialise the `OrdersManager` with it. --- ibpy_native/bridge.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index dad3026..23e390b 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -44,7 +44,8 @@ class IBBridge: def __init__( self, host: str="127.0.0.1", port: int=4001, client_id: int=1, auto_conn: bool=True, - notification_listener:Optional[listeners.NotificationListener]=None, + notification_listener: Optional[listeners.NotificationListener]=None, + order_events_listener: Optional[listeners.OrderEventsListener]=None, accounts_manager: Optional[ib_account.AccountsManager]=None ): self._host = host @@ -54,7 +55,8 @@ def __init__( ib_account.AccountsManager() if accounts_manager is None else accounts_manager ) - self._orders_manager = ibpy_native.OrdersManager() + self._orders_manager = ibpy_native.OrdersManager( + event_listener=order_events_listener) self._wrapper = _wrapper.IBWrapper( orders_manager=self._orders_manager, From a2acc607fd362d9d53ec3f926f988f4e909e19ee Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Thu, 25 Feb 2021 17:19:16 +0000 Subject: [PATCH 106/126] Connection heart beat - Added function `heart_beat` to run an infinity loop to monitor the connection between the client program and TWS/Gateway in the background. --- ibpy_native/bridge.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 23e390b..f5caadc 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -4,6 +4,7 @@ # pylint: disable=protected-access import asyncio import datetime +import time import threading from typing import Awaitable, Iterator, List, Optional @@ -128,8 +129,8 @@ def connect(self): self._client.connect(host=self._host, port=self._port, clientId=self._client_id) - thread = threading.Thread(target=self._client.run) - thread.start() + threading.Thread(name="ib_loop", target=self._client.run).start() + threading.Thread(name="heart_beat", target=self._heart_beat).start() def disconnect(self): """Disconnect the bridge from the connected TWS/IB Gateway instance. @@ -444,3 +445,13 @@ def stop_live_ticks_stream(self, stream_id: int): except error.IBError as err: raise err #endregion - Live data + + #region - Private functions + def _heart_beat(self): + """Infinity loop to monitor connection with TWS/Gateway.""" + while True: + time.sleep(2) + self._client.reqCurrentTime() + if not self._client.isConnected(): + break + #endregion - Private functions From d75a240fa46d28faf89b85e148eb34a0cbc0a852 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sat, 27 Feb 2021 03:12:32 +0000 Subject: [PATCH 107/126] Handle connection drop - Callback `IBWrapper.error` now handles the case of error `504` - "Not connected". - Calls functions to set error signal to all active `FinishableQueue` in the request queue dict, the pending orders dict, and the account updates queue. - Public abstract function `on_disconnected` is added to `OrdersManagementDelegate`, `OrdersManager`, `AccountsManagementDelegate`, and `AccountsManager` for the action of clean up and reset in those classes. - Added error code `504` in enum `IBErrorCode`. - Defined the error message for the case of disconnected in `ibpy_native._internal._global`. --- ibpy_native/_internal/_global.py | 3 ++ ibpy_native/_internal/_wrapper.py | 32 +++++++++++++++++++++ ibpy_native/account.py | 10 +++++++ ibpy_native/error.py | 1 + ibpy_native/interfaces/delegates/account.py | 6 ++++ ibpy_native/interfaces/delegates/order.py | 6 ++++ ibpy_native/order.py | 18 ++++++++++++ tests/test_account.py | 11 +++++++ tests/toolkit/utils.py | 3 ++ 9 files changed, 90 insertions(+) diff --git a/ibpy_native/_internal/_global.py b/ibpy_native/_internal/_global.py index 11cde86..9df65fe 100644 --- a/ibpy_native/_internal/_global.py +++ b/ibpy_native/_internal/_global.py @@ -8,3 +8,6 @@ TIME_FMT: Final[str] = "%Y%m%d %H:%M:%S" # Timezone to match the one set in IB Gateway/TWS at login TZ: datetime.tzinfo = pytz.timezone("America/New_York") + +# Mesages +MSG_NOT_CONNECTED: Final[str] = "Not connected." diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index c1c0205..ec636a6 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -11,6 +11,7 @@ from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import _global from ibpy_native._internal import _typing from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners @@ -158,6 +159,9 @@ def error(self, reqId, errorCode, errorString): reason=errorString) elif reqId is not -1 and reqId in self._req_queue: self._req_queue[reqId].put(element=err) + elif reqId == -1 and errorCode == error.IBErrorCode.NOT_CONNECTED: + # Connection dropped + self._on_disconnected() else: if self._notification_listener is not None: self._notification_listener.on_notify( @@ -324,6 +328,33 @@ def _init_req_queue(self, req_id: int): else: self._req_queue[req_id] = fq.FinishableQueue(queue.Queue()) + def _on_disconnected(self): + """Stop all active requests.""" + if self._req_queue[-1].status is not ( + fq.Status.INIT or fq.Status.FINISHED): + # Send finish signal to the active next order ID request + self._req_queue[-1].put(element=fq.Status.FINISHED) + + for key, f_queue in self._req_queue.items(): + if key == -1: + continue + if f_queue.status is not fq.Status.FINISHED or fq.Status.ERROR: + err = error.IBError( + rid=key, err_code=error.IBErrorCode.NOT_CONNECTED, + err_str=_global.MSG_NOT_CONNECTED + ) + f_queue.put(element=err) + + self._reset() + self._orders_manager.on_disconnected() + if self._ac_man_delegate: + self._ac_man_delegate.on_disconnected() + + def _reset(self): + self._req_queue.clear() + self._req_queue[-1] = fq.FinishableQueue(queue_to_finish=queue.Queue()) + + #region - Ticks handling def _handle_historical_ticks_results( self, req_id: int, ticks: _typing.WrapperResHistoricalTicks, done: bool ): @@ -342,4 +373,5 @@ def _handle_live_ticks(self, req_id: int, received into corresponding queue. """ self._req_queue[req_id].put(element=tick) + #endregion - Ticks handling #endregion - Private functions diff --git a/ibpy_native/account.py b/ibpy_native/account.py index 088cdae..0755139 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/account.py @@ -6,6 +6,7 @@ import queue from typing import Dict, List, Optional, Union +from ibpy_native import error from ibpy_native import models from ibpy_native._internal import _global from ibpy_native.interfaces import delegates @@ -139,6 +140,15 @@ async def unsub_account_updates(self): """Unsubscribes to account updates.""" self._account_updates_queue.put(fq.Status.FINISHED) + def on_disconnected(self): + if self._account_updates_queue.status is not ( + fq.Status.ERROR or fq.Status.FINISHED): + err = error.IBError( + rid=-1, err_code=error.IBErrorCode.NOT_CONNECTED, + err_str=_global.MSG_NOT_CONNECTED + ) + self._account_updates_queue.put(element=err) + #region - Private functions async def _prevent_multi_account_updates(self): """Prevent multi subscriptions of account updates by verifying the diff --git a/ibpy_native/error.py b/ibpy_native/error.py index a3e7d51..c773283 100644 --- a/ibpy_native/error.py +++ b/ibpy_native/error.py @@ -8,6 +8,7 @@ class IBErrorCode(enum.IntEnum): DUPLICATE_TICKER_ID = 102 DUPLICATE_ORDER_ID = 103 INVALID_CONTRACT = 200 + NOT_CONNECTED = 504 # Self-defined error codes REQ_TIMEOUT = 50504 RES_NO_CONTENT = 50204 diff --git a/ibpy_native/interfaces/delegates/account.py b/ibpy_native/interfaces/delegates/account.py index 8c35eb0..f807959 100644 --- a/ibpy_native/interfaces/delegates/account.py +++ b/ibpy_native/interfaces/delegates/account.py @@ -54,3 +54,9 @@ async def unsub_account_updates(self): from an on-going account updates subscription. """ return NotImplemented + + @abc.abstractmethod + def on_disconnected(self): + """INTERNAL FUNCTION! Callback for the event of API connection dropped. + """ + return NotImplemented diff --git a/ibpy_native/interfaces/delegates/order.py b/ibpy_native/interfaces/delegates/order.py index b5ebe5b..dd42310 100644 --- a/ibpy_native/interfaces/delegates/order.py +++ b/ibpy_native/interfaces/delegates/order.py @@ -136,4 +136,10 @@ def on_order_rejected(self, order_id: int, reason: str): """ return NotImplemented #endregion - Order events + + @abc.abstractmethod + def on_disconnected(self): + """INTERNAL FUNCTION! Handles the event of API connection dropped. + """ + return NotImplemented #endregion - Internal functions diff --git a/ibpy_native/order.py b/ibpy_native/order.py index 3d00a8a..f7a0e8b 100644 --- a/ibpy_native/order.py +++ b/ibpy_native/order.py @@ -1,4 +1,5 @@ """IB order related resources.""" +# pylint: disable=protected-access import threading import queue from typing import Dict, Optional @@ -10,6 +11,7 @@ from ibpy_native import error from ibpy_native import models +from ibpy_native._internal import _global from ibpy_native.interfaces import delegates from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype @@ -129,4 +131,20 @@ def on_order_rejected(self, order_id: int, reason: str): self._listener.on_rejected(order=self._open_orders[order_id], reason=reason) #endregion - Order events + + def on_disconnected(self): + for key, f_queue in self._pending_queues.items(): + if f_queue.status is not fq.Status.FINISHED or fq.Status.ERROR: + err = error.IBError( + rid=key, err_code=error.IBErrorCode.NOT_CONNECTED, + err_str=_global.MSG_NOT_CONNECTED + ) + f_queue.put(element=err) + + self._reset() #endregion - Internal functions + + def _reset(self): + self._next_order_id = 0 + self._open_orders.clear() + self._pending_queues.clear() diff --git a/tests/test_account.py b/tests/test_account.py index f7e12ed..25ef69d 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -97,6 +97,17 @@ async def test_sub_account_updates(self): (self._manager.accounts[_MOCK_AC_140] .positions[412888950].last_update_time)) + @utils.async_test + async def test_on_disconnected(self): + """Test the implementation of `on_disconnected`.""" + queue = self._manager.account_updates_queue + # Mock the connection dropped event triggers the `on_disconnected` + # callback. + self._manager.on_disconnected() + + await queue.get() + self.assertEqual(queue.status, fq.Status.ERROR) + #region - Private functions async def _simulate_account_updates(self, account_id: str): self._manager.account_updates_queue.put( diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index b4b0d9e..a0623e7 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -69,6 +69,9 @@ async def sub_account_updates(self, account: models.Account): async def unsub_account_updates(self): pass + def on_disconnected(self): + pass + class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" def __init__(self): From bfe5b47ef0ecde21ba32e3c5ab5978122173107c Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 03:20:18 +0000 Subject: [PATCH 108/126] Connection status events - Created listener interface - `ConnectionListener` for the callbacks of connected & disconnected events. - `IBBridge` & `IBWrapper` takes an optional argument `connection_listener` in the constructor for user to pass in their own listener for connection events. - `IBWrapper` triggers - the `ConnectionListener.on_connected` callback when the `nextValidId` callback is first triggered by IB; - and the `cConnectionListener.on_disconnected` callback when the `NOT_CONNECTED` error (code `504`) is returned from IB. - Added the `MockConnectionListener` in module `tests.toolkit.utils` for unittest. - Implemented unittest to test the connection status events are properly triggered. --- ibpy_native/_internal/_wrapper.py | 16 ++++++++++ ibpy_native/bridge.py | 6 ++++ ibpy_native/interfaces/listeners/__init__.py | 1 + .../interfaces/listeners/connection.py | 18 ++++++++++++ tests/internal/test_wrapper.py | 29 +++++++++++++++++++ tests/toolkit/utils.py | 13 ++++++++- 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 ibpy_native/interfaces/listeners/connection.py diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index ec636a6..c5c2a4d 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -26,6 +26,10 @@ class IBWrapper(wrapper.EWrapper): orders_manager (:obj:`ibpy_native.interfaces.delgates.order .OrdersManagementDelegate`): Manager to handler orders related events. + connection_listener (:obj:`ibpy_native.interfaces.listeners + .ConnectionListener`, optional): Listener to receive connection + status callback on connection with IB TWS/Gateway is established or + dropped. Defaults to `None`. notification_listener (:obj:`ibpy_native.interfaces.listeners .NotificationListener`, optional): Handler to receive system notifications from IB Gateway. Defaults to `None`. @@ -33,6 +37,7 @@ class IBWrapper(wrapper.EWrapper): def __init__( self, orders_manager: delegates.OrdersManagementDelegate, + connection_listener: Optional[listeners.ConnectionListener]=None, notification_listener: Optional[listeners.NotificationListener]=None ): self._lock = threading.Lock() @@ -42,6 +47,7 @@ def __init__( delegates.AccountsManagementDelegate] = None self._orders_manager = orders_manager + self._connection_listener = connection_listener self._notification_listener = notification_listener # Queue with ID -1 is always reserved for next order ID @@ -219,6 +225,13 @@ def contractDetailsEnd(self, reqId): #region - Orders def nextValidId(self, orderId: int): + if (self._connection_listener + and self._orders_manager.next_order_id == 0): + # Next order ID is 0 before any next order ID update. + # Hence can determine this is the initial callback from IB after + # the connection has been established. + self._connection_listener.on_connected() + # Next valid order ID returned from IB self._orders_manager.update_next_order_id(order_id=orderId) # To finish waiting on IBClient.req_next_order_id @@ -350,6 +363,9 @@ def _on_disconnected(self): if self._ac_man_delegate: self._ac_man_delegate.on_disconnected() + if self._connection_listener: + self._connection_listener.on_disconnected() + def _reset(self): self._req_queue.clear() self._req_queue[-1] = fq.FinishableQueue(queue_to_finish=queue.Queue()) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index f5caadc..06a4cb7 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -33,6 +33,10 @@ class IBBridge: interface as `Client {client_id}`. Defaults to `1`. auto_conn (bool, optional): `IBBridge` auto connects to IB Gateway on initial. Defaults to `True`. + connection_listener (:obj:`ibpy_native.interfaces.listeners + .ConnectionListener`, optional): Listener to receive connection + status callback on connection with IB TWS/Gateway is established or + dropped. Defaults to `None`. notification_listener (:obj:`ibpy_native.internfaces.listeners .NotificationListener`, optional): Handler to receive system notifications from IB Gateway. Defaults to `None`. @@ -45,6 +49,7 @@ class IBBridge: def __init__( self, host: str="127.0.0.1", port: int=4001, client_id: int=1, auto_conn: bool=True, + connection_listener: Optional[listeners.ConnectionListener]=None, notification_listener: Optional[listeners.NotificationListener]=None, order_events_listener: Optional[listeners.OrderEventsListener]=None, accounts_manager: Optional[ib_account.AccountsManager]=None @@ -61,6 +66,7 @@ def __init__( self._wrapper = _wrapper.IBWrapper( orders_manager=self._orders_manager, + connection_listener=connection_listener, notification_listener=notification_listener ) self._wrapper.set_accounts_management_delegate( diff --git a/ibpy_native/interfaces/listeners/__init__.py b/ibpy_native/interfaces/listeners/__init__.py index 2f41682..c37ee47 100644 --- a/ibpy_native/interfaces/listeners/__init__.py +++ b/ibpy_native/interfaces/listeners/__init__.py @@ -1,4 +1,5 @@ """Interfaces of event listeners.""" +from .connection import ConnectionListener from .live_ticks import LiveTicksListener from .notification import NotificationListener from .order import OrderEventsListener diff --git a/ibpy_native/interfaces/listeners/connection.py b/ibpy_native/interfaces/listeners/connection.py new file mode 100644 index 0000000..5a248d8 --- /dev/null +++ b/ibpy_native/interfaces/listeners/connection.py @@ -0,0 +1,18 @@ +"""Listener interfaces for connectivity status between the API client and +IB TWS/Gateway. +""" +import abc + +class ConnectionListener(metaclass=abc.ABCMeta): + """Interface of listener for connection between the API client and IB + TWS/Gateway. + """ + @abc.abstractmethod + def on_connected(self): + """Callback on connection is established.""" + return NotImplemented + + @abc.abstractmethod + def on_disconnected(self): + """Callback on connection is dropped.""" + return NotImplemented diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index d50770b..cb82659 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -65,6 +65,35 @@ async def test_error(self): self.assertEqual(result[0].err_code, code) self.assertEqual(result[0].err_str, msg) +class TestConnectionEvents(unittest.TestCase): + """Unit tests for connection events related mechanism implemented in + `IBWrapper`. + + * Connection with IB is NOT REQUIRED. + """ + def setUp(self): + self._listener = utils.MockConnectionListener() + self._wrapper = _wrapper.IBWrapper( + orders_manager=order.OrdersManager(), + connection_listener=self._listener + ) + + def test_on_connected(self): + """Test the event of connection established.""" + # Mock the behaviour of initial handshake callback once the connection + # is made. + self._wrapper.nextValidId(orderId=1) + + self.assertTrue(self._listener.connected) + + def test_on_disconnected(self): + """Test the event of connection dropped.""" + # Mock the `NOT_CONNECTED` error is returned to `error` callback + self._wrapper.error(reqId=-1, errorCode=error.IBErrorCode.NOT_CONNECTED, + errorString=_global.MSG_NOT_CONNECTED) + + self.assertFalse(self._listener.connected) + class TestReqQueue(unittest.TestCase): """Unit tests for `_req_queue` related mechanicisms in `IBWrapper`. diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index a0623e7..aea6ec6 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -3,7 +3,7 @@ import asyncio import os import queue -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from ibapi import wrapper as ib_wrapper @@ -32,6 +32,17 @@ def wrapper(*args, **kwargs): IB_CLIENT_ID: int = int(os.getenv("IB_CLIENT_ID", "1001")) IB_ACC_ID: str = os.getenv("IB_ACC_ID", "") +class MockConnectionListener(listeners.ConnectionListener): + """Mock connection listener.""" + def __init__(self): + self.connected: Optional[bool] = None + + def on_connected(self): + self.connected = True + + def on_disconnected(self): + self.connected = False + class MockNotificationListener(listeners.NotificationListener): """Mock notification listener.""" def __init__(self): From 8fe5ceb696e39d7925a8737e5c6267c30628b834 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 03:31:41 +0000 Subject: [PATCH 109/126] Override `EWrapper.connectionClosed` - Overridden the function `EWrapper.connectionClosed` to perform the tasks for resources release and reset after the connection is dropped successfully. --- ibpy_native/_internal/_wrapper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index c5c2a4d..9f87074 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -175,6 +175,11 @@ def error(self, reqId, errorCode, errorString): msg=errorString ) + # Connection related + def connectionClosed(self): + # Notifies the connection is dropped successfully. + self._on_disconnected() + #region - Accounts & portfolio def managedAccounts(self, accountsList: str): # Trim the spaces in `accountsList` received From 61baf2ef94a3521279d9f2df360d30e3e054e505 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 04:46:40 +0000 Subject: [PATCH 110/126] Housekeeping - Merged modules `ibpy_native.account` & `ibpy_native.order` to `ibpy_native.manager`. - Fixed some condition checkings from `is`/`is not` to `==`/`!=` when comparing numbers. - All error codes used explicitly are now defined in enum `error.IBErrorCode`. - Modified the constructor of `IBWrapper` to take the non-optional `AccountsManagementDelegate` instance so there's less checking needed while working on account related tasks. - Fixed/completed docstring. --- ibpy_native/__init__.py | 3 +- ibpy_native/_internal/_wrapper.py | 54 ++++---- ibpy_native/bridge.py | 42 +++--- ibpy_native/error.py | 2 + ibpy_native/{account.py => manager.py} | 144 +++++++++++++++++++- ibpy_native/order.py | 150 --------------------- tests/internal/test_client.py | 22 ++- tests/internal/test_wrapper.py | 45 +++++-- tests/test_bridge.py | 9 +- tests/{test_account.py => test_manager.py} | 13 +- tests/toolkit/utils.py | 18 +-- 11 files changed, 269 insertions(+), 233 deletions(-) rename ibpy_native/{account.py => manager.py} (57%) delete mode 100644 ibpy_native/order.py rename tests/{test_account.py => test_manager.py} (95%) diff --git a/ibpy_native/__init__.py b/ibpy_native/__init__.py index 85263c8..98b54a0 100644 --- a/ibpy_native/__init__.py +++ b/ibpy_native/__init__.py @@ -1,4 +1,5 @@ """Public classes & functions of `ibpy_native`.""" from .bridge import IBBridge -from .order import OrdersManager +from .manager import AccountsManager +from .manager import OrdersManager from .utils import datatype diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 9f87074..32d604a 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -23,7 +23,10 @@ class IBWrapper(wrapper.EWrapper): TWS instance. Args: - orders_manager (:obj:`ibpy_native.interfaces.delgates.order + accounts_manager (:obj:`ibpy_native.interfaces.delegates + .AccountsManagementDelegate`): Manager to handler accounts related + data. + orders_manager (:obj:`ibpy_native.interfaces.delgates .OrdersManagementDelegate`): Manager to handler orders related events. connection_listener (:obj:`ibpy_native.interfaces.listeners @@ -36,16 +39,15 @@ class IBWrapper(wrapper.EWrapper): """ def __init__( self, + accounts_manager: delegates.AccountsManagementDelegate, orders_manager: delegates.OrdersManagementDelegate, connection_listener: Optional[listeners.ConnectionListener]=None, notification_listener: Optional[listeners.NotificationListener]=None ): self._lock = threading.Lock() - self._req_queue: Dict[int, fq.FinishableQueue] = {} - self._ac_man_delegate: Optional[ - delegates.AccountsManagementDelegate] = None + self._accounts_manager = accounts_manager self._orders_manager = orders_manager self._connection_listener = connection_listener self._notification_listener = notification_listener @@ -139,7 +141,7 @@ def set_accounts_management_delegate( ._AccountsManagementDelegate): Delegate for managing IB account list. """ - self._ac_man_delegate = delegate + self._accounts_manager = delegate def set_on_notify_listener(self, listener: listeners.NotificationListener): """Setter for optional `NotificationListener`. @@ -157,13 +159,13 @@ def error(self, reqId, errorCode, errorString): err = error.IBError(rid=reqId, err_code=errorCode, err_str=errorString) # -1 indicates a notification and not true error condition - if reqId is not -1 and self._orders_manager.is_pending_order( + if reqId != -1 and self._orders_manager.is_pending_order( order_id=reqId): # Is an order error self._orders_manager.order_error(err) - elif reqId is not -1 and errorCode == 201: # 201 == order rejected + elif reqId != -1 and errorCode == error.IBErrorCode.ORDER_REJECTED: self._orders_manager.on_order_rejected(order_id=reqId, reason=errorString) - elif reqId is not -1 and reqId in self._req_queue: + elif reqId != -1 and reqId in self._req_queue: self._req_queue[reqId].put(element=err) elif reqId == -1 and errorCode == error.IBErrorCode.NOT_CONNECTED: # Connection dropped @@ -187,36 +189,29 @@ def managedAccounts(self, accountsList: str): # Separate different account IDs into a list account_list = trimmed.split(",") - if self._ac_man_delegate is not None: - self._ac_man_delegate.on_account_list_update( - account_list=account_list - ) + self._accounts_manager.on_account_list_update(account_list=account_list) #region - account updates def updateAccountValue(self, key: str, val: str, currency: str, accountName: str): - if self._ac_man_delegate: - data = models.RawAccountValueData( - account=accountName, currency=currency, key=key, val=val - ) - self._ac_man_delegate.account_updates_queue.put(data) + data = models.RawAccountValueData( + account=accountName, currency=currency, key=key, val=val) + self._accounts_manager.account_updates_queue.put(data) def updatePortfolio(self, contract: ib_contract.Contract, position: float, marketPrice: float, marketValue: float, averageCost: float, unrealizedPNL: float, realizedPNL: float, accountName: str): - if self._ac_man_delegate: - data = models.RawPortfolioData( - account=accountName, contract=contract, - position=position, market_price=marketPrice, - market_val=marketValue, avg_cost=averageCost, - unrealised_pnl=unrealizedPNL, realised_pnl=realizedPNL - ) - self._ac_man_delegate.account_updates_queue.put(data) + data = models.RawPortfolioData( + account=accountName, contract=contract, position=position, + market_price=marketPrice, market_val=marketValue, + avg_cost=averageCost, unrealised_pnl=unrealizedPNL, + realised_pnl=realizedPNL + ) + self._accounts_manager.account_updates_queue.put(data) def updateAccountTime(self, timeStamp: str): - if self._ac_man_delegate: - self._ac_man_delegate.account_updates_queue.put(timeStamp) + self._accounts_manager.account_updates_queue.put(timeStamp) #endregion - account updates #endregion - Accounts & portfolio @@ -261,6 +256,7 @@ def orderStatus(self, orderId: int, status: str, filled: float, ) #endregion - Orders + #region - Historical data # Get earliest data point for a given instrument and data def headTimestamp(self, reqId: int, headTimestamp: str): # override method @@ -284,6 +280,7 @@ def historicalTicksLast(self, reqId: int, self._handle_historical_ticks_results(req_id=reqId, ticks=ticks, done=done) #endregion - Fetch historical tick data + #endregion - Historical data #region - Stream live tick data def tickByTickAllLast(self, reqId: int, tickType: int, time: int, @@ -365,8 +362,7 @@ def _on_disconnected(self): self._reset() self._orders_manager.on_disconnected() - if self._ac_man_delegate: - self._ac_man_delegate.on_disconnected() + self._accounts_manager.on_disconnected() if self._connection_listener: self._connection_listener.on_disconnected() diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 06a4cb7..b413574 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -11,9 +11,8 @@ from ibapi import contract as ib_contract from ibapi import order as ib_order -import ibpy_native -from ibpy_native import account as ib_account from ibpy_native import error +from ibpy_native import manager from ibpy_native import models from ibpy_native._internal import _client from ibpy_native._internal import _global @@ -33,45 +32,46 @@ class IBBridge: interface as `Client {client_id}`. Defaults to `1`. auto_conn (bool, optional): `IBBridge` auto connects to IB Gateway on initial. Defaults to `True`. + accounts_manager (:obj:`ibpy_native.interfaces.delegates + .AccountsManagementDelegate`, optional): Manager to handle accounts + related data. If omitted, an default `AccountManager` will be + created on initial of `IBBridge` (which should be enough for most + cases unless you have a customised one). Defaults to `None`. connection_listener (:obj:`ibpy_native.interfaces.listeners .ConnectionListener`, optional): Listener to receive connection status callback on connection with IB TWS/Gateway is established or dropped. Defaults to `None`. notification_listener (:obj:`ibpy_native.internfaces.listeners - .NotificationListener`, optional): Handler to receive system + .NotificationListener`, optional): Listener to receive system notifications from IB Gateway. Defaults to `None`. - accounts_manager (:obj:`ibpy_native.account.AccountsManager`, optional): - Object to handle accounts related data. If omitted, an default - one will be created on initial of `IBBridge` (which should be - enough for most cases unless you have a customised one). Defaults - to `None`. + order_events_listener (:obj:`ibpy_native.interfaces.listeners + .OrderEventsListener`, optional): Listener for order events. + Defaults to `None`. """ def __init__( self, host: str="127.0.0.1", port: int=4001, client_id: int=1, auto_conn: bool=True, + accounts_manager: Optional[delegates.AccountsManagementDelegate]=None, connection_listener: Optional[listeners.ConnectionListener]=None, notification_listener: Optional[listeners.NotificationListener]=None, - order_events_listener: Optional[listeners.OrderEventsListener]=None, - accounts_manager: Optional[ib_account.AccountsManager]=None + order_events_listener: Optional[listeners.OrderEventsListener]=None ): self._host = host self._port = port self._client_id = client_id self._accounts_manager = ( - ib_account.AccountsManager() if accounts_manager is None + manager.AccountsManager() if accounts_manager is None else accounts_manager ) - self._orders_manager = ibpy_native.OrdersManager( + self._orders_manager = manager.OrdersManager( event_listener=order_events_listener) self._wrapper = _wrapper.IBWrapper( + accounts_manager=self._accounts_manager, orders_manager=self._orders_manager, connection_listener=connection_listener, notification_listener=notification_listener ) - self._wrapper.set_accounts_management_delegate( - delegate=self._accounts_manager - ) self._client = _client.IBClient(wrapper=self._wrapper) @@ -87,16 +87,18 @@ def is_connected(self) -> bool: return self._client.isConnected() @property - def accounts_manager(self) -> ib_account.AccountsManager: - """:obj:`ibpy_native.account.AccountsManager`: Instance that stores & - manages all IB account(s) related data. + def accounts_manager(self) -> delegates.AccountsManagementDelegate: + """:obj:`ibpy_native.interfaces.delegates.AccountsManagementDelegate`: + `ibpy_native.manager.AccountsManager` instance that stores & manages + all IB account(s) related data. """ return self._accounts_manager @property def orders_manager(self) -> delegates.OrdersManagementDelegate: - """:obj:`ibpy_native.order.OrdersManager`: Instance that handles order - related events. + """:obj:`ibpy_native.interfaces.delegates.OrdersManagementDelegate`: + `ibpy_native.manager.OrdersManager` instance that handles order related + events. """ return self._orders_manager diff --git a/ibpy_native/error.py b/ibpy_native/error.py index c773283..0374557 100644 --- a/ibpy_native/error.py +++ b/ibpy_native/error.py @@ -8,6 +8,8 @@ class IBErrorCode(enum.IntEnum): DUPLICATE_TICKER_ID = 102 DUPLICATE_ORDER_ID = 103 INVALID_CONTRACT = 200 + ORDER_REJECTED = 201 + ORDER_MESSAGE = 399 NOT_CONNECTED = 504 # Self-defined error codes REQ_TIMEOUT = 50504 diff --git a/ibpy_native/account.py b/ibpy_native/manager.py similarity index 57% rename from ibpy_native/account.py rename to ibpy_native/manager.py index 0755139..d93cba1 100644 --- a/ibpy_native/account.py +++ b/ibpy_native/manager.py @@ -1,15 +1,23 @@ -"""IB account related resources.""" +"""Manager classes.""" # pylint: disable=protected-access import asyncio import datetime import re +import threading import queue from typing import Dict, List, Optional, Union +from ibapi import common +from ibapi import contract as ib_contract +from ibapi import order as ib_order +from ibapi import order_state as ib_order_state + from ibpy_native import error from ibpy_native import models from ibpy_native._internal import _global from ibpy_native.interfaces import delegates +from ibpy_native.interfaces import listeners +from ibpy_native.utils import datatype from ibpy_native.utils import finishable_queue as fq class AccountsManager(delegates.AccountsManagementDelegate): @@ -175,3 +183,137 @@ def _update_account_value(self, account: models.Account, currency=data.currency, val=data.val) #endregion - Private functions + +class OrdersManager(delegates.OrdersManagementDelegate): + """Class to handle orders related events.""" + def __init__(self, + event_listener: Optional[listeners.OrderEventsListener]=None): + # Internal members + self._lock = threading.Lock() + self._listener = event_listener + # Property + self._next_order_id = 0 + self._open_orders: Dict[int, models.OpenOrder] = {} + self._pending_queues: Dict[int, fq.FinishableQueue] = {} + + @property + def next_order_id(self) -> int: + return self._next_order_id + + @property + def open_orders(self) -> Dict[int, models.OpenOrder]: + return self._open_orders + + def is_pending_order(self, order_id: int) -> bool: + if order_id in self._pending_queues: + if self._pending_queues[order_id].status is fq.Status.INIT: + return True + + return False + + #region - Internal functions + def update_next_order_id(self, order_id: int): + with self._lock: + self._next_order_id = order_id + + def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: + if order_id in self._pending_queues: + return self._pending_queues[order_id] + + return None + + #region - Order events + def order_error(self, err: error.IBError): + if err.err_code == error.IBErrorCode.ORDER_MESSAGE: + # Warning message only + if self._listener: + self._listener.on_warning(order_id=err.rid, msg=err.msg) + return + if err.rid in self._pending_queues: + if self._listener: + self._listener.on_err(err) + + # Signals the order submission error + if self._pending_queues[err.rid].status is not fq.Status.FINISHED: + self._pending_queues[err.rid].put(element=err) + + def on_order_submission(self, order_id: int): + """INTERNAL FUNCTION! Creates a new `FinishableQueue` with `order_id` + as key in `_pending_queues` for order submission task completeion + status monitoring. + + Args: + order_id (int): The order's identifier on TWS/Gateway. + + Raises: + ibpy_native.error.IBError: If existing `FinishableQueue` assigned + for the `order_id` specificed is found. + """ + if order_id not in self._pending_queues: + self._pending_queues[order_id] = fq.FinishableQueue( + queue_to_finish=queue.Queue() + ) + else: + raise error.IBError( + rid=order_id, err_code=error.IBErrorCode.DUPLICATE_ORDER_ID, + err_str=f"Existing queue assigned for order ID {order_id} " + "found. Possiblely duplicate order ID is being used." + ) + + def on_open_order_updated( + self, contract: ib_contract.Contract, order: ib_order.Order, + order_state: ib_order_state.OrderState + ): + if order.orderId in self._open_orders: + self._open_orders[order.orderId].order_update(order, order_state) + if (self._listener + and order_state.status == datatype.OrderStatus.FILLED.value + and order_state.commission != common.UNSET_DOUBLE): + # Commission validation is to filter out the 1st incomplete + # order filled status update. + self._listener.on_filled(order=self._open_orders[order.orderId]) + else: + self._open_orders[order.orderId] = models.OpenOrder( + contract, order, order_state + ) + if order.orderId in self._pending_queues: + self._pending_queues[order.orderId].put( + element=fq.Status.FINISHED) + + def on_order_status_updated( + self, order_id: int, status: str, filled: float, remaining: float, + avg_fill_price: float, last_fill_price: float, mkt_cap_price: float + ): + if order_id in self._open_orders: + self._open_orders[order_id].order_status_update( + status=datatype.OrderStatus(status), filled=filled, + remaining=remaining, avg_fill_price=avg_fill_price, + last_fill_price=last_fill_price, mkt_cap_price=mkt_cap_price + ) + + if (self._listener + and status == datatype.OrderStatus.CANCELLED.value): + self._listener.on_cancelled(order=self._open_orders[order_id]) + + def on_order_rejected(self, order_id: int, reason: str): + if self._listener and order_id in self._open_orders: + self._listener.on_rejected(order=self._open_orders[order_id], + reason=reason) + #endregion - Order events + + def on_disconnected(self): + for key, f_queue in self._pending_queues.items(): + if f_queue.status is not fq.Status.FINISHED or fq.Status.ERROR: + err = error.IBError( + rid=key, err_code=error.IBErrorCode.NOT_CONNECTED, + err_str=_global.MSG_NOT_CONNECTED + ) + f_queue.put(element=err) + + self._reset() + #endregion - Internal functions + + def _reset(self): + self._next_order_id = 0 + self._open_orders.clear() + self._pending_queues.clear() diff --git a/ibpy_native/order.py b/ibpy_native/order.py deleted file mode 100644 index f7a0e8b..0000000 --- a/ibpy_native/order.py +++ /dev/null @@ -1,150 +0,0 @@ -"""IB order related resources.""" -# pylint: disable=protected-access -import threading -import queue -from typing import Dict, Optional - -from ibapi import common -from ibapi import contract as ib_contract -from ibapi import order as ib_order -from ibapi import order_state as ib_order_state - -from ibpy_native import error -from ibpy_native import models -from ibpy_native._internal import _global -from ibpy_native.interfaces import delegates -from ibpy_native.interfaces import listeners -from ibpy_native.utils import datatype -from ibpy_native.utils import finishable_queue as fq - -class OrdersManager(delegates.OrdersManagementDelegate): - """Class to handle orders related events.""" - def __init__(self, - event_listener: Optional[listeners.OrderEventsListener]=None): - # Internal members - self._lock = threading.Lock() - self._listener = event_listener - # Property - self._next_order_id = 0 - self._open_orders: Dict[int, models.OpenOrder] = {} - self._pending_queues: Dict[int, fq.FinishableQueue] = {} - - @property - def next_order_id(self) -> int: - return self._next_order_id - - @property - def open_orders(self) -> Dict[int, models.OpenOrder]: - return self._open_orders - - def is_pending_order(self, order_id: int) -> bool: - if order_id in self._pending_queues: - if self._pending_queues[order_id].status is fq.Status.INIT: - return True - - return False - - #region - Internal functions - def update_next_order_id(self, order_id: int): - with self._lock: - self._next_order_id = order_id - - def get_pending_queue(self, order_id: int) -> Optional[fq.FinishableQueue]: - if order_id in self._pending_queues: - return self._pending_queues[order_id] - - return None - - #region - Order events - def order_error(self, err: error.IBError): - if err.err_code == 399: # Warning message only - if self._listener: - self._listener.on_warning(order_id=err.rid, msg=err.msg) - return - if err.rid in self._pending_queues: - if self._listener: - self._listener.on_err(err) - - # Signals the order submission error - if self._pending_queues[err.rid].status is not fq.Status.FINISHED: - self._pending_queues[err.rid].put(element=err) - - def on_order_submission(self, order_id: int): - """INTERNAL FUNCTION! Creates a new `FinishableQueue` with `order_id` - as key in `_pending_queues` for order submission task completeion - status monitoring. - - Args: - order_id (int): The order's identifier on TWS/Gateway. - - Raises: - ibpy_native.error.IBError: If existing `FinishableQueue` assigned - for the `order_id` specificed is found. - """ - if order_id not in self._pending_queues: - self._pending_queues[order_id] = fq.FinishableQueue( - queue_to_finish=queue.Queue() - ) - else: - raise error.IBError( - rid=order_id, err_code=error.IBErrorCode.DUPLICATE_ORDER_ID, - err_str=f"Existing queue assigned for order ID {order_id} " - "found. Possiblely duplicate order ID is being used." - ) - - def on_open_order_updated( - self, contract: ib_contract.Contract, order: ib_order.Order, - order_state: ib_order_state.OrderState - ): - if order.orderId in self._open_orders: - self._open_orders[order.orderId].order_update(order, order_state) - if (self._listener - and order_state.status == "Filled" - and order_state.commission != common.UNSET_DOUBLE): - # Commission validation is to filter out the 1st incomplete - # order filled status update. - self._listener.on_filled(order=self._open_orders[order.orderId]) - else: - self._open_orders[order.orderId] = models.OpenOrder( - contract, order, order_state - ) - if order.orderId in self._pending_queues: - self._pending_queues[order.orderId].put( - element=fq.Status.FINISHED) - - def on_order_status_updated( - self, order_id: int, status: str, filled: float, remaining: float, - avg_fill_price: float, last_fill_price: float, mkt_cap_price: float - ): - if order_id in self._open_orders: - self._open_orders[order_id].order_status_update( - status=datatype.OrderStatus(status), filled=filled, - remaining=remaining, avg_fill_price=avg_fill_price, - last_fill_price=last_fill_price, mkt_cap_price=mkt_cap_price - ) - - if self._listener and status == "Cancelled": - self._listener.on_cancelled(order=self._open_orders[order_id]) - - def on_order_rejected(self, order_id: int, reason: str): - if self._listener and order_id in self._open_orders: - self._listener.on_rejected(order=self._open_orders[order_id], - reason=reason) - #endregion - Order events - - def on_disconnected(self): - for key, f_queue in self._pending_queues.items(): - if f_queue.status is not fq.Status.FINISHED or fq.Status.ERROR: - err = error.IBError( - rid=key, err_code=error.IBErrorCode.NOT_CONNECTED, - err_str=_global.MSG_NOT_CONNECTED - ) - f_queue.put(element=err) - - self._reset() - #endregion - Internal functions - - def _reset(self): - self._next_order_id = 0 - self._open_orders.clear() - self._pending_queues.clear() diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 7d60f9e..7258cc2 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -10,7 +10,7 @@ from ibapi import wrapper from ibpy_native import error -from ibpy_native import order +from ibpy_native import manager from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native._internal import _wrapper @@ -28,7 +28,10 @@ class TestOrder(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -96,7 +99,10 @@ class TestContract(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -165,7 +171,10 @@ class TestHistoricalData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -321,7 +330,10 @@ class TestLiveData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index cb82659..6bfcc0c 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -10,7 +10,7 @@ from ibpy_native import error from ibpy_native import models -from ibpy_native import order +from ibpy_native import manager from ibpy_native._internal import _client from ibpy_native._internal import _global from ibpy_native._internal import _wrapper @@ -24,10 +24,13 @@ class TestGeneral(unittest.TestCase): """Unit tests for general/uncategorised things in `IBWrapper`. - Connection with IB is NOT required. + * Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + self._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) def test_set_on_notify_listener(self): """Test setter `set_on_notify_listener` & overridden function `error` @@ -74,7 +77,8 @@ class TestConnectionEvents(unittest.TestCase): def setUp(self): self._listener = utils.MockConnectionListener() self._wrapper = _wrapper.IBWrapper( - orders_manager=order.OrdersManager(), + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager(), connection_listener=self._listener ) @@ -100,7 +104,10 @@ class TestReqQueue(unittest.TestCase): Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + self._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) def test_next_req_id_0(self): """Test property `next_req_id` for retrieval of next usable @@ -197,10 +204,12 @@ class TestAccountAndPortfolio(unittest.TestCase): Connection with IB is NOT required. """ def setUp(self): - self._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) - self._delegate = utils.MockAccountsManagementDelegate() - self._wrapper.set_accounts_management_delegate(delegate=self._delegate) + self._wrapper = _wrapper.IBWrapper( + accounts_manager=self._delegate, + orders_manager=manager.OrdersManager() + ) + def test_managed_accounts(self): """Test overridden function `managedAccounts`.""" @@ -260,7 +269,10 @@ class TestOrder(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -308,7 +320,10 @@ class TestContract(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -356,7 +371,10 @@ class TestHistoricalData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) @@ -455,7 +473,10 @@ class TestTickByTickData(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls._wrapper = _wrapper.IBWrapper(orders_manager=order.OrdersManager()) + cls._wrapper = _wrapper.IBWrapper( + accounts_manager=utils.MockAccountsManagementDelegate(), + orders_manager=manager.OrdersManager() + ) cls._client = _client.IBClient(cls._wrapper) cls._client.connect(utils.IB_HOST, utils.IB_PORT, utils.IB_CLIENT_ID) diff --git a/tests/test_bridge.py b/tests/test_bridge.py index a903b5c..f5a5352 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -222,6 +222,10 @@ def setUp(self): @utils.async_test async def test_next_order_id(self): """Test function `next_order_id`.""" + self._bridge.disconnect() # To reset the orders manager + await asyncio.sleep(0.5) + self._bridge.connect() + old_order_id = self._orders_manager.next_order_id next_order_id = await self._bridge.next_order_id() @@ -267,7 +271,10 @@ async def test_place_orders_err(self): @utils.async_test async def test_cancel_order(self): - """Test function `cancel_order`.""" + """Test function `cancel_order`. + + * This test will fail when the market is closed. + """ order = sample_orders.lmt(order_id=await self._bridge.next_order_id(), action=datatype.OrderAction.SELL, price=3) diff --git a/tests/test_account.py b/tests/test_manager.py similarity index 95% rename from tests/test_account.py rename to tests/test_manager.py index 25ef69d..7a4f4cf 100644 --- a/tests/test_account.py +++ b/tests/test_manager.py @@ -1,11 +1,11 @@ -"""Unit tests for module `ibpy_native.account`.""" +"""Unit tests for module `ibpy_native.manager`.""" # pylint: disable=protected-access import asyncio import datetime import unittest from ibapi import contract as ib_contract -from ibpy_native import account +from ibpy_native import manager from ibpy_native import models from ibpy_native._internal import _global from ibpy_native.utils import finishable_queue as fq @@ -20,10 +20,13 @@ #endregion - Constants class TestAccountsManager(unittest.TestCase): - """Unit tests for class `AccountsManager`.""" + """Unit tests for class `AccountsManager`. + + * Connection with IB is NOT REQUIRED. + """ def setUp(self): - self._manager = account.AccountsManager() + self._manager = manager.AccountsManager() def test_on_account_list_update(self): """Test the implementation of function `on_account_list_update`.""" @@ -42,7 +45,7 @@ def test_on_account_list_update_existing_list(self): non empty account list in the `AccountsManager` instance. """ # Prepends data into account list for test. - self._manager = account.AccountsManager( + self._manager = manager.AccountsManager( accounts={_MOCK_AC_140: models.Account(account_id=_MOCK_AC_140), _MOCK_AC_142: models.Account(account_id=_MOCK_AC_142), _MOCK_AC_143: models.Account(account_id=_MOCK_AC_143),} diff --git a/tests/toolkit/utils.py b/tests/toolkit/utils.py index aea6ec6..f72af6e 100644 --- a/tests/toolkit/utils.py +++ b/tests/toolkit/utils.py @@ -5,7 +5,7 @@ import queue from typing import Dict, List, Optional, Union -from ibapi import wrapper as ib_wrapper +from ibapi import wrapper from ibpy_native import error from ibpy_native import models @@ -17,12 +17,12 @@ def async_test(fn): # pylint: disable=invalid-name """Decorator for testing the async functions.""" - def wrapper(*args, **kwargs): + def fn_wrapper(*args, **kwargs): loop = asyncio.new_event_loop() return loop.run_until_complete(fn(*args, **kwargs)) - return wrapper + return fn_wrapper #endregion - General utils #region - ibpy_native specific @@ -86,15 +86,15 @@ def on_disconnected(self): class MockLiveTicksListener(listeners.LiveTicksListener): """Mock notification listener""" def __init__(self): - self.ticks: List[Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast]] = [] + self.ticks: List[Union[wrapper.HistoricalTick, + wrapper.HistoricalTickBidAsk, + wrapper.HistoricalTickLast]] = [] self.finished = False def on_tick_receive(self, req_id: int, - tick: Union[ib_wrapper.HistoricalTick, - ib_wrapper.HistoricalTickBidAsk, - ib_wrapper.HistoricalTickLast,]): + tick: Union[wrapper.HistoricalTick, + wrapper.HistoricalTickBidAsk, + wrapper.HistoricalTickLast,]): self.ticks.append(tick) def on_finish(self, req_id: int): From 2cecd38667370d439108a8af6e47fdbd4045102e Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 05:54:48 +0000 Subject: [PATCH 111/126] Implement open orders request - Overridden function `EWrapper.reqOpenOrders` for retrieving the active open orders. - Implemented queue and async functions to know when the task is finished. - Added corresponding unit tests. --- ibpy_native/_internal/_client.py | 24 ++++++++++++++++++++++++ ibpy_native/_internal/_global.py | 2 ++ ibpy_native/_internal/_wrapper.py | 20 +++++++++++++++++--- ibpy_native/bridge.py | 14 ++++++++++++++ tests/internal/test_client.py | 18 ++++++++++++++++++ tests/internal/test_wrapper.py | 9 +++++++++ tests/test_bridge.py | 7 +++++++ 7 files changed, 91 insertions(+), 3 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index e09f998..00a5c4a 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -143,6 +143,30 @@ async def req_next_order_id(self) -> int: return self._wrapper.orders_manager.next_order_id + async def req_open_orders(self): + """Request all active orders submitted by the client application + connected with the exact same client ID with which the orders were sent + to the TWS/Gateway. + + Raises: + ibpy_native.error.IBError: If + - queue destinated for the open orders requests is being used + by other on-going task/request; + - connection with IB TWS/Gateway is dropped while waiting for + the task to finish. + """ + try: + queue = self._wrapper.get_request_queue( + req_id=_global.IDX_OPEN_ORDERS) + except error.IBError as err: + raise err + + self.reqOpenOrders() + result = await queue.get() + if queue.status is fq.Status.ERROR: + if isinstance(result[-1], error.IBError): + raise result[-1] + async def submit_order(self, contract: ib_contract.Contract, order: ib_order.Order): """Send the order to IB TWS/Gateway for submission. diff --git a/ibpy_native/_internal/_global.py b/ibpy_native/_internal/_global.py index 9df65fe..b0e6a6b 100644 --- a/ibpy_native/_internal/_global.py +++ b/ibpy_native/_internal/_global.py @@ -9,5 +9,7 @@ # Timezone to match the one set in IB Gateway/TWS at login TZ: datetime.tzinfo = pytz.timezone("America/New_York") +IDX_OPEN_ORDERS: Final[int] = -2 + # Mesages MSG_NOT_CONNECTED: Final[str] = "Not connected." diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 32d604a..53d02cd 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -54,6 +54,9 @@ def __init__( # Queue with ID -1 is always reserved for next order ID self._req_queue[-1] = fq.FinishableQueue(queue.Queue()) + # -2 is reserved for open orders requests + self._req_queue[_global.IDX_OPEN_ORDERS] = fq.FinishableQueue( + queue_to_finish=queue.Queue()) super().__init__() @@ -245,6 +248,12 @@ def openOrder(self, orderId: int, contract: ib_contract.Contract, contract=contract, order=order, order_state=orderState ) + def openOrderEnd(self): + if self._req_queue[_global.IDX_OPEN_ORDERS].status is not ( + fq.Status.INIT or fq.Status.FINISHED): + self._req_queue[_global.IDX_OPEN_ORDERS].put( + element=fq.Status.FINISHED) + def orderStatus(self, orderId: int, status: str, filled: float, remaining: float, avgFillPrice: float, permId: int, parentId: int, lastFillPrice: float, clientId: int, @@ -330,9 +339,12 @@ def _init_req_queue(self, req_id: int): `self.__req_queue[req_id]` and it's not finished. """ if req_id in self._req_queue: - if self._req_queue[req_id].finished or ( - req_id == -1 and self._req_queue[-1].status is fq.Status.INIT - ): + if (self._req_queue[req_id].finished + or (req_id == -1 + and self._req_queue[-1].status is fq.Status.INIT) + or (req_id == _global.IDX_OPEN_ORDERS + and (self._req_queue[_global.IDX_OPEN_ORDERS].status + is fq.Status.INIT))): self._req_queue[req_id].reset() else: raise error.IBError( @@ -370,6 +382,8 @@ def _on_disconnected(self): def _reset(self): self._req_queue.clear() self._req_queue[-1] = fq.FinishableQueue(queue_to_finish=queue.Queue()) + self._req_queue[_global.IDX_OPEN_ORDERS] = fq.FinishableQueue( + queue_to_finish=queue.Queue()) #region - Ticks handling def _handle_historical_ticks_results( diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index b413574..d7c0c49 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -219,6 +219,20 @@ async def next_order_id(self) -> int: """ return await self._client.req_next_order_id() + async def req_open_orders(self): + """Get all active orders submitted by the client application connected + with the exact same client ID with which the orders were sent to + TWS/Gateway. + + Raises: + ibpy_native.error.IBError: If the connection is dropped while + waiting the request to finish. + """ + try: + await self._client.req_open_orders() + except error.IBError as err: + raise err + async def place_orders(self, contract: ib_contract.Contract, orders: List[ib_order.Order]): """Place order(s) to IB. diff --git a/tests/internal/test_client.py b/tests/internal/test_client.py index 7258cc2..8089c7d 100644 --- a/tests/internal/test_client.py +++ b/tests/internal/test_client.py @@ -48,6 +48,24 @@ async def test_req_next_order_id(self): next_order_id = await self._client.req_next_order_id() self.assertGreater(next_order_id, 0) + @utils.async_test + async def test_req_open_orders(self): + """Test function `req_open_orders`.""" + await self._client.req_open_orders() + # Nothing to assert in this test. + # The function is good as long as there's no error thrown. + + @utils.async_test + async def test_req_open_orders_err(self): + """Test function `req_open_orders`. + + * Error returned as the queue is being used. + """ + # Mock queue is occupied + self._wrapper.get_request_queue(req_id=_global.IDX_OPEN_ORDERS) + with self.assertRaises(error.IBError): + await self._client.req_open_orders() + @utils.async_test async def test_submit_order(self): """Test function `submit_order`.""" diff --git a/tests/internal/test_wrapper.py b/tests/internal/test_wrapper.py index 6bfcc0c..9ff6e20 100644 --- a/tests/internal/test_wrapper.py +++ b/tests/internal/test_wrapper.py @@ -296,6 +296,15 @@ async def test_open_order(self): self.assertTrue(order_id in self._orders_manager.open_orders) + @utils.async_test + async def test_open_order_end(self): + """Test overridden function `openOrderEnd`.""" + queue = self._wrapper.get_request_queue(req_id=_global.IDX_OPEN_ORDERS) + self._client.reqOpenOrders() + await queue.get() + + self.assertTrue(queue.finished) + @utils.async_test async def test_order_status(self): """Test overridden function `orderStatus`.""" diff --git a/tests/test_bridge.py b/tests/test_bridge.py index f5a5352..74e0347 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -231,6 +231,13 @@ async def test_next_order_id(self): self.assertGreater(next_order_id, old_order_id) + @utils.async_test + async def test_req_open_orders(self): + """Test function `req_open_orders`.""" + await self._bridge.req_open_orders() + # Nothing to assert. + # The function is good if there's no error thrown. + @utils.async_test async def test_place_orders(self): """Test function `place_orders`.""" From 78955a14d01c9bf3486baf026910af61592f9c8d Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 06:09:12 +0000 Subject: [PATCH 112/126] Define queue ID in `_global` --- ibpy_native/_internal/_client.py | 3 ++- ibpy_native/_internal/_global.py | 1 + ibpy_native/_internal/_wrapper.py | 23 ++++++++++++++--------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ibpy_native/_internal/_client.py b/ibpy_native/_internal/_client.py index 00a5c4a..cec72ec 100644 --- a/ibpy_native/_internal/_client.py +++ b/ibpy_native/_internal/_client.py @@ -134,7 +134,8 @@ async def req_next_order_id(self) -> int: being used by other task. """ try: - f_queue = self._wrapper.get_request_queue(req_id=-1) + f_queue = self._wrapper.get_request_queue( + req_id=_global.IDX_NEXT_ORDER_ID) except error.IBError as err: raise err # Request next valid order ID diff --git a/ibpy_native/_internal/_global.py b/ibpy_native/_internal/_global.py index b0e6a6b..30c29e3 100644 --- a/ibpy_native/_internal/_global.py +++ b/ibpy_native/_internal/_global.py @@ -9,6 +9,7 @@ # Timezone to match the one set in IB Gateway/TWS at login TZ: datetime.tzinfo = pytz.timezone("America/New_York") +IDX_NEXT_ORDER_ID: Final[int] = -1 IDX_OPEN_ORDERS: Final[int] = -2 # Mesages diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 53d02cd..6aae9d8 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -53,7 +53,8 @@ def __init__( self._notification_listener = notification_listener # Queue with ID -1 is always reserved for next order ID - self._req_queue[-1] = fq.FinishableQueue(queue.Queue()) + self._req_queue[_global.IDX_NEXT_ORDER_ID] = fq.FinishableQueue( + queue_to_finish=queue.Queue()) # -2 is reserved for open orders requests self._req_queue[_global.IDX_OPEN_ORDERS] = fq.FinishableQueue( queue_to_finish=queue.Queue()) @@ -238,9 +239,10 @@ def nextValidId(self, orderId: int): # Next valid order ID returned from IB self._orders_manager.update_next_order_id(order_id=orderId) # To finish waiting on IBClient.req_next_order_id - if (self._req_queue[-1].status is not + if (self._req_queue[_global.IDX_NEXT_ORDER_ID].status is not (fq.Status.INIT or fq.Status.FINISHED)): - self._req_queue[-1].put(element=fq.Status.FINISHED) + self._req_queue[_global.IDX_NEXT_ORDER_ID].put( + element=fq.Status.FINISHED) def openOrder(self, orderId: int, contract: ib_contract.Contract, order: ib_order.Order, orderState: order_state.OrderState): @@ -340,8 +342,9 @@ def _init_req_queue(self, req_id: int): """ if req_id in self._req_queue: if (self._req_queue[req_id].finished - or (req_id == -1 - and self._req_queue[-1].status is fq.Status.INIT) + or (req_id == _global.IDX_NEXT_ORDER_ID + and (self._req_queue[_global.IDX_NEXT_ORDER_ID].status + is fq.Status.INIT)) or (req_id == _global.IDX_OPEN_ORDERS and (self._req_queue[_global.IDX_OPEN_ORDERS].status is fq.Status.INIT))): @@ -357,13 +360,14 @@ def _init_req_queue(self, req_id: int): def _on_disconnected(self): """Stop all active requests.""" - if self._req_queue[-1].status is not ( + if self._req_queue[_global.IDX_NEXT_ORDER_ID].status is not ( fq.Status.INIT or fq.Status.FINISHED): # Send finish signal to the active next order ID request - self._req_queue[-1].put(element=fq.Status.FINISHED) + self._req_queue[_global.IDX_NEXT_ORDER_ID].put( + element=fq.Status.FINISHED) for key, f_queue in self._req_queue.items(): - if key == -1: + if key == _global.IDX_NEXT_ORDER_ID: continue if f_queue.status is not fq.Status.FINISHED or fq.Status.ERROR: err = error.IBError( @@ -381,7 +385,8 @@ def _on_disconnected(self): def _reset(self): self._req_queue.clear() - self._req_queue[-1] = fq.FinishableQueue(queue_to_finish=queue.Queue()) + self._req_queue[_global.IDX_NEXT_ORDER_ID] = fq.FinishableQueue( + queue_to_finish=queue.Queue()) self._req_queue[_global.IDX_OPEN_ORDERS] = fq.FinishableQueue( queue_to_finish=queue.Queue()) From 234d6ed291c193881ca247fb3dc266d88a1a2f2c Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 07:16:40 +0000 Subject: [PATCH 113/126] No more static function - Removed the static annotation on function `IBBridge.set_timezone`. --- ibpy_native/bridge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index d7c0c49..212f7fc 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -103,8 +103,7 @@ def orders_manager(self) -> delegates.OrdersManagementDelegate: return self._orders_manager #region - Setters - @staticmethod - def set_timezone(tz: datetime.tzinfo): + def set_timezone(self, tz: datetime.tzinfo): # pylint: disable=invalid-name """Set the timezone for the bridge to match the IB Gateway/TWS timezone specified at login. From 84216047afc07e05a254c457152a39455e5af3f2 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 08:08:40 +0000 Subject: [PATCH 114/126] Correct return type to `AsyncIterator` --- ibpy_native/bridge.py | 4 ++-- ibpy_native/utils/finishable_queue.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 212f7fc..9d5c6ab 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -6,7 +6,7 @@ import datetime import time import threading -from typing import Awaitable, Iterator, List, Optional +from typing import AsyncIterator, Awaitable, List, Optional from ibapi import contract as ib_contract from ibapi import order as ib_order @@ -323,7 +323,7 @@ async def req_historical_ticks( end: Optional[datetime.datetime]=None, tick_type: datatype.HistoricalTicks=datatype.HistoricalTicks.TRADES, retry: int=0 - ) -> Iterator[datatype.ResHistoricalTicks]: + ) -> AsyncIterator[datatype.ResHistoricalTicks]: """Retrieve historical tick data for specificed instrument/contract from IB. diff --git a/ibpy_native/utils/finishable_queue.py b/ibpy_native/utils/finishable_queue.py index a37b7dc..9d44fcf 100644 --- a/ibpy_native/utils/finishable_queue.py +++ b/ibpy_native/utils/finishable_queue.py @@ -4,7 +4,7 @@ import queue import threading -from typing import Iterator, Any +from typing import AsyncIterator, Any # Queue status class Status(enum.Enum): @@ -88,7 +88,7 @@ async def get(self) -> list: return contents_of_queue - async def stream(self) -> Iterator[Any]: + async def stream(self) -> AsyncIterator[Any]: """Yields the elements in queue as soon as an element has been put into the queue. """ From 9dbd6ee4a25c0d47005698a2dad591d578563e44 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 08:14:10 +0000 Subject: [PATCH 115/126] Add interface `IBridge` - Added public interface for `IBBridge` for the purpose of letting the user implement that interface to create their own class for backtest. --- ibpy_native/bridge.py | 12 +- ibpy_native/interfaces/__init__.py | 2 + ibpy_native/interfaces/bridge.py | 288 +++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 ibpy_native/interfaces/bridge.py diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 9d5c6ab..4745dcc 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -12,6 +12,7 @@ from ibapi import order as ib_order from ibpy_native import error +from ibpy_native import interfaces from ibpy_native import manager from ibpy_native import models from ibpy_native._internal import _client @@ -21,7 +22,7 @@ from ibpy_native.interfaces import listeners from ibpy_native.utils import datatype -class IBBridge: +class IBBridge(interfaces.IBridge): """Public class to bridge between `ibpy-native` & IB API. Args: @@ -56,6 +57,8 @@ def __init__( notification_listener: Optional[listeners.NotificationListener]=None, order_events_listener: Optional[listeners.OrderEventsListener]=None ): + super().__init__() + self._host = host self._port = port self._client_id = client_id @@ -163,9 +166,8 @@ async def sub_account_updates(self, account: models.Account): self._client.reqAccountUpdates(subscribe=True, acctCode=account.account_id) - async def unsub_account_updates( - self, account: Optional[models.Account]=None - ): + async def unsub_account_updates(self, + account: Optional[models.Account]=None): """Stop receiving account updates from IB. Args: @@ -285,7 +287,7 @@ def cancel_order(self, order_id: int): async def get_earliest_data_point( self, contract: ib_contract.Contract, data_type: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES - ) -> datetime: + ) -> datetime.datetime: """Returns the earliest data point of specified contract. Args: diff --git a/ibpy_native/interfaces/__init__.py b/ibpy_native/interfaces/__init__.py index e69de29..a9fad78 100644 --- a/ibpy_native/interfaces/__init__.py +++ b/ibpy_native/interfaces/__init__.py @@ -0,0 +1,2 @@ +"""General/Uncategorised interfaces.""" +from .bridge import IBridge diff --git a/ibpy_native/interfaces/bridge.py b/ibpy_native/interfaces/bridge.py new file mode 100644 index 0000000..185c279 --- /dev/null +++ b/ibpy_native/interfaces/bridge.py @@ -0,0 +1,288 @@ +"""Interface module for `IBBridge`.""" +import abc +import datetime +from typing import AsyncIterator, List, Optional + +from ibapi import contract as ib_contract +from ibapi import order as ib_order + +from ibpy_native import models +from ibpy_native.interfaces import delegates +from ibpy_native.interfaces import listeners +from ibpy_native.utils import datatype + +class IBridge(metaclass=abc.ABCMeta): + """Public interface of the class that bridge between `ibpy-native` & IB API. + + Args: + host (str, optional): Hostname/IP address of IB Gateway. Defaults to + `127.0.0.1`. + port (int, optional): Port to connect to IB Gateway. Defaults to `4001`. + client_id (int, optional): Session ID which will be shown on IB Gateway + interface as `Client {client_id}`. Defaults to `1`. + auto_conn (bool, optional): `IBBridge` auto connects to IB Gateway on + initial. Defaults to `True`. + accounts_manager (:obj:`ibpy_native.interfaces.delegates + .AccountsManagementDelegate`, optional): Manager to handle accounts + related data. Defaults to `None`. + connection_listener (:obj:`ibpy_native.interfaces.listeners + .ConnectionListener`, optional): Listener to receive connection + status callback on connection with IB TWS/Gateway is established or + dropped. Defaults to `None`. + notification_listener (:obj:`ibpy_native.internfaces.listeners + .NotificationListener`, optional): Listener to receive system + notifications from IB Gateway. Defaults to `None`. + order_events_listener (:obj:`ibpy_native.interfaces.listeners + .OrderEventsListener`, optional): Listener for order events. + Defaults to `None`. + """ + @abc.abstractmethod + def __init__( + self, host: str="127.0.0.1", port: int=4001, + client_id: int=1, auto_conn: bool=True, + accounts_manager: Optional[delegates.AccountsManagementDelegate]=None, + connection_listener: Optional[listeners.ConnectionListener]=None, + notification_listener: Optional[listeners.NotificationListener]=None, + order_events_listener: Optional[listeners.OrderEventsListener]=None + ): + pass + + @property + @abc.abstractmethod + def is_connected(self) -> bool: + """Check if the bridge is connected to a running & logged in TWS/IB + Gateway instance. + """ + return NotImplemented + + @property + @abc.abstractmethod + def orders_manager(self) -> delegates.OrdersManagementDelegate: + """:obj:`ibpy_native.interfaces.delegates.OrdersManagementDelegate`: + `ibpy_native.manager.OrdersManager` instance that handles order related + events. + """ + return NotImplemented + + @abc.abstractmethod + def set_timezone(self, tz: datetime.tzinfo): + # pylint: disable=invalid-name + """Set the timezone for the bridge to match the IB Gateway/TWS timezone + specified at login. + + Args: + tz (datetime.tzinfo): Timezone. Recommend to set this value via + `pytz.timezone(zone: str)`. + """ + return NotImplemented + + @abc.abstractmethod + def set_on_notify_listener(self, listener: listeners.NotificationListener): + """Setter for optional `NotificationListener`. + + Args: + listener (:obj:`ibpy_native.interfaces.listeners + .NotificationListener`): Listener for IB notifications. + """ + return NotImplemented + + #region - Connections + @abc.abstractmethod + def connect(self): + """Connect the bridge to a running & logged in TWS/IB Gateway instance. + """ + return NotImplemented + + @abc.abstractmethod + def disconnect(self): + """Disconnect the bridge from the connected TWS/IB Gateway instance. + """ + return NotImplemented + #endregion - Connections + + #region - IB account related + @abc.abstractmethod + def req_managed_accounts(self): + """Fetch the accounts handle by the username logged in on IB Gateway.""" + return NotImplemented + + @abc.abstractmethod + async def sub_account_updates(self, account: models.Account): + """Subscribes to account updates from IB. + + Args: + account (:obj:`ibpy_native.models.Account`): Account object + retrieved from `AccountsManager`. + """ + return NotImplemented + + @abc.abstractmethod + async def unsub_account_updates(self, + account: Optional[models.Account]=None): + """Stop receiving account updates from IB. + + Args: + account (:obj:`ibpy_native.models.Account`, optional): + Account that's currently subscribed for account updates. + """ + return NotImplemented + #endregion - IB account related + + @abc.abstractmethod + async def search_detailed_contracts( + self, contract: ib_contract.Contract + ) -> List[ib_contract.ContractDetails]: + """Search the contracts with complete details from IB's database. + + Args: + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + partially completed info (e.g. symbol, currency, etc...) + + Returns: + :obj:`List[ibapi.contract.ContractDetails]`: Fully fledged IB + contract(s) with detailed info. + """ + return NotImplemented + + #region - Orders + @abc.abstractmethod + async def next_order_id(self) -> int: + """Get next valid order ID. + + Returns: + int: The next valid order ID. + """ + return NotImplemented + + @abc.abstractmethod + async def req_open_orders(self): + """Get all active orders submitted by the client application connected + with the exact same client ID with which the orders were sent to + TWS/Gateway. + """ + return NotImplemented + + @abc.abstractmethod + async def place_orders(self, contract: ib_contract.Contract, + orders: List[ib_order.Order]): + """Place order(s) to IB. + + Note: + Order IDs must be unique for each order. Arguments `orders` can be + used to place child order(s) together with the parent order but you + should make sure orders passing in are all valid. All of the orders + passed in will be cancelled if there's error casuse by any of the + order in the list. + + Args: + contract (:obj:`ibapi.contract.Contract`): The order's contract. + orders (:obj:`List[ibapi.order.Order]`): Order(s) to be submitted. + """ + return NotImplemented + + @abc.abstractmethod + def cancel_order(self, order_id: int): + """Cancel a submitted order. + + Note: + No error will be raise even if you pass in an ID which doesn't + match any existing open order. A warning message will be returned + via the `NotificationListener` supplied instead. + + A message will be returned to the `NotificationListener` supplied + once the order is cancelled. + + Args: + order_id (int): The order's identifier. + """ + return NotImplemented + #endregion - Orders + + #region - Historical data + @abc.abstractmethod + async def get_earliest_data_point( + self, contract: ib_contract.Contract, + data_type: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES + ) -> datetime.datetime: + """Returns the earliest data point of specified contract. + + Args: + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + sufficient info to identify the instrument. + data_type (:obj:`ibpy_native.utils.datatype.EarliestPoint`, + optional): Type of data for earliest data point. Defaults to + `EarliestPoint.TRADES`. + + Returns: + :obj:`datetime.datetime`: The earliest data point for the specified + contract in the timezone of whatever timezone set for this + `IBBridge` instance. + """ + return NotImplemented + + @abc.abstractmethod + async def req_historical_ticks( + self, contract: ib_contract.Contract, + start: Optional[datetime.datetime]=None, + end: Optional[datetime.datetime]=None, + tick_type: datatype.HistoricalTicks=datatype.HistoricalTicks.TRADES, + retry: int=0 + ) -> AsyncIterator[datatype.ResHistoricalTicks]: + """Retrieve historical tick data for specificed instrument/contract + from IB. + + Args: + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + sufficient info to identify the instrument. + start (:obj:`datetime.datetime`, optional): Datetime for the + earliest tick data to be included. If is `None`, the start time + will be set as the earliest data point. Defaults to `None`. + end (:obj:`datetime.datetime`, optional): Datetime for the latest + tick data to be included. If is `None`, the end time will be + set as now. Defaults to `None`. + tick_type (:obj:`ibpy_native.utils.datatype.HistoricalTicks`, + optional): Type of tick data. Defaults to + `HistoricalTicks.TRADES`. + retry (int): Max retry attempts if error occur before terminating + the task and rasing the error. + + Yields: + :obj:ibpy_native.utils.datatype.ResHistoricalTicks`: Tick data + received from IB. Attribute `completed` indicates if all ticks + within the specified time period are received. + """ + return NotImplemented + #endregion - Historical data + + #region - Live data + @abc.abstractmethod + async def stream_live_ticks( + self, contract: ib_contract.Contract, + listener: listeners.LiveTicksListener, + tick_type: datatype.LiveTicks=datatype.LiveTicks.LAST + ) -> int: + """Request to stream live tick data. + + Args: + contract (:obj:`ibapi.contract.Contract`): `Contract` object with + sufficient info to identify the instrument. + listener (:obj:`ibpy_native.interfaces.listenersLiveTicksListener`): + Callback listener for receiving ticks, finish signale, and + error from IB API. + tick_type (:obj:`ibpy_native.utils.datatype.LiveTicks`, optional): + Type of ticks to be requested. Defaults to `LiveTicks.Last`. + + Returns: + int: Request identifier. This will be needed to stop the stream + started by this function. + """ + return NotImplemented + + @abc.abstractmethod + def stop_live_ticks_stream(self, stream_id: int): + """Stop the specificed live tick data stream that's currently streaming. + + Args: + stream_id (int): Identifier for the stream. + """ + return NotImplemented From 198675f84d8b09702ed4534e5044acf912cb006d Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 09:17:03 +0000 Subject: [PATCH 116/126] Fix access `NoneType` - Fixed the function `IBBridge.req_historical_ticks` will raise exception of trying to access attribute `tzinfo` while either `start` or `end` is `None`. --- ibpy_native/bridge.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 4745dcc..dcc85d6 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -358,9 +358,14 @@ async def req_historical_ticks( excuteing the task, and max retry attemps has been reached. """ # Error checking - if start.tzinfo is not None or end.tzinfo is not None: - raise ValueError("Value of argument `start` & `end` must be an " - "native `datetime` object.") + if start is not None: + if start.tzinfo is not None: + raise ValueError("Value of argument `start` & `end` must be an " + "native `datetime` object.") + if end is not None: + if end.tzinfo is not None: + raise ValueError("Value of argument `start` & `end` must be an " + "native `datetime` object.") # Prep start and end time try: if tick_type is datatype.HistoricalTicks.TRADES: From f7b637b63f7bc92db0a2957169c55ebc04eb394f Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 10:23:35 +0000 Subject: [PATCH 117/126] Fix `IBBridge.req_historical_ticks` - Fixed the error handling in the while loop. --- ibpy_native/bridge.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index dcc85d6..48acd12 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -394,10 +394,16 @@ async def req_historical_ticks( start_date_time=start_date_time, show=tick_type ) except error.IBError as err: + if err.err_code == error.IBErrorCode.NOT_CONNECTED.value: + raise err if retry_attemps < retry: + retry_attemps += 1 continue + raise err + retry_attemps = 0 + if ticks: # Drop the 1st tick as tick time of it is `start_date_time` # - 1 second @@ -479,7 +485,7 @@ def _heart_beat(self): """Infinity loop to monitor connection with TWS/Gateway.""" while True: time.sleep(2) - self._client.reqCurrentTime() if not self._client.isConnected(): break + self._client.reqCurrentTime() #endregion - Private functions From ffdfd273985003ddf0363091665a7f9b58ff32a0 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 10:25:33 +0000 Subject: [PATCH 118/126] Create example script - Sample script for fetching historical ticks. --- samples/__init__.py | 0 samples/historical_ticks.py | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 samples/__init__.py create mode 100644 samples/historical_ticks.py diff --git a/samples/__init__.py b/samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/historical_ticks.py b/samples/historical_ticks.py new file mode 100644 index 0000000..6da8d29 --- /dev/null +++ b/samples/historical_ticks.py @@ -0,0 +1,93 @@ +"""Sample script for fetching the historical tick data.""" +import asyncio +import datetime +import os + +import pandas as pd +import pytz + +from ibapi import contract + +import ibpy_native +from ibpy_native import error +from ibpy_native.interfaces import listeners +from ibpy_native.utils import datatype + +class ConnectionListener(listeners.ConnectionListener): + """Handles connection events.""" + def __init__(self): + self.connected = False + + def on_connected(self): + self.connected = True + print("Connected") + + def on_disconnected(self): + self.connected = False + print("Disconnected") + +class NotificationListener(listeners.NotificationListener): + """Handles system notifications.""" + + def on_notify(self, msg_code: int, msg: str): + print(f"SYS_MSG (code: {msg_code}) - {msg}") + +async def main(): + """Entry point""" + gbp_usd_fx = contract.Contract() + gbp_usd_fx.symbol = "GBP" + gbp_usd_fx.secType = "CASH" + gbp_usd_fx.currency = "USD" + gbp_usd_fx.exchange = "IDEALPRO" + + connection_listener = ConnectionListener() + + bridge = ibpy_native.IBBridge( + port=int(os.getenv("IB_PORT", "4002")), + connection_listener=connection_listener, + notification_listener=NotificationListener() + ) + + while not connection_listener.connected: + await asyncio.sleep(1) + + contract_results = await bridge.search_detailed_contracts( + contract=gbp_usd_fx) + gbp_usd_fx = contract_results[0].contract + print(f"Contract - {gbp_usd_fx}") + + tickers = [] + try: + async for data in bridge.req_historical_ticks( + contract=gbp_usd_fx, + start=datetime.datetime(year=2021, month=1, day=4, hour=10), + end=datetime.datetime(year=2021, month=1, day=4, hour=10, minute=5), + tick_type=datatype.HistoricalTicks.BID_ASK, + retry=5 + ): + print(".", end="", flush=True) + for tick in data.ticks: + time = (datetime.datetime.fromtimestamp(tick.time) + .astimezone(pytz.timezone("America/New_York"))) + tickers.append({ + "time": time, + "bid_price": tick.priceBid, + "ask_price": tick.priceAsk, + "bid_size": tick.sizeBid, + "ask_size": tick.sizeAsk, + }) + except error.IBError as err: + print(err) + + return + + cols = ["time", "bid_price", "ask_price", "bid_size", "ask_size"] + dataframe = pd.DataFrame(data=tickers, columns=cols) + print(dataframe) + + bridge.disconnect() + +if __name__ == "__main__": + print("Sample - fetch historical tick data") + + asyncio.run(main()) From d641e386eec687f3e12ddd005509b3d9d64926c4 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 11:22:51 +0000 Subject: [PATCH 119/126] Explicity check `None` --- ibpy_native/_internal/_wrapper.py | 4 ++-- ibpy_native/manager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ibpy_native/_internal/_wrapper.py b/ibpy_native/_internal/_wrapper.py index 6aae9d8..b64ffe2 100644 --- a/ibpy_native/_internal/_wrapper.py +++ b/ibpy_native/_internal/_wrapper.py @@ -229,7 +229,7 @@ def contractDetailsEnd(self, reqId): #region - Orders def nextValidId(self, orderId: int): - if (self._connection_listener + if (self._connection_listener is not None and self._orders_manager.next_order_id == 0): # Next order ID is 0 before any next order ID update. # Hence can determine this is the initial callback from IB after @@ -380,7 +380,7 @@ def _on_disconnected(self): self._orders_manager.on_disconnected() self._accounts_manager.on_disconnected() - if self._connection_listener: + if self._connection_listener is not None: self._connection_listener.on_disconnected() def _reset(self): diff --git a/ibpy_native/manager.py b/ibpy_native/manager.py index d93cba1..60bba7e 100644 --- a/ibpy_native/manager.py +++ b/ibpy_native/manager.py @@ -230,7 +230,7 @@ def order_error(self, err: error.IBError): self._listener.on_warning(order_id=err.rid, msg=err.msg) return if err.rid in self._pending_queues: - if self._listener: + if self._listener is not None: self._listener.on_err(err) # Signals the order submission error @@ -296,7 +296,7 @@ def on_order_status_updated( self._listener.on_cancelled(order=self._open_orders[order_id]) def on_order_rejected(self, order_id: int, reason: str): - if self._listener and order_id in self._open_orders: + if self._listener is not None and order_id in self._open_orders: self._listener.on_rejected(order=self._open_orders[order_id], reason=reason) #endregion - Order events From a3eff5c58777be3d444eccde76b2415fa2d292d2 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 11:26:02 +0000 Subject: [PATCH 120/126] Update CHANGELOG --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a968cfa..b4fadae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v1.0.0] - 2021-02-28 +`v1.0.0` is the first usable release of the framework. Accounts & orders +management features are now implemented, so as placing orders. It's a usable +framework for actual trading as of this release. However, it's NOT compatible +with pervious versions as a lot of the functions got revamped, including the +functions for fetching the historical data. + +### Added +- Support of requesting and managing account & portfolio data. +- Support of requesting active orders & orders management (including order + placing, cancellation, and order status updates, etc..). +- Connection status event listener to notify the connection status between + the client program and IB TWS/Gateway. +- Handling for unexpected connection drops. + - Resources will be released and on-going tasks will all be terminated on + disconnected. +- Corresponding interfaces for most if not all public classes for easy mocking, + so it's gonna be easy to backtest the strategies and use the same set of + strategy code for both backtest and live trading. + +### Changed +- Reordered the grouping of packages and modules (breaking change). +- `IBBridge.req_historical_ticks` now works as an async iterator. + +### Removed +- All deprecated functions. +- Deprecated script `cmd/fetch_us_historical_ticks.py`. + ## [v0.2.0] - 2020-09-02 `v0.2.0` is a minor release refactored most of the project to adopt async/await syntax, added support of streaming live "tick-by-tick" data from IB, and @@ -127,7 +155,8 @@ returns with `finished` mark as `True` unexpectedly while IB returns less than 1000 records but there're more historical ticks those should be fetched in next request. -[Unreleased]: https://github.com/Devtography/ibpy_native/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/Devtography/ibpy_native/compare/v1.0.0...HEAD +[v1.0.0]: https://github.com/Devtography/ibpy_native/compare/v1.0.0...v0.2.0 [v0.2.0]: https://github.com/Devtography/ibpy_native/compare/v0.2.0...v0.1.4 [v0.1.4]: https://github.com/Devtography/ibpy_native/compare/v0.1.4...v0.1.3 [v0.1.3]: https://github.com/Devtography/ibpy_native/compare/v0.1.3...v0.1.2 From ed35af5dfea5468d837f0c4f09b7f30d6889abc6 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 12:13:23 +0000 Subject: [PATCH 121/126] Update README --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f00745e..78d0d9c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ A fully asynchronous framework for using the native Python version of Interactive Brokers API. +The whole framework is built on Python's built in `asyncio` and `queue` modules, +no event emitter nor any other heavy 3rd party library. The only 3rd party +package used is `pytz` for timezone related things. In this way the framework +is being kept as native Python as possible and the performance shouldn't get +slowed down due to the libraries used. + +Additionally, most if not all public classes implement their corresponding +interface. Therefore, it's easy to implement a customised version for most of +the classes. Hence, mocking the API calls/data returns for backtest can be +easily done, and you should be able to use the same set of strategy code on both +simulate and live trading sessions. + ## Installation Install from PyPI ```sh @@ -52,23 +64,31 @@ head_timestamp = await bridge.get_earliest_data_point( ) ``` +For more, please have a look on the sample scripts in `samples` folder. There's +one script that shows how to request for historical tick data atm, but there +will be more as I keep developing my own system based on this framework. + +In the meantime, you may want to read the doc and checkout the unittest for the +ideas of how the APIs work. + +_**Do make sure you are using a paper account while running the unittest cases, +as some of those tests do place real orders to IB and orders will get filled.**_ + ## System requirements - Python >= 3.7; Pervious versions are not supported (development is based on -Python 3.7.7) +Python 3.7.9) - _Included IB API version - `9.79.01`_ ## Development status (a.k.a. Words from developers) Although the project is under the stage of active development, up until now -(`v0.2.0`) it focuses on working with FX, stock & future contracts from IB. +(`v1.0.0`) it focuses on working with FX, stock & future contracts from IB. Other security types (e.g. options) may work but those are not yet tested. -This project is not providing full features of IB API yet, but basic features -like retrieving data of contracts from IB, getting live & historical ticks are -ready to be used. Remaining features like retrieving account details, place & -modify orders are planned to be implemented prior the first stable release -(`v1.0.0`), but there is no estimated timeline for those atm, as the project is -being developed alongside Devtography internal algo-trading program, so as my -daily job. For now, the features will be developed and released when needed. +This project is not providing full features of IB API yet, but `v1.0.0` is +already capable to retrieve & manage account and orders data, placing and +cancelling the orders, so as requesting historical and live tick data. More +features will be supported if my internal trading system needs (that's the +highest priority), or by request on the issues board. ## Contributions Contributions via pull requests are welcome and encouraged. If there's any @@ -78,6 +98,18 @@ make a pull request :) _Please follow the [Google Python Style Guide] as much as possible for all the code included in your pull request. Otherwise the pull request may be rejected._ +## Donation +This framework has spent me a whole year to develop from scratch until the first +stable release (`v1.0.0`). Hopefully you'll find this framework useful as much +as I do. + +If you wanna support my work, please consider [donate/sponsor] me. Thus I can +keep on investing time to further develop the framework alongside my job and +other projects. + +## Author +[Wing Chau] [@Devtography] + ## License Modules included in `ibpy_native`, except `ibapi` is licensed under the [Apache License, Version 2.0](LICENSE.md). @@ -93,4 +125,7 @@ application/product, you must contact Interactive Brokers LLC for permission of using IB API commercially. [Google Python Style Guide]: https://google.github.io/styleguide/pyguide.html +[donate/sponsor]: https://github.com/sponsors/iamWing +[Wing Chau]: https://github.com/iamWing +[@Devtography]: https://github.com/Devtography [TWS API Non-Commercial License]: https://interactivebrokers.github.io/index.html From 1a726d669c1b4e6df4fb8d23a35442efdf74fb93 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 12:14:25 +0000 Subject: [PATCH 122/126] Setup FUNDING --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2cc80af --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [iamWing] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['https://paypal.me/iamWing0w0'] \ No newline at end of file From 182396c1b6c70c383a3c2bda480220e86def277f Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 12:54:29 +0000 Subject: [PATCH 123/126] Update dependencies --- Pipfile.lock | 466 +++++++++++++++++++++++++++++---------------------- 1 file changed, 268 insertions(+), 198 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 46be59c..ee0e2d8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,85 +18,88 @@ "default": { "deprecated": { "hashes": [ - "sha256:525ba66fb5f90b07169fdd48b6373c18f1ee12728ca277ca44567a367d9d7f74", - "sha256:a766c1dccb30c5f6eb2b203f87edd1d8588847709c78589e1521d769addc8218" + "sha256:471ec32b2755172046e28102cd46c481f21c6036a0ec027521eba8521aa4ef35", + "sha256:924b6921f822b64ec54f49be6700a126bab0640cfafca78f22c9d429ed590560" ], "index": "pypi", - "version": "==1.2.10" + "version": "==1.2.11" }, "numpy": { "hashes": [ - "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983", - "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065", - "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968", - "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132", - "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129", - "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff", - "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93", - "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a", - "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7", - "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd", - "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055", - "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc", - "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7", - "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624", - "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b", - "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69", - "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491", - "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954", - "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72", - "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7", - "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae", - "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1", - "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a", - "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e", - "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e", - "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc" - ], - "version": "==1.19.1" + "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", + "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", + "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", + "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", + "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", + "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", + "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", + "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", + "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", + "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", + "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", + "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", + "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", + "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", + "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", + "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", + "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", + "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", + "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", + "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", + "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", + "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", + "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", + "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" + ], + "markers": "python_version >= '3.7'", + "version": "==1.20.1" }, "pandas": { "hashes": [ - "sha256:01b1e536eb960822c5e6b58357cad8c4b492a336f4a5630bf0b598566462a578", - "sha256:0246c67cbaaaac8d25fed8d4cf2d8897bd858f0e540e8528a75281cee9ac516d", - "sha256:0366150fe8ee37ef89a45d3093e05026b5f895e42bbce3902ce3b6427f1b8471", - "sha256:16ae070c47474008769fc443ac765ffd88c3506b4a82966e7a605592978896f9", - "sha256:1acc2bd7fc95e5408a4456897c2c2a1ae7c6acefe108d90479ab6d98d34fcc3d", - "sha256:391db82ebeb886143b96b9c6c6166686c9a272d00020e4e39ad63b792542d9e2", - "sha256:41675323d4fcdd15abde068607cad150dfe17f7d32290ee128e5fea98442bd09", - "sha256:53328284a7bb046e2e885fd1b8c078bd896d7fc4575b915d4936f54984a2ba67", - "sha256:57c5f6be49259cde8e6f71c2bf240a26b071569cabc04c751358495d09419e56", - "sha256:84c101d0f7bbf0d9f1be9a2f29f6fcc12415442558d067164e50a56edfb732b4", - "sha256:88930c74f69e97b17703600233c0eaf1f4f4dd10c14633d522724c5c1b963ec4", - "sha256:8c9ec12c480c4d915e23ee9c8a2d8eba8509986f35f307771045c1294a2e5b73", - "sha256:a81c4bf9c59010aa3efddbb6b9fc84a9b76dc0b4da2c2c2d50f06a9ef6ac0004", - "sha256:d9644ac996149b2a51325d48d77e25c911e01aa6d39dc1b64be679cd71f683ec", - "sha256:e4b6c98f45695799990da328e6fd7d6187be32752ed64c2f22326ad66762d179", - "sha256:fe6f1623376b616e03d51f0dd95afd862cf9a33c18cf55ce0ed4bbe1c4444391" + "sha256:05ca6bda50123158eb15e716789083ca4c3b874fd47688df1716daa72644ee1c", + "sha256:08b6bbe74ae2b3e4741a744d2bce35ce0868a6b4189d8b84be26bb334f73da4c", + "sha256:14ed84b463e9b84c8ff9308a79b04bf591ae3122a376ee0f62c68a1bd917a773", + "sha256:214ae60b1f863844e97c87f758c29940ffad96c666257323a4bb2a33c58719c2", + "sha256:230de25bd9791748b2638c726a5f37d77a96a83854710110fadd068d1e2c2c9f", + "sha256:26b4919eb3039a686a86cd4f4a74224f8f66e3a419767da26909dcdd3b37c31e", + "sha256:4d33537a375cfb2db4d388f9a929b6582a364137ea6c6b161b0166440d6ffe36", + "sha256:69a70d79a791fa1fd5f6e84b8b6dec2ec92369bde4ab2e18d43fc8a1825f51d1", + "sha256:8ac028cd9a6e1efe43f3dc36f708263838283535cc45430a98b9803f44f4c84b", + "sha256:a50cf3110a1914442e7b7b9cef394ef6bed0d801b8a34d56f4c4e927bbbcc7d0", + "sha256:c43d1beb098a1da15934262009a7120aac8dafa20d042b31dab48c28868eb5a4", + "sha256:c76a108272a4de63189b8f64086bbaf8348841d7e610b52f50959fbbf401524f", + "sha256:cbad4155028b8ca66aa19a8b13f593ebbf51bfb6c3f2685fe64f04d695a81864", + "sha256:e3c250faaf9979d0ec836d25e420428db37783fa5fed218da49c9fc06f80f51c", + "sha256:e61a089151f1ed78682aa77a3bcae0495cf8e585546c26924857d7e8a9960568", + "sha256:e9bbcc7b5c432600797981706f5b54611990c6a86b2e424329c995eea5f9c42b", + "sha256:fbddbb20f30308ba2546193d64e18c23b69f59d48cdef73676cbed803495c8dc", + "sha256:fc351cd2df318674669481eb978a7799f24fd14ef26987a1aa75105b0531d1a1" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.2.2" }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], "index": "pypi", - "version": "==2020.1" + "version": "==2021.1" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "typing-extensions": { @@ -125,17 +128,19 @@ }, "astroid": { "hashes": [ - "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", - "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" + "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc", + "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d" ], - "version": "==2.4.2" + "markers": "python_version >= '3.6'", + "version": "==2.5.0" }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "black": { "hashes": [ @@ -147,17 +152,18 @@ }, "bleach": { "hashes": [ - "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", - "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" + "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", + "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" ], - "version": "==3.1.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.3.0" }, "cached-property": { "hashes": [ - "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", - "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", + "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" ], - "version": "==1.5.1" + "version": "==1.5.2" }, "cerberus": { "hashes": [ @@ -167,31 +173,34 @@ }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "click": { "hashes": [ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "version": "==0.4.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.4" }, "distlib": { "hashes": [ @@ -205,6 +214,7 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "idna": { @@ -212,55 +222,62 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" + "sha256:24499ffde1b80be08284100393955842be4a59c7c16bbf2738aad0e464a8e0aa", + "sha256:c6af5dbf1126cd959c4a8d8efd61d4d3c83bddb0459a17e554284a077574b614" ], "markers": "python_version < '3.8'", - "version": "==1.7.0" + "version": "==3.7.0" }, "isort": { "hashes": [ - "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750", - "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8" + "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", + "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" ], - "version": "==5.4.2" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.7.0" }, "keyring": { "hashes": [ - "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", - "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + "sha256:16927a444b2c73f983520a48dec79ddab49fe76429ea05b8d528d778c8339522", + "sha256:2bc8363ebdd63886126a012057a85c8cb6e143877afa02619ac7dbc9f38a207b" ], - "version": "==21.4.0" + "markers": "python_version >= '3.6'", + "version": "==22.3.0" }, "lazy-object-proxy": { "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "version": "==1.4.3" + "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39", + "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694", + "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9", + "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84", + "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa", + "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128", + "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871", + "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0", + "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4", + "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302", + "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458", + "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c", + "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7", + "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb", + "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163", + "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847", + "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108", + "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef", + "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f", + "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315", + "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068", + "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166", + "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a", + "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.5.2" }, "mccabe": { "hashes": [ @@ -278,30 +295,32 @@ }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "version": "==20.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "pep517": { "hashes": [ - "sha256:576c480be81f3e1a70a16182c762311eb80d1f8a7b0d11971e5234967d7a342c", - "sha256:8e6199cf1288d48a0c44057f112acf18aa5ebabbf73faa242f598fbe145ba29e" + "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51", + "sha256:aeb78601f2d1aa461960b43add204cc7955667687fbcf9cdb5170f00556f117f" ], - "version": "==0.8.2" + "version": "==0.9.1" }, "pip-shims": { "hashes": [ "sha256:05b00ade9d1e686a98bb656dd9b0608a933897283dc21913fad6ea5409ff7e91", "sha256:16ca9f87485667b16b978b68a1aae4f9cc082c0fa018aed28567f9f34a590569" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.5.3" }, "pipenv-setup": { @@ -320,10 +339,10 @@ }, "pkginfo": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", + "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" ], - "version": "==1.5.0.1" + "version": "==1.7.0" }, "plette": { "extras": [ @@ -333,28 +352,31 @@ "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26", "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.2.3" }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", + "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], - "version": "==2.6.1" + "markers": "python_version >= '3.5'", + "version": "==2.8.0" }, "pylint": { "hashes": [ - "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", - "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" + "sha256:81ce108f6342421169ea039ff1f528208c99d2e5a9c4ca95cfc5291be6dfd982", + "sha256:a251b238db462b71d25948f940568bb5b3ae0e37dbaa05e10523f54f83e6cc7e" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.1" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "python-dateutil": { @@ -362,47 +384,69 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "readme-renderer": { "hashes": [ - "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", - "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" + "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", + "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" ], - "version": "==26.0" + "version": "==29.0" }, "regex": { "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" - ], - "version": "==2020.7.14" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + ], + "version": "==2020.11.13" }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "version": "==2.24.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.25.1" }, "requests-toolbelt": { "hashes": [ @@ -413,10 +457,11 @@ }, "requirementslib": { "hashes": [ - "sha256:cdf8aa652ac52216d156cee2b89c3c9ee53373dded0035184d0b9af569a0f10c", - "sha256:fd98ea873effaede6b3394725a232bcbd3fe3985987e226109a841c85a69e2e3" + "sha256:50d20f27e4515a2393695b0d886219598302163438ae054253147b2bad9b4a44", + "sha256:9c1e8666ca4512724cdd1739adcc7df19ec7ad2ed21f0e748f9631ad6b54f321" ], - "version": "==1.5.13" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.5.16" }, "rfc3986": { "hashes": [ @@ -427,86 +472,110 @@ }, "rope": { "hashes": [ - "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1" + "sha256:786b5c38c530d4846aa68a42604f61b4e69a493390e3ca11b88df0fbfdc3ed04" ], "index": "pypi", - "version": "==0.17.0" + "version": "==0.18.0" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" }, "tomlkit": { "hashes": [ "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831", "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.7.0" }, "tqdm": { "hashes": [ - "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", - "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" + "sha256:2c44efa73b8914dba7807aefd09653ac63c22b5b4ea34f7a80973f418f1a3089", + "sha256:c23ac707e8e8aabb825e4d91f8e17247f9cc14b0d64dd9e97be0781e9e525bba" ], - "version": "==4.48.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.58.0" }, "twine": { "hashes": [ - "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", - "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" + "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", + "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.3.0" }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + ], + "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "version": "==1.4.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "index": "pypi", + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "version": "==1.25.10" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.3" }, "vistir": { "hashes": [ "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb", "sha256:eff1d19ef50c703a329ed294e5ec0b0fbb35b96c1b3ee6dcdb266dddbe1e935a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.2" }, "webencodings": { @@ -518,10 +587,11 @@ }, "wheel": { "hashes": [ - "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2", - "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f" + "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e", + "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e" ], - "version": "==0.35.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.36.2" }, "wrapt": { "hashes": [ @@ -539,11 +609,11 @@ }, "zipp": { "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" ], "markers": "python_version < '3.8'", - "version": "==3.1.0" + "version": "==3.4.0" } } } From 0dccced74a2813b31278cde1d64baa026b3fd39d Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 12:59:20 +0000 Subject: [PATCH 124/126] Fix cannot req historical ticks from start - Fixed the issue of cannot request historical tick data from the earliest availble point as the start time won't advance due to 0 tick received. --- ibpy_native/bridge.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ibpy_native/bridge.py b/ibpy_native/bridge.py index 48acd12..4215f23 100644 --- a/ibpy_native/bridge.py +++ b/ibpy_native/bridge.py @@ -380,7 +380,8 @@ async def req_historical_ticks( except error.IBError as err: raise err - start_date_time = head_time if head_time > start else start + start_date_time = (head_time if start is None or head_time > start + else start) end_date_time = datetime.datetime.now() if end is None else end # Request tick data @@ -407,7 +408,8 @@ async def req_historical_ticks( if ticks: # Drop the 1st tick as tick time of it is `start_date_time` # - 1 second - del ticks[0] + if len(ticks) > 1: + del ticks[0] # Determine if it should fetch next batch of data last_tick_time = datetime.datetime.fromtimestamp( timestamp=ticks[-1].time, tz=_global.TZ @@ -428,6 +430,18 @@ async def req_historical_ticks( # Ready for next request start_date_time = (last_tick_time + datetime.timedelta(seconds=1)) + else: # If no tick is returned + delta = datetime.timedelta(minutes=start_date_time.minute % 30, + seconds=start_date_time.second) + if delta.total_seconds() == 0: # Plus 30 minutes + start_date_time = (start_date_time + + datetime.timedelta(minutes=30)) + else: # Round up to next 30 minutes point + start_date_time = ( + start_date_time + (datetime.datetime.min - + start_date_time) % datetime.timedelta(minutes=30) + ) + # Yield the result yield datatype.ResHistoricalTicks(ticks=ticks, completed=finished) #endregion - Historical data From 5d4fcf5eebcb6646c664420aeeaea7e1321813de Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 13:17:00 +0000 Subject: [PATCH 125/126] Update dependencies - Updated dependencies versions. - Moved `pandas` to `dev-packages`. --- Pipfile | 2 +- Pipfile.lock | 134 ++++++++++++++++++++++++--------------------------- 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/Pipfile b/Pipfile index 4f5598f..8667888 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ verify_ssl = true [dev-packages] rope = "*" yapf = "*" +pandas = "*" pylint = "*" pipenv-setup = "*" twine = "*" @@ -13,7 +14,6 @@ twine = "*" [packages] pytz = "*" typing-extensions = "*" -pandas = "*" deprecated = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index ee0e2d8..b36ba59 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f5c1913e199658254f8e6002ed953c6f27ec22b91334937b845376ed24b1cb3b" + "sha256": "6be5424b0a9b7e7533798edfeb2a61a9880a1168074fbe95d8fe30ecf8f712dd" }, "pipfile-spec": 6, "requires": { @@ -24,68 +24,6 @@ "index": "pypi", "version": "==1.2.11" }, - "numpy": { - "hashes": [ - "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", - "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", - "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", - "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", - "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", - "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", - "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", - "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", - "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", - "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", - "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", - "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", - "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", - "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", - "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", - "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", - "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", - "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", - "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", - "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", - "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", - "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", - "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", - "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" - ], - "markers": "python_version >= '3.7'", - "version": "==1.20.1" - }, - "pandas": { - "hashes": [ - "sha256:05ca6bda50123158eb15e716789083ca4c3b874fd47688df1716daa72644ee1c", - "sha256:08b6bbe74ae2b3e4741a744d2bce35ce0868a6b4189d8b84be26bb334f73da4c", - "sha256:14ed84b463e9b84c8ff9308a79b04bf591ae3122a376ee0f62c68a1bd917a773", - "sha256:214ae60b1f863844e97c87f758c29940ffad96c666257323a4bb2a33c58719c2", - "sha256:230de25bd9791748b2638c726a5f37d77a96a83854710110fadd068d1e2c2c9f", - "sha256:26b4919eb3039a686a86cd4f4a74224f8f66e3a419767da26909dcdd3b37c31e", - "sha256:4d33537a375cfb2db4d388f9a929b6582a364137ea6c6b161b0166440d6ffe36", - "sha256:69a70d79a791fa1fd5f6e84b8b6dec2ec92369bde4ab2e18d43fc8a1825f51d1", - "sha256:8ac028cd9a6e1efe43f3dc36f708263838283535cc45430a98b9803f44f4c84b", - "sha256:a50cf3110a1914442e7b7b9cef394ef6bed0d801b8a34d56f4c4e927bbbcc7d0", - "sha256:c43d1beb098a1da15934262009a7120aac8dafa20d042b31dab48c28868eb5a4", - "sha256:c76a108272a4de63189b8f64086bbaf8348841d7e610b52f50959fbbf401524f", - "sha256:cbad4155028b8ca66aa19a8b13f593ebbf51bfb6c3f2685fe64f04d695a81864", - "sha256:e3c250faaf9979d0ec836d25e420428db37783fa5fed218da49c9fc06f80f51c", - "sha256:e61a089151f1ed78682aa77a3bcae0495cf8e585546c26924857d7e8a9960568", - "sha256:e9bbcc7b5c432600797981706f5b54611990c6a86b2e424329c995eea5f9c42b", - "sha256:fbddbb20f30308ba2546193d64e18c23b69f59d48cdef73676cbed803495c8dc", - "sha256:fc351cd2df318674669481eb978a7799f24fd14ef26987a1aa75105b0531d1a1" - ], - "index": "pypi", - "version": "==1.2.2" - }, - "python-dateutil": { - "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.1" - }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -94,14 +32,6 @@ "index": "pypi", "version": "==2021.1" }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" - }, "typing-extensions": { "hashes": [ "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", @@ -286,6 +216,36 @@ ], "version": "==0.6.1" }, + "numpy": { + "hashes": [ + "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", + "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", + "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", + "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", + "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", + "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", + "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", + "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", + "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", + "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", + "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", + "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", + "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", + "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", + "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", + "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", + "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", + "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", + "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", + "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", + "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", + "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", + "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", + "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" + ], + "markers": "python_version >= '3.7'", + "version": "==1.20.1" + }, "orderedmultidict": { "hashes": [ "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", @@ -301,6 +261,30 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, + "pandas": { + "hashes": [ + "sha256:05ca6bda50123158eb15e716789083ca4c3b874fd47688df1716daa72644ee1c", + "sha256:08b6bbe74ae2b3e4741a744d2bce35ce0868a6b4189d8b84be26bb334f73da4c", + "sha256:14ed84b463e9b84c8ff9308a79b04bf591ae3122a376ee0f62c68a1bd917a773", + "sha256:214ae60b1f863844e97c87f758c29940ffad96c666257323a4bb2a33c58719c2", + "sha256:230de25bd9791748b2638c726a5f37d77a96a83854710110fadd068d1e2c2c9f", + "sha256:26b4919eb3039a686a86cd4f4a74224f8f66e3a419767da26909dcdd3b37c31e", + "sha256:4d33537a375cfb2db4d388f9a929b6582a364137ea6c6b161b0166440d6ffe36", + "sha256:69a70d79a791fa1fd5f6e84b8b6dec2ec92369bde4ab2e18d43fc8a1825f51d1", + "sha256:8ac028cd9a6e1efe43f3dc36f708263838283535cc45430a98b9803f44f4c84b", + "sha256:a50cf3110a1914442e7b7b9cef394ef6bed0d801b8a34d56f4c4e927bbbcc7d0", + "sha256:c43d1beb098a1da15934262009a7120aac8dafa20d042b31dab48c28868eb5a4", + "sha256:c76a108272a4de63189b8f64086bbaf8348841d7e610b52f50959fbbf401524f", + "sha256:cbad4155028b8ca66aa19a8b13f593ebbf51bfb6c3f2685fe64f04d695a81864", + "sha256:e3c250faaf9979d0ec836d25e420428db37783fa5fed218da49c9fc06f80f51c", + "sha256:e61a089151f1ed78682aa77a3bcae0495cf8e585546c26924857d7e8a9960568", + "sha256:e9bbcc7b5c432600797981706f5b54611990c6a86b2e424329c995eea5f9c42b", + "sha256:fbddbb20f30308ba2546193d64e18c23b69f59d48cdef73676cbed803495c8dc", + "sha256:fc351cd2df318674669481eb978a7799f24fd14ef26987a1aa75105b0531d1a1" + ], + "index": "pypi", + "version": "==1.2.2" + }, "pathspec": { "hashes": [ "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", @@ -387,6 +371,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "index": "pypi", + "version": "==2021.1" + }, "readme-renderer": { "hashes": [ "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", From baab4e69b7c568db0e01edb416cfde5bc6eb3330 Mon Sep 17 00:00:00 2001 From: Wing Chau Date: Sun, 28 Feb 2021 13:19:52 +0000 Subject: [PATCH 126/126] Update `setup.py` --- setup.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 13db648..1ceba58 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # For a discussion on single-sourcing the version across setup.py and the # project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="0.2.0", # Required + version="1.0.0", # Required # This is a one-line description or tagline of what your project does. This # corresponds to the "Summary" metadata field: # https://packaging.python.org/specifications/core-metadata/#summary @@ -74,7 +74,7 @@ # 3 - Alpha # 4 - Beta # 5 - Production/Stable - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", # Indicate who your project is intended for "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", @@ -100,7 +100,9 @@ # # py_modules=["my_module"], # - packages=find_packages(exclude=["cmd", "contrib", "docs", "tests", "tests.*"]), # Required + packages=find_packages( + exclude=["contrib", "docs", "samples", "tests", "tests.*"] + ), # Required # Specify which Python versions you support. In contrast to the # 'Programming Language' classifiers above, 'pip install' will check this # and refuse to install the project if the version does not match. If you @@ -114,9 +116,10 @@ # For an analysis of "install_requires" vs pip's requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=[ - "deprecated==1.2.10", - "pytz==2020.1", + "deprecated==1.2.11", + "pytz==2021.1", "typing-extensions==3.7.4.3", + "wrapt==1.12.1", ], # Optional # List additional groups of dependencies here (e.g. development # dependencies). Users will be able to install these using the "extras"