use getset::CopyGetters;
use num::Zero;
use super::MarketUpdate;
use crate::{
market_update::market_update_trait::Exhausted,
order_filters::{
enforce_max_price,
enforce_min_price,
enforce_step_size,
},
prelude::{
Currency,
LimitOrder,
MarketState,
Mon,
Pending,
PriceFilter,
QuoteCurrency,
Side,
Side::*,
},
types::{
PriceFilterError,
TimestampNs,
UserOrderId,
},
utils::min,
};
#[derive(Debug, Clone, Copy, Eq, PartialEq, CopyGetters)]
pub struct Trade<I, const D: u8, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
#[getset(get_copy = "pub")]
pub timestamp_exchange_ns: TimestampNs,
#[getset(get_copy = "pub")]
pub price: QuoteCurrency<I, D>,
#[getset(get_copy = "pub")]
pub quantity: BaseOrQuote,
#[getset(get_copy = "pub")]
pub side: Side,
}
impl<I, const D: u8, BaseOrQuote> Trade<I, D, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
#[inline(always)]
pub fn fills_order<UserOrderIdT: UserOrderId>(
&self,
order: &LimitOrder<I, D, BaseOrQuote, UserOrderIdT, Pending<I, D, BaseOrQuote>>,
) -> bool {
match order.side() {
Buy => self.price < order.limit_price() && matches!(self.side, Sell),
Sell => self.price > order.limit_price() && matches!(self.side, Buy),
}
}
}
impl<I, const D: u8, BaseOrQuote> std::fmt::Display for Trade<I, D, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"price {}, quantity: {}, side: {}",
self.price, self.quantity, self.side
)
}
}
impl<I, const D: u8, BaseOrQuote> MarketUpdate<I, D, BaseOrQuote> for Trade<I, D, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
const CAN_FILL_LIMIT_ORDERS: bool = true;
#[inline]
fn limit_order_filled<UserOrderIdT: UserOrderId>(
&mut self,
order: &LimitOrder<I, D, BaseOrQuote, UserOrderIdT, Pending<I, D, BaseOrQuote>>,
) -> Option<(BaseOrQuote, Exhausted)> {
debug_assert!(
self.quantity > BaseOrQuote::zero(),
"The trade quantity must be greater than zero."
);
debug_assert!(order.remaining_quantity() > BaseOrQuote::zero());
if self.fills_order(order) {
let filled_qty = min(self.quantity, order.remaining_quantity());
self.quantity -= filled_qty;
debug_assert!(self.quantity >= Zero::zero());
Some((filled_qty, self.quantity <= Zero::zero()))
} else {
None
}
}
fn validate_market_update(
&self,
price_filter: &PriceFilter<I, D>,
) -> Result<(), PriceFilterError> {
debug_assert!(self.price > QuoteCurrency::zero());
enforce_min_price(price_filter.min_price(), self.price)?;
enforce_max_price(price_filter.max_price(), self.price)?;
enforce_step_size(price_filter.tick_size(), self.price)?;
Ok(())
}
#[inline(always)]
fn update_market_state(&self, market_state: &mut MarketState<I, D>) {
market_state.set_last_trade_price(self.price);
}
#[inline(always)]
fn timestamp_exchange_ns(&self) -> TimestampNs {
self.timestamp_exchange_ns
}
#[inline(always)]
fn can_fill_bids(&self) -> bool {
match self.side {
Buy => false,
Sell => true,
}
}
#[inline(always)]
fn can_fill_asks(&self) -> bool {
match self.side {
Buy => true,
Sell => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test_case::test_case(Buy, false, true)]
fn trade_update_can_fill_bids_asks(side: Side, can_fill_bid: bool, can_fill_ask: bool) {
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(100, 0),
quantity: BaseCurrency::new(5, 0),
side,
timestamp_exchange_ns: 0.into(),
};
assert_eq!(trade.side(), side);
assert_eq!(trade.price(), trade.price);
assert_eq!(trade.quantity(), trade.quantity);
assert_eq!(trade.can_fill_bids(), can_fill_bid);
assert_eq!(trade.can_fill_asks(), can_fill_ask);
}
#[test_case::test_matrix([90, 95, 100, 105, 110])]
fn trade_update_fills_buy_order_never(price: i64) {
let new_order =
LimitOrder::new(Buy, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Buy,
timestamp_exchange_ns: 0.into(),
};
assert!(!trade.fills_order(&order));
}
#[test_case::test_matrix([90, 95, 99])]
fn trade_update_fills_buy_order(price: i64) {
let new_order =
LimitOrder::new(Buy, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Sell,
timestamp_exchange_ns: 0.into(),
};
assert!(trade.fills_order(&order));
}
#[test_case::test_matrix([100, 101, 105, 110])]
fn trade_update_fills_buy_order_not(price: i64) {
let new_order =
LimitOrder::new(Buy, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Sell,
timestamp_exchange_ns: 0.into(),
};
assert!(!trade.fills_order(&order));
}
#[test_case::test_matrix([90, 95, 100, 105, 110])]
fn trade_update_fills_sell_order_never(price: i64) {
let new_order =
LimitOrder::new(Sell, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Sell,
timestamp_exchange_ns: 0.into(),
};
assert!(!trade.fills_order(&order));
}
#[test_case::test_matrix([101, 105, 110])]
fn trade_update_fills_sell_order(price: i64) {
let new_order =
LimitOrder::new(Sell, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Buy,
timestamp_exchange_ns: 0.into(),
};
assert!(trade.fills_order(&order));
}
#[test_case::test_matrix([90, 95, 99])]
fn trade_update_fills_sell_order_not(price: i64) {
let new_order =
LimitOrder::new(Sell, QuoteCurrency::new(100, 0), BaseCurrency::new(5, 0)).unwrap();
let meta = ExchangeOrderMeta::default();
let order = new_order.into_pending(meta);
let trade = Trade {
price: QuoteCurrency::<i64, 1>::new(price, 0),
quantity: BaseCurrency::new(5, 0),
side: Buy,
timestamp_exchange_ns: 0.into(),
};
assert!(!trade.fills_order(&order));
}
#[test]
fn trade_update_market_state() {
let trade = Trade {
price: QuoteCurrency::<i64, 5>::new(100, 0),
quantity: BaseCurrency::new(5, 0),
side: Buy,
timestamp_exchange_ns: 0.into(),
};
let mut state = MarketState::default();
trade.update_market_state(&mut state);
assert_eq!(state.last_trade_price(), QuoteCurrency::new(100, 0));
}
#[test]
fn trade_update_display() {
let trade = Trade {
price: QuoteCurrency::<i64, 5>::new(100, 0),
quantity: BaseCurrency::new(5, 0),
side: Buy,
timestamp_exchange_ns: 0.into(),
};
assert_eq!(
&trade.to_string(),
"price 100.00000 Quote, quantity: 5.00000 Base, side: Buy"
);
}
#[test_case::test_matrix(
[100, 110, 120],
[1, 2, 3],
[Buy, Sell]
)]
fn trade_limit_order_filled_some(price: i32, qty: i32, side: Side) {
let price = QuoteCurrency::<i32, 2>::new(price, 0);
let quantity = BaseCurrency::new(qty, 0);
let mut trade = Trade {
price,
quantity,
side,
timestamp_exchange_ns: 0.into(),
};
let offset = match side {
Buy => QuoteCurrency::new(-1, 0),
Sell => QuoteCurrency::new(1, 0),
};
let limit_order = LimitOrder::new(side.inverted(), price + offset, quantity).unwrap();
let meta = ExchangeOrderMeta::new(0.into(), 0.into());
let limit_order = limit_order.into_pending(meta);
assert_eq!(
trade.limit_order_filled(&limit_order).unwrap(),
(quantity, true)
);
assert_eq!(
trade.quantity,
Zero::zero(),
"Trade quantity is reduced as well"
);
}
#[test_case::test_matrix(
[100, 110, 120],
[1, 2, 3],
[Buy, Sell]
)]
fn trade_limit_order_filled_none(price: i32, qty: i32, side: Side) {
let price = QuoteCurrency::<i32, 2>::new(price, 0);
let quantity = BaseCurrency::new(qty, 0);
let mut trade = Trade {
price,
quantity,
side,
timestamp_exchange_ns: 0.into(),
};
let offset = match side {
Buy => QuoteCurrency::new(-1, 0),
Sell => QuoteCurrency::new(1, 0),
};
let limit_order = LimitOrder::new(
side.inverted(),
price + offset,
quantity / BaseCurrency::new(2, 0),
)
.unwrap();
let meta = ExchangeOrderMeta::new(0.into(), 0.into());
let limit_order = limit_order.into_pending(meta);
assert_eq!(
trade.limit_order_filled(&limit_order).unwrap(),
(quantity / BaseCurrency::new(2, 0), false)
);
assert_eq!(
trade.quantity,
quantity / BaseCurrency::new(2, 0),
"Trade quantity is reduced as well"
);
}
#[test]
fn size_of_trade() {
assert_eq!(size_of::<Trade<i32, 2, BaseCurrency<i32, 2>>>(), 24);
assert_eq!(size_of::<Trade<i64, 2, BaseCurrency<i64, 2>>>(), 32);
}
}