use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize,
)]
pub struct MarketSnapshot {
pub best_bid: Option<Decimal>,
pub best_ask: Option<Decimal>,
pub last_price: Option<Decimal>,
}
impl MarketSnapshot {
pub fn new(
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> Self {
Self {
best_bid,
best_ask,
last_price,
}
}
pub fn from_last_price(last_price: Option<Decimal>) -> Self {
Self {
best_bid: None,
best_ask: None,
last_price,
}
}
pub fn is_empty(&self) -> bool {
self.best_bid.is_none() && self.best_ask.is_none() && self.last_price.is_none()
}
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_bid, self.best_ask) {
(Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub struct MarketBar {
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
}
impl MarketBar {
pub fn new(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> Self {
Self {
open,
high,
low,
close,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarketObservation {
pub snapshot: MarketSnapshot,
pub depth: MarketDepth,
pub bar: Option<MarketBar>,
pub fresh: bool,
}
impl MarketObservation {
pub fn new(snapshot: MarketSnapshot) -> Self {
Self {
snapshot,
depth: MarketDepth::UNKNOWN,
bar: None,
fresh: true,
}
}
#[must_use]
pub fn with_depth(self, depth: MarketDepth) -> Self {
Self { depth, ..self }
}
#[must_use]
pub fn with_bar(self, bar: Option<MarketBar>) -> Self {
Self { bar, ..self }
}
#[must_use]
pub fn with_fresh(self, fresh: bool) -> Self {
Self { fresh, ..self }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MarketDepth {
pub best_bid: Option<Decimal>,
pub best_ask: Option<Decimal>,
}
impl MarketDepth {
pub const UNKNOWN: Self = Self {
best_bid: None,
best_ask: None,
};
pub fn new(best_bid: Option<Decimal>, best_ask: Option<Decimal>) -> Self {
Self { best_bid, best_ask }
}
pub fn available_to(&self, side: Side) -> Option<Decimal> {
match side {
Side::Buy => self.best_ask,
Side::Sell => self.best_bid,
}
}
pub fn consume(&mut self, side: Side, quantity: Decimal) {
debug_assert!(
quantity >= Decimal::ZERO,
"consuming a negative quantity {quantity} of depth would create size that was never \
on offer"
);
let available = match side {
Side::Buy => &mut self.best_ask,
Side::Sell => &mut self.best_bid,
};
if let Some(available) = available {
*available = (*available - quantity).max(Decimal::ZERO);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn default_snapshot_is_empty() {
assert!(MarketSnapshot::default().is_empty());
}
#[test]
fn a_snapshot_carrying_any_price_is_not_empty() {
assert!(!MarketSnapshot::from_last_price(Some(dec!(100))).is_empty());
assert!(!MarketSnapshot::new(Some(dec!(99)), None, None).is_empty());
assert!(!MarketSnapshot::new(None, Some(dec!(101)), None).is_empty());
}
#[test]
fn an_observation_is_fresh_with_no_size_and_no_bar_until_told_otherwise() {
let snapshot = MarketSnapshot::from_last_price(Some(dec!(100)));
let observation = MarketObservation::new(snapshot);
assert_eq!(
(
observation.snapshot,
observation.depth,
observation.bar,
observation.fresh
),
(snapshot, MarketDepth::UNKNOWN, None, true)
);
let depth = MarketDepth::new(Some(dec!(1)), Some(dec!(2)));
let bar = MarketBar::new(dec!(99), dec!(101), dec!(98), dec!(100));
let observation = observation
.with_depth(depth)
.with_bar(Some(bar))
.with_fresh(false);
assert_eq!(
(
observation.snapshot,
observation.depth,
observation.bar,
observation.fresh
),
(snapshot, depth, Some(bar), false)
);
}
#[test]
fn depth_is_taken_from_the_far_side_of_the_book() {
let depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
assert_eq!(depth.available_to(Side::Buy), Some(dec!(3)));
assert_eq!(depth.available_to(Side::Sell), Some(dec!(5)));
}
#[test]
fn an_unknown_size_stays_unknown_however_much_is_taken() {
let mut depth = MarketDepth::UNKNOWN;
depth.consume(Side::Buy, dec!(1000));
assert_eq!(depth.available_to(Side::Buy), None);
assert_eq!(MarketDepth::default(), MarketDepth::UNKNOWN);
}
#[test]
fn consuming_draws_down_only_the_side_taken_from() {
let mut depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
depth.consume(Side::Buy, dec!(2));
assert_eq!(depth, MarketDepth::new(Some(dec!(5)), Some(dec!(1))));
}
#[test]
fn consuming_more_than_is_on_offer_leaves_nothing_rather_than_a_debt() {
let mut depth = MarketDepth::new(None, Some(dec!(3)));
depth.consume(Side::Buy, dec!(10));
assert_eq!(depth.available_to(Side::Buy), Some(Decimal::ZERO));
}
#[test]
fn mid_price_requires_both_sides() {
assert_eq!(
MarketSnapshot::new(Some(dec!(99)), Some(dec!(101)), None).mid_price(),
Some(dec!(100))
);
assert_eq!(
MarketSnapshot::new(Some(dec!(99)), None, None).mid_price(),
None
);
assert_eq!(
MarketSnapshot::new(None, Some(dec!(101)), None).mid_price(),
None
);
}
}