use std::fmt::Display;
use enum_dispatch::enum_dispatch;
use serde::{Deserialize, Serialize};
use super::{
Order, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder,
market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder,
stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
};
use crate::{events::OrderEventAny, identifiers::OrderListId, types::Price};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[enum_dispatch(Order)]
pub enum OrderAny {
Limit(LimitOrder),
LimitIfTouched(LimitIfTouchedOrder),
Market(MarketOrder),
MarketIfTouched(MarketIfTouchedOrder),
MarketToLimit(MarketToLimitOrder),
StopLimit(StopLimitOrder),
StopMarket(StopMarketOrder),
TrailingStopLimit(TrailingStopLimitOrder),
TrailingStopMarket(TrailingStopMarketOrder),
}
impl OrderAny {
#[allow(clippy::missing_panics_doc)] pub fn from_events(events: Vec<OrderEventAny>) -> anyhow::Result<Self> {
if events.is_empty() {
anyhow::bail!("No order events provided to create OrderAny");
}
let init_event = events.first().unwrap();
match init_event {
OrderEventAny::Initialized(init) => {
let mut order = Self::from(init.clone());
for event in events.into_iter().skip(1) {
order.apply(event)?;
}
Ok(order)
}
_ => {
anyhow::bail!("First event must be `OrderInitialized`");
}
}
}
#[must_use]
pub fn init_event(&self) -> &crate::events::OrderInitialized {
match self
.events()
.first()
.expect("Order invariant violated: no events")
{
OrderEventAny::Initialized(init) => init,
_ => panic!("Order invariant violated: first event must be OrderInitialized"),
}
}
pub fn set_order_list_id(&mut self, id: OrderListId) {
match self {
Self::Limit(o) => o.order_list_id = Some(id),
Self::LimitIfTouched(o) => o.order_list_id = Some(id),
Self::Market(o) => o.order_list_id = Some(id),
Self::MarketIfTouched(o) => o.order_list_id = Some(id),
Self::MarketToLimit(o) => o.order_list_id = Some(id),
Self::StopLimit(o) => o.order_list_id = Some(id),
Self::StopMarket(o) => o.order_list_id = Some(id),
Self::TrailingStopLimit(o) => o.order_list_id = Some(id),
Self::TrailingStopMarket(o) => o.order_list_id = Some(id),
}
}
}
impl PartialEq for OrderAny {
fn eq(&self, other: &Self) -> bool {
self.client_order_id() == other.client_order_id()
}
}
impl Eq for OrderAny {}
impl Display for OrderAny {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Limit(order) => order.to_string(),
Self::LimitIfTouched(order) => order.to_string(),
Self::Market(order) => order.to_string(),
Self::MarketIfTouched(order) => order.to_string(),
Self::MarketToLimit(order) => order.to_string(),
Self::StopLimit(order) => order.to_string(),
Self::StopMarket(order) => order.to_string(),
Self::TrailingStopLimit(order) => order.to_string(),
Self::TrailingStopMarket(order) => order.to_string(),
}
)
}
}
impl TryFrom<OrderAny> for PassiveOrderAny {
type Error = String;
fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
match order {
OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
}
}
}
impl From<PassiveOrderAny> for OrderAny {
fn from(order: PassiveOrderAny) -> Self {
match order {
PassiveOrderAny::Limit(order) => order.into(),
PassiveOrderAny::Stop(order) => order.into(),
}
}
}
impl TryFrom<OrderAny> for StopOrderAny {
type Error = String;
fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
match order {
OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
_ => Err(format!(
"Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
order.order_type()
)),
}
}
}
impl From<StopOrderAny> for OrderAny {
fn from(order: StopOrderAny) -> Self {
match order {
StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
StopOrderAny::StopLimit(order) => Self::StopLimit(order),
StopOrderAny::StopMarket(order) => Self::StopMarket(order),
StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
}
}
}
impl TryFrom<OrderAny> for LimitOrderAny {
type Error = String;
fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
match order {
OrderAny::Limit(order) => Ok(Self::Limit(order)),
OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
_ => Err(format!(
"Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
order.order_type()
)),
}
}
}
impl From<LimitOrderAny> for OrderAny {
fn from(order: LimitOrderAny) -> Self {
match order {
LimitOrderAny::Limit(order) => Self::Limit(order),
LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
}
}
}
#[derive(Clone, Debug)]
#[enum_dispatch(Order)]
pub enum PassiveOrderAny {
Limit(LimitOrderAny),
Stop(StopOrderAny),
}
impl PassiveOrderAny {
#[must_use]
pub fn to_any(&self) -> OrderAny {
match self {
Self::Limit(order) => order.clone().into(),
Self::Stop(order) => order.clone().into(),
}
}
}
impl PartialEq for PassiveOrderAny {
fn eq(&self, rhs: &Self) -> bool {
match self {
Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
}
}
}
#[derive(Clone, Debug)]
#[enum_dispatch(Order)]
pub enum LimitOrderAny {
Limit(LimitOrder),
MarketToLimit(MarketToLimitOrder),
StopLimit(StopLimitOrder),
TrailingStopLimit(TrailingStopLimitOrder),
MarketOrderWithProtection(MarketOrder),
}
impl LimitOrderAny {
#[must_use]
pub fn limit_px(&self) -> Price {
match self {
Self::Limit(order) => order.price,
Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
Self::StopLimit(order) => order.price,
Self::TrailingStopLimit(order) => order.price,
Self::MarketOrderWithProtection(order) => {
order.protection_price.expect("No price for order")
}
}
}
}
impl PartialEq for LimitOrderAny {
fn eq(&self, rhs: &Self) -> bool {
match self {
Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
Self::MarketOrderWithProtection(order) => {
order.client_order_id == rhs.client_order_id()
}
}
}
}
#[derive(Clone, Debug)]
#[enum_dispatch(Order)]
pub enum StopOrderAny {
LimitIfTouched(LimitIfTouchedOrder),
MarketIfTouched(MarketIfTouchedOrder),
StopLimit(StopLimitOrder),
StopMarket(StopMarketOrder),
TrailingStopLimit(TrailingStopLimitOrder),
TrailingStopMarket(TrailingStopMarketOrder),
}
impl StopOrderAny {
#[must_use]
pub fn stop_px(&self) -> Price {
match self {
Self::LimitIfTouched(o) => o.trigger_price,
Self::MarketIfTouched(o) => o.trigger_price,
Self::StopLimit(o) => o.trigger_price,
Self::StopMarket(o) => o.trigger_price,
Self::TrailingStopLimit(o) => o.activation_price.unwrap_or(o.trigger_price),
Self::TrailingStopMarket(o) => o.activation_price.unwrap_or(o.trigger_price),
}
}
}
impl PartialEq for StopOrderAny {
fn eq(&self, rhs: &Self) -> bool {
match self {
Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::*;
use crate::{
enums::{OrderType, TrailingOffsetType},
events::{OrderEventAny, OrderUpdated, order::initialized::OrderInitializedBuilder},
identifiers::{ClientOrderId, InstrumentId, StrategyId},
orders::builder::OrderTestBuilder,
types::{Price, Quantity},
};
#[rstest]
fn test_order_any_equality() {
let client_order_id = ClientOrderId::from("ORDER-001");
let market_order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.client_order_id(client_order_id)
.build();
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(100.0, 2))
.client_order_id(client_order_id)
.build();
assert_eq!(market_order, limit_order);
}
#[rstest]
fn test_order_any_conversion_from_events() {
let init_event = OrderInitializedBuilder::default()
.order_type(OrderType::Market)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.build()
.unwrap();
let events = vec![OrderEventAny::Initialized(init_event.clone())];
let order = OrderAny::from_events(events).unwrap();
assert_eq!(order.order_type(), OrderType::Market);
assert_eq!(order.instrument_id(), init_event.instrument_id);
assert_eq!(order.quantity(), init_event.quantity);
}
#[rstest]
fn test_order_any_from_events_empty_error() {
let events: Vec<OrderEventAny> = vec![];
let result = OrderAny::from_events(events);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"No order events provided to create OrderAny"
);
}
#[rstest]
fn test_order_any_from_events_wrong_first_event() {
let client_order_id = ClientOrderId::from("ORDER-001");
let strategy_id = StrategyId::from("STRATEGY-001");
let update_event = OrderUpdated {
client_order_id,
strategy_id,
quantity: Quantity::from(20),
..Default::default()
};
let events = vec![OrderEventAny::Updated(update_event)];
let result = OrderAny::from_events(events);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"First event must be `OrderInitialized`"
);
}
#[rstest]
fn test_passive_order_any_conversion() {
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(100.0, 2))
.build();
let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
let order_any: OrderAny = passive_order.into();
assert_eq!(order_any.order_type(), OrderType::Limit);
assert_eq!(order_any.quantity(), Quantity::from(10));
}
#[rstest]
fn test_stop_order_any_conversion() {
let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.trigger_price(Price::new(100.0, 2))
.build();
let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
let order_any: OrderAny = stop_order_any.into();
assert_eq!(order_any.order_type(), OrderType::StopMarket);
assert_eq!(order_any.quantity(), Quantity::from(10));
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
}
#[rstest]
fn test_limit_order_any_conversion() {
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(100.0, 2))
.build();
let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
let order_any: OrderAny = limit_order_any.into();
assert_eq!(order_any.order_type(), OrderType::Limit);
assert_eq!(order_any.quantity(), Quantity::from(10));
}
#[rstest]
fn test_limit_order_any_limit_price() {
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(100.0, 2))
.build();
let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
let limit_px = limit_order_any.limit_px();
assert_eq!(limit_px, Price::new(100.0, 2));
}
#[rstest]
fn test_stop_order_any_stop_price() {
let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.trigger_price(Price::new(100.0, 2))
.build();
let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
let stop_px = stop_order_any.stop_px();
assert_eq!(stop_px, Price::new(100.0, 2));
}
#[rstest]
fn test_trailing_stop_market_order_conversion() {
let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.trigger_price(Price::new(100.0, 2))
.trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
.build();
let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
let order_any: OrderAny = stop_order_any.into();
assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
assert_eq!(order_any.quantity(), Quantity::from(10));
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
assert_eq!(
order_any.trailing_offset_type(),
Some(TrailingOffsetType::NoTrailingOffset)
);
}
#[rstest]
fn test_trailing_stop_limit_order_conversion() {
let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(99.0, 2))
.trigger_price(Price::new(100.0, 2))
.limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
.build();
let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
let order_any: OrderAny = limit_order_any.into();
assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
assert_eq!(order_any.quantity(), Quantity::from(10));
assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
}
#[rstest]
fn test_passive_order_any_to_any() {
let limit_order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
.quantity(Quantity::from(10))
.price(Price::new(100.0, 2))
.build();
let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
let order_any = passive_order.to_any();
assert_eq!(order_any.order_type(), OrderType::Limit);
assert_eq!(order_any.quantity(), Quantity::from(10));
assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
}
}