AlgoDaily

Order Management & FIX Integration

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

From Signals To Orders

Why this matters

Now that we've covered Low-Latency C++ for HFT, Networking & Kernel Bypass, and Concurrency & Lock-Free Programming, we're ready to connect your trading logic to the outside world.

A strategy does not trade directly with an exchange. It asks an Order Management System, or OMS, to create, amend, cancel, and track orders safely.

In HFT, the OMS must be:

  • Fast: avoid unnecessary allocation and blocking
  • Correct: never lose track of order state
  • Deterministic: handle duplicate, delayed, and rejected messages
  • Auditable: every action must be explainable later

Today we'll build the mental model and C++ building blocks for order lifecycle handling and basic FIX integration.

From Signals To Orders

OMS Responsibilities

The OMS is the safety layer

The OMS sits between your strategy and the venue.

Strategy  ->  OMS  ->  FIX Gateway  ->  Exchange
          <-       <-               <-
        fills, rejects, cancels, acks

A minimal OMS owns these responsibilities:

  1. Order identity

    • Generate client order IDs
    • Map client IDs to exchange IDs when available
  2. State tracking

    • New, acknowledged, partially filled, filled, canceled, rejected
  3. Risk and validity gates

    • Symbol allowed?
    • Quantity valid?
    • Price within limits?
  4. Outbound message creation

    • New order
    • Cancel request
    • Replace/amend request
  5. Inbound message handling

    • Execution reports
    • Rejects
    • Cancel acknowledgments

The key idea: your strategy should not guess whether an order is live. It asks the OMS.

OMS Responsibilities

Order State Machine

Orders are state machines

An order lifecycle is not just a boolean like isFilled.

Common states:

PendingNew -> Live -> PartiallyFilled -> Filled
                       |                  
                       +-> PendingCancel -> Canceled
                       +-> Rejected

The OMS should only allow valid transitions. For example:

  • You can move from PendingNew to Live after an acknowledgment.
  • You can move from Live to PartiallyFilled after a partial fill.
  • You should not move from Filled back to Live.

This is where correctness matters more than cleverness. A small explicit state machine beats scattered if flags.

```cpp
#include <iostream>
#include <string>

enum class OrderState {
    PendingNew,
    Live,
    PartiallyFilled,
    Filled,
    PendingCancel,
    Canceled,
    Rejected
};

std::string name(OrderState s) {
    switch (s) {
        case OrderState::PendingNew: return "PendingNew";
        case OrderState::Live: return "Live";
        case OrderState::PartiallyFilled: return "PartiallyFilled";
        case OrderState::Filled: return "Filled";
        case OrderState::PendingCancel: return "PendingCancel";
        case OrderState::Canceled: return "Canceled";
        case OrderState::Rejected: return "Rejected";
    }
    return "Unknown";
}

bool canTransition(OrderState from, OrderState to) {
    if (from == OrderState::PendingNew) {
        return to == OrderState::Live || to == OrderState::Rejected;
    }
    if (from == OrderState::Live) {
        return to == OrderState::PartiallyFilled || to == OrderState::Filled ||
               to == OrderState::PendingCancel || to == OrderState::Rejected;
    }
    if (from == OrderState::PartiallyFilled) {
        return to == OrderState::Filled || to == OrderState::PendingCancel;
    }
    if (from == OrderState::PendingCancel) {
        return to == OrderState::Canceled || to == OrderState::Filled;
    }
    return false; // terminal states do not move
}

void apply(OrderState& state, OrderState next) {
    std::cout << "Transition " << name(state) << " -> " << name(next);
    if (canTransition(state, next)) {
        state = next;
        std::cout << " accepted\n";
    } else {
        std::cout << " rejected\n";
    }
}

int main() {
    OrderState order = OrderState::PendingNew;

    apply(order, OrderState::Live);
    apply(order, OrderState::PartiallyFilled);
    apply(order, OrderState::Filled);
    apply(order, OrderState::Live); // invalid

    std::cout << "Final state: " << name(order) << "\n";
    return 0;
}
```

State Transition Check

An order is currently in Filled state. Your OMS receives a late cancel acknowledgment for that same order.

What should the OMS generally do?

FIX Message Basics

FIX is tag-value messaging

FIX, the Financial Information eXchange protocol, is widely used for order entry and execution reports.

A FIX message is mostly a sequence of numeric tags:

8=FIX.4.4|35=D|49=BUY_SIDE|56=BROKER|11=ORD123|55=AAPL|54=1|38=100|40=2|44=185.25|10=...

In real FIX, fields are separated by the byte SOH (\x01), but logs often display it as |.

Important tags for a simple new order:

  • 35=D means NewOrderSingle
  • 11 is ClOrdID, your client order ID
  • 55 is Symbol
  • 54 is Side: 1 buy, 2 sell
  • 38 is OrderQty
  • 40 is OrdType: 2 limit
  • 44 is Price

You do not need to memorize every tag today. You need the pattern: encode outbound intent, decode inbound truth.

FIX Message Basics

Encode New Order

Build messages deliberately

For production FIX, teams usually use a certified FIX engine or a carefully tested internal gateway.

Still, it is useful to understand message construction. The example below builds a simplified human-readable FIX-like NewOrderSingle.

Notice the separation:

  • The strategy decides intent.
  • The OMS assigns identity and validates state.
  • The FIX layer encodes the message.
```cpp
#include <iostream>
#include <sstream>
#include <string>

struct NewOrder {
    std::string clOrdId;
    std::string symbol;
    char side;       // '1' buy, '2' sell
    int quantity;
    double price;
};

std::string encodeNewOrderSingle(const NewOrder& o) {
    std::ostringstream msg;
    msg << "8=FIX.4.4|";
    msg << "35=D|";                 // NewOrderSingle
    msg << "49=CLIENT|56=BROKER|";  // sender/target example
    msg << "11=" << o.clOrdId << "|";
    msg << "55=" << o.symbol << "|";
    msg << "54=" << o.side << "|";
    msg << "38=" << o.quantity << "|";
    msg << "40=2|";                 // limit order
    msg << "44=" << o.price << "|";
    msg << "59=0|";                 // day order
    msg << "10=000|";               // placeholder checksum
    return msg.str();
}

int main() {
    NewOrder order{"C10001", "AAPL", '1', 100, 185.25};
    std::string fix = encodeNewOrderSingle(order);

    std::cout << "Outbound FIX-like message:\n" << fix << "\n";
    return 0;
}
```

FIX Engines

True or false:

For a production trading system, it is safe to treat FIX as plain string concatenation without validating required tags, sequence numbers, sessions, rejects, or checksums.

Handle Execution Reports

Inbound messages update truth

Once an order is sent, the exchange or broker responds with execution reports.

A simplified execution report might contain:

  • 35=8: ExecutionReport
  • 11: ClOrdID
  • 150: ExecType, such as new, trade, canceled, rejected
  • 39: OrdStatus
  • 14: CumQty, cumulative filled quantity
  • 151: LeavesQty, remaining quantity

The OMS should update state from these reports, not from optimism. Sending an order does not mean it is live. Sending a cancel does not mean it is canceled.

```cpp
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>

std::unordered_map<std::string, std::string> parseFixLike(const std::string& msg) {
    std::unordered_map<std::string, std::string> fields;
    std::stringstream ss(msg);
    std::string token;

    while (std::getline(ss, token, '|')) {
        auto pos = token.find('=');
        if (pos != std::string::npos) {
            fields[token.substr(0, pos)] = token.substr(pos + 1);
        }
    }
    return fields;
}

std::string orderStatusName(const std::string& status) {
    if (status == "0") return "New";
    if (status == "1") return "PartiallyFilled";
    if (status == "2") return "Filled";
    if (status == "4") return "Canceled";
    if (status == "8") return "Rejected";
    return "Unknown";
}

int main() {
    std::string report = "8=FIX.4.4|35=8|11=C10001|150=F|39=1|14=40|151=60|55=AAPL|";
    auto f = parseFixLike(report);

    std::cout << "Inbound ExecutionReport\n";
    std::cout << "ClOrdID: " << f["11"] << "\n";
    std::cout << "Symbol: " << f["55"] << "\n";
    std::cout << "Status: " << orderStatusName(f["39"]) << "\n";
    std::cout << "CumQty: " << f["14"] << "\n";
    std::cout << "LeavesQty: " << f["151"] << "\n";

    return 0;
}
```

Tag Recall

In FIX, the client-generated order identifier used to track an order across outbound requests and inbound reports is tag ____.

Production Guardrails

What changes in real systems

The lesson examples were intentionally small. A production OMS/FIX path adds several guardrails:

  • Session sequence numbers: detect gaps and duplicates
  • Heartbeats and test requests: confirm the session is alive
  • Resend logic: recover after disconnects
  • Idempotency: avoid double-processing duplicate execution reports
  • Risk checks: reject invalid orders before they leave your system
  • Audit trail: persist every request, acknowledgment, reject, and fill
  • Backpressure policy: decide what happens when outbound or inbound queues fill

Connect this to the previous lesson: your lock-free queues can move events quickly, but the OMS must still preserve order identity and legal state transitions.

Next, we'll shift attention to Market Data Handling & Order Book Reconstruction, where the same care around sequence numbers and event ordering becomes crucial on the data side.

Production Guardrails

Open interactive view →