Actions
The Actions buffer is how your algo sends orders. Each callback can emit up to 16 actions.
Order Types
The SDK supports five order types:
| Type | Description | Use Case |
|---|---|---|
| LIMIT | Sits on book until filled or canceled | Passive market making |
| MARKET | Fills immediately at best available | Aggressive taking |
| IOC | Fill what you can, cancel the rest | Take available liquidity |
| FOK | Fill entire quantity or reject | All-or-nothing execution |
| POST_ONLY | Rejected if it would cross the spread | Maker-only, fee-friendly |
Limit Orders (Default)
Limit orders rest on the order book until filled or canceled. This is the default order type.
// Limit buy - joins the bid side
actions.buy(order_id, qty_1e8, px_1e9);
// Limit sell - joins the ask side
actions.sell(order_id, qty_1e8, px_1e9);
// Generic with explicit side (1=buy, -1=sell)
actions.order(order_id, side, qty_1e8, px_1e9);Parameters
| Parameter | Type | Description |
|---|---|---|
order_id | u64 | Unique ID (you generate this) |
qty_1e8 | i64 | Quantity × 10⁸ (1.0 = 100,000,000) |
px_1e9 | u64 | Price × 10⁹ ($100 = 100,000,000,000) |
Example: Quote Both Sides
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
let mid = book.mid_px_1e9();
let offset = mid / 2000; // 5 bps from mid
// Place bid below mid
self.next_id += 1;
actions.buy(self.next_id, 10_000_000, mid - offset);
// Place ask above mid
self.next_id += 1;
actions.sell(self.next_id, 10_000_000, mid + offset);
}Market Orders
Market orders execute immediately at the best available price. No price parameter needed.
// Market buy - takes from asks
actions.market_buy(order_id, qty_1e8);
// Market sell - hits bids
actions.market_sell(order_id, qty_1e8);Market orders have no price protection. Use limit orders with aggressive prices if you need a price cap.
Example: Flatten Position
fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
actions.cancel_all(state);
if state.position_1e8 > 0 {
self.next_id += 1;
actions.market_sell(self.next_id, state.position_1e8);
} else if state.position_1e8 < 0 {
self.next_id += 1;
actions.market_buy(self.next_id, -state.position_1e8);
}
}IOC Orders (Immediate-Or-Cancel)
IOC orders fill whatever liquidity is available at your price, then cancel the unfilled portion.
// IOC buy - take liquidity up to price limit
actions.ioc_buy(order_id, qty_1e8, px_1e9);
// IOC sell - hit bids down to price limit
actions.ioc_sell(order_id, qty_1e8, px_1e9);Example: Take Top of Book
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Take whatever's available at the best ask
if book.ask_ct > 0 {
let best_ask = &book.asks[0];
self.next_id += 1;
actions.ioc_buy(self.next_id, best_ask.sz_1e8 as i64, best_ask.px_1e9);
}
}FOK Orders (Fill-Or-Kill)
FOK orders must fill the entire quantity or are rejected completely. No partial fills.
// FOK buy - fill all or reject
actions.fok_buy(order_id, qty_1e8, px_1e9);
// FOK sell - fill all or reject
actions.fok_sell(order_id, qty_1e8, px_1e9);Example: All-Or-Nothing Entry
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Only enter if we can get the full size
let entry_size = 100_000_000; // 1.0 units
if book.ask_depth_1e8(3) >= entry_size as u64 {
self.next_id += 1;
// Take up to 3 levels if needed
let worst_price = book.asks[2].px_1e9;
actions.fok_buy(self.next_id, entry_size, worst_price);
}
}Post-Only Orders
Post-only orders are guaranteed to rest on the book as maker orders. If the order would immediately match (cross the spread), it's rejected instead. This ensures you always pay maker fees.
// Post-only buy - rests on bid side or rejected
actions.post_only_buy(order_id, qty_1e8, px_1e9);
// Post-only sell - rests on ask side or rejected
actions.post_only_sell(order_id, qty_1e8, px_1e9);Post-only orders are ideal for market making - you get maker fee rebates and avoid crossing the spread accidentally.
Example: Maker-Only Quoting
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
if book.spread_bps() < 5 { return; }
// Post-only bid at best bid - guaranteed maker
self.next_id += 1;
actions.post_only_buy(self.next_id, 100_000_000, book.bids[0].px_1e9);
// Post-only ask at best ask
self.next_id += 1;
actions.post_only_sell(self.next_id, 100_000_000, book.asks[0].px_1e9);
}
fn on_reject(&mut self, reject: &Reject) {
// Post-only orders that would cross get rejected - this is normal
log_info!("post-only reject: order={}", reject.order_id);
}Venue-Targeted Orders (Multi-Venue)
For single-venue algos that need venue-targeted orders, or custom executors in multi-venue strategies, use the _on variants. The venue_id comes from NbboSnapshot.venue_ids[slot].
// Limit buy on a specific venue
actions.buy_on(venue_id, order_id, qty_1e8, px_1e9);
// Limit sell on a specific venue
actions.sell_on(venue_id, order_id, qty_1e8, px_1e9);
// IOC buy on a specific venue
actions.ioc_buy_on(venue_id, order_id, qty_1e8, px_1e9);
// IOC sell on a specific venue
actions.ioc_sell_on(venue_id, order_id, qty_1e8, px_1e9);| Parameter | Type | Description |
|---|---|---|
venue_id | u8 | Target venue from NbboSnapshot.venue_ids[]. Use 0 for default/local venue. |
Example: Venue-Targeted Orders
// In a custom Executor's on_intent callback, or any context with Actions + venue_id
let venue_id: u8 = intent.venue_id;
actions.ioc_buy_on(venue_id, 1, 1_000_000, intent.limit_px_1e9 as u64);For multi-venue strategies, you typically don't use _on variants directly — instead you set venue_id on each LegIntent and the system routes to the correct edge. See Strategies & Executors.
Advanced: Typed Orders
For full control, use order_typed with explicit order type:
use algo_sdk::OrderType;
actions.order_typed(
order_id,
side, // 1=buy, -1=sell
qty_1e8,
px_1e9,
OrderType::IOC, // LIMIT, MARKET, IOC, FOK, or POST_ONLY
);Order Type Constants
pub mod OrderType {
pub const LIMIT: u8 = 0; // Default - rests on book
pub const MARKET: u8 = 1; // Immediate execution
pub const IOC: u8 = 2; // Immediate-or-cancel
pub const FOK: u8 = 3; // Fill-or-kill
pub const POST_ONLY: u8 = 4; // Maker-only, rejected if would cross
}Canceling Orders
Single Cancel
actions.cancel(order_id) -> boolCancel All Open Orders
actions.cancel_all(&state);Always call cancel_all in on_shutdown to clean up when the algo stops.
Example: Cancel and Replace
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Cancel all existing orders
for i in 0..state.order_ct as usize {
let order = &state.orders[i];
if order.is_live() {
actions.cancel(order.order_id);
}
}
// Place new orders at updated prices
self.next_id += 1;
actions.buy(self.next_id, 10_000_000, book.bids[0].px_1e9);
self.next_id += 1;
actions.sell(self.next_id, 10_000_000, book.asks[0].px_1e9);
}Order IDs
You generate order IDs. They must be unique for tracking fills and rejects.
struct MyAlgo {
next_id: u64,
}
impl Algo for MyAlgo {
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
self.next_id += 1;
actions.buy(self.next_id, 10_000_000, book.bids[0].px_1e9);
}
fn on_fill(&mut self, fill: &Fill, _state: &AlgoState) {
// fill.order_id matches what you sent
log_info!("Filled order {}", fill.order_id);
}
}Buffer Management
| Method | Returns | Description |
|---|---|---|
len() | usize | Number of actions in buffer |
is_empty() | bool | Buffer has no actions |
is_full() | bool | Buffer at capacity (16) |
clear() | - | Clear all actions |
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
if actions.is_full() {
log_warn!("Action buffer full!");
return;
}
// Safe to add orders...
}The actions buffer is cleared before each callback. Previous actions are not preserved.
Risk Checks
Before orders reach the exchange, the server applies risk checks:
| Check | Description | Reject Code |
|---|---|---|
| Position Limit | Order would exceed max position | POSITION_LIMIT |
| Rate Limit | Too many orders per second | RATE_LIMIT |
| Notional Limit | Order value too large | RISK |
| Price Deviation | Limit price too far from reference book side | PRICE_DEVIATION |
| Daily Loss Limit | Session loss threshold breached (algo paused) | DAILY_LOSS_LIMIT |
| Kill Switch | Trading halted | KILL_SWITCH |
| Balance Check | Insufficient funds | INSUFFICIENT_BALANCE |
| Bad Venue | Target venue_id not in registered venue set (multi-venue) | BAD_VENUE |
| Stale Venue | Target venue data is too old to trade (multi-venue) | STALE_VENUE |
Failed orders trigger on_reject:
use algo_sdk::RejectCode;
fn on_reject(&mut self, reject: &Reject) {
match reject.code {
RejectCode::POSITION_LIMIT => {
log_warn!("Hit position limit on order {}", reject.order_id);
}
RejectCode::RATE_LIMIT => {
log_warn!("Rate limited, backing off");
}
RejectCode::INSUFFICIENT_BALANCE => {
log_error!("Not enough funds!");
}
_ => {
log_error!("Reject {}: {}", reject.order_id, reject.reason());
}
}
}reject.reason() returns normalized reason text. Exchange-native reasons are also surfaced in edge logs (for example EOrder:Rate limit exceeded on Kraken).
Your algo can read its risk limits via state.risk (RiskSnapshot) and pre-check orders before sending - saving action buffer slots and avoiding rejects. See Algo State - Risk Limits for details.
Complete Example: Aggressive Market Maker
use algo_sdk::*;
struct AggressiveMM {
next_id: u64,
max_position: i64,
order_size: i64,
}
impl Algo for AggressiveMM {
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Skip if spread too tight
if book.spread_bps() < 3 { return; }
// Cancel stale orders
actions.cancel_all(state);
let pos = state.position_1e8;
let mid = book.mid_px_1e9();
// Skew quotes based on inventory
let skew = (pos * 100) / self.max_position; // -100 to +100
let bid_offset = mid / 5000 + (skew as u64 * mid / 100000);
let ask_offset = mid / 5000 - (skew as u64 * mid / 100000);
// Quote if we have room
if pos < self.max_position {
self.next_id += 1;
actions.buy(self.next_id, self.order_size, mid - bid_offset);
}
if pos > -self.max_position {
self.next_id += 1;
actions.sell(self.next_id, self.order_size, mid + ask_offset);
}
}
fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
log_warn!("FILL: {} @ {} | pos={}",
fill.qty_1e8, fill.px_1e9, state.position_1e8);
}
fn on_reject(&mut self, reject: &Reject) {
log_error!("REJECT: order={} reason={}", reject.order_id, reject.reason());
}
fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
actions.cancel_all(state);
// Flatten with market orders
if state.position_1e8 > 0 {
self.next_id += 1;
actions.market_sell(self.next_id, state.position_1e8);
} else if state.position_1e8 < 0 {
self.next_id += 1;
actions.market_buy(self.next_id, -state.position_1e8);
}
}
}
export_algo!(AggressiveMM {
next_id: 0,
max_position: 500_000_000, // 5 units
order_size: 50_000_000, // 0.5 units
});