This is primer about OUCH protocol which is Nasdaq low latency protocol for trading companies to send trading orders to the exchange. These are snippets from the specification document:

NASDAQ accepts limit orders from system participants and executes matching orders when possible. Non-matching orders may be added to the NASDAQ Limit Order Book, a database of available limit orders, where they wait to be matched in price- time priority.

OUCH is a simple protocol that allows NASDAQ participants to enter, replace, and cancel orders and receive executions. It is intended to allow participants and their software developers to integrate NASDAQ into their proprietary trading systems or to build custom front ends.

OUCH only provides a method for participants to send orders to NASDAQ and receive updates on those orders entered. For information about all orders entered into and executed on the NASDAQ book, refer to the ITCH protocol (available separately).

Messages Link to heading

There are 2 kinds of messages between host and client. Host here means Nasdaq and client means the broker or trader.

  • Inbound - From client to host
  • Outbound - From host to client

Inbound Link to heading

These are messages from client to host. client can re-send the message in case of failures. the all messages are sequences in order to the host.

  • Type O - Enter Order
  • Type U - Replace Order
  • Type X - Cancel Order
  • Type M - Modify Order Request
  • Type C - Mass Cancel Request
  • Type D - Disable Order Entry Request
  • Type E - Enable Order entry request
  • Type Q - Account Query Request

The original “Enter Order” has UserRefNum field which will be referenced in other messages to amend that order

As described above in Data Types. UserRefNum must be day- unique and strictly increasing for each OUCH account.

Outbound Link to heading

These are message sent from host to client. The big ones are Order Accepted and Order Executed.

  • Type A - Order Accepted
  • Type U - Order replaced
  • Type C - Order canceled
  • Type E - Order executed
  • Type B - Broken Trade
  • Type J - Rejected
  • Type P - Cancel Pending

The specification describes Order Accepted as follows. This means after Order accepted, there should Order executed or Order cancelled.

This message acknowledges the receipt and acceptance of a valid Enter Order Message. The data fields from the Enter Order Message are echoed back in this message. Note that the accepted values may differ from the entered values for some fields.

Accepted Messages normally come before any Executed Messages or Canceled Messages for an order. However, when the Order State field of an Accepted Message is Order Dead (“D”), no additional messages will be received for that order, as Order Dead means that the order was accepted and automatically canceled.

Implementation Link to heading

This is a quick and dirty generated by sonnet 3.5 for Enter Order message pack and unpack binary string.

import struct
from dataclasses import dataclass
from enum import Enum
from typing import Iterator, BinaryIO

class Side(Enum):
    BUY = 'B'
    SELL = 'S'
    SELL_SHORT = 'T'
    SELL_SHORT_EXEMPT = 'E'

class TimeInForce(Enum):
    DAY = '0'  # Day (Market Hours)
    IOC = '3'  # Immediate or Cancel
    GTX = '5'  # Extended Hours
    GTT = '6'  # GTT (ExpireTime needs to be specified)
    AFTER_HOURS = 'E'  # After hours

class Capacity(Enum):
    AGENCY = 'A'  # Agency
    PRINCIPAL = 'P'  # Principal
    RISKLESS = 'R'  # Riskless
    OTHER = 'O'  # Other

class Display(Enum):
    VISIBLE = 'Y'  # Visible
    HIDDEN = 'N'  # Hidden
    ATTRIBUTABLE = 'A'  # Attributable

class CrossType(Enum):
    CONTINUOUS = 'N'  # Continuous market
    OPENING = 'O'  # Opening cross (Nasdaq only)
    CLOSING = 'C'  # Closing cross (Nasdaq only)
    HALT_IPO = 'H'  # Halt/IPO (Nasdaq only)
    SUPPLEMENTAL = 'S'  # Supplemental (Nasdaq only)
    RETAIL = 'R'  # Retail (BX only)
    EXTENDED = 'E'  # Extended life (Nasdaq only)
    AFTER_HOURS = 'A'  # After hours close (Nasdaq only)

class OUCHMessage:
    """Base class for OUCH messages"""
    def pack(self) -> bytes:
        """Pack the message into bytes"""
        raise NotImplementedError("Subclasses must implement pack()")

    @classmethod
    def unpack(cls, data: bytes):
        """Unpack bytes into a message"""
        raise NotImplementedError("Subclasses must implement unpack()")

@dataclass
class EnterOrder(OUCHMessage):
    """
    Enter Order Message

    The enter order message requests NASDAQ to enter a new order into the system.

    Message Format:
    Name                    Offset  Length  Value   Notes
    Type                    0       1       'O'     Enter Order Message type
    UserRefNum              1       4       Integer Day-unique and strictly increasing for each OUCH account
    Side                    5       1       Alpha   B = Buy, S = Sell, T = Sell Short, E = Sell Short Exempt
    Quantity               6       4       Integer Total number of shares, must be > 0 and < 1,000,000
    Symbol                 10      8       Alpha   Stock symbol
    Price                  18      8       Price   The price of the order
    TimeInForce            26      1       Alpha   0 = Day, 3 = IOC, 5 = GTX, 6 = GTT, E = After hours
    Display                27      1       Alpha   Y = Visible, N = Hidden, A = Attributable
    Capacity               28      1       Alpha   A = Agency, P = Principal, R = Riskless, O = Other
    InterMarketSweep       29      1       Alpha   Y = Eligible, N = Not eligible
    CrossType              30      1       Alpha   See CrossType enum
    ClOrdID                31      14      Alpha   Customer order identifier
    AppendageLength        45      2       Integer Length of Optional Appendage
    OptionalAppendage      47      var     TagValue See Appendix B for details
    """
    user_ref_num: int
    side: Side
    quantity: int
    symbol: str
    price: int
    time_in_force: TimeInForce
    display: Display
    capacity: Capacity
    intermarket_sweep: str
    cross_type: CrossType
    clordid: str
    appendage_length: int = 0
    optional_appendage: bytes = b''

    def __post_init__(self):
        if self.user_ref_num < 0 or self.user_ref_num > 0xFFFFFFFF:
            raise ValueError("UserRefNum must be a 32-bit unsigned integer")
        if len(self.symbol) > 8:
            raise ValueError("Symbol must be 8 characters or less")
        if self.quantity <= 0 or self.quantity >= 1000000:
            raise ValueError("Quantity must be greater than 0 and less than 1,000,000")
        if len(self.clordid) > 14:
            raise ValueError("ClOrdID must be 14 characters or less")
        if self.intermarket_sweep not in ('Y', 'N'):
            raise ValueError("InterMarketSweep must be 'Y' or 'N'")
        if self.appendage_length < 0 or self.appendage_length > 0xFFFF:
            raise ValueError("AppendageLength must be a 16-bit unsigned integer")
        if len(self.optional_appendage) != self.appendage_length:
            raise ValueError("OptionalAppendage length must match AppendageLength")

    def pack(self) -> bytes:
        """Pack the message into bytes"""
        symbol_padded = self.symbol.ljust(8)
        clordid_padded = self.clordid.ljust(14)

        # Pack the fixed part of the message
        fixed_part = struct.pack("!cIcI8sQccccch",
            b'O',                             # Message type
            self.user_ref_num,               # UserRefNum (4 bytes)
            self.side.value.encode('ascii'), # Side indicator
            self.quantity,                   # Quantity (4 bytes)
            symbol_padded.encode('ascii'),   # Symbol (8 chars)
            self.price,                      # Price (8 bytes)
            self.time_in_force.value.encode('ascii'),  # TimeInForce
            self.display.value.encode('ascii'),        # Display
            self.capacity.value.encode('ascii'),       # Capacity
            self.intermarket_sweep.encode('ascii'),    # InterMarketSweep
            self.cross_type.value.encode('ascii'),     # CrossType
            self.appendage_length)                     # AppendageLength (2 bytes)

        # Add ClOrdID and optional appendage
        return fixed_part + clordid_padded.encode('ascii') + self.optional_appendage

    @classmethod
    def unpack(cls, data: bytes):
        """Unpack bytes into an EnterOrder message"""
        if len(data) < 47:  # Minimum message size (without optional appendage)
            raise ValueError("Data too short for EnterOrder message")

        msg_type = data[0:1].decode('ascii')
        if msg_type != 'O':
            raise ValueError(f"Invalid message type {msg_type} for EnterOrder")

        # Unpack the fixed part
        (_, user_ref_num, side, quantity, symbol, price, time_in_force,
         display, capacity, intermarket_sweep, cross_type,
         appendage_length) = struct.unpack("!cIcI8sQccccch", data[:33])

        # Get ClOrdID
        clordid = data[33:47].decode('ascii').rstrip()

        # Get optional appendage if present
        optional_appendage = data[47:47+appendage_length] if appendage_length > 0 else b''

        # Convert to python types
        return cls(
            user_ref_num=user_ref_num,
            side=Side(side.decode('ascii')),
            quantity=quantity,
            symbol=symbol.decode('ascii').rstrip(),
            price=price,
            time_in_force=TimeInForce(time_in_force.decode('ascii')),
            display=Display(display.decode('ascii')),
            capacity=Capacity(capacity.decode('ascii')),
            intermarket_sweep=intermarket_sweep.decode('ascii'),
            cross_type=CrossType(cross_type.decode('ascii')),
            clordid=clordid,
            appendage_length=appendage_length,
            optional_appendage=optional_appendage
        )