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:
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
}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:
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
);
}pub struct PnlSnapshot {
pub realized_1e9: i64,
pub unrealized_1e9: i64,
pub total_1e9: i64,
}USD Convenience Methods
| Method | Returns | Description |
|---|---|---|
realized_pnl_usd() | f64 | Realized PnL in USD |
unrealized_pnl_usd() | f64 | Unrealized PnL in USD |
total_pnl_usd() | f64 | Total PnL in USD |
get_pnl() | PnlSnapshot | All three in fixed-point |
total_pnl_1e9() | i64 | Realized + unrealized (fixed-point) |
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.
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 / Method | Type | Description |
|---|---|---|
session_pnl_1e9 | i64 | Session realized PnL (resets at UTC midnight) |
session_pnl_usd() | f64 | Session PnL as USD float |
total_fill_count | u64 | Lifetime fill count (never resets) |
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
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
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
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
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);
}| Method | Returns | Description |
|---|---|---|
since_ns(start_ns) | u64 | recv_ns - start_ns (saturating) |
since_us(start_ns) | u64 | Elapsed microseconds |
since_ms(start_ns) | u64 | Elapsed milliseconds |
Use book.recv_ns as your start_ns - capture it when you send the order, then compare in on_fill.
Example: Round-Trip Measurement
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
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);
}| Function | Returns | Description |
|---|---|---|
time::start(now_ns) | u64 | Returns now_ns (alias for clarity) |
time::stop_ns(start, now) | u64 | Elapsed nanoseconds |
time::stop_us(start, now) | u64 | Elapsed microseconds |
time::stop_ms(start, now) | u64 | Elapsed milliseconds |
Timer Struct
For strategies that need multiple named timers:
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);
}
}| Method | Description |
|---|---|
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) includesrealized_pnl_1e9,unrealized_pnl_1e9,session_realized_pnl_1e9 - Algo Studio shows
rPnL,uPnL,sPnLper 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
Log PnL on fills (important events) and periodically in on_book (e.g. every N updates), not on every tick.