erdewit / ib_insync

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

Multiple contract objects for the the same IB contract leads to bugs #659

Closed t1user closed 7 months ago

t1user commented 7 months ago

Currently it's possible to create several separate Contract objects representing the same IB contract. Active ticker subscriptions are recalled based on Contract object id rather than it's hash. It leads to problems when two different Contract objects represent the same IB contract, as illustrated in the session below:

In [1]: import ib_insync as ibi

In [2]: nq1 = ibi.ContFuture('NQ', 'CME')

In [3]: nq2 = ibi.ContFuture('NQ', 'CME')

In [5]: ib = ibi.IB().connect(port=4002, clientId=10)

In [6]: ib.qualifyContracts(nq1, nq2)
Out[6]: 
[ContFuture(conId=563947728, symbol='NQ', lastTradeDateOrContractMonth='20231215', multiplier='20', exchange='CME', currency='USD', localSymbol='NQZ3', tradingClass='NQ'),
 ContFuture(conId=563947728, symbol='NQ', lastTradeDateOrContractMonth='20231215', multiplier='20', exchange='CME', currency='USD', localSymbol='NQZ3', tradingClass='NQ')]

In [7]: id(nq1)
Out[7]: 139837970225376

In [8]: id(nq2)
Out[8]: 139837969818528

In [10]: ib.reqMktData(nq1, 221)
Out[10]: Ticker(contract=ContFuture(conId=563947728, symbol='NQ', lastTradeDateOrContractMonth='20231215', multiplier='20', exchange='CME', currency='USD', localSymbol='NQZ3', tradingClass='NQ'))

In [11]: ib.ticker(nq1)
Out[11]: Ticker(contract=ContFuture(conId=563947728, symbol='NQ', lastTradeDateOrContractMonth='20231215', multiplier='20', exchange='CME', currency='USD', localSymbol='NQZ3', tradingClass='NQ'))

In [12]: ib.ticker(nq2)
Out[12]: None

I propose to change ib.ticker so that it looks up Contract based on Contract's hash rather than object id and either object nq1 or nq2 in the code above could be used to recall active ticker subscription.

erdewit commented 7 months ago

The contracts hash already uses the conId property. The equality operator does also, so nq1 == nq2 is true. The identity nq1 is nq2 does of course not hold if these are different objects but that is just how the identity operator is defined.

The ticker lookup is a different issue. It doesn't use the contracts hash so as to allow unhashable contracts, which are basically contracts without a conId, and also Bag contracts.

t1user commented 7 months ago

Sorry for a little bit of ramble before I fully researched the subject - I did update my post in the end to address what I really see as the issue, ie. the ticker lookup.

Don't you think it would make sense to useUserDict to store ticker references, __getitem__ and __setitem__ would call Contrac't isHashable, if it is hashable hash would be used, otherwise object id. Just an idea.

Of course I can do a workaround in my code (I already did) but it would be more elegant if ib_insync just understood that I created several objects that should correspond to the same ticker.

I have a similar problem with Trade and Order objects. On re-connect new objects are created referencing the same IB orders as objects already present in the running programme. Any events connected to existing objects no longer work... Wouldn't it make sense to keep references to all instances created (e.g. on Trade and Order class objects, via __new__ method.) and on re-connect relink them to existing IB entities based on permId?

It's part of the same theme, where multiple ib_insync objects don't match IB entities that they represent. It somehow keeps creeping up in many places... Would you consider addressing this more broadly across all ib_insync objects, maybe starting with Trade, Order and Contract? There would be a kind of singleton for every IB entity that would get re-linked on reconnect.

I'm happy to help but, as you can probably tell, I'm pretty clueless and maybe it's easier for you just to do it yourself. I realize there's a lot of edge cases that I'm not even aware of, but you probably know most of them by now. Anyway, happy to help if you tell me so.

On Wed, 15 Nov 2023 at 11:36, Ewald de Wit @.***> wrote:

The contracts hash already uses the conId property. The equality operator does also, so nq1 == nq2 is true. The identity nq1 is nq2 does of course not hold if these are different objects but that is just how the identity operator is defined.

The ticker lookup is a different issue. It doesn't use the contracts hash so as to allow unhashable contracts, which are basically contracts without a conId, and also Bag contracts.

— Reply to this email directly, view it on GitHub https://github.com/erdewit/ib_insync/issues/659#issuecomment-1812212333, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4SBMMPFIYD7V6UKTUPH7LYESLJNAVCNFSM6AAAAAA7LPZK4GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMJSGIYTEMZTGM . You are receiving this because you authored the thread.Message ID: @.***>

erdewit commented 7 months ago

There would be a kind of singleton for every IB entity that would get re-linked on reconnect.

I don't think you realize the amount of effort this requires.

t1user commented 7 months ago

You're definitely right, I don't. I wouldn't propose it otherwise.

How about a UserDict to store ticker subscriptions to start with? This seems pretty straightforward (to my ignorant and limited brain).

On Wed, 15 Nov 2023 at 18:43, Ewald de Wit @.***> wrote:

There would be a kind of singleton for every IB entity that would get re-linked on reconnect.

I don't think you realize the amount of effort this requires.

— Reply to this email directly, view it on GitHub https://github.com/erdewit/ib_insync/issues/659#issuecomment-1812983882, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4SBMOZGXBLHS677WK2N2TYET5MHAVCNFSM6AAAAAA7LPZK4GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMJSHE4DGOBYGI . You are receiving this because you authored the thread.Message ID: @.***>

erdewit commented 7 months ago

What if a contract starts without condId, and later gets its conId filled in? Then it can't be found in your UserDict....

Anyway I think the current way is pretty straightforward: The original contract object is required to fetch a specific ticker. If it is too much bother to hold on to the original object then it can be retrieved from the ib.tickers() list. This list can also be used for any other sort of querying.

t1user commented 7 months ago

I thought that request to ib.reqMktData(c) will fail if c is not a qualified contract and if contract is qualified it will have conId.

But anyway, I will not argue, you have an idea how you want this to work, and it does. Thanks for looking into it.

On Wed, 15 Nov 2023 at 20:32, Ewald de Wit @.***> wrote:

What if a contract starts without condId, and later gets its conId filled in? Then it can't be found in your UserDict....

Anyway I think the current way is pretty straightforward: The original contract object is required to fetch a specific ticker. If it is too much bother to hold on to the original object then it can be retrieved from the ib.tickers() list. This list can also be used for any other sort of querying.

— Reply to this email directly, view it on GitHub https://github.com/erdewit/ib_insync/issues/659#issuecomment-1813137292, or unsubscribe https://github.com/notifications/unsubscribe-auth/AE4SBMPDHXW6B43A4574UT3YEUKDLAVCNFSM6AAAAAA7LPZK4GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMJTGEZTOMRZGI . You are receiving this because you authored the thread.Message ID: @.***>