13
13
// limitations under the License.
14
14
// -------------------------------------------------------------------------------------------------
15
15
16
- use std:: ops:: { Deref , DerefMut } ;
16
+ use std:: {
17
+ fmt:: Display ,
18
+ ops:: { Deref , DerefMut } ,
19
+ } ;
17
20
18
21
use indexmap:: IndexMap ;
19
- use nautilus_core:: { UUID4 , UnixNanos } ;
22
+ use nautilus_core:: {
23
+ UUID4 , UnixNanos ,
24
+ correctness:: { FAILED , check_predicate_false} ,
25
+ } ;
20
26
use rust_decimal:: Decimal ;
21
27
use serde:: { Deserialize , Serialize } ;
22
28
use ustr:: Ustr ;
23
29
24
- use super :: { Order , OrderAny , OrderCore , OrderError } ;
30
+ use super :: { Order , OrderAny , OrderCore } ;
25
31
use crate :: {
26
32
enums:: {
27
33
ContingencyType , LiquiditySide , OrderSide , OrderStatus , OrderType , PositionSide ,
@@ -32,7 +38,11 @@ use crate::{
32
38
AccountId , ClientOrderId , ExecAlgorithmId , InstrumentId , OrderListId , PositionId ,
33
39
StrategyId , Symbol , TradeId , TraderId , Venue , VenueOrderId ,
34
40
} ,
35
- types:: { Currency , Money , Price , Quantity } ,
41
+ orders:: OrderError ,
42
+ types:: {
43
+ Currency , Money , Price , Quantity , price:: check_positive_price,
44
+ quantity:: check_positive_quantity,
45
+ } ,
36
46
} ;
37
47
38
48
#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -53,6 +63,7 @@ pub struct LimitIfTouchedOrder {
53
63
core : OrderCore ,
54
64
}
55
65
66
+ #[ allow( clippy:: too_many_arguments) ]
56
67
impl LimitIfTouchedOrder {
57
68
/// Creates a new [`LimitIfTouchedOrder`] instance.
58
69
///
@@ -90,7 +101,94 @@ impl LimitIfTouchedOrder {
90
101
init_id : UUID4 ,
91
102
ts_init : UnixNanos ,
92
103
) -> Self {
93
- // TODO: Implement new_checked and check quantity positive, add error docs.
104
+ Self :: new_checked (
105
+ trader_id,
106
+ strategy_id,
107
+ instrument_id,
108
+ client_order_id,
109
+ order_side,
110
+ quantity,
111
+ price,
112
+ trigger_price,
113
+ trigger_type,
114
+ time_in_force,
115
+ expire_time,
116
+ post_only,
117
+ reduce_only,
118
+ quote_quantity,
119
+ display_qty,
120
+ emulation_trigger,
121
+ trigger_instrument_id,
122
+ contingency_type,
123
+ order_list_id,
124
+ linked_order_ids,
125
+ parent_order_id,
126
+ exec_algorithm_id,
127
+ exec_algorithm_params,
128
+ exec_spawn_id,
129
+ tags,
130
+ init_id,
131
+ ts_init,
132
+ )
133
+ . expect ( FAILED )
134
+ }
135
+
136
+ #[ allow( clippy:: too_many_arguments) ]
137
+ pub fn new_checked (
138
+ trader_id : TraderId ,
139
+ strategy_id : StrategyId ,
140
+ instrument_id : InstrumentId ,
141
+ client_order_id : ClientOrderId ,
142
+ order_side : OrderSide ,
143
+ quantity : Quantity ,
144
+ price : Price ,
145
+ trigger_price : Price ,
146
+ trigger_type : TriggerType ,
147
+ time_in_force : TimeInForce ,
148
+ expire_time : Option < UnixNanos > ,
149
+ post_only : bool ,
150
+ reduce_only : bool ,
151
+ quote_quantity : bool ,
152
+ display_qty : Option < Quantity > ,
153
+ emulation_trigger : Option < TriggerType > ,
154
+ trigger_instrument_id : Option < InstrumentId > ,
155
+ contingency_type : Option < ContingencyType > ,
156
+ order_list_id : Option < OrderListId > ,
157
+ linked_order_ids : Option < Vec < ClientOrderId > > ,
158
+ parent_order_id : Option < ClientOrderId > ,
159
+ exec_algorithm_id : Option < ExecAlgorithmId > ,
160
+ exec_algorithm_params : Option < IndexMap < Ustr , Ustr > > ,
161
+ exec_spawn_id : Option < ClientOrderId > ,
162
+ tags : Option < Vec < Ustr > > ,
163
+ init_id : UUID4 ,
164
+ ts_init : UnixNanos ,
165
+ ) -> anyhow:: Result < Self > {
166
+ check_positive_quantity ( quantity, "quantity" ) ?;
167
+ check_positive_price ( price, "price" ) ?;
168
+ check_positive_price ( trigger_price, "trigger_price" ) ?;
169
+
170
+ if let Some ( disp) = display_qty {
171
+ check_positive_quantity ( disp, "display_qty" ) ?;
172
+ check_predicate_false ( disp > quantity, "`display_qty` may not exceed `quantity`" ) ?;
173
+ }
174
+
175
+ if time_in_force == TimeInForce :: Gtd {
176
+ check_predicate_false (
177
+ expire_time. unwrap_or_default ( ) == 0 ,
178
+ "Condition failed: `expire_time` is required for `GTD` order" ,
179
+ ) ?;
180
+ }
181
+
182
+ match order_side {
183
+ OrderSide :: Buy if trigger_price > price => {
184
+ anyhow:: bail!( "BUY Limit-If-Touched must have `trigger_price` <= `price`" )
185
+ }
186
+ OrderSide :: Sell if trigger_price < price => {
187
+ anyhow:: bail!( "SELL Limit-If-Touched must have `trigger_price` >= `price`" )
188
+ }
189
+ _ => { }
190
+ }
191
+
94
192
let init_order = OrderInitialized :: new (
95
193
trader_id,
96
194
strategy_id,
@@ -126,8 +224,8 @@ impl LimitIfTouchedOrder {
126
224
exec_spawn_id,
127
225
tags,
128
226
) ;
129
- Self {
130
- core : OrderCore :: new ( init_order ) ,
227
+
228
+ Ok ( Self {
131
229
price,
132
230
trigger_price,
133
231
trigger_type,
@@ -137,7 +235,8 @@ impl LimitIfTouchedOrder {
137
235
trigger_instrument_id,
138
236
is_triggered : false ,
139
237
ts_triggered : None ,
140
- }
238
+ core : OrderCore :: new ( init_order) ,
239
+ } )
141
240
}
142
241
}
143
242
@@ -421,7 +520,7 @@ impl Order for LimitIfTouchedOrder {
421
520
}
422
521
423
522
fn set_liquidity_side ( & mut self , liquidity_side : LiquiditySide ) {
424
- self . liquidity_side = Some ( liquidity_side)
523
+ self . liquidity_side = Some ( liquidity_side) ;
425
524
}
426
525
427
526
fn would_reduce_only ( & self , side : PositionSide , position_qty : Quantity ) -> bool {
@@ -433,6 +532,23 @@ impl Order for LimitIfTouchedOrder {
433
532
}
434
533
}
435
534
535
+ impl Display for LimitIfTouchedOrder {
536
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
537
+ write ! (
538
+ f,
539
+ "LimitIfTouchedOrder({} {} {} @ {} / trigger {} ({:?}) {}, status={})" ,
540
+ self . side,
541
+ self . quantity. to_formatted_string( ) ,
542
+ self . instrument_id,
543
+ self . price,
544
+ self . trigger_price,
545
+ self . trigger_type,
546
+ self . time_in_force,
547
+ self . status
548
+ )
549
+ }
550
+ }
551
+
436
552
impl From < OrderInitialized > for LimitIfTouchedOrder {
437
553
fn from ( event : OrderInitialized ) -> Self {
438
554
Self :: new (
@@ -474,3 +590,85 @@ impl From<OrderInitialized> for LimitIfTouchedOrder {
474
590
)
475
591
}
476
592
}
593
+
594
+ ////////////////////////////////////////////////////////////////////////////////
595
+ // Tests
596
+ ////////////////////////////////////////////////////////////////////////////////
597
+ #[ cfg( test) ]
598
+ mod tests {
599
+ use rstest:: rstest;
600
+
601
+ use crate :: {
602
+ enums:: { OrderSide , OrderType , TimeInForce , TriggerType } ,
603
+ instruments:: { CurrencyPair , stubs:: * } ,
604
+ orders:: builder:: OrderTestBuilder ,
605
+ types:: { Price , Quantity } ,
606
+ } ;
607
+
608
+ #[ rstest]
609
+ fn ok ( audusd_sim : CurrencyPair ) {
610
+ let _ = OrderTestBuilder :: new ( OrderType :: LimitIfTouched )
611
+ . instrument_id ( audusd_sim. id )
612
+ . side ( OrderSide :: Buy )
613
+ . trigger_price ( Price :: from ( "30200" ) )
614
+ . price ( Price :: from ( "30200" ) )
615
+ . trigger_type ( TriggerType :: LastPrice )
616
+ . quantity ( Quantity :: from ( 1 ) )
617
+ . build ( ) ;
618
+ }
619
+
620
+ #[ rstest]
621
+ #[ should_panic(
622
+ expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
623
+ ) ]
624
+ fn quantity_zero ( audusd_sim : CurrencyPair ) {
625
+ let _ = OrderTestBuilder :: new ( OrderType :: LimitIfTouched )
626
+ . instrument_id ( audusd_sim. id )
627
+ . side ( OrderSide :: Buy )
628
+ . price ( Price :: from ( "30000" ) )
629
+ . trigger_price ( Price :: from ( "30200" ) )
630
+ . trigger_type ( TriggerType :: LastPrice )
631
+ . quantity ( Quantity :: from ( 0 ) )
632
+ . build ( ) ;
633
+ }
634
+
635
+ #[ rstest]
636
+ #[ should_panic( expected = "Condition failed: `expire_time` is required for `GTD` order" ) ]
637
+ fn gtd_without_expire ( audusd_sim : CurrencyPair ) {
638
+ let _ = OrderTestBuilder :: new ( OrderType :: LimitIfTouched )
639
+ . instrument_id ( audusd_sim. id )
640
+ . side ( OrderSide :: Buy )
641
+ . price ( Price :: from ( "30000" ) )
642
+ . trigger_price ( Price :: from ( "30200" ) )
643
+ . trigger_type ( TriggerType :: LastPrice )
644
+ . quantity ( Quantity :: from ( 1 ) )
645
+ . time_in_force ( TimeInForce :: Gtd )
646
+ . build ( ) ;
647
+ }
648
+
649
+ #[ rstest]
650
+ #[ should_panic( expected = "BUY Limit-If-Touched must have `trigger_price` <= `price`" ) ]
651
+ fn buy_trigger_gt_price ( audusd_sim : CurrencyPair ) {
652
+ OrderTestBuilder :: new ( OrderType :: LimitIfTouched )
653
+ . instrument_id ( audusd_sim. id )
654
+ . side ( OrderSide :: Buy )
655
+ . trigger_price ( Price :: from ( "30300" ) ) // Invalid trigger > price
656
+ . price ( Price :: from ( "30200" ) )
657
+ . trigger_type ( TriggerType :: LastPrice )
658
+ . quantity ( Quantity :: from ( 1 ) )
659
+ . build ( ) ;
660
+ }
661
+
662
+ #[ rstest]
663
+ #[ should_panic( expected = "SELL Limit-If-Touched must have `trigger_price` >= `price`" ) ]
664
+ fn sell_trigger_lt_price ( audusd_sim : CurrencyPair ) {
665
+ OrderTestBuilder :: new ( OrderType :: LimitIfTouched )
666
+ . instrument_id ( audusd_sim. id )
667
+ . side ( OrderSide :: Sell )
668
+ . trigger_price ( Price :: from ( "30100" ) ) // Invalid trigger < price
669
+ . price ( Price :: from ( "30200" ) )
670
+ . trigger_type ( TriggerType :: LastPrice )
671
+ . quantity ( Quantity :: from ( 1 ) )
672
+ . build ( ) ;
673
+ }
674
+ }
0 commit comments