Overview
AlgoState contains your server-managed position and open orders. This is authoritative—you don't need to track fills yourself.
pub struct AlgoState {
// Position
pub position_1e8: i64, // Net position (positive = long)
pub avg_entry_1e9: u64, // Average entry price
pub realized_pnl_1e9: i64, // Realized PnL (lifetime)
pub unrealized_pnl_1e9: i64, // Unrealized PnL (mark-to-market)
// Orders
pub orders: [OpenOrder; 32], // Your open orders
pub order_ct: u8, // Number of orders
// Session & diagnostics (new in v0.2)
pub session_pnl_1e9: i64, // Session realized PnL (resets at UTC midnight)
pub total_fill_count: u64, // Lifetime fill count (never resets)
// Symbol metadata (new in v0.2)
pub symbol: SymbolMeta, // Tick size, lot size, min qty, min notional
// Risk limits (new in v0.2)
pub risk: RiskSnapshot, // Current risk limits for this algo
}The server calculates unrealized_pnl_1e9 using the mid price from the current book. session_pnl_1e9 resets at UTC midnight; total_fill_count is lifetime and never resets.
symbol and risk are populated by the runtime from venue pair specs and your risk configuration. All fields use zero-means-unknown semantics - helpers return safe defaults when data is unavailable.
Position
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Check your position
if state.is_flat() {
// No position
} else if state.is_long() {
// Long position
let size = state.position_1e8; // Positive
} else {
// Short position
let size = state.position_1e8; // Negative
}
// Position limit check
if state.position_1e8.abs() > 10_000_000_000 {
return; // Over limit, don't trade
}
}Position Methods
| Method | Returns | Description |
|---|---|---|
is_flat() | bool | Position is zero |
is_long() | bool | Position > 0 |
is_short() | bool | Position < 0 |
total_pnl_1e9() | i64 | Realized + unrealized PnL |
get_pnl() | PnlSnapshot | Snapshot of realized, unrealized, total |
realized_pnl_usd() | f64 | Realized PnL as USD float |
unrealized_pnl_usd() | f64 | Unrealized PnL as USD float |
total_pnl_usd() | f64 | Total PnL as USD float |
session_pnl_usd() | f64 | Session realized PnL as USD float |
See PnL & Timing for full PnL access patterns, PnlSnapshot, session PnL, and latency measurement.
Symbol Metadata
SymbolMeta gives your algo access to the trading pair's tick size, lot size, and minimum order constraints - populated from venue data at deploy time.
pub struct SymbolMeta {
pub tick_size_1e9: u64, // Min price increment (e.g. 10_000_000 = $0.01)
pub lot_size_1e8: u64, // Min qty increment (e.g. 1_000 = 0.00001)
pub min_qty_1e8: u64, // Min order quantity
pub min_notional_1e9: u64, // Min order value (price × qty)
pub price_precision: u8, // Decimal places for prices
pub qty_precision: u8, // Decimal places for quantities
}SymbolMeta Helpers
| Method | Returns | Description |
|---|---|---|
round_px(px_1e9) | u64 | Round price DOWN to nearest tick |
round_qty(qty_1e8) | i64 | Round quantity DOWN to nearest lot |
check_notional(qty, px) | bool | True if order meets min notional |
check_min_qty(qty) | bool | True if quantity meets minimum |
All helpers treat 0 as "unknown" - round_px returns the input unchanged, check_* returns true. This prevents false rejects when venue data is unavailable.
Example: Tick-Safe Quoting
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
let meta = &state.symbol;
let mid = book.mid_px_1e9();
let offset = mid / 2000; // 5 bps
// Round prices to valid tick increments
let bid_px = meta.round_px(mid - offset);
let ask_px = meta.round_px(mid + offset);
// Validate quantity before sending
let qty = 10_000_000i64;
if meta.check_min_qty(qty) && meta.check_notional(qty, bid_px) {
self.next_id += 1;
actions.buy(self.next_id, qty, bid_px);
self.next_id += 1;
actions.sell(self.next_id, qty, ask_px);
}
}Risk Limits
RiskSnapshot exposes the current risk limits applied to your algo. Use it to pre-check orders before sending, saving action slots and avoiding rejects.
pub struct RiskSnapshot {
pub max_position_1e8: i64, // Max absolute position
pub max_daily_loss_1e9: i64, // Daily loss limit (0 = disabled)
pub max_order_notional_1e9: u64, // Max single order value
pub max_order_rate: u32, // Max orders/sec
pub reduce_only: u8, // 1 = reduce-only mode active
}Example: Pre-Check Before Ordering
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
let risk = &state.risk;
// Skip if reduce-only mode is active
if risk.reduce_only == 1 && state.is_flat() { return; }
// Pre-check position limit
let desired_qty = 10_000_000i64;
if (state.position_1e8 + desired_qty).abs() > risk.max_position_1e8 {
return; // Would exceed position limit
}
self.next_id += 1;
actions.buy(self.next_id, desired_qty, book.bids[0].px_1e9);
}Pre-checking risk limits in your algo saves action buffer slots (you only have 16 per tick) and avoids the round-trip overhead of a server-side reject.
Open Orders
Each order you send is tracked server-side:
pub struct OpenOrder {
pub order_id: u64, // Your order ID
pub px_1e9: u64, // Limit price
pub qty_1e8: i64, // Signed quantity
pub filled_1e8: i64, // Amount filled
pub side: i8, // 1 = buy, -1 = sell
pub status: u8, // PENDING, ACKED, PARTIAL, DEAD
}Order Status
| Status | Value | Description |
|---|---|---|
PENDING | 0 | Sent, awaiting exchange ack |
ACKED | 1 | Acknowledged by exchange |
PARTIAL | 2 | Partially filled |
DEAD | 3 | Filled, cancelled, or rejected |
use algo_sdk::Status;
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
// Iterate open orders
for i in 0..state.order_ct as usize {
let order = &state.orders[i];
if order.status == Status::ACKED || order.status == Status::PARTIAL {
// This order is live on the exchange
let remaining = order.remaining_1e8();
}
}
}Order Methods
| Method | Returns | Description |
|---|---|---|
is_live() | bool | ACKED or PARTIAL |
is_pending() | bool | PENDING status |
remaining_1e8() | i64 | qty - filled |
State Methods
| Method | Returns | Description |
|---|---|---|
has_orders() | bool | Has any orders |
live_order_count() | usize | Count of ACKED/PARTIAL orders |
find_order(id) | Option<&OpenOrder> | Find by order ID |
open_buy_qty_1e8() | i64 | Total open buy quantity |
open_sell_qty_1e8() | i64 | Total open sell quantity |
Example: Inventory Management
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
let pos = state.position_1e8;
let max_pos = 1_000_000_000; // 10 units
// Calculate available capacity
let buy_capacity = max_pos - pos - state.open_buy_qty_1e8();
let sell_capacity = max_pos + pos - state.open_sell_qty_1e8();
// Only bid if we have buy capacity
if buy_capacity > 10_000_000 {
self.next_id += 1;
actions.buy(self.next_id, 10_000_000, book.bids[0].px_1e9);
}
// Only offer if we have sell capacity
if sell_capacity > 10_000_000 {
self.next_id += 1;
actions.sell(self.next_id, 10_000_000, book.asks[0].px_1e9);
}
}Example: Cancel Stale Orders
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
let mid = book.mid_px_1e9();
let threshold = mid / 1000; // 0.1% from mid
for i in 0..state.order_ct as usize {
let order = &state.orders[i];
if !order.is_live() { continue; }
// Cancel if price is too far from mid
let distance = if order.px_1e9 > mid {
order.px_1e9 - mid
} else {
mid - order.px_1e9
};
if distance > threshold {
actions.cancel(order.order_id);
}
}
}Events
Fill
pub struct Fill {
pub order_id: u64,
pub px_1e9: u64,
pub qty_1e8: i64,
pub recv_ns: u64, // Fill receive timestamp (nanoseconds)
pub side: i8, // 1 = buy, -1 = sell
}
fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
// State already updated with this fill
let latency_ms = fill.since_ms(self.order_sent_ns);
log_warn!("FILL: {} @ {} latency={}ms pos={}",
fill.qty_1e8, fill.px_1e9, latency_ms, state.position_1e8);
}Fill Timing Helpers
| Method | Returns | Description |
|---|---|---|
since_ns(start_ns) | u64 | Elapsed nanoseconds since start_ns |
since_us(start_ns) | u64 | Elapsed microseconds |
since_ms(start_ns) | u64 | Elapsed milliseconds |
Capture book.recv_ns when you send an order, then use fill.since_ms(sent_ns) to measure round-trip latency. See PnL & Timing for full examples.
Reject
pub struct Reject {
pub order_id: u64,
pub code: u8,
}
fn on_reject(&mut self, reject: &Reject) {
use algo_sdk::RejectCode;
match reject.code {
RejectCode::INSUFFICIENT_BALANCE => {
log_error!("Not enough funds for order {}", reject.order_id);
}
RejectCode::INVALID_PARAMS => {
log_error!("Invalid params on order {}", reject.order_id);
}
RejectCode::RATE_LIMIT => {
log_warn!("Rate limited, backing off");
}
RejectCode::POSITION_LIMIT => {
log_warn!("Position limit hit");
}
RejectCode::KILL_SWITCH => {
log_error!("Kill switch active!");
}
_ => {
log_warn!("Reject {}: {}", reject.order_id, reject.reason());
}
}
}Reject Codes
| Code | Value | Description |
|---|---|---|
UNKNOWN | 0 | Unknown error |
INSUFFICIENT_BALANCE | 1 | Not enough funds on exchange |
INVALID_PARAMS | 2 | Invalid price, quantity, or symbol |
RATE_LIMIT | 3 | Exchange rate limit hit |
EXCHANGE_BUSY | 4 | Exchange temporarily unavailable |
NETWORK | 5 | Network error |
AUTH | 6 | Invalid API key/secret |
RISK | 100 | Internal risk check failed |
POSITION_LIMIT | 101 | Would exceed position limit |
KILL_SWITCH | 102 | Kill switch is active |
PRICE_DEVIATION | 103 | Price too far from reference |
DAILY_LOSS_LIMIT | 104 | Daily loss limit breached |
Use reject.reason() to get a human-readable string for logging.