Skip to content

Commit 87e7f50

Browse files
authored
Improve validations for MarketIfTouchedOrder (#2577)
1 parent 01f69ce commit 87e7f50

File tree

1 file changed

+159
-13
lines changed

1 file changed

+159
-13
lines changed

crates/model/src/orders/market_if_touched.rs

+159-13
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
use std::ops::{Deref, DerefMut};
1717

1818
use indexmap::IndexMap;
19-
use nautilus_core::{UUID4, UnixNanos};
19+
use nautilus_core::{
20+
UUID4, UnixNanos,
21+
correctness::{FAILED, check_predicate_false},
22+
};
2023
use rust_decimal::Decimal;
2124
use serde::{Deserialize, Serialize};
2225
use 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 {
5460
impl 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

Comments
 (0)