Sequence/docs

PnL & Timing

The SDK gives you full access to realized and unrealized PnL, plus timing primitives for measuring fill latency and round-trip times - all without heap allocation.


PnL Access

Direct Fields

AlgoState carries both realized and unrealized PnL, updated by the server on every book tick and fill:

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    // Fixed-point access (1e9 = $1.00)
    let realized = state.realized_pnl_1e9;
    let unrealized = state.unrealized_pnl_1e9;
    let total = state.total_pnl_1e9();  // realized + unrealized
 
    // USD float helpers (convenience, not for hot-path math)
    let total_usd = state.total_pnl_usd();  // e.g. -0.042
}
Note

unrealized_pnl_1e9 is mark-to-mid - the server recalculates it from your position and the current book mid price before every on_book call.

PnL Snapshot

For strategies that want a single call:

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let pnl = state.get_pnl();
 
    log_info!("r={} u={} t={}",
        pnl.realized_1e9,
        pnl.unrealized_1e9,
        pnl.total_1e9
    );
}
rust
pub struct PnlSnapshot {
    pub realized_1e9: i64,
    pub unrealized_1e9: i64,
    pub total_1e9: i64,
}

USD Convenience Methods

MethodReturnsDescription
realized_pnl_usd()f64Realized PnL in USD
unrealized_pnl_usd()f64Unrealized PnL in USD
total_pnl_usd()f64Total PnL in USD
get_pnl()PnlSnapshotAll three in fixed-point
total_pnl_1e9()i64Realized + unrealized (fixed-point)
Warning

The _usd() methods use f64 division - fine for logging, but use the _1e9 fields for any arithmetic in your trading logic.

Session PnL

session_pnl_1e9 tracks realized PnL for the current trading session, resetting at UTC midnight. Use it for intraday risk management without tracking resets yourself.

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    // Session PnL (resets at UTC midnight)
    let session_pnl = state.session_pnl_1e9;
    let session_usd = state.session_pnl_usd();
 
    // Lifetime fill counter (never resets)
    let fills = state.total_fill_count;
 
    log_info!("session=${:.2} fills={}", session_usd, fills);
}
Field / MethodTypeDescription
session_pnl_1e9i64Session realized PnL (resets at UTC midnight)
session_pnl_usd()f64Session PnL as USD float
total_fill_countu64Lifetime fill count (never resets)
Note

session_pnl_1e9 only includes realized PnL from fills - it does not include unrealized mark-to-market. total_fill_count is a lifetime counter useful for diagnostics.

Example: Intraday Loss Limit

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    // Stop trading if session loss exceeds $10
    if state.session_pnl_1e9 < -10_000_000_000 {
        log_error!("Session loss limit: ${:.2}", state.session_pnl_usd());
        actions.cancel_all(state);
        return;
    }
 
    // Normal trading logic...
}

Example: Lifetime Loss Limit

rust
const MAX_LOSS_1E9: i64 = -5_000_000_000; // -$5.00
 
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    if state.total_pnl_1e9() < MAX_LOSS_1E9 {
        log_error!("Lifetime loss limit hit: ${:.2}", state.total_pnl_usd());
        actions.cancel_all(state);
        return;
    }
 
    // Normal trading logic...
}

Fill Timing

Every Fill carries a recv_ns timestamp - the nanosecond wall-clock time when the fill arrived at the edge. Use it to measure order-to-fill latency.

Fill Struct

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

Elapsed-Time Helpers

rust
fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
    // Measure time from when you sent the order to when it filled
    let latency_ms = fill.since_ms(self.order_sent_ns);
    let latency_us = fill.since_us(self.order_sent_ns);
    let latency_ns = fill.since_ns(self.order_sent_ns);
 
    log_warn!("FILL: order={} latency={}ms", fill.order_id, latency_ms);
}
MethodReturnsDescription
since_ns(start_ns)u64recv_ns - start_ns (saturating)
since_us(start_ns)u64Elapsed microseconds
since_ms(start_ns)u64Elapsed milliseconds
Tip

Use book.recv_ns as your start_ns - capture it when you send the order, then compare in on_fill.

Example: Round-Trip Measurement

rust
struct MyAlgo {
    next_id: u64,
    order_sent_ns: u64,
}
 
impl Algo for MyAlgo {
    fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
        if state.is_flat() {
            self.next_id += 1;
            self.order_sent_ns = book.recv_ns;  // Capture send time
            actions.market_buy(self.next_id, 100_000_000);
        }
    }
 
    fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
        let ms = fill.since_ms(self.order_sent_ns);
        log_warn!("FILL: {}ms round-trip", ms);
    }
 
    fn on_reject(&mut self, _reject: &Reject) {}
    fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
        actions.cancel_all(state);
    }
}

Timing Primitives

The time module provides standalone timing functions and a stateful Timer struct. These are zero-allocation and work in both WASM and native algos.

Free Functions

rust
use algo_sdk::time;
 
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let t0 = time::start(book.recv_ns);
 
    // ... your logic ...
 
    let elapsed_us = time::stop_us(t0, book.recv_ns);
    let elapsed_ms = time::stop_ms(t0, book.recv_ns);
    let elapsed_ns = time::stop_ns(t0, book.recv_ns);
}
FunctionReturnsDescription
time::start(now_ns)u64Returns now_ns (alias for clarity)
time::stop_ns(start, now)u64Elapsed nanoseconds
time::stop_us(start, now)u64Elapsed microseconds
time::stop_ms(start, now)u64Elapsed milliseconds

Timer Struct

For strategies that need multiple named timers:

rust
use algo_sdk::time::Timer;
 
struct MyAlgo {
    bid_timer: Timer,
    ask_timer: Timer,
    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;
        self.bid_timer.start(book.recv_ns);
        actions.post_only_buy(self.next_id, 100_000_000, book.bids[0].px_1e9);
    }
 
    fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
        let ms = self.bid_timer.stop_ms(fill.recv_ns);
        log_warn!("bid fill latency={}ms", ms);
    }
 
    fn on_reject(&mut self, _reject: &Reject) {}
    fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
        actions.cancel_all(state);
    }
}
MethodDescription
Timer::new()Create (const, zero-init)
.start(now_ns)Record start time
.stop_ns(now_ns)Elapsed nanoseconds
.stop_us(now_ns)Elapsed microseconds
.stop_ms(now_ns)Elapsed milliseconds

Dashboard Visibility

PnL and timing data flows through to the dashboard and is available in your algo:

  • In your algo: state.realized_pnl_1e9 (lifetime), state.session_pnl_1e9 (resets at UTC midnight), state.unrealized_pnl_1e9 (mark-to-mid), state.total_fill_count (lifetime fills)
  • Per-edge detail (GET /v1/algos?detail=edge) includes realized_pnl_1e9, unrealized_pnl_1e9, session_realized_pnl_1e9
  • Algo Studio shows rPnL, uPnL, sPnL per edge and a color-coded Total PnL in the strategy summary
  • Algo logs show whatever you log_*!() - the SDK doesn't auto-log PnL or timing; you control what gets logged
Tip

Log PnL on fills (important events) and periodically in on_book (e.g. every N updates), not on every tick.