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