diff --git a/README.md b/README.md index 9a0604a..4e80eda 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,27 @@ The same can be done without using decorators: bfx.wss.on("candles_update", callback=on_candles_update) ``` +### Heartbeat events + +The WebSocket server sends periodic heartbeat messages to keep connections alive. +These are now exposed as `heartbeat` events that users can listen to: + +```python +@bfx.wss.on("heartbeat") +def on_heartbeat(subscription=None): + if subscription: + # Heartbeat for a specific subscription (public channels) + print(f"Heartbeat for {subscription['channel']}") + else: + # Heartbeat for authenticated connection + print("Heartbeat on authenticated channel") +``` + +For public channel subscriptions, the heartbeat event includes the subscription information. +For authenticated connections, heartbeats are sent without subscription data. + +--- + # Advanced features ## Using custom notifications diff --git a/bfxapi/websocket/_client/bfx_websocket_bucket.py b/bfxapi/websocket/_client/bfx_websocket_bucket.py index fa6262f..83f85ca 100644 --- a/bfxapi/websocket/_client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/_client/bfx_websocket_bucket.py @@ -66,9 +66,11 @@ async def start(self) -> None: if ( (chan_id := cast(int, message[0])) and (subscription := self.__subscriptions.get(chan_id)) - and (message[1] != Connection._HEARTBEAT) ): - self.__handler.handle(subscription, message[1:]) + if message[1] == Connection._HEARTBEAT: + self.__event_emitter.emit("heartbeat", subscription) + else: + self.__handler.handle(subscription, message[1:]) def __on_subscribed(self, message: Dict[str, Any]) -> None: chan_id = cast(int, message["chan_id"]) diff --git a/bfxapi/websocket/_client/bfx_websocket_client.py b/bfxapi/websocket/_client/bfx_websocket_client.py index ffae0ad..3bca373 100644 --- a/bfxapi/websocket/_client/bfx_websocket_client.py +++ b/bfxapi/websocket/_client/bfx_websocket_client.py @@ -266,9 +266,11 @@ async def __connect(self) -> None: if ( isinstance(message, list) and message[0] == 0 - and message[1] != Connection._HEARTBEAT ): - self.__handler.handle(message[1], message[2]) + if message[1] == Connection._HEARTBEAT: + self.__event_emitter.emit("heartbeat") + else: + self.__handler.handle(message[1], message[2]) async def __new_bucket(self) -> BfxWebSocketBucket: bucket = BfxWebSocketBucket(self._host, self.__event_emitter) diff --git a/bfxapi/websocket/_event_emitter/bfx_event_emitter.py b/bfxapi/websocket/_event_emitter/bfx_event_emitter.py index 21bbfd6..97c1372 100644 --- a/bfxapi/websocket/_event_emitter/bfx_event_emitter.py +++ b/bfxapi/websocket/_event_emitter/bfx_event_emitter.py @@ -32,6 +32,7 @@ _COMMON = [ "disconnected", + "heartbeat", "t_ticker_update", "f_ticker_update", "t_trade_execution", diff --git a/examples/websocket/auth/heartbeat.py b/examples/websocket/auth/heartbeat.py new file mode 100644 index 0000000..5acb69c --- /dev/null +++ b/examples/websocket/auth/heartbeat.py @@ -0,0 +1,39 @@ +# python -c "import examples.websocket.auth.heartbeat" + +import os + +from bfxapi import Client + +bfx = Client( + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET"), +) + + +@bfx.wss.on("heartbeat") +def on_heartbeat(): + """ + Handle heartbeat events from the WebSocket server for authenticated connections. + + These heartbeats are sent on the authenticated channel (channel 0) and don't + have an associated subscription. + """ + print("Heartbeat received on authenticated connection") + + +@bfx.wss.on("authenticated") +async def on_authenticated(event): + print(f"Authentication successful: {event}") + print("Now listening for heartbeats on the authenticated connection...") + + +@bfx.wss.on("open") +async def on_open(): + print("WebSocket connection opened") + + +print("Starting authenticated WebSocket client...") +print("You should see heartbeat events after authentication.") +print("Press Ctrl+C to stop.") + +bfx.wss.run() diff --git a/examples/websocket/public/heartbeat.py b/examples/websocket/public/heartbeat.py new file mode 100644 index 0000000..12e779c --- /dev/null +++ b/examples/websocket/public/heartbeat.py @@ -0,0 +1,38 @@ +# python -c "import examples.websocket.public.heartbeat" + +from bfxapi import Client +from bfxapi.websocket.subscriptions import Subscription + +bfx = Client() + + +@bfx.wss.on("heartbeat") +def on_heartbeat(subscription: Subscription = None): + """ + Handle heartbeat events from the WebSocket server. + + For public channels, the subscription parameter contains the subscription info. + For authenticated connections, subscription will be None. + """ + if subscription: + print(f"Heartbeat received for subscription: {subscription['channel']} - {subscription.get('symbol', subscription.get('key', 'N/A'))}") + else: + print("Heartbeat received for authenticated connection") + + +@bfx.wss.on("open") +async def on_open(): + # Subscribe to a ticker to see heartbeats for subscriptions + await bfx.wss.subscribe("ticker", symbol="tBTCUSD") + + +@bfx.wss.on("subscribed") +def on_subscribed(subscription: Subscription): + print(f"Subscribed to {subscription['channel']} for {subscription.get('symbol', subscription.get('key', 'N/A'))}") + + +print("Starting WebSocket client - you should see heartbeat events...") +print("Heartbeats are sent by the server to keep the connection alive.") +print("Press Ctrl+C to stop.") + +bfx.wss.run()