How to stream Market Data using the Binance FIX API

Understand

In January 2025, Binance added the feature of streaming market data through their FIX API. This provides an alternative data source than the WebSocket / RESTful APIs.

The FIX Market Data API sends depth book data at the same interval as the websocket API (100ms), however it's actually possible to get a limited amount of data at every single update to the top of the order book.

Request Types

Individual Symbol Book Ticker Stream

This subscription type provides an update at every single change to the best bid/ask price in the order book. That mean's it can't be used to be sure about the fill price of large orders, but it's useful for other purposes such as analysis or visualisations. This subscription type is activated by setting the MarketDepth field to 1.

Diff. Depth Stream

This subscription type provides the order book at a specified depth, useful for backtesting, paper trading and other usecases that require checking the fill price or expected fill price at specified times. Updates are provided by Binance using `MarketDataIncrementalRefresh` messages, with the following fields particularly interesting:

 (268) NoMDEntries
 (269) MDEntryType
 (270) MDEntryPx
 (271) MDEntrySize
 (279) MDUpdateAction
These updates do not simply send the bid/ask price every time, instead the user must keep track of the prices and change them as the updates come in. The response message contains order book updates since the previous response. For instance, if MDUpdateAction is 0 (NEW) or 1 (DELETE), then you should update your locally managed order book by adding / deleting entries.

Specification

Defining a request follows the FIX protocol and the Binance API specified here. Binance explicitly supports QuickFix library. For example, the following code demonstrates a Python function to generate a Market Data request for BTCUSDT.

def compose_market_data_request():
   market_data_request = fix44.MarketDataRequest()

   market_data_request.setField(fix.MDReqID('BOOK_TICKER_STREAM'))
   market_data_request.setField(fix.SubscriptionRequestType(fix.SubscriptionRequestType_SNAPSHOT_PLUS_UPDATES))
   market_data_request.setField(fix.MarketDepth(1))
   market_data_request.setField(fix.NoMDEntryTypes(2))

   group = fix44.MarketDataRequest().NoMDEntryTypes()
   group.setField(fix.MDEntryType(fix.MDEntryType_BID))  # bid orders
   market_data_request.addGroup(group)
   group.setField(fix.MDEntryType(fix.MDEntryType_OFFER))  # ask orders
   market_data_request.addGroup(group)

   market_data_request.setField(fix.NoRelatedSym(1))

   symbol = fix44.MarketDataRequest().NoRelatedSym()
   symbol.setField(fix.StringField(55, 'BTCUSDT'))
   market_data_request.addGroup(symbol)

   return market_data_request
The message needs to be sent after successfully logging on to the FIX session.

QuickFix

QuickFix allows us to use the onLogon hook to create our Market Data request after the log on procedure completes. To read about how to log on to a FIX session, see here.

class BinanceFIXApplication(fix.Application):

  ...

  def onLogon(self, sessionID):
    print("Successfully logged in to FIX session.")
    market_data_request = compose_market_data_request()
    fix.Session.sendToTarget(market_data_request, sessionID)

  def onLogout(self, sessionID):
    print("Logged out from FIX session.")
The messages are then handled in the fromApp hook:

class BinanceFIXApplication(fix.Application):

  def onCreate(self, sessionID):
    self.skipped_first_message = False
    self.bids = []
    self.asks = []
    return

  ...

  def fromApp(self, response, sessionID):
    no_entries = response.groupCount(268)
    for i in range(1, no_entries+1):
      group1 = fix44.MarketDataIncrementalRefresh.NoMDEntries()
      response.getGroup(i, group1)

      # The first message does not have field 279, because it's an initial snapshot
      if self.skipped_first_message == False:
        update_type = "0"
      else:
        update_type = group1.getField(279)
      tick_type = group1.getField(269)

      price = float(group1.getField(270))
      if update_type == "0":
        if tick_type == "0":
          self.bids.append(price)
        elif tick_type == "1":
          self.asks.append(price)
      elif update_type == "2":
        if tick_type == "0":
          if price in self.bids:
            self.bids.remove(price)
        elif tick_type == "1":
          if price in self.asks:
            self.asks.remove(price)
      update_type = None
    self.skipped_first_message = True

    min_ask = min(self.asks)
    max_bid = max(self.bids)
    if max_bid >= 0 and min_ask >= 0:
        timestamp = str(datetime.strptime(response.getHeader().getField(52), "%Y%m%d-%H:%M:%S.%f").timestamp())
        print(f"{timestamp}: Bid: {max_bid} Ask: {min_ask} Spread: {min_ask - max_bid}")

Remember to change the QuickFix config to use spot-fix-md.xml, which can be found on Binance's API reference:

[SESSION]
...
DataDictionary=spot-fix-md.xml
<