orderbook-rs 0.13.1

A high-performance, lock-free price level implementation for limit order books in Rust. This library provides the building blocks for creating efficient trading systems with support for multiple order types and concurrent access patterns.
Documentation
//! Operational Prometheus-style metrics for the order book core.
//!
//! Issue #60 — feature-gated, additive observability hooks. When the
//! `metrics` feature is enabled the helpers in this module forward to
//! the `metrics` crate's global recorder; when the feature is off every
//! helper compiles down to a no-op so that call-sites in the matching
//! hot path stay unconditional and allocation-free.
//!
//! # Metrics surface
//!
//! - `orderbook_rejects_total{reason="…"}` — counter, incremented on
//!   every rejection that flows through `record_reject`. The label
//!   value is the [`RejectReason`] [`Display`] string (stable across
//!   `0.7.x`).
//! - `orderbook_depth_levels_bid` / `orderbook_depth_levels_ask` —
//!   gauges, updated on every book change to reflect the current count
//!   of distinct price levels on each side.
//! - `orderbook_trades_total` — counter, incremented exactly once per
//!   emitted trade transaction (a `MatchResult` may contain several).
//! - `orderbook_reserve_discards_total` /
//!   `orderbook_reserve_hidden_discarded_total` — counters, incremented
//!   whenever a reserve without automatic replenishment loses its hidden
//!   tranche because its visible one was exhausted (#230). The first
//!   counts orders, the second sums the hidden quantity that was dropped;
//!   neither can be derived from the other. Both sides of the trade feed
//!   them: the aggressive residual guard in `modifications.rs`
//!   (`add_order_inner`) and the maker removal in `matching.rs`, which
//!   `pricelevel` performs when a depleted non-replenishing maker leaves
//!   its level. The matching `INFO` trace carries a `path` field
//!   (`"taker"` / `"maker"`) so the two are distinguishable.
//!
//! # Determinism
//!
//! Metrics emission is **out-of-band**: it does not influence matching,
//! does not allocate on the happy path, and does not cross the
//! determinism boundary. `restore_from_snapshot_package` deliberately
//! does **not** rehydrate metric counters — they are operational only
//! and live for the process lifetime.
//!
//! [`RejectReason`]: crate::orderbook::reject_reason::RejectReason
//! [`Display`]: std::fmt::Display

use crate::orderbook::reject_reason::RejectReason;

/// Counter name: total order rejections, labelled by reject reason.
pub const REJECTS_TOTAL: &str = "orderbook_rejects_total";

/// Gauge name: current count of distinct bid price levels.
pub const DEPTH_LEVELS_BID: &str = "orderbook_depth_levels_bid";

/// Gauge name: current count of distinct ask price levels.
pub const DEPTH_LEVELS_ASK: &str = "orderbook_depth_levels_ask";

/// Counter name: monotonic count of every emitted trade transaction.
pub const TRADES_TOTAL: &str = "orderbook_trades_total";

/// Counter name: reserve residuals discarded for lack of automatic
/// replenishment (#230), counted in orders.
pub const RESERVE_DISCARDS_TOTAL: &str = "orderbook_reserve_discards_total";

/// Counter name: hidden quantity dropped by those discards (#230),
/// counted in quantity units.
pub const RESERVE_HIDDEN_DISCARDED_TOTAL: &str = "orderbook_reserve_hidden_discarded_total";

/// Record an order rejection.
///
/// Increments `orderbook_rejects_total` by 1 with the
/// `reason="<RejectReason::Display>"` label. Compiles to a no-op when
/// the `metrics` feature is disabled.
#[inline]
#[cfg(feature = "metrics")]
pub fn record_reject(reason: RejectReason) {
    let label = reason.to_string();
    metrics::counter!(REJECTS_TOTAL, "reason" => label).increment(1);
}

/// No-op when the `metrics` feature is disabled.
#[inline]
#[cfg(not(feature = "metrics"))]
pub fn record_reject(_reason: RejectReason) {}

/// Update the bid / ask depth gauges to the supplied counts.
///
/// Called from book-change emission paths. Compiles to a no-op when
/// the `metrics` feature is disabled.
#[inline]
#[cfg(feature = "metrics")]
pub fn record_depth(bid_levels: u64, ask_levels: u64) {
    // `gauge!` accepts an `f64`; the input is a level count that
    // comfortably fits in `f64` precision for any realistic book.
    metrics::gauge!(DEPTH_LEVELS_BID).set(bid_levels as f64);
    metrics::gauge!(DEPTH_LEVELS_ASK).set(ask_levels as f64);
}

/// No-op when the `metrics` feature is disabled.
#[inline]
#[cfg(not(feature = "metrics"))]
pub fn record_depth(_bid_levels: u64, _ask_levels: u64) {}

/// Record `n` newly emitted trade transactions.
///
/// Called once per `TradeListener` callback with the number of
/// transactions in the underlying `MatchResult`. Compiles to a no-op
/// when the `metrics` feature is disabled.
#[inline]
#[cfg(feature = "metrics")]
pub fn record_trades(n: u64) {
    if n == 0 {
        return;
    }
    metrics::counter!(TRADES_TOTAL).increment(n);
}

/// No-op when the `metrics` feature is disabled.
#[inline]
#[cfg(not(feature = "metrics"))]
pub fn record_trades(_n: u64) {}

/// Record one discarded reserve residual and the hidden quantity it
/// dropped (#230).
///
/// Increments `orderbook_reserve_discards_total` by 1 and
/// `orderbook_reserve_hidden_discarded_total` by `quantity`. Called once
/// per dropped order from each of the two paths that can drop one, both
/// requiring `auto_replenish == false` and an exhausted visible tranche:
///
/// - the aggressive residual guard in `add_order_inner`
///   (`modifications.rs`), where the taker's residual is discarded instead
///   of rested;
/// - the maker removal in `match_order_inner` (`matching.rs`), where
///   `pricelevel` takes a resting maker off its level and strands the
///   hidden depth behind it.
///
/// A zero `quantity` is not a discard and is ignored. Compiles to a no-op
/// when the `metrics` feature is disabled.
#[inline]
#[cfg(feature = "metrics")]
pub fn record_reserve_hidden_discarded(quantity: u64) {
    if quantity == 0 {
        return;
    }
    metrics::counter!(RESERVE_DISCARDS_TOTAL).increment(1);
    metrics::counter!(RESERVE_HIDDEN_DISCARDED_TOTAL).increment(quantity);
}

/// No-op when the `metrics` feature is disabled.
#[inline]
#[cfg(not(feature = "metrics"))]
pub fn record_reserve_hidden_discarded(_quantity: u64) {}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every call-site must compile and run without panicking
    /// regardless of feature state. The actual counter behaviour is
    /// covered by `tests/metrics/` (feature-gated).
    #[test]
    fn helpers_are_callable_unconditionally() {
        record_reject(RejectReason::KillSwitchActive);
        record_reject(RejectReason::Other(7777));
        record_depth(0, 0);
        record_depth(3, 5);
        record_trades(0);
        record_trades(4);
        record_reserve_hidden_discarded(0);
        record_reserve_hidden_discarded(20);
    }
}