-
-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ib_async.sleep should not create a new async loop to run asyncio.sleep() #103
Comments
did you run |
here now my ignorance will be highlighted most prominently:
I did not. Was I supposed to? It appears that my code works without it. If you can point to documentation that states otherwise, please direct me there. I run my wrapper class inside an existing event loop. I don't want ib_async to run its own event loop; I want it to use the existing (running) event loop instead. So that everything, including my ASGI server, is running in the event loop in the main thread (for obvious reasons). it appears that docs state that startLoop is for nesting an event loop in for example jupyter Notebook contexts:
so then, is ib_sleep not meant for traditional async event loops? That's why I asked, it would appear that perhaps not all methods are meant to be used in all contexts, and that's OK, I was just wanting to understand. |
this works fine import ib_async
import asyncio
ib_async.util.startLoop()
ib_async.util.run(asyncio.sleep(1))
print('long time no see') asyncio get's tricky when you mix sync and async code, and ib_async(and eventkit) does a great job hiding the complexity. but it's not magic. i would suggest you to start with a simple piece of code and build up slowly until you find the issue |
Perfect, thanks for the response. For now, my issue has been resolved by using asyncio.sleep instead of ib_client.sleep() I believe there is still an issue in the getLoop() functionality not returning the current running_event_loop (but admittedly I could be 100% wrong). Your absolutely right that asyncio is tricky! Give me a couple days and I'll investigate further, and update you with my testing results. Thank you for your patience and support! |
take into account that this works as well ib_async.util.sleep(1)
print('long time no see') which is equivalent(sort of) to await asyncio.sleep(1)
print('long time no see') |
I have also had issues with ib.sleep() throwing the same runtime error. Initially, my code was not async and used a while loop with a call to ib.sleep(30) to control the flow but still allow Events to trigger in the background. The problems started when I added code in a different module that used ib_async Events; say a filledEvent was triggered and I want to place a new trade in the method triggered by the async filledEvent. Adding a call to ib.sleep() here would throw the runtime error (presumably since it happened asynchronously when sleep() was already running in the main loop??). I tried using @mattsta s nest_asyncio library but that just caused a different error, to do with entry points. I tried running a "custom sleep" using asyncio.sleep(), this would not cause the runtime error, but it was blocking the main loop so that no Events were triggered during the sleep so was basically useless. In the end, I rewrote all the code for the main entry point as async using asyncio and now I am no longer using ib.sleep() in the main loop, rather waiting on Events from the trading bot to trigger the main loop to shutdown. I dunno if this is considered "best practice" but seems to work. Perhaps @mattsta could weigh in on this topic since it pops up quite often? (and is damn confusing for non-pro coders like me!) PS: @gnzsnz could you clarify whether ib.util.startLoop() is required in contexts other than running a Jupyter notebook (like a regular .py script)? |
@gnzsnz I dug into the issue a bit further using the PyCharm remote debugger. I was able to determine that getLoop is (of course!!!!) returning the correct event loop. Using debug logging, I confirmed everything is running in the main thread, which, of course, means that there is a running event loop, i.e. I have already started an event loop to run my app (which incorporates ib_async) using Debug Logging:
patched ib_async::util.py debug statement
This confirms that the problem is not that ib_async is somehow not returning the correct running event loop; it's actually exactly the opposite, that it is in fact returning the main event loop. So attempting to call
The problem IMHO is that ib_async is not handling correctly when it is being run in a pure async environment, i.e. in a thread that is already running event loop. So to make a long story short you can maybe do something like this instead, however after thinking about it, what would be the point of running asyncio sleep in a task? LOL I'm not sure at all. I think the point of ib_async sleep is to not block the ib_async event loop nested in the Jupyter event loop? (I'm not exactly sure here?).
This returns an awaitable which cannot be awaited from run because run is not an async method, and also run would have to return the awaitable to the caller, i.e. In other words, if you're already running ib_async in an event loop there is no need to call Admittedly, there is a lot of trickery in ib_async to deal with running in both sync and async environments. Even though the library is called ib_async, and so it's focus should be on async compatibility, the tws_api implements both sync and async calls. I'm not an expert in asyncio and neither in ib_async internals, so take this again with a grain of salt, but there you go. Do what thou wilt :) |
We should really provide a better training workflow for onboarding people into the ecosystem here since these problems are the most common confusion around usage.
The problem happens when users try to combine the wrapper helper functions which are managing the event loop and async coroutines for you with your own externally created event loop and async API patterns. Usage should either be 100% user-controlled event loop and using python-level async operations, or 100% using the More complex situations like users wanting multiple threads with each thread having their own event loops are currently unsupported throughout the entire |
@mattsta 👍
Perfect, thanks for confirming
Yup, I figured this one out. Maybe a blurb about this at the beginning would clear up a lot of confusion for new users. Although, it might be useful to split up the wrapper functions into a totally different class that distinguishes the (wrapped) sync method calls from the async ones, e.g.
Yes, but, also, some of the method declarations are a bit confusing, example:
the method declaration says async, and I see it returns an awaitable (future), but it's not defined as async, i.e. not: But more importantly:
Although admittedly this is technically not 100% directly related to the Async stuff, there are numerous accountSummary related calls in
If I want to be notified of account summary updates, should I just await accountSummaryAsync in a while True loop, subscribe to the accountSummaryEvent with my own eventHandler, or? I mean, I know I'm being somewhat rhetorical here, but I am actually looking for the answer to this one.
I surmised this :)
Yeah, that sounds complex - not something I personally want to do but at least now I have an inkling of how it might be done now (hindsight is always 20/20). |
This is actually perfectly valid and a normal use case. These are all equivalent: # "Normal" usage
async def doSomething():
return 3
await doSomething()
# "Wrapper" usage
async def doItAgain():
return await doSomething()
await doItAgain()
# "Reduced Wrapper" usage
def doItDifferently():
return doSomething()
await doItDifferently() All of those functions return coroutines when called even if not defined with Before Python 3.12, using the
I'd say you may be over-thinking it? I don't know why anybody would want to stop receiving account value updates (that's also kinda the entire purpose of the library — to keep all values updated as quickly as possible 🙃).
Again, probably over-thinking it a bit. When you subscribe to account summary updates, your account values are now updated in real time for the lifetime of your connection. When running You can subscribe to each account value update tick using the regular built-in event handlers: Lines 197 to 201 in 38cf54a
|
hah! Story of my life!
because it's a server, and there are no active front-end clients. I mean I totally get it right, who doesn't want this data all the time?. I guess I can just subscribe and then connect/disconnect the I just think it's wasteful to request/receive the data if it's not being used. I am a bit of a pedant, you know, fastidious. However, I do know that the This way I would know to wait for the end signal before accessing the obj. However, if there's an easier way to do this for an event-driven framework, then the high-level requirement is, "Wait for an update to finish, when it comes in, send it". Maybe that's already the implementation, but I try to avoid polling and instead rely on events to signal when I can update the client.
Now that is good information. Thank you, I researched the topic and the implementation makes a lot of sense. A lot of my async code doesn't block, so that avoids the context switching overhead. Very nice indeed. BTW, if gratitude didn't come across in my previous comments, let me fix that now. Thank you for your responses, and also more generally, I want to thank the contributors and maintainers of the project for all their hard work. Let's not let this get lost in minor technical gripes. Truly great work on the library, it's easy to work with, and there's a lot of work put in to make our lives easier. |
That's an important part to understand too! When requesting account data (or any data with the IBKR APIs) every field is populated async. The operation is "request account data updates" not "FETCH ALL ACCOUNT DATA IMMEDIATELY." When you request account data updates, your program asks the IBKR API to "please start sending account data when you have a free moment," then each of the maybe 50+ account data fields receives an independent initial sync then receives future updates only when they change. You can see here where the account data gets populated as just individually received name-value pairs over time, so there is practically no one final "the account object is fully populated" moment. Lines 345 to 349 in 38cf54a
After you begin receiving account updates, all the values will independently sync up-front for an initial summary population, then each value will continue changing one-by-one as the underlying value updates are pushed from IBKR -> gateway -> ib_async -> your program. (this is obviously for the "streaming account data" which is more valuable, i think, than the static "fetch account value as batch" thing also with a closing event here: Lines 351 to 353 in 38cf54a
Original docs here of course, and if we are missing the explicit cancel summary API call, we should add it eventually just for completeness: |
Disclaimer: Perhaps it is my fault for using the builtin sleep function - maybe I did that a long time ago when I was first learning ib_async and asyncio in general. Perhaps, it is not intended to be used in async contexts, or when trying to use ib_async to manage the event loop.
On with the proposed description - but again, perhaps I am wrong here so take it with a grain of salt:
This library should not use a loop to run asyncio.sleep(). That's because you can cause an event loop error that's difficult to determine the root cause for in large async environments where many components are using the same event loop.
I actually think the problem is in util.py line 484
gpt analysis:
The text was updated successfully, but these errors were encountered: