Skip to content

Commit 9b5cf5d

Browse files
authored
Improve testing for Rust orders module (#2578)
1 parent 1e87dd6 commit 9b5cf5d

File tree

12 files changed

+1603
-18
lines changed

12 files changed

+1603
-18
lines changed

crates/model/src/orders/any.rs

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,269 @@ impl PartialEq for StopOrderAny {
281281
}
282282
}
283283
}
284+
285+
////////////////////////////////////////////////////////////////////////////////
286+
// Tests
287+
////////////////////////////////////////////////////////////////////////////////
288+
#[cfg(test)]
289+
mod tests {
290+
use rust_decimal::Decimal;
291+
292+
use super::*;
293+
use crate::{
294+
enums::{OrderType, TrailingOffsetType},
295+
events::{OrderEventAny, OrderUpdated, order::initialized::OrderInitializedBuilder},
296+
identifiers::{ClientOrderId, InstrumentId, StrategyId},
297+
orders::builder::OrderTestBuilder,
298+
types::{Price, Quantity},
299+
};
300+
301+
#[test]
302+
fn test_order_any_equality() {
303+
// Create two orders with different types but same client_order_id
304+
let client_order_id = ClientOrderId::from("ORDER-001");
305+
306+
let market_order = OrderTestBuilder::new(OrderType::Market)
307+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
308+
.quantity(Quantity::from(10))
309+
.client_order_id(client_order_id.clone())
310+
.build();
311+
312+
let limit_order = OrderTestBuilder::new(OrderType::Limit)
313+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
314+
.quantity(Quantity::from(10))
315+
.price(Price::new(100.0, 2))
316+
.client_order_id(client_order_id)
317+
.build();
318+
319+
// They should be equal because they have the same client_order_id
320+
assert_eq!(market_order, limit_order);
321+
}
322+
323+
#[test]
324+
fn test_order_any_conversion_from_events() {
325+
// Create an OrderInitialized event
326+
let init_event = OrderInitializedBuilder::default()
327+
.order_type(OrderType::Market)
328+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
329+
.quantity(Quantity::from(10))
330+
.build()
331+
.unwrap();
332+
333+
// Create a vector of events
334+
let events = vec![OrderEventAny::Initialized(init_event.clone())];
335+
336+
// Create OrderAny from events
337+
let order = OrderAny::from_events(events).unwrap();
338+
339+
// Verify the order was created properly
340+
assert_eq!(order.order_type(), OrderType::Market);
341+
assert_eq!(order.instrument_id(), init_event.instrument_id);
342+
assert_eq!(order.quantity(), init_event.quantity);
343+
}
344+
345+
#[test]
346+
fn test_order_any_from_events_empty_error() {
347+
let events: Vec<OrderEventAny> = vec![];
348+
let result = OrderAny::from_events(events);
349+
350+
assert!(result.is_err());
351+
assert_eq!(
352+
result.unwrap_err().to_string(),
353+
"No order events provided to create OrderAny"
354+
);
355+
}
356+
357+
#[test]
358+
fn test_order_any_from_events_wrong_first_event() {
359+
// Create an event that is not OrderInitialized
360+
let client_order_id = ClientOrderId::from("ORDER-001");
361+
let strategy_id = StrategyId::from("STRATEGY-001");
362+
363+
let update_event = OrderUpdated {
364+
client_order_id,
365+
strategy_id,
366+
quantity: Quantity::from(20),
367+
..Default::default()
368+
};
369+
370+
// Create a vector with a non-initialization event first
371+
let events = vec![OrderEventAny::Updated(update_event)];
372+
373+
// Attempt to create order should fail
374+
let result = OrderAny::from_events(events);
375+
assert!(result.is_err());
376+
assert_eq!(
377+
result.unwrap_err().to_string(),
378+
"First event must be `OrderInitialized`"
379+
);
380+
}
381+
382+
#[test]
383+
fn test_passive_order_any_conversion() {
384+
// Create a limit order
385+
let limit_order = OrderTestBuilder::new(OrderType::Limit)
386+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
387+
.quantity(Quantity::from(10))
388+
.price(Price::new(100.0, 2))
389+
.build();
390+
391+
// Convert to PassiveOrderAny and back
392+
let passive_order: PassiveOrderAny = limit_order.clone().into();
393+
let order_any: OrderAny = passive_order.into();
394+
395+
// Verify it maintained its properties
396+
assert_eq!(order_any.order_type(), OrderType::Limit);
397+
assert_eq!(order_any.quantity(), Quantity::from(10));
398+
}
399+
400+
#[test]
401+
fn test_stop_order_any_conversion() {
402+
// Create a stop market order
403+
let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
404+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
405+
.quantity(Quantity::from(10))
406+
.trigger_price(Price::new(100.0, 2))
407+
.build();
408+
409+
// Convert to StopOrderAny and back
410+
let stop_order_any: StopOrderAny = stop_order.into();
411+
let order_any: OrderAny = stop_order_any.into();
412+
413+
// Verify it maintained its properties
414+
assert_eq!(order_any.order_type(), OrderType::StopMarket);
415+
assert_eq!(order_any.quantity(), Quantity::from(10));
416+
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
417+
}
418+
419+
#[test]
420+
fn test_limit_order_any_conversion() {
421+
// Create a limit order
422+
let limit_order = OrderTestBuilder::new(OrderType::Limit)
423+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
424+
.quantity(Quantity::from(10))
425+
.price(Price::new(100.0, 2))
426+
.build();
427+
428+
// Convert to LimitOrderAny and back
429+
let limit_order_any: LimitOrderAny = limit_order.into();
430+
let order_any: OrderAny = limit_order_any.into();
431+
432+
// Verify it maintained its properties
433+
assert_eq!(order_any.order_type(), OrderType::Limit);
434+
assert_eq!(order_any.quantity(), Quantity::from(10));
435+
}
436+
437+
#[test]
438+
fn test_limit_order_any_limit_price() {
439+
// Create a limit order
440+
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+
// Convert to LimitOrderAny
447+
let limit_order_any: LimitOrderAny = limit_order.into();
448+
449+
// Check limit price accessor
450+
let limit_px = limit_order_any.limit_px();
451+
assert_eq!(limit_px, Price::new(100.0, 2));
452+
}
453+
454+
#[test]
455+
fn test_stop_order_any_stop_price() {
456+
// Create a stop market order
457+
let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
458+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
459+
.quantity(Quantity::from(10))
460+
.trigger_price(Price::new(100.0, 2))
461+
.build();
462+
463+
// Convert to StopOrderAny
464+
let stop_order_any: StopOrderAny = stop_order.into();
465+
466+
// Check stop price accessor
467+
let stop_px = stop_order_any.stop_px();
468+
assert_eq!(stop_px, Price::new(100.0, 2));
469+
}
470+
471+
#[test]
472+
fn test_trailing_stop_market_order_conversion() {
473+
// Create a trailing stop market order
474+
let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
475+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
476+
.quantity(Quantity::from(10))
477+
.trigger_price(Price::new(100.0, 2))
478+
.trailing_offset(Decimal::new(5, 1)) // 0.5
479+
.trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
480+
.build();
481+
482+
// Convert to StopOrderAny
483+
let stop_order_any: StopOrderAny = trailing_stop_order.clone().into();
484+
485+
// And back to OrderAny
486+
let order_any: OrderAny = stop_order_any.into();
487+
488+
// Verify properties are preserved
489+
assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
490+
assert_eq!(order_any.quantity(), Quantity::from(10));
491+
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
492+
assert_eq!(order_any.trailing_offset(), Some(Decimal::new(5, 1)));
493+
assert_eq!(
494+
order_any.trailing_offset_type(),
495+
Some(TrailingOffsetType::NoTrailingOffset)
496+
);
497+
}
498+
499+
#[test]
500+
fn test_trailing_stop_limit_order_conversion() {
501+
// Create a trailing stop limit order
502+
let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
503+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
504+
.quantity(Quantity::from(10))
505+
.price(Price::new(99.0, 2))
506+
.trigger_price(Price::new(100.0, 2))
507+
.limit_offset(Decimal::new(10, 1)) // 1.0
508+
.trailing_offset(Decimal::new(5, 1)) // 0.5
509+
.trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
510+
.build();
511+
512+
// Convert to LimitOrderAny
513+
let limit_order_any: LimitOrderAny = trailing_stop_limit.clone().into();
514+
515+
// Check limit price
516+
assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
517+
518+
// Convert back to OrderAny
519+
let order_any: OrderAny = limit_order_any.into();
520+
521+
// Verify properties are preserved
522+
assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
523+
assert_eq!(order_any.quantity(), Quantity::from(10));
524+
assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
525+
assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
526+
assert_eq!(order_any.trailing_offset(), Some(Decimal::new(5, 1)));
527+
}
528+
529+
#[test]
530+
fn test_passive_order_any_to_any() {
531+
// Create a limit order
532+
let limit_order = OrderTestBuilder::new(OrderType::Limit)
533+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
534+
.quantity(Quantity::from(10))
535+
.price(Price::new(100.0, 2))
536+
.build();
537+
538+
// Convert to PassiveOrderAny
539+
let passive_order: PassiveOrderAny = limit_order.into();
540+
541+
// Use to_any method
542+
let order_any = passive_order.to_any();
543+
544+
// Verify it maintained its properties
545+
assert_eq!(order_any.order_type(), OrderType::Limit);
546+
assert_eq!(order_any.quantity(), Quantity::from(10));
547+
assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
548+
}
549+
}

crates/model/src/orders/limit.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,12 +582,15 @@ impl From<OrderInitialized> for LimitOrder {
582582
////////////////////////////////////////////////////////////////////////////////
583583
#[cfg(test)]
584584
mod tests {
585+
use nautilus_core::UnixNanos;
585586
use rstest::rstest;
586587

587588
use crate::{
588589
enums::{OrderSide, OrderType, TimeInForce},
590+
events::{OrderEventAny, OrderUpdated},
591+
identifiers::InstrumentId,
589592
instruments::{CurrencyPair, stubs::*},
590-
orders::{Order, builder::OrderTestBuilder},
593+
orders::{Order, OrderTestBuilder, stubs::TestOrderStubs},
591594
types::{Price, Quantity},
592595
};
593596

@@ -652,4 +655,112 @@ mod tests {
652655
.time_in_force(TimeInForce::Gtd)
653656
.build();
654657
}
658+
659+
#[test]
660+
fn test_limit_order_creation() {
661+
let order = OrderTestBuilder::new(OrderType::Limit)
662+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
663+
.quantity(Quantity::from(10))
664+
.price(Price::new(100.0, 2))
665+
.side(OrderSide::Buy)
666+
.time_in_force(TimeInForce::Gtc)
667+
.build();
668+
669+
assert_eq!(order.price(), Some(Price::new(100.0, 2)));
670+
assert_eq!(order.quantity(), Quantity::from(10));
671+
assert_eq!(order.time_in_force(), TimeInForce::Gtc);
672+
assert_eq!(order.order_side(), OrderSide::Buy);
673+
}
674+
675+
#[test]
676+
fn test_limit_order_with_expire_time() {
677+
let expire_time = UnixNanos::from(1_700_000_000_000_000);
678+
let order = OrderTestBuilder::new(OrderType::Limit)
679+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
680+
.quantity(Quantity::from(10))
681+
.price(Price::new(100.0, 2))
682+
.time_in_force(TimeInForce::Gtd)
683+
.expire_time(expire_time)
684+
.build();
685+
686+
assert_eq!(order.expire_time(), Some(expire_time));
687+
assert_eq!(order.time_in_force(), TimeInForce::Gtd);
688+
}
689+
690+
#[test]
691+
#[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
692+
fn test_limit_order_missing_expire_time() {
693+
let _ = OrderTestBuilder::new(OrderType::Limit)
694+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
695+
.quantity(Quantity::from(10))
696+
.price(Price::new(100.0, 2))
697+
.time_in_force(TimeInForce::Gtd)
698+
.build();
699+
}
700+
701+
#[test]
702+
fn test_limit_order_post_only() {
703+
let order = OrderTestBuilder::new(OrderType::Limit)
704+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
705+
.quantity(Quantity::from(10))
706+
.price(Price::new(100.0, 2))
707+
.post_only(true)
708+
.build();
709+
710+
assert!(order.is_post_only());
711+
}
712+
713+
#[test]
714+
fn test_limit_order_display_quantity() {
715+
let display_qty = Quantity::from(5);
716+
let order = OrderTestBuilder::new(OrderType::Limit)
717+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
718+
.quantity(Quantity::from(10))
719+
.price(Price::new(100.0, 2))
720+
.display_qty(display_qty)
721+
.build();
722+
723+
assert_eq!(order.display_qty(), Some(display_qty));
724+
}
725+
726+
#[test]
727+
fn test_limit_order_update() {
728+
let order = OrderTestBuilder::new(OrderType::Limit)
729+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
730+
.quantity(Quantity::from(10))
731+
.price(Price::new(100.0, 2))
732+
.build();
733+
734+
let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
735+
736+
let updated_price = Price::new(105.0, 2);
737+
let updated_quantity = Quantity::from(5);
738+
739+
let event = OrderUpdated {
740+
client_order_id: accepted_order.client_order_id(),
741+
strategy_id: accepted_order.strategy_id(),
742+
price: Some(updated_price),
743+
quantity: updated_quantity,
744+
..Default::default()
745+
};
746+
747+
accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
748+
749+
assert_eq!(accepted_order.quantity(), updated_quantity);
750+
assert_eq!(accepted_order.price(), Some(updated_price));
751+
}
752+
753+
#[test]
754+
fn test_limit_order_expire_time() {
755+
let expire_time = UnixNanos::from(1_700_000_000_000_000);
756+
let order = OrderTestBuilder::new(OrderType::Limit)
757+
.instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
758+
.quantity(Quantity::from(10))
759+
.price(Price::new(100.0, 2))
760+
.time_in_force(TimeInForce::Gtd)
761+
.expire_time(expire_time)
762+
.build();
763+
764+
assert_eq!(order.expire_time(), Some(expire_time));
765+
}
655766
}

0 commit comments

Comments
 (0)