Order Routing — Quick Start¶
Get orderd running and place your first order.
Install¶
See Install + Run for binary installation. You need:
orderdbinarymetadrunning (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¶
- Message Reference — full enum tables, struct layouts, and payload definitions
- Integration Guide — state machine, normalization, recovery, and error handling
- Wire Protocol — ring buffer transport mechanics
- Metadata — instrument lookup and price conversion