erdewit / ib_insync

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

qualifyContractsAsync requires only 1 match per pattern #649

Closed dmoklaf closed 1 year ago

dmoklaf commented 1 year ago

qualifyContractsAsync works in 4 steps: (a) Retrieve qualified contracts from IB database based on a contract pattern argument (b) Ensure that there is only one qualified contract per pattern (c) Fix some faulty data fields of the qualified contract (d) Copy its data into the initial argument's fields

However, there are legitimate cases where (b) may be problematic. There could be several matches for a given contract pattern, that only some posterior client logic can eliminate. E.g., when looking for a US stock by ISIN and currency, without knowing its primary exchange, 2 contracts are returned in some cases. E.g., MSFT (ISIN US594918104) has 2 contracts in USD in the database:

The solution could be to: (1) Create an additional method searchContractsAsync that take as argument a single contract pattern and return a list of candidate contracts - that would be steps (a) and (c) above (2) Factor out from qualifyContractsAsync into a separate private method called by both public methods the contract data repair logic (c) (https://github.com/erdewit/ib_insync/blob/7337c9a3dd93b3b0eb9b129f77956401aad70a05/ib_insync/ib.py#L1860)

I can submit a PR if this is useful

erdewit commented 1 year ago

qualifyContracts is a higher-level convenience method that sits atop of reqContractDetails. The latter can be used to get a list of matching contract details, which also includes the fully qualified contracts.

dmoklaf commented 1 year ago

That's exactly what I thought initially. But it doesn't work because of step (c) above: qualifyContracts also performs fixes in the data fields of the contract, fixes which have to be duplicated (and thus unmaintained) if operating separately:

https://github.com/erdewit/ib_insync/blob/7337c9a3dd93b3b0eb9b129f77956401aad70a05/ib_insync/ib.py#L1860

An alternative solution would be to refactor these fixes into a util package function that "fixes" a Contract object received from the IB server. The API user would be free to call it if he wants to process raw contract objects.

erdewit commented 1 year ago

OK, so it's strictly about the contract 'fixup' part. Which is this:

                c = detailsList[0].contract
                expiry = c.lastTradeDateOrContractMonth
                if expiry:
                    # remove time and timezone part as it will cause problems
                    expiry = expiry.split()[0]
                    c.lastTradeDateOrContractMonth = expiry
                if contract.exchange == 'SMART':
                    # overwriting 'SMART' exchange can create invalid contract
                    c.exchange = contract.exchange
                util.dataclassUpdate(contract, c)

The expiry fixup has become part of the Decoder and can be removed here (will do that later). The exchange fixup doesn't apply here as one does want all the different exchanges returned.

If IB sends contracts that they then reject when sent back as-is, then that is not something that ib_insync can generally fix.

dmoklaf commented 1 year ago

It answers perfectly my point. Indeed the fixup generated all this and is now not an issue. Thanks for the explanations. Created a follow-up tracking issue for the move to the decoder (to allow tracking by my development environment as I have currently some workaround code there)