async def resolve_head_timestamp( self, req_id: int, contract: ib_contract.Contract, show: datatype.EarliestDataPoint=datatype.EarliestDataPoint.TRADES ) -> int: """Fetch the earliest available data point 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. 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. Raises: ibpy_native.error.IBError: If - queue associated with `req_id` is being used by other tasks; - there's any error returned from IB; """ try: f_queue = self._wrapper.get_request_queue(req_id=req_id) except error.IBError as err: raise err self.reqHeadTimeStamp(reqId=req_id, contract=contract, whatToShow=show.value, useRTH=0, formatDate=2) res = await f_queue.get() # Cancel the head time stamp request to release the ID after the # request queue is finished self.cancelHeadTimeStamp(reqId=req_id) if res: if f_queue.status is fq.Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] raise self._unknown_error(req_id=req_id) if len(res) > 1: raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_UNEXPECTED.value, err_str="[Abnormal] Multiple result received" ) return int(res[0]) raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_NO_CONTENT.value, err_str="Failed to get the earliest available data point" )
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.value, err_str=_global.MSG_NOT_CONNECTED) self._account_updates_queue.put(element=err)
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. Raises: 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: if (self._req_queue[req_id].finished 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))): self._req_queue[req_id].reset() 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") else: self._req_queue[req_id] = fq.FinishableQueue(queue.Queue())
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()
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 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)
async def resolve_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... Returns: ibapi.contract.Contract: Fully resolved IB contract. Raises: ibpy_native.error.IBError: If - queue associated with `req_id` is being used by other tasks; - there's any error returned from IB; - no item found in received result. """ # Make place to store the data that will be returned try: f_queue = self._wrapper.get_request_queue(req_id=req_id) 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) res = await f_queue.get() if res: if f_queue.status is fq._Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] 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 raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_NO_CONTENT, err_str="Failed to get additional contract details")
async def resolve_contracts( 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... Returns: List[ibapi.contract.ContractDetails]: Fully fledged IB contract(s) with detailed info. Raises: ibpy_native.error.IBError: If - queue associated with `req_id` is being used by other tasks; - there's any error returned from IB; - no item found in received result. """ # Prepare queue to store the data that will be returned try: f_queue = self._wrapper.get_request_queue(req_id=req_id) 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]] = \ await f_queue.get() if res: if f_queue.status is fq._Status.ERROR: if isinstance(res[-1], error.IBError): raise res[-1] return res raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_NO_CONTENT, err_str="Failed to get additional contract details")
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 != -1 and self._orders_manager.is_pending_order( order_id=reqId): # Is an order error self._orders_manager.order_error(err) elif reqId != -1 and errorCode == error.IBErrorCode.ORDER_REJECTED: self._orders_manager.on_order_rejected(order_id=reqId, reason=errorString) 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 self._on_disconnected() else: if self._notification_listener is not None: self._notification_listener.on_notify(msg_code=errorCode, msg=errorString)
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. Raises: 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: if self._req_queue[req_id].finished: self._req_queue[req_id].reset() 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" ) else: self._req_queue[req_id] = fq._FinishableQueue(queue.Queue())
def _unknown_error(self, req_id: int, extra: Any = None): """Constructs `IBError` with error code `UNKNOWN` For siturations which internal `FinishableQueue` reports error status but not exception received. Args: req_id (int): Request ID (ticker ID in IB API). extra (:obj:`Any`, optional): Extra data to be passed through the exception. Defaults to `None`. Returns: ibpy_native.error.IBError: Preconfigured `IBError` object with error code `50500: UNKNOWN` """ return error.IBError( rid=req_id, err_code=error.IBErrorCode.UNKNOWN.value, err_str="Unknown error: Internal queue reported error " "status but no exception received", err_extra=extra )
def cancel_live_ticks_stream(self, req_id: int): """Stop the live tick data stream that's currently streaming. Args: req_id (int): Request ID (ticker ID in IB API). Raises: ibpy_native.error.IBError: If there's no `FinishableQueue` object associated with the specified `req_id` found in the internal `IBWrapper` object. """ f_queue = self._wrapper.get_request_queue_no_throw(req_id=req_id) if f_queue is not None: self.cancelTickByTickData(reqId=req_id) f_queue.put(element=fq.Status.FINISHED) else: raise error.IBError( rid=req_id, err_code=error.IBErrorCode.RES_NOT_FOUND.value, err_str=f"Task associated with request ID {req_id} not found" )
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.value, err_str=f"Existing queue assigned for order ID {order_id} " "found. Possiblely duplicate order ID is being used.")
def _on_disconnected(self): """Stop all active requests.""" 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[_global.IDX_NEXT_ORDER_ID].put( element=fq.Status.FINISHED) for key, f_queue in self._req_queue.items(): if key == _global.IDX_NEXT_ORDER_ID: 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() self._accounts_manager.on_disconnected() if self._connection_listener is not None: self._connection_listener.on_disconnected()
async def req_historical_ticks( self, req_id: int, contract: ib_contract.Contract, start_date_time: datetime.datetime, show: datatype.HistoricalTicks=datatype.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.value, 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, end: Optional[datetime.datetime] = datetime.datetime.now()\ .astimezone(TZ), show: Optional[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 (Literal['MIDPOINT', 'BID_ASK', 'TRADES'], optional): Type of data requested. Defaults to 'TRADES'. Returns: 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; - 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): 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 = _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 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( const._IB.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(const._IB.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 (all_ticks, finished) return {'ticks': all_ticks, 'completed': finished}