use std::collections::BTreeMap;
use serde::Deserialize;
use crate::client::http::{ApiError, Call, Transport, read};
use crate::markets::listed;
use crate::markets::trades::Trade;
use crate::utils::dec::{Dec, MAX_SCALE, Round};
use crate::utils::ids::whole;
use crate::utils::{AccountIndex, MarketId, OrderIndex, Side};
pub const MOST_BOOK_ORDERS: u32 = 250;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct BookOrder {
#[serde(deserialize_with = "whole")]
pub order_index: u64,
#[serde(default)]
pub order_id: String,
pub owner_account_index: AccountIndex,
pub initial_base_amount: Dec,
pub remaining_base_amount: Dec,
pub price: Dec,
#[serde(default, deserialize_with = "whole")]
pub order_expiry: u64,
#[serde(default, deserialize_with = "whole")]
pub transaction_time: u64,
}
impl BookOrder {
#[must_use]
pub fn order(&self) -> Option<OrderIndex> {
let index = self.order_id.parse::<u64>().unwrap_or(self.order_index);
OrderIndex::new(index).ok()
}
#[must_use]
pub fn filled(&self) -> Dec {
self.initial_base_amount.checked_sub(self.remaining_base_amount).unwrap_or(Dec::ZERO)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Level {
pub price: Dec,
pub size: Dec,
pub orders: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Walk {
pub filled: Dec,
pub notional: Dec,
pub average: Option<Dec>,
pub worst: Option<Dec>,
pub complete: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct Book {
#[serde(skip)]
pub limit: u32,
#[serde(default, deserialize_with = "listed")]
pub bids: Vec<BookOrder>,
#[serde(default, deserialize_with = "listed")]
pub asks: Vec<BookOrder>,
}
impl Book {
#[must_use]
pub fn side(&self, side: Side) -> &[BookOrder] {
match side {
Side::Bid => &self.bids,
Side::Ask => &self.asks,
}
}
pub fn resting(&self, side: Side) -> impl Iterator<Item = &BookOrder> {
self.side(side).iter().filter(|order| order.remaining_base_amount.is_positive() && order.price.is_positive())
}
#[must_use]
pub fn best_bid(&self) -> Option<Dec> {
self.levels(Side::Bid).first().map(|level| level.price)
}
#[must_use]
pub fn best_ask(&self) -> Option<Dec> {
self.levels(Side::Ask).first().map(|level| level.price)
}
#[must_use]
pub fn touch(&self) -> (Option<Dec>, Option<Dec>) {
(self.best_bid(), self.best_ask())
}
#[must_use]
pub fn mid(&self) -> Option<Dec> {
let (Some(bid), Some(ask)) = self.touch() else { return None };
let places = bid.places().max(ask.places()).saturating_add(1);
bid.checked_add(ask)?.checked_div(Dec::from_int(2), places, Round::Down)
}
#[must_use]
pub fn spread(&self) -> Option<Dec> {
let (Some(bid), Some(ask)) = self.touch() else { return None };
ask.checked_sub(bid)
}
#[must_use]
pub fn is_cut(&self, side: Side) -> bool {
self.limit > 0 && self.side(side).len() >= usize::try_from(self.limit).unwrap_or(usize::MAX)
}
#[must_use]
pub fn edge(&self, side: Side) -> Option<Dec> {
if !self.is_cut(side) {
return None;
}
let prices = self.side(side).iter().map(|order| order.price);
match side {
Side::Bid => prices.min(),
Side::Ask => prices.max(),
}
}
#[must_use]
pub fn levels(&self, side: Side) -> Vec<Level> {
let mut by_price: BTreeMap<Dec, Level> = BTreeMap::new();
for order in self.resting(side) {
let level = by_price.entry(order.price).or_insert(Level { price: order.price, size: Dec::ZERO, orders: 0 });
level.size = level.size.checked_add(order.remaining_base_amount).unwrap_or(level.size);
level.orders = level.orders.saturating_add(1);
}
let mut levels: Vec<Level> = by_price.into_values().collect();
if side == Side::Bid {
levels.reverse();
}
levels
}
#[must_use]
pub fn whole_levels(&self, side: Side, depth: usize) -> Vec<Level> {
let mut levels = self.levels(side);
if self.is_cut(side) {
levels.pop();
}
levels.truncate(depth);
levels
}
#[must_use]
pub fn size_to(&self, side: Side, price: Dec) -> Dec {
let within = |level: &&Level| match side {
Side::Bid => level.price >= price,
Side::Ask => level.price <= price,
};
let levels = self.levels(side);
Dec::checked_sum(levels.iter().filter(within).map(|level| level.size)).unwrap_or(Dec::ZERO)
}
#[must_use]
pub fn walk(&self, taker: Side, size: Dec) -> Walk {
let mut walk = Walk { filled: Dec::ZERO, notional: Dec::ZERO, average: None, worst: None, complete: false };
if !size.is_positive() {
walk.complete = size.is_zero();
return walk;
}
for level in self.levels(taker.opposite()) {
let Some(left) = size.checked_sub(walk.filled).filter(|left| left.is_positive()) else { break };
let take = left.min(level.size);
let (Some(filled), Some(notional)) = (
walk.filled.checked_add(take),
take.checked_mul(level.price).and_then(|cost| walk.notional.checked_add(cost)),
) else {
break;
};
walk.filled = filled;
walk.notional = notional;
walk.worst = Some(level.price);
}
let round = match taker {
Side::Bid => Round::Up,
Side::Ask => Round::Down,
};
walk.average = walk.notional.checked_div(walk.filled, MAX_SCALE, round);
walk.complete = walk.filled >= size;
walk
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FilledOrder {
pub order: OrderIndex,
pub owner: AccountIndex,
pub at_ms: u64,
}
pub fn orders_filled<'a>(trades: impl IntoIterator<Item = &'a Trade>) -> Vec<FilledOrder> {
let mut filled = Vec::new();
for trade in trades {
let sides = [(trade.ask_order(), trade.ask_account_id), (trade.bid_order(), trade.bid_account_id)];
for (order, owner) in sides {
if let Some(order) = order {
filled.push(FilledOrder { order, owner, at_ms: trade.timestamp });
}
}
}
filled
}
#[must_use]
pub fn order_book_orders_call(market: MarketId, limit: u32) -> Call {
Call::get("orderBookOrders").field("market_id", market).field("limit", limit.clamp(1, MOST_BOOK_ORDERS))
}
pub async fn order_book_orders(transport: &impl Transport, market: MarketId, limit: u32) -> Result<Book, ApiError> {
let book: Book = read(transport, order_book_orders_call(market, limit)).await?;
Ok(Book { limit: limit.clamp(1, MOST_BOOK_ORDERS), ..book })
}
#[cfg(test)]
#[path = "tests/book.rs"]
mod tests;