@@ -19,7 +19,10 @@ use std::{
19
19
} ;
20
20
21
21
use indexmap:: IndexMap ;
22
- use nautilus_core:: { UUID4 , UnixNanos } ;
22
+ use nautilus_core:: {
23
+ UUID4 , UnixNanos ,
24
+ correctness:: { FAILED , check_predicate_false} ,
25
+ } ;
23
26
use rust_decimal:: Decimal ;
24
27
use serde:: { Deserialize , Serialize } ;
25
28
use ustr:: Ustr ;
@@ -35,7 +38,10 @@ use crate::{
35
38
AccountId , ClientOrderId , ExecAlgorithmId , InstrumentId , OrderListId , PositionId ,
36
39
StrategyId , Symbol , TradeId , TraderId , Venue , VenueOrderId ,
37
40
} ,
38
- types:: { Currency , Money , Price , Quantity } ,
41
+ types:: {
42
+ Currency , Money , Price , Quantity , price:: check_positive_price,
43
+ quantity:: check_positive_quantity,
44
+ } ,
39
45
} ;
40
46
41
47
#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -58,8 +64,12 @@ pub struct StopLimitOrder {
58
64
59
65
impl StopLimitOrder {
60
66
/// Creates a new [`StopLimitOrder`] instance.
67
+ ///
68
+ /// # Errors
69
+ ///
70
+ /// Returns an error if the order is invalid.
61
71
#[ allow( clippy:: too_many_arguments) ]
62
- pub fn new (
72
+ pub fn new_checked (
63
73
trader_id : TraderId ,
64
74
strategy_id : StrategyId ,
65
75
instrument_id : InstrumentId ,
@@ -87,8 +97,23 @@ impl StopLimitOrder {
87
97
tags : Option < Vec < Ustr > > ,
88
98
init_id : UUID4 ,
89
99
ts_init : UnixNanos ,
90
- ) -> Self {
91
- // TODO: Implement new_checked and check quantity positive, add error docs.
100
+ ) -> anyhow:: Result < Self > {
101
+ check_positive_quantity ( quantity, stringify ! ( quantity) ) ?;
102
+ check_positive_price ( price, stringify ! ( price) ) ?;
103
+ check_positive_price ( trigger_price, stringify ! ( trigger_price) ) ?;
104
+
105
+ if let Some ( disp) = display_qty {
106
+ check_positive_quantity ( disp, stringify ! ( display_qty) ) ?;
107
+ check_predicate_false ( disp > quantity, "`display_qty` may not exceed `quantity`" ) ?;
108
+ }
109
+
110
+ if time_in_force == TimeInForce :: Gtd {
111
+ check_predicate_false (
112
+ expire_time. unwrap_or_default ( ) . is_zero ( ) ,
113
+ "`expire_time` is required for `GTD` order" ,
114
+ ) ?;
115
+ }
116
+
92
117
let init_order = OrderInitialized :: new (
93
118
trader_id,
94
119
strategy_id,
@@ -125,7 +150,7 @@ impl StopLimitOrder {
125
150
tags,
126
151
) ;
127
152
128
- Self {
153
+ Ok ( Self {
129
154
core : OrderCore :: new ( init_order) ,
130
155
price,
131
156
trigger_price,
@@ -136,13 +161,74 @@ impl StopLimitOrder {
136
161
trigger_instrument_id,
137
162
is_triggered : false ,
138
163
ts_triggered : None ,
139
- }
164
+ } )
165
+ }
166
+
167
+ #[ allow( clippy:: too_many_arguments) ]
168
+ pub fn new (
169
+ trader_id : TraderId ,
170
+ strategy_id : StrategyId ,
171
+ instrument_id : InstrumentId ,
172
+ client_order_id : ClientOrderId ,
173
+ order_side : OrderSide ,
174
+ quantity : Quantity ,
175
+ price : Price ,
176
+ trigger_price : Price ,
177
+ trigger_type : TriggerType ,
178
+ time_in_force : TimeInForce ,
179
+ expire_time : Option < UnixNanos > ,
180
+ post_only : bool ,
181
+ reduce_only : bool ,
182
+ quote_quantity : bool ,
183
+ display_qty : Option < Quantity > ,
184
+ emulation_trigger : Option < TriggerType > ,
185
+ trigger_instrument_id : Option < InstrumentId > ,
186
+ contingency_type : Option < ContingencyType > ,
187
+ order_list_id : Option < OrderListId > ,
188
+ linked_order_ids : Option < Vec < ClientOrderId > > ,
189
+ parent_order_id : Option < ClientOrderId > ,
190
+ exec_algorithm_id : Option < ExecAlgorithmId > ,
191
+ exec_algorithm_params : Option < IndexMap < Ustr , Ustr > > ,
192
+ exec_spawn_id : Option < ClientOrderId > ,
193
+ tags : Option < Vec < Ustr > > ,
194
+ init_id : UUID4 ,
195
+ ts_init : UnixNanos ,
196
+ ) -> Self {
197
+ Self :: new_checked (
198
+ trader_id,
199
+ strategy_id,
200
+ instrument_id,
201
+ client_order_id,
202
+ order_side,
203
+ quantity,
204
+ price,
205
+ trigger_price,
206
+ trigger_type,
207
+ time_in_force,
208
+ expire_time,
209
+ post_only,
210
+ reduce_only,
211
+ quote_quantity,
212
+ display_qty,
213
+ emulation_trigger,
214
+ trigger_instrument_id,
215
+ contingency_type,
216
+ order_list_id,
217
+ linked_order_ids,
218
+ parent_order_id,
219
+ exec_algorithm_id,
220
+ exec_algorithm_params,
221
+ exec_spawn_id,
222
+ tags,
223
+ init_id,
224
+ ts_init,
225
+ )
226
+ . expect ( FAILED )
140
227
}
141
228
}
142
229
143
230
impl Deref for StopLimitOrder {
144
231
type Target = OrderCore ;
145
-
146
232
fn deref ( & self ) -> & Self :: Target {
147
233
& self . core
148
234
}
@@ -449,15 +535,13 @@ impl From<OrderInitialized> for StopLimitOrder {
449
535
event. client_order_id ,
450
536
event. order_side ,
451
537
event. quantity ,
538
+ event. price . expect ( "`price` was None for StopLimitOrder" ) ,
452
539
event
453
- . price // TODO: Improve this error, model order domain errors
454
- . expect ( "Error initializing order: `price` was `None` for `StopLimitOrder" ) ,
455
- event
456
- . trigger_price // TODO: Improve this error, model order domain errors
457
- . expect ( "Error initializing order: `trigger_price` was `None` for `StopLimitOrder" ) ,
540
+ . trigger_price
541
+ . expect ( "`trigger_price` was None for StopLimitOrder" ) ,
458
542
event
459
543
. trigger_type
460
- . expect ( "Error initializing order: `trigger_type` was ` None` " ) ,
544
+ . expect ( "`trigger_type` was None for StopLimitOrder " ) ,
461
545
event. time_in_force ,
462
546
event. expire_time ,
463
547
event. post_only ,
@@ -509,3 +593,126 @@ impl Display for StopLimitOrder {
509
593
)
510
594
}
511
595
}
596
+
597
+ ////////////////////////////////////////////////////////////////////////////////
598
+ // Tests
599
+ ////////////////////////////////////////////////////////////////////////////////
600
+ #[ cfg( test) ]
601
+ mod tests {
602
+ use rstest:: rstest;
603
+
604
+ use crate :: {
605
+ enums:: { OrderSide , OrderType , TimeInForce , TriggerType } ,
606
+ instruments:: { CurrencyPair , stubs:: * } ,
607
+ orders:: { Order , builder:: OrderTestBuilder } ,
608
+ types:: { Price , Quantity } ,
609
+ } ;
610
+
611
+ #[ rstest]
612
+ fn buy_breakout_ok ( _audusd_sim : CurrencyPair ) {
613
+ // ---------------------------------------------------------------------
614
+ let order = OrderTestBuilder :: new ( OrderType :: StopLimit )
615
+ . instrument_id ( _audusd_sim. id )
616
+ . side ( OrderSide :: Buy )
617
+ . trigger_price ( Price :: from ( "0.68000" ) )
618
+ . price ( Price :: from ( "0.68100" ) )
619
+ . trigger_type ( TriggerType :: LastPrice )
620
+ . quantity ( Quantity :: from ( 1 ) )
621
+ . build ( ) ;
622
+
623
+ assert_eq ! ( order. trigger_price( ) , Some ( Price :: from( "0.68000" ) ) ) ;
624
+ assert_eq ! ( order. price( ) , Some ( Price :: from( "0.68100" ) ) ) ;
625
+
626
+ assert_eq ! ( order. time_in_force( ) , TimeInForce :: Gtc ) ;
627
+
628
+ assert_eq ! ( order. is_triggered( ) , Some ( false ) ) ;
629
+ assert_eq ! ( order. filled_qty( ) , Quantity :: from( 0 ) ) ;
630
+ assert_eq ! ( order. leaves_qty( ) , Quantity :: from( 1 ) ) ;
631
+
632
+ assert_eq ! ( order. display_qty( ) , None ) ;
633
+ assert_eq ! ( order. trigger_instrument_id( ) , None ) ;
634
+ assert_eq ! ( order. order_list_id( ) , None ) ;
635
+ }
636
+
637
+ #[ rstest]
638
+ #[ should_panic]
639
+ fn display_qty_gt_quantity_err ( audusd_sim : CurrencyPair ) {
640
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
641
+ . instrument_id ( audusd_sim. id )
642
+ . side ( OrderSide :: Buy )
643
+ . trigger_price ( Price :: from ( "30300" ) )
644
+ . price ( Price :: from ( "30100" ) )
645
+ . trigger_type ( TriggerType :: LastPrice )
646
+ . quantity ( Quantity :: from ( 1 ) )
647
+ . display_qty ( Quantity :: from ( 2 ) )
648
+ . build ( ) ;
649
+ }
650
+
651
+ #[ rstest]
652
+ #[ should_panic]
653
+ fn display_qty_zero_err ( audusd_sim : CurrencyPair ) {
654
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
655
+ . instrument_id ( audusd_sim. id )
656
+ . side ( OrderSide :: Buy )
657
+ . trigger_price ( Price :: from ( "30300" ) )
658
+ . price ( Price :: from ( "30100" ) )
659
+ . trigger_type ( TriggerType :: LastPrice )
660
+ . quantity ( Quantity :: from ( 1 ) )
661
+ . display_qty ( Quantity :: from ( 0 ) )
662
+ . build ( ) ;
663
+ }
664
+
665
+ #[ rstest]
666
+ #[ should_panic]
667
+ fn display_qty_negative_err ( audusd_sim : CurrencyPair ) {
668
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
669
+ . instrument_id ( audusd_sim. id )
670
+ . side ( OrderSide :: Buy )
671
+ . trigger_price ( Price :: from ( "30300" ) )
672
+ . price ( Price :: from ( "30100" ) )
673
+ . trigger_type ( TriggerType :: LastPrice )
674
+ . quantity ( Quantity :: from ( 1 ) )
675
+ . display_qty ( Quantity :: from ( "-1" ) )
676
+ . build ( ) ;
677
+ }
678
+
679
+ #[ rstest]
680
+ #[ should_panic]
681
+ fn limit_price_zero_err ( audusd_sim : CurrencyPair ) {
682
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
683
+ . instrument_id ( audusd_sim. id )
684
+ . side ( OrderSide :: Buy )
685
+ . trigger_price ( Price :: from ( "30300" ) )
686
+ . price ( Price :: from ( "0" ) )
687
+ . trigger_type ( TriggerType :: LastPrice )
688
+ . quantity ( Quantity :: from ( 1 ) )
689
+ . build ( ) ;
690
+ }
691
+
692
+ #[ rstest]
693
+ #[ should_panic]
694
+ fn limit_price_negative_err ( audusd_sim : CurrencyPair ) {
695
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
696
+ . instrument_id ( audusd_sim. id )
697
+ . side ( OrderSide :: Buy )
698
+ . trigger_price ( Price :: from ( "30300" ) )
699
+ . price ( Price :: from ( "-1" ) ) // <-- bad
700
+ . trigger_type ( TriggerType :: LastPrice )
701
+ . quantity ( Quantity :: from ( 1 ) )
702
+ . build ( ) ;
703
+ }
704
+
705
+ #[ rstest]
706
+ #[ should_panic]
707
+ fn gtd_without_expire_time_err ( audusd_sim : CurrencyPair ) {
708
+ OrderTestBuilder :: new ( OrderType :: StopLimit )
709
+ . instrument_id ( audusd_sim. id )
710
+ . side ( OrderSide :: Buy )
711
+ . trigger_price ( Price :: from ( "30300" ) )
712
+ . price ( Price :: from ( "30100" ) )
713
+ . trigger_type ( TriggerType :: LastPrice )
714
+ . time_in_force ( TimeInForce :: Gtd )
715
+ . quantity ( Quantity :: from ( 1 ) )
716
+ . build ( ) ;
717
+ }
718
+ }
0 commit comments