Интеграция FIX-протокола
FIX (Financial Information eXchange) — стандартный протокол обмена финансовыми данными, используемый с 1992 года. Большинство институциональных трейдеров, prime brokers и HFT-фирм требуют FIX API для подключения к бирже. Без FIX биржа закрыта для профессиональных участников рынка.
Что такое FIX и зачем он нужен
FIX — это текстовый протокол над TCP. Сообщение выглядит как набор тег=значение полей, разделённых символом SOH (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 |
Ключевые теги:
-
35=D— тип сообщения (D = New Order Single) -
55— символ инструмента -
54— сторона (1=Buy, 2=Sell) -
38— количество -
40— тип ордера (1=Market, 2=Limit, 3=Stop) -
44— цена (для limit) -
59— Time in Force (0=DAY, 1=GTC, 3=IOC, 4=FOK)
FIX предпочтителен для институционалов по нескольким причинам:
- Стандартный протокол — их системы уже умеют работать с FIX
- Низкая latency: бинарно эффективнее чем HTTP/JSON
- Надёжная сессионная модель: автоматическое восстановление после разрывов, message sequencing
Архитектура FIX сервера
QuickFIX/Go — основной выбор
QuickFIX — reference implementation FIX engine, порты для Go, Java, C++, Python. Go версия (quickfixgo) — хорошая база для production биржи.
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 — вызывается при создании FIX сессии
func (app *FIXApplication) OnCreate(sessionID quickfix.SessionID) {
log.Info("FIX session created", "sessionID", sessionID)
}
// OnLogon — вызывается при успешном логоне
func (app *FIXApplication) OnLogon(sessionID quickfix.SessionID) {
log.Info("FIX client logged on", "sessionID", sessionID)
app.sessionManager.SetOnline(sessionID)
}
// OnLogout — вызывается при разрыве соединения
func (app *FIXApplication) OnLogout(sessionID quickfix.SessionID) {
log.Info("FIX client logged out", "sessionID", sessionID)
app.sessionManager.SetOffline(sessionID)
}
// FromApp — обработка входящих сообщений от клиента
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)
}
Обработка 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
}
// Парсим поля
clOrdID, _ := nos.GetClOrdID()
symbol, _ := nos.GetSymbol()
sideInt, _ := nos.GetSide()
ordType, _ := nos.GetOrdType()
qty, _ := nos.GetOrderQty()
price, _ := nos.GetPrice()
tif, _ := nos.GetTimeInForce()
// Конвертируем FIX типы в внутренние
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),
}
// Отправляем Execution Report — Pending New
app.sendExecReport(sessionID, order, ExecTypeNew, OrdStatusPendingNew)
// Размещаем ордер в matching engine
trades, err := app.orderEngine.PlaceOrder(order)
if err != nil {
app.sendExecReport(sessionID, order, ExecTypeRejected, OrdStatusRejected)
return nil
}
// Отправляем Execution Reports по каждому fill
for _, trade := range trades {
app.sendFillReport(sessionID, order, trade)
}
// Если есть остаток — New или PartiallyFilled
if order.RemainingQty().IsPositive() {
status := OrdStatusNew
if len(trades) > 0 {
status = OrdStatusPartiallyFilled
}
app.sendExecReport(sessionID, order, ExecTypeNew, status)
}
return nil
}
Execution Report (8) — ответ на ордер
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
FIX сессия поддерживает sequencing сообщений. Каждое сообщение имеет MsgSeqNum (34). При разрыве соединения и переподключении:
- Клиент восстанавливает сессию с последним известным SeqNum
- Сервер может отправить ResendRequest (2) для запроса пропущенных сообщений
- Или отправить SequenceReset (4) если хранение истории не поддерживается
// Конфигурация QuickFIX сессии
func createFIXSettings() *quickfix.Settings {
settings := quickfix.NewSettings()
globalSection := quickfix.NewSessionSettings()
globalSection.Set("FileStorePath", "./fix-sessions") // хранение сессий для 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 каждые 30 сек
sessionSection.Set("ReconnectInterval", "5") // переподключение через 5 сек
sessionSection.Set("StartTime", "00:00:00")
sessionSection.Set("EndTime", "00:00:00") // нет дневного сброса для крипто
return settings
}
Аутентификация и безопасность
FIX 4.4 не имеет встроенной аутентификации. Стандартные подходы:
- IP whitelist: только разрешённые IP могут подключиться к FIX порту
- TLS: шифрование соединения (FIX over SSL)
- Logon (35=A) с password: поле 96 (RawData) или кастомный тег для API key + подписи
func (app *FIXApplication) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
msgType, _ := msg.Header.GetString(field.NewMsgType())
if msgType == "A" { // Logon
// Верифицируем кастомный тег с API подписью
apiKey, _ := msg.Body.GetString(9001) // кастомный тег
signature, _ := msg.Body.GetString(9002) // кастомный тег
timestamp, _ := msg.Body.GetString(9003) // кастомный тег
if !app.auth.Verify(apiKey, signature, timestamp) {
return quickfix.NewMessageRejectError("Authentication failed", 58, nil)
}
app.sessionManager.SetAPIKey(sessionID, apiKey)
}
return nil
}
Market Data через FIX
FIX 4.4 поддерживает market data подписки:
-
35=V— Market Data Request (подписка на ticker, orderbook) -
35=W— Market Data Snapshot/Full Refresh -
35=X— Market Data Incremental Refresh
func (app *FIXApplication) handleMDRequest(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError {
mdReqID, _ := msg.Body.GetString(field.NewMDReqID())
subscriptionType, _ := msg.Body.GetInt(field.NewSubscriptionRequestType())
symbols := extractSymbols(msg)
switch subscriptionType {
case 1: // Subscribe
app.subscribeMD(sessionID, mdReqID, symbols)
case 2: // Unsubscribe
app.unsubscribeMD(sessionID, mdReqID)
}
return nil
}
Drop Copy
Drop Copy — копирование всех исполнений на дополнительную FIX сессию (например, для compliance или risk monitoring системы клиента):
// При каждом fill — отправляем Drop Copy подписчикам
func (app *FIXApplication) onFill(trade Trade) {
primarySession := app.getSessionByUser(trade.TakerUserID)
app.sendFillReport(primarySession, trade)
// Drop copy для risk/compliance систем клиента
if dropCopySession := app.getDropCopySession(trade.TakerUserID); dropCopySession != nil {
app.sendFillReport(dropCopySession, trade)
}
}
FIX версии
| Версия | Применение |
|---|---|
| FIX 4.2 | Legacy системы, широко поддерживается |
| FIX 4.4 | Современный стандарт для большинства бирж |
| FIXT 1.1 + FIX 5.0 | Разделение транспорта и приложения, нишевые случаи |
| FIX/FAST | Бинарная компрессия для market data, ultra-low latency |
Для криптобиржи: FIX 4.4 оптимален. Большинство institutional клиентов его поддерживают, документация обширная.
Тестирование
FIX интеграция тестируется с FIX клиентами:
- QuickFIX Executor: базовый FIX client для тестирования
- ATDL (Algorithmic Trading Definition Language): спецификация алготрейдинговых параметров
- Нагрузочное тестирование: 1000+ ордеров/сек через FIX
Сроки разработки
- Базовый FIX 4.4 сервер (New Order, Cancel, Execution Reports): 4–6 недель
- Market Data feed через FIX: 2–3 недели
- TLS + аутентификация + IP whitelist: 1–2 недели
- Drop Copy: 1–2 недели
- Тестирование и документация: 2–3 недели
- Полная production-ready FIX интеграция: 2–3 месяца







