1use std::fmt::Display;
17
18use enum_dispatch::enum_dispatch;
19use serde::{Deserialize, Serialize};
20
21use super::{
22 Order, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder,
23 market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder,
24 stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
25 trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
26};
27use crate::{events::OrderEventAny, identifiers::OrderListId, types::Price};
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30#[enum_dispatch(Order)]
31pub enum OrderAny {
32 Limit(LimitOrder),
33 LimitIfTouched(LimitIfTouchedOrder),
34 Market(MarketOrder),
35 MarketIfTouched(MarketIfTouchedOrder),
36 MarketToLimit(MarketToLimitOrder),
37 StopLimit(StopLimitOrder),
38 StopMarket(StopMarketOrder),
39 TrailingStopLimit(TrailingStopLimitOrder),
40 TrailingStopMarket(TrailingStopMarketOrder),
41}
42
43impl OrderAny {
44 #[expect(clippy::missing_panics_doc)] pub fn from_events(events: Vec<OrderEventAny>) -> anyhow::Result<Self> {
55 if events.is_empty() {
56 anyhow::bail!("No order events provided to create OrderAny");
57 }
58
59 let init_event = events.first().unwrap();
61 match init_event {
62 OrderEventAny::Initialized(init) => {
63 let mut order = Self::from(init.clone());
64 for event in events.into_iter().skip(1) {
66 order.apply(event)?;
68 }
69 Ok(order)
70 }
71 _ => {
72 anyhow::bail!("First event must be `OrderInitialized`");
73 }
74 }
75 }
76
77 #[must_use]
85 pub fn init_event(&self) -> &crate::events::OrderInitialized {
86 match self
87 .events()
88 .first()
89 .expect("Order invariant violated: no events")
90 {
91 OrderEventAny::Initialized(init) => init,
92 _ => panic!("Order invariant violated: first event must be OrderInitialized"),
93 }
94 }
95
96 pub fn set_order_list_id(&mut self, id: OrderListId) {
100 match self {
101 Self::Limit(o) => o.order_list_id = Some(id),
102 Self::LimitIfTouched(o) => o.order_list_id = Some(id),
103 Self::Market(o) => o.order_list_id = Some(id),
104 Self::MarketIfTouched(o) => o.order_list_id = Some(id),
105 Self::MarketToLimit(o) => o.order_list_id = Some(id),
106 Self::StopLimit(o) => o.order_list_id = Some(id),
107 Self::StopMarket(o) => o.order_list_id = Some(id),
108 Self::TrailingStopLimit(o) => o.order_list_id = Some(id),
109 Self::TrailingStopMarket(o) => o.order_list_id = Some(id),
110 }
111 }
112}
113
114impl PartialEq for OrderAny {
115 fn eq(&self, other: &Self) -> bool {
116 self.client_order_id() == other.client_order_id()
117 }
118}
119
120impl Eq for OrderAny {}
122
123impl Display for OrderAny {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(
126 f,
127 "{}",
128 match self {
129 Self::Limit(order) => order.to_string(),
130 Self::LimitIfTouched(order) => order.to_string(),
131 Self::Market(order) => order.to_string(),
132 Self::MarketIfTouched(order) => order.to_string(),
133 Self::MarketToLimit(order) => order.to_string(),
134 Self::StopLimit(order) => order.to_string(),
135 Self::StopMarket(order) => order.to_string(),
136 Self::TrailingStopLimit(order) => order.to_string(),
137 Self::TrailingStopMarket(order) => order.to_string(),
138 }
139 )
140 }
141}
142
143impl TryFrom<OrderAny> for PassiveOrderAny {
144 type Error = String;
145
146 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
147 match order {
148 OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
149 OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
150 OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
151 OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
152 OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
153 OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
154 OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
155 OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
156 OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
157 }
158 }
159}
160
161impl From<PassiveOrderAny> for OrderAny {
162 fn from(order: PassiveOrderAny) -> Self {
163 match order {
164 PassiveOrderAny::Limit(order) => order.into(),
165 PassiveOrderAny::Stop(order) => order.into(),
166 }
167 }
168}
169
170impl TryFrom<OrderAny> for StopOrderAny {
171 type Error = String;
172
173 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
174 match order {
175 OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
176 OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
177 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
178 OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
179 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
180 OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
181 _ => Err(format!(
182 "Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
183 order.order_type()
184 )),
185 }
186 }
187}
188
189impl From<StopOrderAny> for OrderAny {
190 fn from(order: StopOrderAny) -> Self {
191 match order {
192 StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
193 StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
194 StopOrderAny::StopLimit(order) => Self::StopLimit(order),
195 StopOrderAny::StopMarket(order) => Self::StopMarket(order),
196 StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
197 StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
198 }
199 }
200}
201
202impl TryFrom<OrderAny> for LimitOrderAny {
203 type Error = String;
204
205 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
206 match order {
207 OrderAny::Limit(order) => Ok(Self::Limit(order)),
208 OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
209 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
210 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
211 OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
212 _ => Err(format!(
213 "Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
214 order.order_type()
215 )),
216 }
217 }
218}
219
220impl From<LimitOrderAny> for OrderAny {
221 fn from(order: LimitOrderAny) -> Self {
222 match order {
223 LimitOrderAny::Limit(order) => Self::Limit(order),
224 LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
225 LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
226 LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
227 LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
228 }
229 }
230}
231
232#[derive(Clone, Debug)]
233#[enum_dispatch(Order)]
234pub enum PassiveOrderAny {
235 Limit(LimitOrderAny),
236 Stop(StopOrderAny),
237}
238
239impl PassiveOrderAny {
240 #[must_use]
241 pub fn to_any(&self) -> OrderAny {
242 match self {
243 Self::Limit(order) => order.clone().into(),
244 Self::Stop(order) => order.clone().into(),
245 }
246 }
247}
248
249impl PartialEq for PassiveOrderAny {
251 fn eq(&self, rhs: &Self) -> bool {
252 match self {
253 Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
254 Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
255 }
256 }
257}
258
259#[derive(Clone, Debug)]
260#[enum_dispatch(Order)]
261pub enum LimitOrderAny {
262 Limit(LimitOrder),
263 MarketToLimit(MarketToLimitOrder),
264 StopLimit(StopLimitOrder),
265 TrailingStopLimit(TrailingStopLimitOrder),
266 MarketOrderWithProtection(MarketOrder),
267}
268
269impl LimitOrderAny {
270 #[must_use]
276 pub fn limit_px(&self) -> Price {
277 match self {
278 Self::Limit(order) => order.price,
279 Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
280 Self::StopLimit(order) => order.price,
281 Self::TrailingStopLimit(order) => order.price,
282 Self::MarketOrderWithProtection(order) => {
283 order.protection_price.expect("No price for order")
284 }
285 }
286 }
287}
288
289impl PartialEq for LimitOrderAny {
290 fn eq(&self, rhs: &Self) -> bool {
291 match self {
292 Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
293 Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
294 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
295 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
296 Self::MarketOrderWithProtection(order) => {
297 order.client_order_id == rhs.client_order_id()
298 }
299 }
300 }
301}
302
303#[derive(Clone, Debug)]
304#[enum_dispatch(Order)]
305pub enum StopOrderAny {
306 LimitIfTouched(LimitIfTouchedOrder),
307 MarketIfTouched(MarketIfTouchedOrder),
308 StopLimit(StopLimitOrder),
309 StopMarket(StopMarketOrder),
310 TrailingStopLimit(TrailingStopLimitOrder),
311 TrailingStopMarket(TrailingStopMarketOrder),
312}
313
314impl StopOrderAny {
315 #[must_use]
316 pub fn stop_px(&self) -> Price {
317 match self {
318 Self::LimitIfTouched(o) => o.trigger_price,
319 Self::MarketIfTouched(o) => o.trigger_price,
320 Self::StopLimit(o) => o.trigger_price,
321 Self::StopMarket(o) => o.trigger_price,
322 Self::TrailingStopLimit(o) => o.activation_price.unwrap_or(o.trigger_price),
323 Self::TrailingStopMarket(o) => o.activation_price.unwrap_or(o.trigger_price),
324 }
325 }
326}
327
328impl PartialEq for StopOrderAny {
330 fn eq(&self, rhs: &Self) -> bool {
331 match self {
332 Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
333 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
334 Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
335 Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
336 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
337 Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use rstest::rstest;
345 use rust_decimal::Decimal;
346 use rust_decimal_macros::dec;
347
348 use super::*;
349 use crate::{
350 enums::{OrderType, TrailingOffsetType},
351 events::{OrderEventAny, OrderUpdated, order::spec::OrderInitializedSpec},
352 identifiers::{ClientOrderId, InstrumentId, StrategyId},
353 orders::builder::OrderTestBuilder,
354 types::{Price, Quantity},
355 };
356
357 #[rstest]
358 fn test_order_any_equality() {
359 let client_order_id = ClientOrderId::from("ORDER-001");
361
362 let market_order = OrderTestBuilder::new(OrderType::Market)
363 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
364 .quantity(Quantity::from(10))
365 .client_order_id(client_order_id)
366 .build();
367
368 let limit_order = OrderTestBuilder::new(OrderType::Limit)
369 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
370 .quantity(Quantity::from(10))
371 .price(Price::new(100.0, 2))
372 .client_order_id(client_order_id)
373 .build();
374
375 assert_eq!(market_order, limit_order);
377 }
378
379 #[rstest]
380 fn test_order_any_conversion_from_events() {
381 let init_event = OrderInitializedSpec::builder()
383 .order_type(OrderType::Market)
384 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
385 .quantity(Quantity::from(10))
386 .build();
387
388 let events = vec![OrderEventAny::Initialized(init_event.clone())];
390
391 let order = OrderAny::from_events(events).unwrap();
393
394 assert_eq!(order.order_type(), OrderType::Market);
396 assert_eq!(order.instrument_id(), init_event.instrument_id);
397 assert_eq!(order.quantity(), init_event.quantity);
398 }
399
400 #[rstest]
401 fn test_order_any_from_events_empty_error() {
402 let events: Vec<OrderEventAny> = vec![];
403 let result = OrderAny::from_events(events);
404
405 assert!(result.is_err());
406 assert_eq!(
407 result.unwrap_err().to_string(),
408 "No order events provided to create OrderAny"
409 );
410 }
411
412 #[rstest]
413 fn test_order_any_from_events_wrong_first_event() {
414 let client_order_id = ClientOrderId::from("ORDER-001");
416 let strategy_id = StrategyId::from("STRATEGY-001");
417
418 let update_event = OrderUpdated {
419 client_order_id,
420 strategy_id,
421 quantity: Quantity::from(20),
422 ..Default::default()
423 };
424
425 let events = vec![OrderEventAny::Updated(update_event)];
427
428 let result = OrderAny::from_events(events);
430 assert!(result.is_err());
431 assert_eq!(
432 result.unwrap_err().to_string(),
433 "First event must be `OrderInitialized`"
434 );
435 }
436
437 #[rstest]
438 fn test_passive_order_any_conversion() {
439 let limit_order = OrderTestBuilder::new(OrderType::Limit)
441 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
442 .quantity(Quantity::from(10))
443 .price(Price::new(100.0, 2))
444 .build();
445
446 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
448 let order_any: OrderAny = passive_order.into();
449
450 assert_eq!(order_any.order_type(), OrderType::Limit);
452 assert_eq!(order_any.quantity(), Quantity::from(10));
453 }
454
455 #[rstest]
456 fn test_stop_order_any_conversion() {
457 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
459 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
460 .quantity(Quantity::from(10))
461 .trigger_price(Price::new(100.0, 2))
462 .build();
463
464 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
466 let order_any: OrderAny = stop_order_any.into();
467
468 assert_eq!(order_any.order_type(), OrderType::StopMarket);
470 assert_eq!(order_any.quantity(), Quantity::from(10));
471 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
472 }
473
474 #[rstest]
475 fn test_limit_order_any_conversion() {
476 let limit_order = OrderTestBuilder::new(OrderType::Limit)
478 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
479 .quantity(Quantity::from(10))
480 .price(Price::new(100.0, 2))
481 .build();
482
483 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
485 let order_any: OrderAny = limit_order_any.into();
486
487 assert_eq!(order_any.order_type(), OrderType::Limit);
489 assert_eq!(order_any.quantity(), Quantity::from(10));
490 }
491
492 #[rstest]
493 fn test_limit_order_any_limit_price() {
494 let limit_order = OrderTestBuilder::new(OrderType::Limit)
496 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
497 .quantity(Quantity::from(10))
498 .price(Price::new(100.0, 2))
499 .build();
500
501 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
503
504 let limit_px = limit_order_any.limit_px();
506 assert_eq!(limit_px, Price::new(100.0, 2));
507 }
508
509 #[rstest]
510 fn test_stop_order_any_stop_price() {
511 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
513 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
514 .quantity(Quantity::from(10))
515 .trigger_price(Price::new(100.0, 2))
516 .build();
517
518 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
520
521 let stop_px = stop_order_any.stop_px();
523 assert_eq!(stop_px, Price::new(100.0, 2));
524 }
525
526 #[rstest]
527 fn test_trailing_stop_market_order_conversion() {
528 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
530 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
531 .quantity(Quantity::from(10))
532 .trigger_price(Price::new(100.0, 2))
533 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
535 .build();
536
537 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
539
540 let order_any: OrderAny = stop_order_any.into();
542
543 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
545 assert_eq!(order_any.quantity(), Quantity::from(10));
546 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
547 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
548 assert_eq!(
549 order_any.trailing_offset_type(),
550 Some(TrailingOffsetType::NoTrailingOffset)
551 );
552 }
553
554 #[rstest]
555 fn test_trailing_stop_limit_order_conversion() {
556 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
558 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
559 .quantity(Quantity::from(10))
560 .price(Price::new(99.0, 2))
561 .trigger_price(Price::new(100.0, 2))
562 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
565 .build();
566
567 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
569
570 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
572
573 let order_any: OrderAny = limit_order_any.into();
575
576 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
578 assert_eq!(order_any.quantity(), Quantity::from(10));
579 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
580 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
581 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
582 }
583
584 #[rstest]
585 fn test_passive_order_any_to_any() {
586 let limit_order = OrderTestBuilder::new(OrderType::Limit)
588 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
589 .quantity(Quantity::from(10))
590 .price(Price::new(100.0, 2))
591 .build();
592
593 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
595
596 let order_any = passive_order.to_any();
598
599 assert_eq!(order_any.order_type(), OrderType::Limit);
601 assert_eq!(order_any.quantity(), Quantity::from(10));
602 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
603 }
604}