16
16
use std:: ops:: { Deref , DerefMut } ;
17
17
18
18
use indexmap:: IndexMap ;
19
- use nautilus_core:: { UUID4 , UnixNanos } ;
19
+ use nautilus_core:: {
20
+ UUID4 , UnixNanos ,
21
+ correctness:: { FAILED , check_predicate_false} ,
22
+ } ;
20
23
use rust_decimal:: Decimal ;
21
24
use serde:: { Deserialize , Serialize } ;
22
25
use ustr:: Ustr ;
@@ -32,7 +35,10 @@ use crate::{
32
35
AccountId , ClientOrderId , ExecAlgorithmId , InstrumentId , OrderListId , PositionId ,
33
36
StrategyId , Symbol , TradeId , TraderId , Venue , VenueOrderId ,
34
37
} ,
35
- types:: { Currency , Money , Price , Quantity } ,
38
+ types:: {
39
+ Currency , Money , Price , Quantity , price:: check_positive_price,
40
+ quantity:: check_positive_quantity,
41
+ } ,
36
42
} ;
37
43
38
44
#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -54,7 +60,7 @@ pub struct MarketIfTouchedOrder {
54
60
impl MarketIfTouchedOrder {
55
61
/// Creates a new [`MarketIfTouchedOrder`] instance.
56
62
#[ allow( clippy:: too_many_arguments) ]
57
- pub fn new (
63
+ pub fn new_checked (
58
64
trader_id : TraderId ,
59
65
strategy_id : StrategyId ,
60
66
instrument_id : InstrumentId ,
@@ -80,7 +86,22 @@ impl MarketIfTouchedOrder {
80
86
tags : Option < Vec < Ustr > > ,
81
87
init_id : UUID4 ,
82
88
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
+
84
105
let init_order = OrderInitialized :: new (
85
106
trader_id,
86
107
strategy_id,
@@ -116,7 +137,8 @@ impl MarketIfTouchedOrder {
116
137
exec_spawn_id,
117
138
tags,
118
139
) ;
119
- Self {
140
+
141
+ Ok ( Self {
120
142
core : OrderCore :: new ( init_order) ,
121
143
trigger_price,
122
144
trigger_type,
@@ -125,7 +147,65 @@ impl MarketIfTouchedOrder {
125
147
trigger_instrument_id,
126
148
is_triggered : false ,
127
149
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 )
129
209
}
130
210
}
131
211
@@ -429,13 +509,13 @@ impl From<OrderInitialized> for MarketIfTouchedOrder {
429
509
event. order_side ,
430
510
event. quantity ,
431
511
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
+ ) ,
439
519
event. time_in_force ,
440
520
event. expire_time ,
441
521
event. reduce_only ,
@@ -456,3 +536,69 @@ impl From<OrderInitialized> for MarketIfTouchedOrder {
456
536
)
457
537
}
458
538
}
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