Lesson 63 of 71
Market Data, Orders, and FIX
This lesson defines the trading terms that the order book, matching engine, and market data projects of this section use, and models each one in a small Java program. After it, a reader can follow an order from submission to fill, read a depth ladder, and parse and build a FIX message with a correct checksum.
| Term | Meaning |
|---|---|
| Bid / ask | Highest price a buyer offers / lowest price a seller accepts |
| Spread | Ask minus bid |
| Depth | Total quantity resting at each price level |
| Limit / market order | Order with a worst acceptable price / order that takes any available price |
| Fill | One execution of part or all of an order |
| OMS | Order management system: tracks every order's state and fills |
| Risk check | A rule run before an order leaves the firm |
| Snapshot / incremental | Full book state / a single change to it |
| FIX | Financial Information eXchange, a tag=value text protocol |
Prices in every example are long ticks. A tick is the smallest price step an instrument allows (for example 0.01 USD), and a price of 10,025 ticks with a tick size of 0.01 means 100.25. Integer ticks give exact arithmetic with no allocation, the same idea as long minor units in Money and BigDecimal. Quantities are long as well.
Example 1: bid, ask, spread, and mid
A quote is the best bid and best ask at one moment. The spread is the cost of buying and selling back immediately. The mid price is the average of bid and ask; it can fall between two ticks, so it is kept in half ticks.
record Quote(long bid, long ask, long bidQty, long askQty) {
Quote {
if (bid >= ask) throw new IllegalArgumentException("crossed quote: " + bid + " >= " + ask);
}
long spread() { return ask - bid; }
long midHalfTicks() { return bid + ask; } // (bid + ask) / 2 in units of half a tick
}
public class Main {
static String fmt(long ticks) { // tick size 0.01
return ticks / 100 + "." + String.format("%02d", ticks % 100);
}
public static void main(String[] args) {
Quote q = new Quote(10_025, 10_027, 300, 500);
System.out.println("bid " + fmt(q.bid()) + " x " + q.bidQty() + " ask " + fmt(q.ask()) + " x " + q.askQty());
System.out.println("spread ticks: " + q.spread());
System.out.println("mid half-ticks: " + q.midHalfTicks() + " -> mid ticks: " + q.midHalfTicks() / 2
+ (q.midHalfTicks() % 2 == 1 ? ".5" : ""));
Quote wide = new Quote(10_025, 10_028, 100, 100);
System.out.println("wide spread " + wide.spread() + ", mid ticks: " + wide.midHalfTicks() / 2
+ (wide.midHalfTicks() % 2 == 1 ? ".5" : ""));
try {
new Quote(10_030, 10_030, 1, 1);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}bid 100.25 x 300 ask 100.27 x 500
spread ticks: 2
mid half-ticks: 20052 -> mid ticks: 10026
wide spread 3, mid ticks: 10026.5
crossed quote: 10030 >= 10030A book where the bid is at or above the ask is "crossed" or "locked"; on an exchange that state does not persist, because the matching engine trades the overlapping orders at once. The compact constructor rejects it so that a bug in a book builder fails fast.
Example 2: depth and the price ladder
Depth is the quantity at each price level beyond the best. A depth snapshot of N levels lists the N best bids (highest first) and the N best asks (lowest first). The program sums orders per level and prints a ladder in the usual layout: asks above, bids below.
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
record RestingOrder(long id, boolean buy, long price, long qty) {}
public class Main {
public static void main(String[] args) {
List<RestingOrder> orders = List.of(
new RestingOrder(1, true, 10_025, 100), new RestingOrder(2, true, 10_025, 200),
new RestingOrder(3, true, 10_024, 400), new RestingOrder(4, true, 10_022, 50),
new RestingOrder(5, false, 10_027, 500), new RestingOrder(6, false, 10_028, 150),
new RestingOrder(7, false, 10_028, 250), new RestingOrder(8, false, 10_030, 75));
Map<Long, Long> bids = new TreeMap<>(Comparator.reverseOrder());
Map<Long, Long> asks = new TreeMap<>();
for (RestingOrder o : orders) {
(o.buy() ? bids : asks).merge(o.price(), o.qty(), Long::sum);
}
List<Map.Entry<Long, Long>> askList = List.copyOf(asks.entrySet());
for (int i = askList.size() - 1; i >= 0; i--) {
System.out.printf(" %6d | %5d%n", askList.get(i).getKey(), askList.get(i).getValue());
}
System.out.println(" --------------------------");
bids.forEach((px, qty) -> System.out.printf(" %5d %6d |%n", qty, px));
long bidDepth = bids.values().stream().mapToLong(Long::longValue).sum();
long askDepth = asks.values().stream().mapToLong(Long::longValue).sum();
System.out.println("levels bid/ask: " + bids.size() + "/" + asks.size()
+ ", total qty bid/ask: " + bidDepth + "/" + askDepth);
}
} 10030 | 75
10028 | 400
10027 | 500
--------------------------
300 10025 |
400 10024 |
50 10022 |
levels bid/ask: 3/3, total qty bid/ask: 750/975Two orders at 10,025 add up to one level of 300. Level-based data of this kind is called market-by-price (MBP, or L2); data that shows each order separately is market-by-order (MBO, or L3).
Example 3: order types and the order lifecycle
A limit order states a worst price: a buy limit at 10,026 never pays more than 10,026, and whatever does not trade rests in the book. A market order states no price and takes liquidity until it is filled or the opposite side is empty. Time in force (TIF) says how long an order lives: DAY rests until the session ends, IOC (immediate or cancel) cancels whatever does not fill at once, and FOK (fill or kill) fills completely at once or not at all.
An order moves through states. The enum below lists the legal transitions, and an illegal one throws, which is how an OMS detects out-of-order or duplicate messages.
import java.util.EnumSet;
import java.util.Set;
enum OrderState {
PENDING_NEW, NEW, PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED;
Set<OrderState> next() {
return switch (this) {
case PENDING_NEW -> EnumSet.of(NEW, REJECTED);
case NEW, PARTIALLY_FILLED -> EnumSet.of(PARTIALLY_FILLED, FILLED, CANCELLED);
case FILLED, CANCELLED, REJECTED -> EnumSet.noneOf(OrderState.class);
};
}
boolean terminal() { return next().isEmpty(); }
OrderState to(OrderState target) {
if (!next().contains(target)) throw new IllegalStateException(this + " -> " + target);
return target;
}
}
public class Main {
public static void main(String[] args) {
OrderState s = OrderState.PENDING_NEW;
for (OrderState t : new OrderState[] {OrderState.NEW, OrderState.PARTIALLY_FILLED,
OrderState.PARTIALLY_FILLED, OrderState.FILLED}) {
s = s.to(t);
System.out.println("-> " + s + (s.terminal() ? " (terminal)" : ""));
}
try {
s.to(OrderState.CANCELLED);
} catch (IllegalStateException e) {
System.out.println("rejected transition: " + e.getMessage());
}
}
}-> NEW
-> PARTIALLY_FILLED
-> PARTIALLY_FILLED
-> FILLED (terminal)
rejected transition: FILLED -> CANCELLEDPENDING_NEW is the state between sending and the venue's acknowledgement. A cancel request that arrives after the last fill fails, because FILLED is terminal; venues answer such a request with a cancel reject.
Example 4: fills, leaves quantity, and average price
A fill reports a quantity and a price. The OMS keeps three numbers per order: cumulative quantity (filled so far), leaves quantity (still open, qty - cum until a cancel sets it to zero), and the average fill price. The average is kept as a notional sum (price * qty added up) and divided only for display, so no rounding error accumulates.
final class Order {
final long id;
final long qty;
long cumQty;
long notional; // sum of price * qty in ticks
String state = "NEW";
Order(long id, long qty) { this.id = id; this.qty = qty; }
long leaves() { return state.equals("CANCELLED") ? 0 : qty - cumQty; }
void fill(long px, long fillQty) {
if (fillQty <= 0 || fillQty > leaves()) throw new IllegalArgumentException("overfill " + fillQty);
cumQty += fillQty;
notional = Math.addExact(notional, Math.multiplyExact(px, fillQty));
state = cumQty == qty ? "FILLED" : "PARTIALLY_FILLED";
}
void cancel() { state = "CANCELLED"; }
String avgPx() { // ticks with two decimals, rounded half up
long scaled = (notional * 100 + cumQty / 2) / cumQty;
return scaled / 100 + "." + String.format("%02d", scaled % 100);
}
@Override public String toString() {
return "order " + id + " " + state + " cum=" + cumQty + " leaves=" + leaves() + " avg=" + avgPx() + " ticks";
}
}
public class Main {
public static void main(String[] args) {
Order o = new Order(42, 1_000);
o.fill(10_027, 300);
System.out.println(o);
o.fill(10_028, 400);
System.out.println(o);
o.cancel();
System.out.println(o);
try {
o.fill(10_027, 1);
} catch (IllegalArgumentException e) {
System.out.println("after cancel: " + e.getMessage());
}
}
}order 42 PARTIALLY_FILLED cum=300 leaves=700 avg=10027.00 ticks
order 42 PARTIALLY_FILLED cum=700 leaves=300 avg=10027.57 ticks
order 42 CANCELLED cum=700 leaves=0 avg=10027.57 ticks
after cancel: overfill 1The average after two fills is (10027*300 + 10028*400) / 700 ticks. Math.multiplyExact and Math.addExact throw on overflow instead of wrapping, which matters when notional is summed over many fills.
Example 5: OMS and pre-trade risk checks
An order management system (OMS) is the component that accepts orders from traders or strategies, assigns ids, runs risk checks, sends orders to venues, and applies execution reports. Pre-trade risk checks run before an order leaves the firm; regulations such as SEC Rule 15c3-5 in the US require brokers with market access to have them. Typical checks are a maximum order quantity, a price collar around a reference price (fat-finger protection), and a position limit that counts open orders as if they filled.
import java.util.ArrayList;
import java.util.List;
record NewOrder(long id, boolean buy, long price, long qty) {}
final class RiskLimits {
final long maxQty, collarTicks, maxPosition;
RiskLimits(long maxQty, long collarTicks, long maxPosition) {
this.maxQty = maxQty; this.collarTicks = collarTicks; this.maxPosition = maxPosition;
}
}
final class Oms {
private final RiskLimits limits;
private long position; // filled, signed
private long openBuys, openSells;
final List<String> log = new ArrayList<>();
Oms(RiskLimits limits) { this.limits = limits; }
String check(NewOrder o, long referencePx) {
if (o.qty() <= 0) return "qty must be positive";
if (o.qty() > limits.maxQty) return "qty " + o.qty() + " > max " + limits.maxQty;
if (Math.abs(o.price() - referencePx) > limits.collarTicks) return "price outside collar";
long worst = o.buy() ? position + openBuys + o.qty() : position - openSells - o.qty();
if (Math.abs(worst) > limits.maxPosition) return "position limit, worst case " + worst;
return null;
}
void submit(NewOrder o, long referencePx) {
String reason = check(o, referencePx);
if (reason != null) {
log.add("REJECT " + o.id() + ": " + reason);
return;
}
if (o.buy()) openBuys += o.qty(); else openSells += o.qty();
log.add("SEND " + o.id());
}
void onFill(NewOrder o, long qty) {
if (o.buy()) { openBuys -= qty; position += qty; } else { openSells -= qty; position -= qty; }
log.add("FILL " + o.id() + " " + qty + ", position " + position);
}
}
public class Main {
public static void main(String[] args) {
Oms oms = new Oms(new RiskLimits(1_000, 50, 1_500));
long ref = 10_026;
NewOrder a = new NewOrder(1, true, 10_027, 800);
oms.submit(a, ref);
oms.submit(new NewOrder(2, true, 10_027, 5_000), ref);
oms.submit(new NewOrder(3, false, 11_000, 100), ref);
oms.submit(new NewOrder(4, true, 10_026, 900), ref);
oms.onFill(a, 800);
oms.submit(new NewOrder(5, false, 10_025, 1_000), ref);
oms.log.forEach(System.out::println);
}
}SEND 1
REJECT 2: qty 5000 > max 1000
REJECT 3: price outside collar
REJECT 4: position limit, worst case 1700
FILL 1 800, position 800
SEND 5Order 4 is rejected even though nothing has filled yet: with order 1 open, a full fill of both would reach 1,700. Counting open orders at their full size is the conservative choice, because a venue can fill them at any moment.
Example 6: market data snapshots and incremental updates
A market data feed sends two kinds of messages. A snapshot carries the full book; an incremental update carries one change (new level, changed quantity, deleted level). Every message has a sequence number. A receiver applies updates in order and, if a number is skipped (a gap), stops trusting its book until it loads a new snapshot. The market data handler project builds a binary version of this.
import java.util.Comparator;
import java.util.List;
import java.util.TreeMap;
sealed interface Md permits Snapshot, Update {}
record Snapshot(long seq, List<long[]> bids, List<long[]> asks) implements Md {}
record Update(long seq, boolean bid, long price, long qty) implements Md {} // qty 0 deletes
final class BookBuilder {
final TreeMap<Long, Long> bids = new TreeMap<>(Comparator.reverseOrder());
final TreeMap<Long, Long> asks = new TreeMap<>();
long lastSeq = -1;
boolean stale = true;
void on(Md m) {
switch (m) {
case Snapshot s -> {
bids.clear(); asks.clear();
s.bids().forEach(l -> bids.put(l[0], l[1]));
s.asks().forEach(l -> asks.put(l[0], l[1]));
lastSeq = s.seq(); stale = false;
System.out.println("snapshot @" + s.seq() + " " + top());
}
case Update u when stale -> System.out.println("drop " + u.seq() + " (stale)");
case Update u when u.seq() <= lastSeq -> System.out.println("drop " + u.seq() + " (duplicate)");
case Update u when u.seq() != lastSeq + 1 -> {
stale = true;
System.out.println("gap: expected " + (lastSeq + 1) + " got " + u.seq() + ", request snapshot");
}
case Update u -> {
TreeMap<Long, Long> side = u.bid() ? bids : asks;
if (u.qty() == 0) side.remove(u.price()); else side.put(u.price(), u.qty());
lastSeq = u.seq();
System.out.println("update " + u.seq() + " " + top());
}
}
}
String top() {
return bids.isEmpty() || asks.isEmpty() ? "one side empty"
: bids.firstKey() + "x" + bids.firstEntry().getValue() + " / " + asks.firstKey() + "x" + asks.firstEntry().getValue();
}
}
public class Main {
public static void main(String[] args) {
BookBuilder b = new BookBuilder();
List<Md> feed = List.of(
new Update(9, true, 10_025, 100),
new Snapshot(10, List.of(new long[] {10_025, 300}, new long[] {10_024, 400}),
List.of(new long[] {10_027, 500})),
new Update(11, true, 10_026, 200),
new Update(11, true, 10_026, 200),
new Update(12, false, 10_027, 0),
new Update(14, false, 10_028, 100),
new Update(15, true, 10_026, 0),
new Snapshot(15, List.of(new long[] {10_025, 300}), List.of(new long[] {10_028, 100})),
new Update(16, true, 10_026, 50));
feed.forEach(b::on);
}
}drop 9 (stale)
snapshot @10 10025x300 / 10027x500
update 11 10026x200 / 10027x500
drop 11 (duplicate)
update 12 one side empty
gap: expected 13 got 14, request snapshot
drop 15 (stale)
snapshot @15 10025x300 / 10028x100
update 16 10026x50 / 10028x100The switch uses record patterns with guards from Pattern Matching; the order of the cases matters, because the first matching guard wins. Update 13 never arrived, so the book stays stale and drops update 15 until snapshot 15 arrives. Real feeds usually send incrementals over UDP multicast and snapshots on a separate channel or by request over TCP.
Example 7: FIX message structure and the checksum
FIX (Financial Information eXchange) is the standard text protocol between brokers, buy-side firms, and venues. A message is a sequence of tag=value fields, each ended by the SOH character (ASCII 1). The first three fields are fixed: 8 (BeginString, the version), 9 (BodyLength), and 35 (MsgType). The last field is 10 (CheckSum).
- BodyLength counts the bytes from the field after
9=up to and including the SOH before10=. - CheckSum is the sum of every byte before
10=, modulo 256, written as three digits.
The parser below reads fields into a map keyed by tag and verifies both numbers. Messages are written with | for readability and converted to SOH before parsing.
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
final class Fix {
static final byte SOH = 1;
static int checksum(byte[] msg, int end) {
int sum = 0;
for (int i = 0; i < end; i++) sum += msg[i] & 0xFF;
return sum % 256;
}
static Map<Integer, String> parse(byte[] msg) {
Map<Integer, String> fields = new LinkedHashMap<>();
int start = 0, bodyStart = -1, checksumAt = -1;
for (int i = 0; i < msg.length; i++) {
if (msg[i] != SOH) continue;
String field = new String(msg, start, i - start, StandardCharsets.US_ASCII);
int eq = field.indexOf('=');
int tag = Integer.parseInt(field.substring(0, eq));
if (tag == 10) checksumAt = start;
fields.put(tag, field.substring(eq + 1));
if (tag == 9) bodyStart = i + 1;
start = i + 1;
}
int declaredLength = Integer.parseInt(fields.get(9));
if (checksumAt - bodyStart != declaredLength) {
throw new IllegalArgumentException("BodyLength " + declaredLength + " != " + (checksumAt - bodyStart));
}
int expected = checksum(msg, checksumAt);
if (expected != Integer.parseInt(fields.get(10))) {
throw new IllegalArgumentException("CheckSum " + fields.get(10) + " != " + String.format("%03d", expected));
}
return fields;
}
}
public class Main {
static byte[] soh(String s) { return s.replace('|', (char) 1).getBytes(StandardCharsets.US_ASCII); }
public static void main(String[] args) {
String body = "35=D|49=CLIENT|56=VENUE|34=7|11=ORD-1|55=ACME|54=1|38=100|40=2|44=100.25|59=0|";
String head = "8=FIX.4.4|9=" + body.length() + "|";
byte[] noTrailer = soh(head + body);
String text = head + body + "10=" + String.format("%03d", Fix.checksum(noTrailer, noTrailer.length)) + "|";
System.out.println(text);
Map<Integer, String> f = Fix.parse(soh(text));
System.out.println("MsgType=" + f.get(35) + " ClOrdID=" + f.get(11) + " Side=" + f.get(54)
+ " Qty=" + f.get(38) + " OrdType=" + f.get(40) + " Price=" + f.get(44));
try {
Fix.parse(soh(text.replace("38=100", "38=900")));
} catch (IllegalArgumentException e) {
System.out.println("tampered: " + e.getMessage());
}
}
}8=FIX.4.4|9=78|35=D|49=CLIENT|56=VENUE|34=7|11=ORD-1|55=ACME|54=1|38=100|40=2|44=100.25|59=0|10=146|
MsgType=D ClOrdID=ORD-1 Side=1 Qty=100 OrdType=2 Price=100.25
tampered: CheckSum 146 != 154Common tags in the message: 49 SenderCompID, 56 TargetCompID, 34 MsgSeqNum, 11 ClOrdID (the client's order id), 55 Symbol, 54 Side (1 buy, 2 sell), 38 OrderQty, 40 OrdType (1 market, 2 limit), 44 Price, 59 TimeInForce (0 DAY, 3 IOC, 4 FOK). D is NewOrderSingle. Changing 100 to 900 keeps the length but changes the byte sum, so the checksum catches it. The checksum detects corruption, not tampering; it is not a cryptographic hash.
Example 8: execution reports and session-level rules
A venue answers a NewOrderSingle with execution reports (MsgType 8). Each carries 39 OrdStatus (0 new, 1 partially filled, 2 filled, 4 canceled, 8 rejected), 150 ExecType, 14 CumQty, 151 LeavesQty, and for a fill 32 LastQty and 31 LastPx. The FIX session layer numbers every message with 34 MsgSeqNum per direction; a receiver that sees a higher number than expected sends a ResendRequest (MsgType 2). The program builds three reports, checks sequence numbers, and feeds the order state from Example 3's rules.
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class FixWriter {
private final StringBuilder body = new StringBuilder();
FixWriter field(int tag, Object value) { body.append(tag).append('=').append(value).append('\u0001'); return this; }
byte[] finish() {
String head = "8=FIX.4.4\u00019=" + body.length() + "\u0001";
byte[] partial = (head + body).getBytes(StandardCharsets.US_ASCII);
int sum = 0;
for (byte b : partial) sum += b & 0xFF;
return (head + body + "10=" + String.format("%03d", sum % 256) + "\u0001").getBytes(StandardCharsets.US_ASCII);
}
}
public class Main {
static Map<Integer, String> fields(byte[] msg) {
Map<Integer, String> m = new LinkedHashMap<>();
for (String f : new String(msg, StandardCharsets.US_ASCII).split("\u0001")) {
int eq = f.indexOf('=');
m.put(Integer.parseInt(f.substring(0, eq)), f.substring(eq + 1));
}
return m;
}
static byte[] report(int seq, char status, long cum, long leaves, long lastQty, String lastPx) {
FixWriter w = new FixWriter().field(35, "8").field(49, "VENUE").field(56, "CLIENT").field(34, seq)
.field(11, "ORD-1").field(39, status).field(150, status).field(14, cum).field(151, leaves);
if (lastQty > 0) w.field(32, lastQty).field(31, lastPx);
return w.finish();
}
public static void main(String[] args) {
List<byte[]> inbound = List.of(
report(1, '0', 0, 100, 0, null),
report(2, '1', 40, 60, 40, "100.25"),
report(4, '2', 100, 0, 60, "100.26"));
int expectedSeq = 1;
for (byte[] raw : inbound) {
Map<Integer, String> m = fields(raw);
int seq = Integer.parseInt(m.get(34));
if (seq > expectedSeq) {
System.out.println("seq gap: expected " + expectedSeq + " got " + seq
+ " -> send 35=2 BeginSeqNo=" + expectedSeq + " EndSeqNo=" + (seq - 1));
}
expectedSeq = seq + 1;
String status = switch (m.get(39)) {
case "0" -> "NEW";
case "1" -> "PARTIALLY_FILLED";
case "2" -> "FILLED";
default -> "OTHER " + m.get(39);
};
System.out.println("seq " + seq + " " + status + " cum=" + m.get(14) + " leaves=" + m.get(151)
+ (m.containsKey(32) ? " last=" + m.get(32) + "@" + m.get(31) : "") + " checksum=" + m.get(10));
}
}
}seq 1 NEW cum=0 leaves=100 checksum=068
seq 2 PARTIALLY_FILLED cum=40 leaves=60 last=40@100.25 checksum=038
seq gap: expected 3 got 4 -> send 35=2 BeginSeqNo=3 EndSeqNo=3
seq 4 FILLED cum=100 leaves=0 last=60@100.26 checksum=036The program applies message 4 and also requests 3; a real FIX engine queues messages after a gap until the resend fills it, so that state changes apply in order. Sessions start with Logon (A), stay alive with Heartbeat (0), and end with Logout (5).
Exchange connectivity in short
| Piece | Role |
|---|---|
| Order entry gateway | Accepts orders over FIX or a binary protocol, one session per member connection |
| Native binary protocols | Fixed-layout messages (for example Nasdaq OUCH for orders and ITCH for data) that skip text parsing |
| Market data | UDP multicast of incrementals (two identical A/B feeds for redundancy) plus snapshot or replay service |
| Colocation | Client servers in the venue's data center, so network latency is a short cross-connect |
| Drop copy | A separate session that copies all fills to the firm's risk and back office |
Interview points
- Prices are integer ticks and quantities are
long, so arithmetic is exact and creates no objects. - Spread is ask minus bid; a bid at or above the ask cannot persist on a venue because the engine trades it.
- Limit orders cap the price and may rest; market orders take liquidity until filled or the book is empty.
- IOC cancels the unfilled rest at once; FOK fills completely at once or cancels everything.
- Order states: pending new, new, partially filled, filled, cancelled, rejected; filled, cancelled, and rejected are terminal.
- Keep cumulative notional, not a running average, to avoid rounding drift.
- Pre-trade risk counts open orders at full size against position limits, and checks quantity and price collars.
- Market data: apply incrementals in sequence order; a gap makes the book stale until a new snapshot.
- FIX:
tag=valuefields ended by SOH;8,9,35first,10last; BodyLength and CheckSum (byte sum mod 256) are checked on receipt. - FIX MsgSeqNum gaps trigger a ResendRequest; the checksum detects corruption, not tampering.
Summary
- Bid, ask, spread, mid, and depth describe the book; MBP shows levels, MBO shows orders.
- Orders have type (limit, market), side, price in ticks, quantity, and time in force (DAY, IOC, FOK).
- An order's lifecycle is a state machine; the OMS tracks cum, leaves, and notional per order.
- Pre-trade risk checks run before an order leaves the firm.
- Market data feeds combine snapshots with sequenced incremental updates and gap recovery.
- FIX messages have a fixed header, a body, and a trailer; BodyLength and CheckSum are computed over bytes.
- Execution reports carry OrdStatus, CumQty, LeavesQty, LastQty, and LastPx.
