Handling Websockets

Major, much-requested, changes have been made to our WebSockets for fine-grain control over specific event subscriptions:

  • PING-PONG heartbeats to stay connected (every 10 seconds the server sends a PING and the client should respond within 15 seconds).
  • We allow up to 20 client connections using the same account address (which is obtained through the JWT token provided for the subscription).
  • Auth token generation is different and has expiry → You must get a fresh token within 5 minutes (the current expiration time) by calling the refresh token endpoint or with this function in the SDK.
    • This token is used for authentication at the moment of subscription for Account Data streams in our WebSockets. If the connection is kept alive, then you won’t have to change it in order to keep receiving updates; however, if you get disconnected and have to establish a new connection, you need to pass in a new Auth token (provided the current one expired).

Account Data Streams

Listen to the Account Data streams for account, trade, position, and order updates.

  • Limited connections → 20 client-server connections per account (as explained above).
  • Auth required → Need to refresh the JWT token if the connection is lost.
  • Now you subscribe to individual streams (shown in the screenshots below) as opposed to rooms allowing you granular control of what you want to receive updates for.

Available Streams

FrequencyStream NameEvent
AccountReal TimeAccountUpdateAccountUpdate
TransactionsReal TimeAccountTransactionUpdateAccountTransactionUpdate
PositionsReal TimeAccountPositionUpdateAccountPositionUpdate
TradesReal TimeAccountTradeUpdateAccountTradeUpdate
OrdersReal TimeAccountOrderUpdateAccountOrderUpdate

Note that the AccountOrderUpdate is a special message, represented as an enum which can either be:

  • *ActiveOrderUpdate: This is used to track the lifecycle of an active order, basically any status other than cancelled.*
  • *OrderCancellationUpdate: This is used to alert a user of a cancelled or a rejected order. An order is considered as “cancelled” either if it has been successfully cancelled OR if it has been rejected by the engine, both of these scenarios will send an OrderCancellationUpdate socket message.

How to Subscribe

  1. Choose the streams you want to individually subscribe to:
async def listen_bluefin_account():
    async with BluefinProSdk(sui_wallet, network, debug=True) as client:
        async with await client.create_account_data_stream_listener(
            handler=handle_account_data_event
        ) as account_data_listener:
            await account_data_listener.subscribe(
                subscription=[
                    AccountDataStream.ACCOUNTORDERUPDATE,
                    AccountDataStream.ACCOUNTPOSITIONUPDATE,
                    AccountDataStream.ACCOUNTTRADEUPDATE,
                    AccountDataStream.ACCOUNTTRANSACTIONUPDATE,
                    AccountDataStream.ACCOUNTUPDATE
                ]
            )
            await asyncio.Event().wait()

  1. Handle the events:
# Handle account data stream events
async def handle_account_data_event(msg):
    if isinstance(msg, AccountUpdate):
        logger.info(f"AccountUpdate: {msg}")
    if isinstance(msg, AccountTransactionUpdate):
        logger.info(f"AccountTransactionUpdate: {msg}")
    if isinstance(msg, AccountTradeUpdate):
        logger.info(f"AccountTradeUpdate: {msg}")
    if isinstance(msg, AccountPositionUpdate):
        logger.info(f"AccountPositionUpdate: {msg}")
    if isinstance(msg, AccountOrderUpdate):
        if isinstance(msg.actual_instance, ActiveOrderUpdate):
            logger.info(f"ActiveOrderUpdate: {msg.actual_instance}")
        if isinstance(msg.actual_instance, OrderCancellationUpdate):
            logger.info(f"OrderCancellationUpdate: {msg.actual_instance}")
        order = Order(
            client_order_id="123456", 
            type=OrderType.LIMIT, 
            symbol="ETH-PERP",
            price_e9="500000000", 
            quantity_e9="100000000000", 
            side=OrderSide.LONG,
            leverage_e9="100000000", 
            is_isolated=False,
            expires_at_utc_millis=int(time.time() * 1000) + 60000
        )
        await create_order(order)

Market Data Streams

Listen to the Market Data streams for updates on the orderbook, recent trades, candlesticks, tickers, etc.

  • Unlimited connections.
  • No auth required.
  • Again, you can subscribe to each stream individually, allowing granular control of what you want to listen to.

Available Streams

FrequencyStream NameEvent
Diff Depth10ms, 200ms or 500ms depending on the streamDiff_Depth_10_ms, Diff_Depth_200_ms, Diff_Depth_500_msOrderbookDepthUpdate
Partial DepthReal TimePartial_Depth_5, Partial_Depth_10, Diff_Depth_20OrderbookPartialDepthUpdate
Oracle Price1sOracle_PriceOraclePriceUpdate
Mark Price1sMark_PriceMarkPriceUpdate
Market Price1sMarket_PriceMarketPriceUpdate
Recent Trades100msRecent_TradeRecentTradesUpdate
Tickers3sTicker , Ticker_AllTickerUpdate, TickerAllUpdates
Candlesticks200msCandlestick_{interval}_{type}CandlestickUpdate
  • Candlesticks
  • Intervals: 1m, 3m, 5m, … see OpenAPI spec for all enums
  • Type: Last, Oracle, Market
  • Example: Candlestick_1m_Last

How to Subscribe

  1. Choose the streams you want to individually subscribe to:
async def listen_bluefin_market(market_symbol: str):
    async with BluefinProSdk(sui_wallet, network, debug=True) as client:
        async with await client.create_market_data_stream_listener(
            handler=handle_market_data_event
        ) as market_data_stream:
            await market_data_stream.subscribe(
                subscription=[
                    MarketSubscriptionStreams.model_construct(
                        symbol=market_symbol,
                        streams=[
                            MarketDataStreamName.MARK_PRICE,
                            MarketDataStreamName.RECENT_TRADE,
                            MarketDataStreamName.ORACLE_PRICE,
                            MarketDataStreamName.TICKER,
                            MarketDataStreamName.TICKER_ALL,
                            MarketDataStreamName.DIFF_DEPTH_10_MS,
                            MarketDataStreamName.DIFF_DEPTH_200_MS,
                            MarketDataStreamName.DIFF_DEPTH_500_MS,
                            MarketDataStreamName.PARTIAL_DEPTH_5,
                            MarketDataStreamName.PARTIAL_DEPTH_5,
                            MarketDataStreamName.PARTIAL_DEPTH_10,
                            MarketDataStreamName.PARTIAL_DEPTH_20
                        ]
                    )
                ]
            )
            await asyncio.Event().wait()

  1. Handle the events:
# Handle market data stream events
async def handle_market_data_event(msg):
    if isinstance(msg, OraclePriceUpdate):
        logger.info(f"OraclePriceUpdate: {msg}")
    if isinstance(msg, MarkPriceUpdate):
        logger.info(f"MarkPriceUpdate: {msg}")
    if isinstance(msg, MarketPriceUpdate):
        logger.info(f"MarketPriceUpdate: {msg}")
    if isinstance(msg, CandlestickUpdate):
        logger.info(f"CandlestickUpdate: {msg}")
    if isinstance(msg, OrderbookDiffDepthUpdate):
        logger.info(f"OrderbookDiffDepthUpdate: {msg}")
    if isinstance(msg, OrderbookPartialDepthUpdate):
        logger.info(f"OrderbookPartialDepthUpdate: {msg}")
    if isinstance(msg, RecentTradesUpdates):
        logger.info(f"RecentTradesUpdates: {msg}")
    if isinstance(msg, TickerAllUpdate):
        logger.info(f"TickerAllUpdate: {msg}")
    if isinstance(msg, TickerUpdate):
        logger.info(f"TickerUpdate: {msg}")

How to manage a local orderbook:

  1. Start by openning a stream and listening to the DiffDepth socket (Diff_Depth_10_ms, Diff_Depth_200_ms, Diff_Depth_500_ms)
  2. Buffer the events you receive from the selected stream
  3. Get a depth snapshot from the /exchange/depth endpoint
  4. Drop any event where update.lastUpdateId is < depthResponse.lastUpdateId in the snapshot.
  5. The first processed event should have update.firestUpdateId <= depthResponse.lastUpdateId AND update.lastUpdateId >= depthResponse.lastUpdateId
  6. Note that you must ensure your apply the very first orderbookUpdate after the orderbookUpdateId received from the snapshot (if you fail to do so, you risk missing updates yielding an inconsistent state). This is why it's recommended to open the socket connection first in step 1 before fetching the snapshot.
  7. While listening to the stream, each new event's firstUpdateId should be equal to the previous event's lastUpdateId + 1
  8. Otherwise we must rest and initialize the process from step 3 again.

Additional Notes:

  1. OrderbookUpdateId will not be sequential since multiple updates are aggregated before being sent every n ms. It will however always be an increasing number.
  2. If the returned quantity is 0 at any level, you should remove that price level.
  3. Receiving an event that removes a price level that is not in your local order book can happen and is normal