FIX API Trading Software: The Complete Trader's Guide
A practical guide to FIX API trading software — covering protocol basics, how it compares to REST, real Python code examples, and which exchanges support it.
A practical guide to FIX API trading software — covering protocol basics, how it compares to REST, real Python code examples, and which exchanges support it.
If you've spent time building trading bots with REST APIs, you already know the friction — HTTP overhead, a fresh connection on every request, and latency that compounds when you're firing dozens of orders per second. FIX API trading software solves that directly. The Financial Information eXchange protocol runs over persistent TCP connections, eliminates the HTTP layer entirely, and delivers the kind of execution speed that institutional desks have relied on since the 1990s. Major crypto exchanges including Binance, Bybit, and OKX now offer FIX connectivity, putting institutional-grade infrastructure within reach of serious independent traders and quant teams.
FIX (Financial Information eXchange) is a standardized messaging protocol designed specifically for transmitting financial orders between trading systems. It was created in 1992 for equity markets and became the global backbone of institutional order flow. In crypto, it arrived late but arrived properly — Binance launched FIX API support for spot trading in 2024, with Bybit and OKX rolling out their own implementations for derivatives and institutional clients.
The simplest answer to what is FIX API: a persistent, binary-efficient TCP connection for order management. Unlike a REST call that opens a connection, sends a request, waits for a response, and closes — a FIX session stays open. You authenticate once, and orders flow across that same connection at near-wire speed. For high-frequency strategies, market making, or any execution where entry price determines whether a trade is profitable, this distinction is the entire game.
FIX API is not the starting point for a first trading bot. It requires managing TCP sockets, session-level heartbeats, and message sequencing manually. If you haven't built with REST API yet, start there. Return to FIX when latency is demonstrably your bottleneck.
A FIX session is a two-way TCP connection between your software (the initiator) and the exchange (the acceptor). After establishing the connection, you send a Logon message (tag 35=A) with your credentials. From that point, every order, cancellation, and status request travels as a FIX message — a structured sequence of tag=value pairs separated by the SOH byte (ASCII 01). The messages are compact by design, which is a large part of why they're faster to parse and transmit than JSON over HTTP.
The core message types you'll work with daily are: NewOrderSingle (D) for placing orders, OrderCancelRequest (F) for cancellations, OrderCancelReplaceRequest (G) for amendments, and ExecutionReport (8) which is the exchange's reply to everything. The exchange also sends Heartbeat messages (tag 35=0) at regular intervals — your client must echo these back or the session terminates after a configurable timeout. Session management is the part that trips up most developers new to FIX.
| Message Type | Tag 35 Value | Direction | Purpose |
|---|---|---|---|
| Logon | A | Client → Exchange | Authenticate and open session |
| NewOrderSingle | D | Client → Exchange | Submit a new order |
| OrderCancelRequest | F | Client → Exchange | Cancel an open order |
| OrderCancelReplaceRequest | G | Client → Exchange | Amend price or quantity |
| ExecutionReport | 8 | Exchange → Client | Order ack, fill, or rejection |
| Heartbeat | 0 | Both directions | Keep session alive |
| Logout | 5 | Both directions | Terminate session cleanly |
Most crypto FIX API trading platform implementations use FIX 4.2 or FIX 4.4. Binance's spot order entry gateway runs FIX 4.2. Your client library must match the version the exchange expects — a version mismatch causes logon rejection without a clear error message, which has wasted many engineers' afternoons.
Before writing any code, you need API credentials with FIX access specifically enabled. On Binance, go to API Management and enable FIX API as a separate permission — it's not on by default. Bybit handles it through their API dashboard for institutional accounts. OKX gates FIX behind a volume threshold and requires contacting their institutional team.
The standard Python library for FIX protocol is quickfix, a Python binding to the QuickFIX C++ engine. Here's how to configure a session application and handle authentication with Binance's FIX gateway:
import quickfix as fix
import quickfix42 as fix42
class FIXApplication(fix.Application):
def __init__(self, api_key: str, api_secret: str):
super().__init__()
self.api_key = api_key
self.api_secret = api_secret
self.session_id = None
def onCreate(self, sessionID):
print(f"[FIX] Session created: {sessionID}")
def onLogon(self, sessionID):
self.session_id = sessionID
print(f"[FIX] Logged in: {sessionID}")
def onLogout(self, sessionID):
self.session_id = None
print(f"[FIX] Logged out: {sessionID}")
def toAdmin(self, message, sessionID):
# Inject API credentials into every Logon message
msg_type = fix.MsgType()
message.getHeader().getField(msg_type)
if msg_type.getValue() == fix.MsgType_Logon:
message.setField(fix.Username(self.api_key))
message.setField(fix.Password(self.api_secret))
# Binance requires this field on logon
message.setField(fix.ResetSeqNumFlag(True))
def fromApp(self, message, sessionID):
self.handle_message(message)
def toApp(self, message, sessionID):
pass
def fromAdmin(self, message, sessionID):
pass
def handle_message(self, message):
msg_type = fix.MsgType()
message.getHeader().getField(msg_type)
if msg_type.getValue() == '8': # ExecutionReport
self.on_execution_report(message)
def on_execution_report(self, message):
order_id = fix.OrderID()
exec_type = fix.ExecType()
try:
message.getField(order_id)
message.getField(exec_type)
status_map = {'0': 'NEW', '1': 'PARTIAL_FILL', '2': 'FILL',
'4': 'CANCELLED', '8': 'REJECTED'}
status = status_map.get(exec_type.getValue(), 'UNKNOWN')
print(f"[ORDER] {order_id.getValue()} -> {status}")
except fix.FieldNotFound as e:
print(f"[ERROR] Missing field in ExecutionReport: {e}")
# Start the FIX engine
settings = fix.SessionSettings("binance_fix.cfg")
app = FIXApplication(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
store_factory = fix.FileStoreFactory(settings)
log_factory = fix.FileLogFactory(settings)
initiator = fix.SocketInitiator(app, store_factory, settings, log_factory)
initiator.start()
The config file (binance_fix.cfg) specifies the connection target. For Binance production spot FIX, the endpoint is fix-oe.binance.com on port 9000. Always validate your setup against the testnet first — Binance provides fix-oe.testnet.binance.com. Bybit's FIX testnet runs at stream-testnet.bybit.com. Getting logon working in testnet before touching production is not optional.
With a live session established, placing an order means constructing a NewOrderSingle message with the required tags and calling sendToTarget. Here's a complete FIX API example for submitting a limit order, including proper timestamp formatting which Binance's gateway requires:
import quickfix as fix
import quickfix42 as fix42
from datetime import datetime, timezone
import uuid
def place_limit_order(
app: FIXApplication,
symbol: str,
side: str, # '1' = Buy, '2' = Sell
quantity: float,
price: float,
time_in_force: str = fix.TimeInForce_GOOD_TILL_CANCEL
) -> str:
"""Submit a limit order. Returns the client order ID."""
if not app.session_id:
raise RuntimeError("FIX session not active — check logon status")
client_order_id = str(uuid.uuid4())[:20] # Binance max 36 chars
order = fix42.NewOrderSingle()
order.setField(fix.ClOrdID(client_order_id)) # Tag 11
order.setField(fix.Symbol(symbol)) # Tag 55
order.setField(fix.Side(side)) # Tag 54
order.setField(fix.OrdType(fix.OrdType_LIMIT)) # Tag 40: '2'
order.setField(fix.Price(price)) # Tag 44
order.setField(fix.OrderQty(quantity)) # Tag 38
order.setField(fix.TimeInForce(time_in_force)) # Tag 59
# TransactTime required by Binance FIX (UTC, millisecond precision)
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
transact_time = fix.TransactTime()
transact_time.setString(now)
order.setField(transact_time)
fix.Session.sendToTarget(order, app.session_id)
print(f"[SENT] {side} {quantity} {symbol} @ {price} | clOrdID={client_order_id}")
return client_order_id
def cancel_order(app: FIXApplication, symbol: str, orig_client_order_id: str) -> None:
"""Cancel an open order by its original client order ID."""
cancel = fix42.OrderCancelRequest()
cancel.setField(fix.ClOrdID(str(uuid.uuid4())[:20])) # New cancel request ID
cancel.setField(fix.OrigClOrdID(orig_client_order_id)) # Tag 41
cancel.setField(fix.Symbol(symbol)) # Tag 55
cancel.setField(fix.Side('1')) # Must match original
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
transact_time = fix.TransactTime()
transact_time.setString(now)
cancel.setField(transact_time)
fix.Session.sendToTarget(cancel, app.session_id)
# Buy 0.001 BTC at $62,000 on Binance
order_id = place_limit_order(app, 'BTCUSDT', '1', 0.001, 62000.0)
The execution report you receive back has an ExecType field (tag 150) that tells you exactly what happened. Values '0' through '2' are the normal happy path: pending new, partial fill, complete fill. '4' is cancelled, '8' is rejected. Rejected orders always come with a Text field (tag 58) explaining why — insufficient margin, invalid price increment, or rate limit exceeded. Building proper rejection handling early saves significant debugging time later.
def parse_execution_report(message: fix.Message) -> dict:
"""Extract key fields from an ExecutionReport (35=8)."""
result = {}
field_map = [
(fix.OrderID(), 'exchange_order_id'), # Tag 37
(fix.ClOrdID(), 'client_order_id'), # Tag 11
(fix.ExecType(), 'exec_type'), # Tag 150
(fix.OrdStatus(), 'ord_status'), # Tag 39
(fix.Symbol(), 'symbol'), # Tag 55
(fix.Side(), 'side'), # Tag 54
(fix.Price(), 'price'), # Tag 44
(fix.LastQty(), 'last_fill_qty'), # Tag 32
(fix.LastPx(), 'last_fill_price'), # Tag 31
(fix.CumQty(), 'total_filled'), # Tag 14
(fix.LeavesQty(), 'remaining_qty'), # Tag 151
(fix.Text(), 'reject_reason'), # Tag 58 (present on rejections)
]
for field_obj, key in field_map:
try:
message.getField(field_obj)
result[key] = field_obj.getValue()
except fix.FieldNotFound:
pass # Not all fields present on every report type
# Human-readable status
status_map = {
'0': 'PENDING_NEW', '1': 'PARTIAL_FILL', '2': 'FILLED',
'3': 'DONE_FOR_DAY', '4': 'CANCELLED', '8': 'REJECTED'
}
result['status'] = status_map.get(result.get('exec_type', ''), 'UNKNOWN')
if result.get('status') == 'REJECTED':
print(f"[REJECTED] Reason: {result.get('reject_reason', 'unknown')}")
elif result.get('status') in ('PARTIAL_FILL', 'FILLED'):
print(f"[FILL] {result.get('symbol')} qty={result.get('last_fill_qty')} "
f"@ {result.get('last_fill_price')}")
return result
Not all exchanges offering FIX are equal in accessibility, feature completeness, or rate limits. Understanding where each venue sits helps you pick the right one for your strategy before investing in integration work.
| Exchange | FIX Version | Market | Access Level | Testnet |
|---|---|---|---|---|
| Binance | FIX 4.2 | Spot | All verified users | Yes |
| Bybit | FIX 4.4 | Derivatives/Spot | Institutional / VIP | Yes |
| OKX | FIX 4.4 | Spot, Futures, Options | Institutional (apply) | Yes |
| Bitget | FIX 4.2 | Futures | API users | Limited |
| Gate.io | FIX 4.2 | Spot/Futures | Institutional | No |
| KuCoin | Custom | Spot/Futures | VIP tier | No |
Binance is the most practical starting point. They opened FIX access to all verified users — no institutional application, no minimum volume, just enable it in API settings. That's a meaningful departure from the traditional model. Bybit is the preferred choice for perpetuals and basis trading; their FIX implementation is clean and their documentation for derivatives is thorough. OKX is where institutional quant firms go when they need simultaneous access to spot, linear futures, and options with a single session connection. For most independent algo traders, Binance or Bybit is the right entry point.
If you're using a real-time signal platform like VoiceOfChain to generate trade signals, routing execution through FIX instead of REST can meaningfully reduce the gap between signal time and fill price — particularly on momentum signals where every second of delay eats into expected return.
When evaluating any FIX API trading platform for production use, check these specifics: order rate limits per session (Binance defaults to 10 orders/second per connection, with higher limits available), supported order types (IOC and FOK availability matters for execution strategies), drop copy session support for real-time fill reporting to external risk systems, and sequence number recovery behavior after a disconnect. Co-locating servers in the same AWS region as the exchange matching engine — Tokyo for Binance, Singapore for Bybit — delivers the biggest single latency improvement after the protocol switch itself.
FIX API trading software isn't the right tool for every trader, but for anyone building latency-sensitive execution systems, market making bots, or professional-grade infrastructure, it's the standard worth knowing. The protocol is mature, well-documented, and now genuinely accessible — Binance opened it to all verified users, Bybit supports it for derivatives, and OKX covers the full product suite for institutional needs. Start with the quickfix Python library, test everything against the exchange testnet, and build your session management carefully before touching production. Once you can reliably logon, place orders, handle fills, and cancel cleanly, you have the execution foundation for almost any institutional-grade strategy. Pair it with high-quality real-time signals from a platform like VoiceOfChain, and you have both the intelligence layer and the execution layer working together at professional speed.