Skip to content

Order Routing — Quick Start

Get orderd running and place your first order.

Install

See Install + Run for binary installation. You need:

  • orderd binary
  • metad running (provides instrument and asset metadata)
  • Exchange API credentials configured

Configure

orderd reads its configuration from /etc/sorcery/orderd.toml (or the path specified by --config):

stack = "master"

[venues.binance]
api_key_env    = "BINANCE_API_KEY"
api_secret_env = "BINANCE_API_SECRET"

Credential note: keep API key/secret in environment variables and reference their names in TOML.

Ring segment names are derived from the stack name: /sorcery-{stack}-ord-req and /sorcery-{stack}-ord-rsp.

For production, keep a single writer on /sorcery-{stack}-ord-req. If multiple strategies submit orders, route them through one gateway process.

Run

# Start metad first (provides instrument metadata)
metad --config /etc/sorcery/metad.toml &

# Export venue credentials for orderd
export BINANCE_API_KEY="..."
export BINANCE_API_SECRET="..."

# Start orderd
orderd --config /etc/sorcery/orderd.toml

orderd will connect to configured venues and begin publishing STATUS messages on the response ring. Wait for conn_state = CONNECTED before sending orders.

Send an order

This example uses the raw ring API directly. Write the header + payload into the request ring, then flush.

#include <sorcery/ord_types.h>
#include <sorcery/ring.h>
#include <sorcery/metadata.h>

// Load metadata to look up instrument and venue IDs
sorcery::MetadataStore store("/sorcery-master-metadata");
store.load_instruments({"perp.binance:BTCUSDT"});
auto inst_id = store.resolve("perp.binance:BTCUSDT");
if (!inst_id) return 1;
auto* inst = store.find_instrument(*inst_id);
if (!inst) return 1;

// Open rings — names derived from stack
auto req_ring = sorcery::Ring::open("/sorcery-master-ord-req");
auto rsp_ring = sorcery::Ring::open("/sorcery-master-ord-rsp");
sorcery::Producer producer{req_ring};
sorcery::Consumer consumer{rsp_ring};

// Build a limit buy order
sorcery::ord::NewOrder order{};
order.order_id = 1;                       // client-assigned unique ID in request stream
order.px       = 950000;                  // 95000.0 USDT (tick = 0.1)
order.qty      = 100000000;              // 1.0 BTC (step = 0.00000001)
order.side     = sorcery::ord::BUY;
order.tif      = sorcery::ord::GTC;
order.flags    = 0;

// Submit: write header + payload, then flush
auto buf = producer.get_buffer(sizeof(sorcery::Header) + sizeof(order));
sorcery::encode_header(buf, inst->inst_id, sorcery::ord::NEW, inst->venue);
std::memcpy(buf.data() + sizeof(sorcery::Header), &order, sizeof(order));
producer.flush();

Observe the response

sorcery::DrainBuffer drain;

while (running) {
    for (auto& frame : consumer.drain(drain, 64)) {
        if (frame.is_gap()) {
            reconcile_all_orders();
            continue;
        }

        auto* hdr  = frame.header();
        auto* body = frame.body();

        switch (hdr->msg_type) {
            case sorcery::ord::ORDER_ACK: {
                auto* ack = reinterpret_cast<const sorcery::ord::OrderAckMsg*>(body);
                printf("Order %lu accepted, exch_oid=%lu, state=%u\n",
                       ack->order_id, ack->exch_oid, ack->order_state);
                break;
            }
            case sorcery::ord::ORDER_REJECT: {
                auto* rej = reinterpret_cast<const sorcery::ord::OrderRejectMsg*>(body);
                printf("Order %lu rejected, reason=%u\n",
                       rej->order_id, rej->reject_reason);
                break;
            }
            case sorcery::ord::FILL: {
                auto* fill = reinterpret_cast<const sorcery::ord::FillMsg*>(body);
                double px  = inst->to_price(fill->fill_px);
                double qty = inst->to_qty(fill->fill_qty);
                printf("Fill: order=%lu px=%.2f qty=%.8f state=%u maker=%u\n",
                       fill->order_id, px, qty, fill->order_state, fill->is_maker);
                break;
            }
            case sorcery::ord::STATUS: {
                auto* st = reinterpret_cast<const sorcery::ord::StatusMsg*>(body);
                printf("Status: conn=%u orders=%u pending=%u\n",
                       st->conn_state, st->open_orders, st->pending_requests);
                break;
            }
            default:
                break;
        }
    }
}

Expected output (on a successful limit order that fills):

Status: conn=1 orders=0 pending=0
Order 1 accepted, exch_oid=8234561, state=2
Fill: order=1 px=95000.00 qty=1.00000000 state=5 maker=1

Modify an order price

// Move order_id=1 to a new price (price-only — qty cannot be changed)
sorcery::ord::ModifyOrder modify{};
modify.order_id = 1;
modify.new_px   = 951000;   // 95100.0 USDT

auto buf = producer.get_buffer(sizeof(sorcery::Header) + sizeof(modify));
sorcery::encode_header(buf, inst->inst_id, sorcery::ord::MODIFY, inst->venue);
std::memcpy(buf.data() + sizeof(sorcery::Header), &modify, sizeof(modify));
producer.flush();

// Response: MODIFY_ACK with new confirmed px=951000, state=LIVE
// Or MODIFY_REJECT if the modify failed (order reverts to original price)

Cancel an order

sorcery::ord::CancelOrder cancel{};
cancel.order_id = 1;

auto buf = producer.get_buffer(sizeof(sorcery::Header) + sizeof(cancel));
sorcery::encode_header(buf, inst->inst_id, sorcery::ord::CANCEL, inst->venue);
std::memcpy(buf.data() + sizeof(sorcery::Header), &cancel, sizeof(cancel));
producer.flush();

// Response: CANCEL_ACK with order_state=CANCELED (7)

Check balances

auto buf = producer.get_buffer(sizeof(sorcery::Header));
sorcery::encode_header(buf, 0, sorcery::ord::QUERY_BALANCES, inst->venue);
producer.flush();

// Read the BALANCES response from the response ring
// ...
case sorcery::ord::BALANCES: {
    sorcery::ord::BalancesView view{body};
    for (auto& entry : view.entries()) {
        auto* asset = store.find_asset(entry.asset_id);
        double avail = entry.available * pow(10, -asset->decimals);
        double locked = entry.locked * pow(10, -asset->decimals);
        printf("Asset %lu: available=%.8f locked=%.8f\n",
               entry.asset_id, avail, locked);
    }
    break;
}

What's next