feat: WebSocket client v2 (ws_v2)#151
Draft
Voyz wants to merge 73 commits into
Draft
Conversation
…hreading/lifecycle model, making (un)subscribing actions idempotent and introducing Pydantic models at input and output
… expecting only str and Enum
…and changed subscription_controller._bindings key from Subscription to new the binding_key. - added SubscriptionResolver which allow SubscriptionController to automatically detect binding_keys that need confirmation on (un)subscriptions - finished implementing ibkr_subscriptions
# Conflicts: # ibind/ibkr_ws_v2/ibkr_events.py
…marking as degraded across WsRuntime
…amed 'channel' to 'topic', implemented QueueSink as replacement of QueueController/Accessor chore(ws_v2): cleaned up ws_runtime and ws_transport
fix(ws_v2): fixed TransportEvent attempts refactor(ws_v2): renamed ClientInternalEvents to LifecycleEvents
This was referenced May 3, 2026
…xport `WsEvent`, and relocate IBKR topic-to-event resolution into the router for clearer ownership fix(ws_v2): harden runtime shutdown and transport state improve runtime stop/close flow to set closed state consistently, separate graceful vs unexpected disconnects, and mark transport degraded when thread shutdown fails reduce websocket lifecycle log noise by moving thread start/stop logs to debug while keeping key connection, auth, and send events visible
…cate privacy, expecting imports to point at ibind.events. Also added docstrings
…tions for consistency
…empotent and added CallbackSink.has_callback Also updated tests.
…added _preprocess_orders removing bgColor and fgColor1
…eue by event.received_at to ensure correct order of order emission/processing
…rrectly shared between instances Also updated tests.
…ondition Use a condition to coordinate WebSocketApp replacement during reset. Wait for close/recreate transitions under the same lock and notify when `_wsa` is set or cleared in the transport loop. This prevents race conditions where reset could abandon or overwrite a newly recreated socket instance while the transport thread is running. Expand unit coverage for condition-based waiting, timeout paths, and threaded reset behaviour.
When subscribe/unsubscribe is called again for a binding in FAILED state, treat it as an explicit retry request by resetting attempts and last_attempt and moving status back to NEW. Also raise default subscription_retries and subscription_timeout to give more time for transient websocket failures, and add unit tests covering failed-binding reset behaviour for both subscribe and unsubscribe paths.
Update unit tests for new runtime worker flow.
Make `AUTHENTICATED` the only readiness state in `WsRuntime` and consistently gate start/send/subscription reconciliation on it. When auth is lost, transition back to `OPEN` and invalidate subscriptions instead of degrading the connection state. Update unit tests to reflect the new authentication lifecycle and state transitions.
… ws_v2.runtime. Moved internal_sink from ibkr_ws_client_v2 to ws_runtime. Added WsStarting, WsStopping and WsStopped events. Also added unit tests for all these new modules.
…s and ibkr_ws_client_v2
…nal callbacks on `__init__`
…duce valid logs that can be captured
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full refactor of the WebSocket handling in IBind.
Work is still in progress, no changes are final.
Available as
0.2.1rc16.New documentation can be found in: https://github.com/Voyz/ibind/tree/feat/ws_v2/docs/websocket
Overview:
The
IbkrWsClientV1 implementation - existing until now - had several unfavourable angles:Although usable, it required close familiarity with the documentation, presented slightly awkward usage, and its threading model lead to complex logic, difficult testability and race conditions.
The
IbkrWsClientV2 at its core introduces better threading and lifecycle management. It is implemented using three threads:As a result, most connectivity and authentication issues are handled gracefully without need of restarting threads or recreating the WebSocketApp instance.
This refactor was also taken as an opportunity to re-evaluate the API, resulting in a number of improvements to the subscription API, event consumption and connection health. Some responsibilities previously expected of the user are now handled by the client out of the box.
BREAKING CHANGE:
channelanddatain favour of new PydanticSubscriptionclasses which encapsulate necessary parameters for each topic, reducing ambiguity and necessity to depend on IBKR docs (as discussed inIbkrWsKeyshould haveconidfor market data subscriptions #18)EventSinkprotocol instead of depending on theQueueAccessorAPI. Currently implemented areCallbackSink(propagating events into user-specified callbacks),QueueSink(supporting the QueueAccessor API),CompositeSink(allowing to mix multiple sinks together) andLogSink(logging all events, useful for debugging). Custom sinks that implementEventSinkprotocal are accepted.WsEventclass events, replacing propagatingdictobjectsBindingobjects, used for managing the lifecycle of each subscription. As a result,IbkrWsClient.subscribe()andIbkrWsClient.unsubscribe()are no longer blocking and instead they return aSubscriptionHandlethat can be waited on using.wait()SubscriptionProcessorclass - insteadSubscriptionobjects define the subscribe and unsubscribe payload generationrestart_on_closeandrestart_on_criticalparameters - both are now fixed as true: the WebSocketApp will always reconnect on close, and the transport thread will always recreate a new WebSocketApp on critical.connected,readyandrunningproperties - now replaced byis_running()andget_state()IbkrWsKey- identifying subscriptions and events now uses concreteWsEventclass types instead of the enumQueueAccessor- currently still available throughQueueSink.new_queue_accessor()for backwards compatibility. InsteadQueueSinkserves as a 1-1 replacement forQueueAccessor, passing appropriate queue-identifying key to itsget()andempty()methods.Feature:
expiry_secondsallowing to periodically resubscribe (fixes Feature request: auto-refresh IBKR WS smd subscriptions on 15-min server-side expiry #145)LIFECYCLEkey, facilitating responding to lifecycle changes in user applicationsSubscriptionProcessor(as demonstrated in https://github.com/Voyz/ibind/blob/master/examples/ws_03_market_history.py)Refactor:
ibind.ibkr_ws_clientlogger.Chore:
channelwas replaced bytopicto align with IBKR nomenclature (as discussed inIbkrWsKeyshould haveconidfor market data subscriptions #18)Behind The Scenes
QueueControllerSubscriptionControllerIbkrWsClient(still present inIbkrClient)Implementation Details
Event Lifecycle
Most events received by the WebSocket go through the following flow:
WebSocketAppTransportEventand enqueued in runtime transport queueTransportEventTransportEventIbkrRouterintoWsEventinstances (multiple instances can be generated from a single message)WsEventAsyncSinkby default, or directly to user sink if disabled)WsEventTwo event classes
TransportEventandWsEvent- rather than just a single commonEvent- are used to create clear separation of concerns.TransportEventinstances are used only forWebSocket -> Clientinternal communication, whileWsEventinstances are used for both inner-client andClient -> User Applicationcommunication.Event Propagation
Events are now propagated using a dedicated thread. Once received by the client, they're parsed and enqueued into an outgoing queue. The new thread then consumes and propagates the events through sinks.
This ensures that slow user application code does not affect the functionality of the client.
This method can be disabled using a parameter, causing event propagation to be carried out by the runtime thread, which may be useful for debugging.
Example Usage
In V1:
In V2:
Sink types:
Roadmap:
I'm very much open to feedback - please feel free to share it here if it is related to the refactor, or as a new issue if you'd like to suggest other changes/enhancements.
You can install and test this version out as
0.2.1rc16.