erdewit / ib_insync

Python sync/async framework for Interactive Brokers API
BSD 2-Clause "Simplified" License
2.75k stars 723 forks source link

Feature request: Raise Errors On Calls #686

Open gdassori opened 5 months ago

gdassori commented 5 months ago

Hello. AFAIK Ib Insync methods doesn't raise errors, let me explain better:

A call like this one:

        bars = await self.ib.reqHistoricalDataAsync(
            contract,
            endDateTime=to_date.strftime('%Y%m%d %H:%M:%S UTC'),
            durationStr=f'{int(duration)} S',
            barSizeSetting='5 mins',
            whatToShow='TRADES',
            useRTH=True,
            formatDate=2,
            keepUpToDate=False
        )

would produce, according to my contract and my code, the following log:

Error 162, reqId 65121: Historical Market Data Service error message:No historical market data for EUR/CASH@FXSUBPIP Last 300, contract: Contract(secType='CASH', conId=12087792, symbol='EUR', exchange='IDEALPRO', currency='USD', localSymbol='EUR.USD', tradingClass='EUR.USD')

And an empty "bars" list returned.

To work around this issue (the blindness of a developer on errors) I wrote a small decorator to speculate over the input argument and catch some errors:

def catch_ib_error_on_contract(contract_id_key: str, error_code: int):
    def _raise_ib_error_on_contract(func):
        async def wrapper(*args, **kwargs):
            _errors = []

            def _error_handler(*x):
                _errors.append(x)

            contract_id = kwargs[contract_id_key]
            self = args[0]
            self.ib.errorEvent += _error_handler
            try:
                resp = await func(*args, **kwargs)
                for e in _errors:
                    if int(e[1]) == error_code:
                        if int(e[3].conId) == int(contract_id):
                            raise BrokerResponseErrorException(
                                f'Broker Response Error: {e[2]} - {e[3]}'
                            )
                return resp
            finally:
                self.ib.errorEvent -= _error_handler

        return wrapper
    return _raise_ib_error_on_contract

But is tricky to maintain it and it's definitely smelly.

Also, we can have the requestId as a better hook and be clear on the error source without speculating over the errors array in search of a relevant one.

IMHO the best way to handle this would be using a dedicated object and return, at any request, all the data regarding the response received (an errors field, the request id, the response data which may be empty, and so on), but I think that would be hard to switch all the calls to this new behaviour, so my proposal is just to raise exceptions regarding errors.

Having the request id in your hand would make this easy.

To avoid breaking changes on previously releases, you may add an argument in the IB object constructor to raise or not exceptions on methods, that may have a false default.