1616use std:: ops:: { Deref , DerefMut } ;
1717
1818use indexmap:: IndexMap ;
19- use nautilus_core:: { UUID4 , UnixNanos } ;
19+ use nautilus_core:: {
20+ UUID4 , UnixNanos ,
21+ correctness:: { FAILED , check_predicate_false} ,
22+ } ;
2023use rust_decimal:: Decimal ;
2124use serde:: { Deserialize , Serialize } ;
2225use ustr:: Ustr ;
@@ -32,7 +35,10 @@ use crate::{
3235 AccountId , ClientOrderId , ExecAlgorithmId , InstrumentId , OrderListId , PositionId ,
3336 StrategyId , Symbol , TradeId , TraderId , Venue , VenueOrderId ,
3437 } ,
35- types:: { Currency , Money , Price , Quantity } ,
38+ types:: {
39+ Currency , Money , Price , Quantity , price:: check_positive_price,
40+ quantity:: check_positive_quantity,
41+ } ,
3642} ;
3743
3844#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -54,7 +60,7 @@ pub struct MarketIfTouchedOrder {
5460impl MarketIfTouchedOrder {
5561 /// Creates a new [`MarketIfTouchedOrder`] instance.
5662 #[ allow( clippy:: too_many_arguments) ]
57- pub fn new (
63+ pub fn new_checked (
5864 trader_id : TraderId ,
5965 strategy_id : StrategyId ,
6066 instrument_id : InstrumentId ,
@@ -80,7 +86,22 @@ impl MarketIfTouchedOrder {
8086 tags : Option < Vec < Ustr > > ,
8187 init_id : UUID4 ,
8288 ts_init : UnixNanos ,
83- ) -> Self {
89+ ) -> anyhow:: Result < Self > {
90+ check_positive_quantity ( quantity, "quantity" ) ?;
91+ check_positive_price ( trigger_price, "trigger_price" ) ?;
92+
93+ if let Some ( disp) = display_qty {
94+ check_positive_quantity ( disp, "display_qty" ) ?;
95+ check_predicate_false ( disp > quantity, "`display_qty` may not exceed `quantity`" ) ?;
96+ }
97+
98+ if time_in_force == TimeInForce :: Gtd {
99+ check_predicate_false (
100+ expire_time. unwrap_or_default ( ) == 0 ,
101+ "Condition failed: `expire_time` is required for `GTD` order" ,
102+ ) ?;
103+ }
104+
84105 let init_order = OrderInitialized :: new (
85106 trader_id,
86107 strategy_id,
@@ -116,7 +137,8 @@ impl MarketIfTouchedOrder {
116137 exec_spawn_id,
117138 tags,
118139 ) ;
119- Self {
140+
141+ Ok ( Self {
120142 core : OrderCore :: new ( init_order) ,
121143 trigger_price,
122144 trigger_type,
@@ -125,7 +147,65 @@ impl MarketIfTouchedOrder {
125147 trigger_instrument_id,
126148 is_triggered : false ,
127149 ts_triggered : None ,
128- }
150+ } )
151+ }
152+
153+ #[ allow( clippy:: too_many_arguments) ]
154+ pub fn new (
155+ trader_id : TraderId ,
156+ strategy_id : StrategyId ,
157+ instrument_id : InstrumentId ,
158+ client_order_id : ClientOrderId ,
159+ order_side : OrderSide ,
160+ quantity : Quantity ,
161+ trigger_price : Price ,
162+ trigger_type : TriggerType ,
163+ time_in_force : TimeInForce ,
164+ expire_time : Option < UnixNanos > ,
165+ reduce_only : bool ,
166+ quote_quantity : bool ,
167+ display_qty : Option < Quantity > ,
168+ emulation_trigger : Option < TriggerType > ,
169+ trigger_instrument_id : Option < InstrumentId > ,
170+ contingency_type : Option < ContingencyType > ,
171+ order_list_id : Option < OrderListId > ,
172+ linked_order_ids : Option < Vec < ClientOrderId > > ,
173+ parent_order_id : Option < ClientOrderId > ,
174+ exec_algorithm_id : Option < ExecAlgorithmId > ,
175+ exec_algorithm_params : Option < IndexMap < Ustr , Ustr > > ,
176+ exec_spawn_id : Option < ClientOrderId > ,
177+ tags : Option < Vec < Ustr > > ,
178+ init_id : UUID4 ,
179+ ts_init : UnixNanos ,
180+ ) -> Self {
181+ Self :: new_checked (
182+ trader_id,
183+ strategy_id,
184+ instrument_id,
185+ client_order_id,
186+ order_side,
187+ quantity,
188+ trigger_price,
189+ trigger_type,
190+ time_in_force,
191+ expire_time,
192+ reduce_only,
193+ quote_quantity,
194+ display_qty,
195+ emulation_trigger,
196+ trigger_instrument_id,
197+ contingency_type,
198+ order_list_id,
199+ linked_order_ids,
200+ parent_order_id,
201+ exec_algorithm_id,
202+ exec_algorithm_params,
203+ exec_spawn_id,
204+ tags,
205+ init_id,
206+ ts_init,
207+ )
208+ . expect ( FAILED )
129209 }
130210}
131211
@@ -429,13 +509,13 @@ impl From<OrderInitialized> for MarketIfTouchedOrder {
429509 event. order_side ,
430510 event. quantity ,
431511 event
432- . trigger_price // TODO: Improve this error, model order domain errors
433- . expect (
434- "Error initializing order: `trigger_price` was `None` for `MarketIfTouchedOrder`" ,
435- ) ,
436- event
437- . trigger_type
438- . expect ( "Error initializing order: `trigger_type` was `None` for `MarketIfTouchedOrder`" ) ,
512+ . trigger_price // TODO: Improve this error, model order domain errors
513+ . expect (
514+ "Error initializing order: `trigger_price` was `None` for `MarketIfTouchedOrder`" ,
515+ ) ,
516+ event. trigger_type . expect (
517+ "Error initializing order: ` trigger_type` was `None` for `MarketIfTouchedOrder`" ,
518+ ) ,
439519 event. time_in_force ,
440520 event. expire_time ,
441521 event. reduce_only ,
@@ -456,3 +536,69 @@ impl From<OrderInitialized> for MarketIfTouchedOrder {
456536 )
457537 }
458538}
539+
540+ ////////////////////////////////////////////////////////////////////////////////
541+ // Tests
542+ ////////////////////////////////////////////////////////////////////////////////
543+ #[ cfg( test) ]
544+ mod tests {
545+ use rstest:: rstest;
546+
547+ use crate :: {
548+ enums:: { OrderSide , OrderType , TimeInForce , TriggerType } ,
549+ instruments:: { CurrencyPair , stubs:: * } ,
550+ orders:: builder:: OrderTestBuilder ,
551+ types:: { Price , Quantity } ,
552+ } ;
553+
554+ #[ rstest]
555+ fn ok ( audusd_sim : CurrencyPair ) {
556+ let _ = OrderTestBuilder :: new ( OrderType :: MarketIfTouched )
557+ . instrument_id ( audusd_sim. id )
558+ . side ( OrderSide :: Buy )
559+ . trigger_price ( Price :: from ( "30000" ) )
560+ . trigger_type ( TriggerType :: LastPrice )
561+ . quantity ( Quantity :: from ( 1 ) )
562+ . build ( ) ;
563+ }
564+
565+ #[ rstest]
566+ #[ should_panic(
567+ expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
568+ ) ]
569+ fn quantity_zero ( audusd_sim : CurrencyPair ) {
570+ let _ = OrderTestBuilder :: new ( OrderType :: MarketIfTouched )
571+ . instrument_id ( audusd_sim. id )
572+ . side ( OrderSide :: Buy )
573+ . trigger_price ( Price :: from ( "30000" ) )
574+ . trigger_type ( TriggerType :: LastPrice )
575+ . quantity ( Quantity :: from ( 0 ) )
576+ . build ( ) ;
577+ }
578+
579+ #[ rstest]
580+ #[ should_panic( expected = "Condition failed: `expire_time` is required for `GTD` order" ) ]
581+ fn gtd_without_expire ( audusd_sim : CurrencyPair ) {
582+ let _ = OrderTestBuilder :: new ( OrderType :: MarketIfTouched )
583+ . instrument_id ( audusd_sim. id )
584+ . side ( OrderSide :: Buy )
585+ . trigger_price ( Price :: from ( "30000" ) )
586+ . trigger_type ( TriggerType :: LastPrice )
587+ . quantity ( Quantity :: from ( 1 ) )
588+ . time_in_force ( TimeInForce :: Gtd )
589+ . build ( ) ;
590+ }
591+
592+ #[ rstest]
593+ #[ should_panic( expected = "`display_qty` may not exceed `quantity`" ) ]
594+ fn display_qty_gt_quantity ( audusd_sim : CurrencyPair ) {
595+ let _ = OrderTestBuilder :: new ( OrderType :: MarketIfTouched )
596+ . instrument_id ( audusd_sim. id )
597+ . side ( OrderSide :: Buy )
598+ . trigger_price ( Price :: from ( "30000" ) )
599+ . trigger_type ( TriggerType :: LastPrice )
600+ . quantity ( Quantity :: from ( 1 ) )
601+ . display_qty ( Quantity :: from ( 2 ) )
602+ . build ( ) ;
603+ }
604+ }
0 commit comments