use std::{
cmp::Ordering,
collections::BTreeMap,
fmt::{Debug, Display},
hash::{Hash, Hasher},
};
use ahash::{AHashMap, AHashSet};
use indexmap::IndexMap;
use nautilus_core::{UnixNanos, time::nanos_since_unix_epoch};
use rust_decimal::Decimal;
use super::{BookViewError, display::pprint_own_book};
use crate::{
enums::{OrderSideSpecified, OrderStatus, OrderType, TimeInForce},
identifiers::{ClientOrderId, InstrumentId, TraderId, VenueOrderId},
orderbook::BookPrice,
orders::{Order, OrderAny},
types::{Price, Quantity},
};
#[repr(C)]
#[derive(Clone, Copy, Eq)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct OwnBookOrder {
pub trader_id: TraderId,
pub client_order_id: ClientOrderId,
pub venue_order_id: Option<VenueOrderId>,
pub side: OrderSideSpecified,
pub price: Price,
pub size: Quantity,
pub order_type: OrderType,
pub time_in_force: TimeInForce,
pub status: OrderStatus,
pub ts_last: UnixNanos,
pub ts_accepted: UnixNanos,
pub ts_submitted: UnixNanos,
pub ts_init: UnixNanos,
}
impl OwnBookOrder {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
trader_id: TraderId,
client_order_id: ClientOrderId,
venue_order_id: Option<VenueOrderId>,
side: OrderSideSpecified,
price: Price,
size: Quantity,
order_type: OrderType,
time_in_force: TimeInForce,
status: OrderStatus,
ts_last: UnixNanos,
ts_accepted: UnixNanos,
ts_submitted: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self {
trader_id,
client_order_id,
venue_order_id,
side,
price,
size,
order_type,
time_in_force,
status,
ts_last,
ts_accepted,
ts_submitted,
ts_init,
}
}
#[must_use]
pub fn to_book_price(&self) -> BookPrice {
BookPrice::new(self.price, self.side)
}
#[must_use]
pub fn exposure(&self) -> f64 {
self.price.as_f64() * self.size.as_f64()
}
#[must_use]
pub fn signed_size(&self) -> f64 {
match self.side {
OrderSideSpecified::Buy => self.size.as_f64(),
OrderSideSpecified::Sell => -(self.size.as_f64()),
}
}
}
impl Ord for OwnBookOrder {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.ts_init
.cmp(&other.ts_init)
.then_with(|| self.client_order_id.cmp(&other.client_order_id))
}
}
impl PartialOrd for OwnBookOrder {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for OwnBookOrder {
fn eq(&self, other: &Self) -> bool {
self.client_order_id == other.client_order_id
}
}
impl Hash for OwnBookOrder {
fn hash<H: Hasher>(&self, state: &mut H) {
self.client_order_id.hash(state);
}
}
impl Debug for OwnBookOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(trader_id={}, client_order_id={}, venue_order_id={:?}, side={}, price={}, size={}, order_type={}, time_in_force={}, status={}, ts_last={}, ts_accepted={}, ts_submitted={}, ts_init={})",
stringify!(OwnBookOrder),
self.trader_id,
self.client_order_id,
self.venue_order_id,
self.side,
self.price,
self.size,
self.order_type,
self.time_in_force,
self.status,
self.ts_last,
self.ts_accepted,
self.ts_submitted,
self.ts_init,
)
}
}
impl Display for OwnBookOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{},{},{:?},{},{},{},{},{},{},{},{},{},{}",
self.trader_id,
self.client_order_id,
self.venue_order_id,
self.side,
self.price,
self.size,
self.order_type,
self.time_in_force,
self.status,
self.ts_last,
self.ts_accepted,
self.ts_submitted,
self.ts_init,
)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct OwnOrderBook {
pub instrument_id: InstrumentId,
pub ts_last: UnixNanos,
pub update_count: u64,
pub(crate) bids: OwnBookLadder,
pub(crate) asks: OwnBookLadder,
}
impl PartialEq for OwnOrderBook {
fn eq(&self, other: &Self) -> bool {
self.instrument_id == other.instrument_id
}
}
impl Display for OwnOrderBook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(instrument_id={}, orders={}, update_count={})",
stringify!(OwnOrderBook),
self.instrument_id,
self.bids.cache.len() + self.asks.cache.len(),
self.update_count,
)
}
}
impl OwnOrderBook {
#[must_use]
pub fn new(instrument_id: InstrumentId) -> Self {
Self {
instrument_id,
ts_last: UnixNanos::default(),
update_count: 0,
bids: OwnBookLadder::new(OrderSideSpecified::Buy),
asks: OwnBookLadder::new(OrderSideSpecified::Sell),
}
}
fn increment(&mut self, order: &OwnBookOrder) {
self.ts_last = order.ts_last;
self.update_count += 1;
}
pub fn reset(&mut self) {
self.bids.clear();
self.asks.clear();
self.ts_last = UnixNanos::default();
self.update_count = 0;
}
pub fn add(&mut self, order: OwnBookOrder) {
self.increment(&order);
match order.side {
OrderSideSpecified::Buy => self.bids.add(order),
OrderSideSpecified::Sell => self.asks.add(order),
}
}
pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
let result = match order.side {
OrderSideSpecified::Buy => self.bids.update(order),
OrderSideSpecified::Sell => self.asks.update(order),
};
if result.is_ok() {
self.increment(&order);
}
result
}
pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
let result = match order.side {
OrderSideSpecified::Buy => self.bids.delete(order),
OrderSideSpecified::Sell => self.asks.delete(order),
};
if result.is_ok() {
self.increment(&order);
}
result
}
pub fn clear(&mut self) {
self.bids.clear();
self.asks.clear();
}
pub fn bids(&self) -> impl Iterator<Item = &OwnBookLevel> {
self.bids.levels.values()
}
pub fn asks(&self) -> impl Iterator<Item = &OwnBookLevel> {
self.asks.levels.values()
}
pub fn bid_client_order_ids(&self) -> Vec<ClientOrderId> {
self.bids.cache.keys().copied().collect()
}
pub fn ask_client_order_ids(&self) -> Vec<ClientOrderId> {
self.asks.cache.keys().copied().collect()
}
pub fn is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
self.asks.cache.contains_key(client_order_id)
|| self.bids.cache.contains_key(client_order_id)
}
pub fn bids_as_map(
&self,
status: Option<&AHashSet<OrderStatus>>,
accepted_buffer_ns: Option<u64>,
ts_now: Option<u64>,
) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
filter_orders(self.bids(), status, accepted_buffer_ns, ts_now)
}
pub fn asks_as_map(
&self,
status: Option<&AHashSet<OrderStatus>>,
accepted_buffer_ns: Option<u64>,
ts_now: Option<u64>,
) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
filter_orders(self.asks(), status, accepted_buffer_ns, ts_now)
}
pub fn bid_quantity(
&self,
status: Option<&AHashSet<OrderStatus>>,
depth: Option<usize>,
group_size: Option<Decimal>,
accepted_buffer_ns: Option<u64>,
ts_now: Option<u64>,
) -> IndexMap<Decimal, Decimal> {
let quantities = self
.bids_as_map(status, accepted_buffer_ns, ts_now)
.into_iter()
.map(|(price, orders)| (price, sum_order_sizes(orders.iter())))
.filter(|(_, quantity)| *quantity > Decimal::ZERO)
.collect::<IndexMap<Decimal, Decimal>>();
if let Some(group_size) = group_size {
group_quantities(quantities, group_size, depth, true)
} else if let Some(depth) = depth {
quantities.into_iter().take(depth).collect()
} else {
quantities
}
}
pub fn ask_quantity(
&self,
status: Option<&AHashSet<OrderStatus>>,
depth: Option<usize>,
group_size: Option<Decimal>,
accepted_buffer_ns: Option<u64>,
ts_now: Option<u64>,
) -> IndexMap<Decimal, Decimal> {
let quantities = self
.asks_as_map(status, accepted_buffer_ns, ts_now)
.into_iter()
.map(|(price, orders)| {
let quantity = sum_order_sizes(orders.iter());
(price, quantity)
})
.filter(|(_, quantity)| *quantity > Decimal::ZERO)
.collect::<IndexMap<Decimal, Decimal>>();
if let Some(group_size) = group_size {
group_quantities(quantities, group_size, depth, false)
} else if let Some(depth) = depth {
quantities.into_iter().take(depth).collect()
} else {
quantities
}
}
pub fn combined_with_opposite(&self, opposite: &Self) -> Result<Self, BookViewError> {
if self.instrument_id == opposite.instrument_id {
return Err(BookViewError::OppositeInstrumentMatch(
self.instrument_id,
opposite.instrument_id,
));
}
let mut combined = self.clone();
for level in opposite.asks() {
for order in level.iter() {
combined.add(transform_opposite_order(*order, OrderSideSpecified::Buy));
}
}
for level in opposite.bids() {
for order in level.iter() {
combined.add(transform_opposite_order(*order, OrderSideSpecified::Sell));
}
}
Ok(combined)
}
#[must_use]
pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
pprint_own_book(self, num_levels, group_size)
}
pub fn audit_open_orders(&mut self, open_order_ids: &AHashSet<ClientOrderId>) {
log::debug!("Auditing {self}");
let bids_to_remove: Vec<ClientOrderId> = self
.bids
.cache
.keys()
.filter(|&key| !open_order_ids.contains(key))
.copied()
.collect();
let asks_to_remove: Vec<ClientOrderId> = self
.asks
.cache
.keys()
.filter(|&key| !open_order_ids.contains(key))
.copied()
.collect();
for client_order_id in bids_to_remove {
log_audit_error(&client_order_id);
if let Err(e) = self.bids.remove(&client_order_id) {
log::error!("{e}");
}
}
for client_order_id in asks_to_remove {
log_audit_error(&client_order_id);
if let Err(e) = self.asks.remove(&client_order_id) {
log::error!("{e}");
}
}
}
}
fn log_audit_error(client_order_id: &ClientOrderId) {
log::error!(
"Audit error - {client_order_id} cached order already closed, deleting from own book"
);
}
fn transform_opposite_order(order: OwnBookOrder, side: OrderSideSpecified) -> OwnBookOrder {
let parity_price = Price::from_decimal(Decimal::ONE - order.price.as_decimal())
.expect("Invalid parity transformed price for OwnOrderBook::combined_with_opposite");
OwnBookOrder::new(
order.trader_id,
order.client_order_id,
order.venue_order_id,
side,
parity_price,
order.size,
order.order_type,
order.time_in_force,
order.status,
order.ts_last,
order.ts_accepted,
order.ts_submitted,
order.ts_init,
)
}
fn filter_orders<'a>(
levels: impl Iterator<Item = &'a OwnBookLevel>,
status: Option<&AHashSet<OrderStatus>>,
accepted_buffer_ns: Option<u64>,
ts_now: Option<u64>,
) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
let accepted_buffer_ns = accepted_buffer_ns.unwrap_or(0);
let ts_now = ts_now.unwrap_or_else(nanos_since_unix_epoch);
levels
.map(|level| {
let orders = level
.orders
.values()
.filter(|order| status.is_none_or(|f| f.contains(&order.status)))
.filter(|order| order.ts_accepted + accepted_buffer_ns <= ts_now)
.copied()
.collect::<Vec<OwnBookOrder>>();
(level.price.value.as_decimal(), orders)
})
.filter(|(_, orders)| !orders.is_empty())
.collect::<IndexMap<Decimal, Vec<OwnBookOrder>>>()
}
fn group_quantities(
quantities: IndexMap<Decimal, Decimal>,
group_size: Decimal,
depth: Option<usize>,
is_bid: bool,
) -> IndexMap<Decimal, Decimal> {
if group_size <= Decimal::ZERO {
log::error!("Invalid group_size: {group_size}, must be positive; returning empty map");
return IndexMap::new();
}
let mut grouped = IndexMap::new();
let depth = depth.unwrap_or(usize::MAX);
for (price, size) in quantities {
let grouped_price = if is_bid {
(price / group_size).floor() * group_size
} else {
(price / group_size).ceil() * group_size
};
grouped
.entry(grouped_price)
.and_modify(|total| *total += size)
.or_insert(size);
if grouped.len() > depth {
if is_bid {
if let Some((lowest_price, _)) = grouped.iter().min_by_key(|(price, _)| *price) {
let lowest_price = *lowest_price;
grouped.shift_remove(&lowest_price);
}
} else {
if let Some((highest_price, _)) = grouped.iter().max_by_key(|(price, _)| *price) {
let highest_price = *highest_price;
grouped.shift_remove(&highest_price);
}
}
}
}
grouped
}
fn sum_order_sizes<'a, I>(orders: I) -> Decimal
where
I: Iterator<Item = &'a OwnBookOrder>,
{
orders.fold(Decimal::ZERO, |total, order| {
total + order.size.as_decimal()
})
}
#[derive(Clone)]
pub(crate) struct OwnBookLadder {
pub side: OrderSideSpecified,
pub levels: BTreeMap<BookPrice, OwnBookLevel>,
pub cache: AHashMap<ClientOrderId, BookPrice>,
}
impl OwnBookLadder {
#[must_use]
pub fn new(side: OrderSideSpecified) -> Self {
Self {
side,
levels: BTreeMap::new(),
cache: AHashMap::new(),
}
}
#[must_use]
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.levels.len()
}
#[must_use]
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.levels.is_empty()
}
pub fn clear(&mut self) {
self.levels.clear();
self.cache.clear();
}
pub fn add(&mut self, order: OwnBookOrder) {
let book_price = order.to_book_price();
self.cache.insert(order.client_order_id, book_price);
match self.levels.get_mut(&book_price) {
Some(level) => {
level.add(order);
}
None => {
let level = OwnBookLevel::from_order(order);
self.levels.insert(book_price, level);
}
}
}
pub fn update(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
let Some(price) = self.cache.get(&order.client_order_id).copied() else {
log::error!(
"Own book update failed - order {client_order_id} not in cache",
client_order_id = order.client_order_id
);
anyhow::bail!(
"Order {} not found in own book (cache)",
order.client_order_id
);
};
let Some(level) = self.levels.get_mut(&price) else {
log::error!(
"Own book update failed - order {client_order_id} cached level {price:?} missing",
client_order_id = order.client_order_id
);
anyhow::bail!(
"Order {} not found in own book (level)",
order.client_order_id
);
};
if order.price == level.price.value {
level.update(order);
if order.size.is_zero() {
self.cache.remove(&order.client_order_id);
if level.is_empty() {
self.levels.remove(&price);
}
}
return Ok(());
}
level.delete(&order.client_order_id)?;
self.cache.remove(&order.client_order_id);
if level.is_empty() {
self.levels.remove(&price);
}
self.add(order);
Ok(())
}
pub fn delete(&mut self, order: OwnBookOrder) -> anyhow::Result<()> {
self.remove(&order.client_order_id)
}
pub fn remove(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
let Some(price) = self.cache.get(client_order_id).copied() else {
log::error!("Own book remove failed - order {client_order_id} not in cache");
anyhow::bail!("Order {client_order_id} not found in own book (cache)");
};
let Some(level) = self.levels.get_mut(&price) else {
log::error!(
"Own book remove failed - order {client_order_id} cached level {price:?} missing"
);
anyhow::bail!("Order {client_order_id} not found in own book (level)");
};
level.delete(client_order_id)?;
if level.is_empty() {
self.levels.remove(&price);
}
self.cache.remove(client_order_id);
Ok(())
}
#[must_use]
#[allow(dead_code)]
pub fn sizes(&self) -> f64 {
self.levels.values().map(OwnBookLevel::size).sum()
}
#[must_use]
#[allow(dead_code)]
pub fn exposures(&self) -> f64 {
self.levels.values().map(OwnBookLevel::exposure).sum()
}
#[must_use]
#[allow(dead_code)]
pub fn top(&self) -> Option<&OwnBookLevel> {
match self.levels.iter().next() {
Some((_, l)) => Option::Some(l),
None => Option::None,
}
}
}
impl Debug for OwnBookLadder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(OwnBookLadder))
.field("side", &self.side)
.field("levels", &self.levels)
.finish()
}
}
impl Display for OwnBookLadder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}(side={})", stringify!(OwnBookLadder), self.side)?;
for (price, level) in &self.levels {
writeln!(f, " {} -> {} orders", price, level.len())?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct OwnBookLevel {
pub price: BookPrice,
pub orders: IndexMap<ClientOrderId, OwnBookOrder>,
}
impl OwnBookLevel {
#[must_use]
pub fn new(price: BookPrice) -> Self {
Self {
price,
orders: IndexMap::new(),
}
}
#[must_use]
pub fn from_order(order: OwnBookOrder) -> Self {
let mut level = Self {
price: order.to_book_price(),
orders: IndexMap::new(),
};
level.orders.insert(order.client_order_id, order);
level
}
#[must_use]
pub fn len(&self) -> usize {
self.orders.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.orders.is_empty()
}
#[must_use]
pub fn first(&self) -> Option<&OwnBookOrder> {
self.orders.get_index(0).map(|(_key, order)| order)
}
pub fn iter(&self) -> impl Iterator<Item = &OwnBookOrder> {
self.orders.values()
}
#[must_use]
pub fn get_orders(&self) -> Vec<OwnBookOrder> {
self.orders.values().copied().collect()
}
#[must_use]
pub fn size(&self) -> f64 {
self.orders.iter().map(|(_, o)| o.size.as_f64()).sum()
}
#[must_use]
pub fn size_decimal(&self) -> Decimal {
self.orders.iter().map(|(_, o)| o.size.as_decimal()).sum()
}
#[must_use]
pub fn exposure(&self) -> f64 {
self.orders
.iter()
.map(|(_, o)| o.price.as_f64() * o.size.as_f64())
.sum()
}
pub fn add_bulk(&mut self, orders: &[OwnBookOrder]) {
for order in orders {
self.add(*order);
}
}
pub fn add(&mut self, order: OwnBookOrder) {
debug_assert_eq!(order.price, self.price.value);
self.orders.insert(order.client_order_id, order);
}
pub fn update(&mut self, order: OwnBookOrder) {
debug_assert_eq!(order.price, self.price.value);
if order.size.is_zero() {
self.orders.shift_remove(&order.client_order_id);
} else {
self.orders[&order.client_order_id] = order;
}
}
pub fn delete(&mut self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
if self.orders.shift_remove(client_order_id).is_none() {
anyhow::bail!("Order {client_order_id} not found for delete");
}
Ok(())
}
}
impl PartialEq for OwnBookLevel {
fn eq(&self, other: &Self) -> bool {
self.price == other.price
}
}
impl Eq for OwnBookLevel {}
impl PartialOrd for OwnBookLevel {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OwnBookLevel {
fn cmp(&self, other: &Self) -> Ordering {
self.price.cmp(&other.price)
}
}
pub fn should_handle_own_book_order(order: &OrderAny) -> bool {
order.has_price()
&& order.time_in_force() != TimeInForce::Ioc
&& order.time_in_force() != TimeInForce::Fok
}