FIX Protocol Integration
FIX (Financial Information eXchange) is a standard protocol for financial data exchange used since 1992. Most institutional traders, prime brokers, and HFT firms require FIX API to connect to an exchange. Without FIX, the exchange is closed for professional market participants.
What is FIX and Why It's Needed
FIX is a text protocol over TCP. A message looks like a set of tag=value fields separated by SOH character (0x01):
8=FIX.4.4 | 9=178 | 35=D | 49=CLIENT1 | 56=EXCHANGE | 34=123 | 52=20240115-14:30:00.000 |
11=ORDER-001 | 55=BTC/USD | 54=1 | 38=0.1 | 40=2 | 44=42000 | 59=1 | 10=087 |
Key tags:
-
35=D— message type (D = New Order Single) -
55— instrument symbol -
54— side (1=Buy, 2=Sell) -
38— quantity -
40— order type (1=Market, 2=Limit, 3=Stop) -
44— price (for limit) -
59— Time in Force (0=DAY, 1=GTC, 3=IOC, 4=FOK)
FIX is preferred for institutionals for several reasons:
- Standard protocol — their systems already know FIX
- Low latency: more efficient than HTTP/JSON
- Reliable session model: automatic recovery after disconnects, message sequencing
FIX Server Architecture
QuickFIX/Go — Primary Choice
QuickFIX — reference implementation of FIX engine, ports for Go, Java, C++, Python. Go version (quickfixgo) is a good foundation for production exchange.
import (
"github.com/quickfixgo/quickfix"
"github.com/quickfixgo/quickfix/field"
"github.com/quickfixgo/quickfix/fix44"
"github.com/quickfixgo/quickfix/fix44/newordersingle"
)
type FIXApplication struct {
orderEngine *OrderEngine
sessionManager *SessionManager
}
// OnCreate — called when FIX session is created
func (app *FIXApplication) OnCreate(sessionID quickfix.SessionID) {
log.Info("FIX session created", "sessionID", sessionID)
}
// OnLogon — called on successful logon
func (app *FIXApplication) OnLogon(sessionID quickfix.SessionID) {
log.Info("FIX client logged on", "sessionID", sessionID)
app.sessionManager.SetOnline(sessionID)
}
// OnLogout — called on disconnect
func (app *FIXApplication) OnLogout(sessionID quickfix.SessionID) {
log.Info("FIX client logged out", "sessionID", sessionID)
app.sessionManager.SetOffline(sessionID)
}
// FromApp — handle incoming messages from client
func (app *FIXApplication) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
msgType, err := msg.Header.GetString(field.NewMsgType())
if err != nil {
return err
}
switch msgType {
case "D": // New Order Single
return app.handleNewOrder(msg, sessionID)
case "F": // Order Cancel Request
return app.handleCancelOrder(msg, sessionID)
case "G": // Order Cancel/Replace Request (amend)
return app.handleAmendOrder(msg, sessionID)
case "H": // Order Status Request
return app.handleStatusRequest(msg, sessionID)
}
return quickfix.NewMessageRejectError("Unknown message type", 35, nil)
}
Handling New Order Single (D)
func (app *FIXApplication) handleNewOrder(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
nos := newordersingle.New(
field.NewClOrdID(""),
field.NewSide(0),
field.NewTransactTime(time.Now()),
field.NewOrdType(0),
)
if err := quickfix.Unmarshal(msg, &nos); err != nil {
return err
}
// Parse fields
clOrdID, _ := nos.GetClOrdID()
symbol, _ := nos.GetSymbol()
sideInt, _ := nos.GetSide()
ordType, _ := nos.GetOrdType()
qty, _ := nos.GetOrderQty()
price, _ := nos.GetPrice()
tif, _ := nos.GetTimeInForce()
// Convert FIX types to internal
order := Order{
ClientOrderID: string(clOrdID),
Pair: normalizePair(string(symbol)),
Side: fixSideToInternal(sideInt),
Type: fixOrdTypeToInternal(ordType),
Quantity: decimal.NewFromFloat(float64(qty)),
Price: decimal.NewFromFloat(float64(price)),
TimeInForce: fixTIFToInternal(tif),
}
// Send Execution Report — Pending New
app.sendExecReport(sessionID, order, ExecTypeNew, OrdStatusPendingNew)
// Place order in matching engine
trades, err := app.orderEngine.PlaceOrder(order)
if err != nil {
app.sendExecReport(sessionID, order, ExecTypeRejected, OrdStatusRejected)
return nil
}
// Send Execution Reports for each fill
for _, trade := range trades {
app.sendFillReport(sessionID, order, trade)
}
// If remainder — New or PartiallyFilled
if order.RemainingQty().IsPositive() {
status := OrdStatusNew
if len(trades) > 0 {
status = OrdStatusPartiallyFilled
}
app.sendExecReport(sessionID, order, ExecTypeNew, status)
}
return nil
}
Execution Report (8) — Order Response
func (app *FIXApplication) sendExecReport(sessionID quickfix.SessionID, order Order,
execType ExecType, ordStatus OrdStatus) {
report := fix44executionreport.New(
field.NewOrderID(order.ID),
field.NewExecID(generateExecID()),
field.NewExecType(fix44.ExecType(execType)),
field.NewOrdStatus(fix44.OrdStatus(ordStatus)),
field.NewSymbol(denormalizePair(order.Pair)),
field.NewSide(fix44.Side(internalSideToFIX(order.Side))),
field.NewLeavesQty(order.RemainingQty().InexactFloat64(), 8),
field.NewCumQty(order.FilledQty.InexactFloat64(), 8),
field.NewAvgPx(order.AvgPrice().InexactFloat64(), 8),
)
report.SetClOrdID(order.ClientOrderID)
report.SetOrderQty(order.Quantity.InexactFloat64(), 8)
report.SetTransactTime(time.Now())
quickfix.SendToTarget(report.ToMessage(), sessionID)
}
FIX Session Model
FIX session maintains message sequencing. Each message has MsgSeqNum (34). On disconnect and reconnect:
- Client restores session with last known SeqNum
- Server can send ResendRequest (2) to request missed messages
- Or send SequenceReset (4) if history storage not supported
Configuration of QuickFIX session:
func createFIXSettings() *quickfix.Settings {
settings := quickfix.NewSettings()
globalSection := quickfix.NewSessionSettings()
globalSection.Set("FileStorePath", "./fix-sessions") // session storage for resend
globalSection.Set("FileLogPath", "./fix-logs")
settings.GlobalSettings().SetGlobalSection(globalSection)
sessionSection := quickfix.NewSessionSettings()
sessionSection.Set(quickfix.BeginString, "FIX.4.4")
sessionSection.Set(quickfix.SenderCompID, "EXCHANGE")
sessionSection.Set(quickfix.TargetCompID, "CLIENT1")
sessionSection.Set("HeartBtInt", "30") // heartbeat every 30 sec
sessionSection.Set("ReconnectInterval", "5") // reconnect after 5 sec
sessionSection.Set("StartTime", "00:00:00")
sessionSection.Set("EndTime", "00:00:00") // no daily reset for crypto
return settings
}
Authentication and Security
FIX 4.4 has no built-in authentication. Standard approaches:
- IP whitelist: only allowed IPs can connect to FIX port
- TLS: connection encryption (FIX over SSL)
- Logon (35=A) with password: field 96 (RawData) or custom tag for API key + signature
func (app *FIXApplication) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
msgType, _ := msg.Header.GetString(field.NewMsgType())
if msgType == "A" { // Logon
// Verify custom tag with API signature
apiKey, _ := msg.Body.GetString(9001) // custom tag
signature, _ := msg.Body.GetString(9002) // custom tag
timestamp, _ := msg.Body.GetString(9003) // custom tag
if !app.auth.Verify(apiKey, signature, timestamp) {
return quickfix.NewMessageRejectError("Authentication failed", 58, nil)
}
app.sessionManager.SetAPIKey(sessionID, apiKey)
}
return nil
}
Market Data via FIX
FIX 4.4 supports market data subscriptions:
-
35=V— Market Data Request (subscribe to ticker, orderbook) -
35=W— Market Data Snapshot/Full Refresh -
35=X— Market Data Incremental Refresh
FIX Versions:
| Version | Application |
|---|---|
| FIX 4.2 | Legacy systems, widely supported |
| FIX 4.4 | Modern standard for most exchanges |
| FIXT 1.1 + FIX 5.0 | Separation of transport and application, niche cases |
| FIX/FAST | Binary compression for market data, ultra-low latency |
For crypto exchange: FIX 4.4 is optimal. Most institutional clients support it, documentation is extensive.
Testing
FIX integration tested with FIX clients:
- QuickFIX Executor: basic FIX client for testing
- ATDL: Algorithmic Trading Definition Language specification
- Load testing: 1000+ orders/sec via FIX
Development Timeline
- Basic FIX 4.4 server (New Order, Cancel, Execution Reports): 4–6 weeks
- Market Data feed via FIX: 2–3 weeks
- TLS + authentication + IP whitelist: 1–2 weeks
- Drop Copy: 1–2 weeks
- Testing and documentation: 2–3 weeks
- Full production-ready FIX integration: 2–3 months







