FIX User Guide

The nasdaq_protocols.fix module provides a client-side implementation of the FIX protocol, including the session layer. Refer FIX API Reference for more information on the API.

This package only provides the core implementation of the FIX protocol (message serialization, deserialization, validation and session handling). It does not define any actual FIX messages – those are application/dictionary specific and must be defined by the user of this package (for example, by generating them from a FIX dictionary). The session layer operates generically on the standard header/body tags and message types, so the same session works for any conforming message set.

Follow the steps in Create New Project Guide to create a new python package for the application specific messages.

Connecting to a FIX server

import asyncio
from nasdaq_protocols import fix
# `Fix44Session`/`Fix50Session` are provided by the library; the login message type
# is defined by your application/dictionary.
from nasdaq_protocols.fix.session import Fix44Session


async def main():
    login_msg = ...  # build your application Logon message

    session = await fix.connect_async(
        ('hostname', 1234),
        login_msg,
        Fix44Session,
    )

    # ... use the session ...

    await session.close()


if __name__ == '__main__':
    asyncio.run(main())

Session-level sequence recovery

FixSession always behaves as a compliant FIX client on the receiver side: it checks for a sequence gap once, at login, and transparently performs the session-layer recovery protocol before handing messages to your application. This is core functionality and cannot be disabled.

session = await fix.connect_async(
    ('hostname', 1234),
    login_msg,
    lambda: Fix44Session(
        expected_seq_num=42,        # the next inbound MsgSeqNum you expect from the server
        client_heartbeat_interval=10,
        server_heartbeat_interval=10,
    ),
)

expected_seq_num defaults to None: the session bootstraps itself from the MsgSeqNum of the Logon response, so whatever the server sends first is accepted as the baseline. Pass an explicit value (e.g. derived from your persisted sequence store on a reconnect) to detect a gap right from the login handshake.

A FIX session runs over a single TCP connection, which guarantees in-order, gap-free delivery for the life of that connection – a real gap can only arise from a disconnect and reconnect. Because of this, gap detection happens only once, during login: the session compares the MsgSeqNum the server reports on its Logon response against expected_seq_num. If the server’s sequence is higher, messages were missed on a prior connection and recovery kicks in; once recovery completes, messages flow straight through to your application with no further per-message sequence bookkeeping.

What the session does automatically:

  • No gap at login: the Logon response’s own MsgSeqNum matches (or bootstraps) expected_seq_num; every subsequent message is checked against the expected sequence (a cheap O(1) comparison) and delivered to your on_msg_coro in order.

  • Gap detected at login: the Logon response reports a MsgSeqNum ahead of expected_seq_num. The session sends an open-ended ResendRequest(BeginSeqNo=expected_seq_num, EndSeqNo=0), asking the server to replay everything from the missing sequence onward. The optional on_gap hook is invoked with (expected_seq_num, received_seq).

  • No buffering: the session never buffers out-of-order traffic. Any message that arrives ahead of expected_seq_num (for example, live traffic racing the in-flight ResendRequest) is dropped silently before it is ever queued for your application – because the ResendRequest is open-ended, the server’s replay naturally re-delivers that same message later (marked PossDupFlag=Y), in the correct position, once the gap has been filled.

  • Gap fill / replay: a SequenceReset with GapFillFlag=Y advances the expected sequence to NewSeqNo (the server may collapse a run of housekeeping messages, e.g. several Heartbeats, into a single GapFill); a SequenceReset without the gap-fill flag forces the expected sequence to NewSeqNo.

  • Duplicates: a message below the expected sequence carrying PossDupFlag=Y is a replayed duplicate and is dropped silently.

  • Fatal sequence error: a message below the expected sequence without PossDupFlag=Y is a fatal condition – the session sends a Logout and closes.

  • TestRequest: an inbound TestRequest is always answered with a Heartbeat echoing the TestReqID – the standard FIX flow.

This filtering happens at the point messages are received off the wire, before they are ever handed to the dispatcher – so your application only ever sees messages in strict sequence order, and on_msg_coro/receive_msg() behave exactly as documented by the base session, regardless of whether sequence recovery is in progress.

There is no cap on how long the server can take to complete the replay; if it never does, the normal heartbeat-timeout logic closes the session as a safety net.

Note

This is Phase 1 (receiver side). Responder-side replay – replaying our own previously sent messages when the server sends us a ResendRequest – requires a persistent outbound message store and is planned as future work.

Sending messages with an explicit sequence

send_msg auto-stamps the outbound MsgSeqNum from the session counter. Two keyword-only parameters let you override that when producing recovery messages:

# Normal send: MsgSeqNum is taken from the session's outbound counter.
session.send_msg(msg)

# Send with an explicit sequence number; the auto counter is NOT advanced.
session.send_msg(msg, seq_num=100)

# Mark a message as a possible duplicate: sets PossDupFlag and OrigSendingTime.
session.send_msg(msg, seq_num=100, poss_dup=True)