Sequence/docs

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:

TypeDescriptionUse Case
LIMITSits on book until filled or canceledPassive market making
MARKETFills immediately at best availableAggressive taking
IOCFill what you can, cancel the restTake available liquidity
FOKFill entire quantity or rejectAll-or-nothing execution
POST_ONLYRejected if it would cross the spreadMaker-only, fee-friendly

Limit Orders (Default)

Limit orders rest on the order book until filled or canceled. This is the default order type.

rust
// 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

ParameterTypeDescription
order_idu64Unique ID (you generate this)
qty_1e8i64Quantity × 10⁸ (1.0 = 100,000,000)
px_1e9u64Price × 10⁹ ($100 = 100,000,000,000)

Example: Quote Both Sides

rust
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.

rust
// Market buy - takes from asks
actions.market_buy(order_id, qty_1e8);
 
// Market sell - hits bids
actions.market_sell(order_id, qty_1e8);
Warning

Market orders have no price protection. Use limit orders with aggressive prices if you need a price cap.

Example: Flatten Position

rust
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.

rust
// 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

rust
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.

rust
// 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

rust
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.

rust
// 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);
Tip

Post-only orders are ideal for market making - you get maker fee rebates and avoid crossing the spread accidentally.

Example: Maker-Only Quoting

rust
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].

rust
// 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);
ParameterTypeDescription
venue_idu8Target venue from NbboSnapshot.venue_ids[]. Use 0 for default/local venue.

Example: Venue-Targeted Orders

rust
// 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:

rust
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

rust
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

rust
actions.cancel(order_id) -> bool

Cancel All Open Orders

rust
actions.cancel_all(&state);
Tip

Always call cancel_all in on_shutdown to clean up when the algo stops.

Example: Cancel and Replace

rust
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.

rust
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

MethodReturnsDescription
len()usizeNumber of actions in buffer
is_empty()boolBuffer has no actions
is_full()boolBuffer at capacity (16)
clear()-Clear all actions
rust
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...
}
Warning

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:

CheckDescriptionReject Code
Position LimitOrder would exceed max positionPOSITION_LIMIT
Rate LimitToo many orders per secondRATE_LIMIT
Notional LimitOrder value too largeRISK
Price DeviationLimit price too far from reference book sidePRICE_DEVIATION
Daily Loss LimitSession loss threshold breached (algo paused)DAILY_LOSS_LIMIT
Kill SwitchTrading haltedKILL_SWITCH
Balance CheckInsufficient fundsINSUFFICIENT_BALANCE
Bad VenueTarget venue_id not in registered venue set (multi-venue)BAD_VENUE
Stale VenueTarget venue data is too old to trade (multi-venue)STALE_VENUE

Failed orders trigger on_reject:

rust
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());
        }
    }
}
Note

reject.reason() returns normalized reason text. Exchange-native reasons are also surfaced in edge logs (for example EOrder:Rate limit exceeded on Kraken).

Tip

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

rust
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
});