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
)