Skip to content

Commit df5f6b1

Browse files
authored
Improve validations for MarketToLimitOrder (#2584)
1 parent 53c71a6 commit df5f6b1

File tree

1 file changed

+136
-8
lines changed

1 file changed

+136
-8
lines changed

crates/model/src/orders/market_to_limit.rs

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
use std::ops::{Deref, DerefMut};
1717

18+
use anyhow;
1819
use indexmap::IndexMap;
19-
use nautilus_core::{UUID4, UnixNanos};
20+
use nautilus_core::{
21+
UUID4, UnixNanos,
22+
correctness::{FAILED, check_predicate_false},
23+
};
2024
use rust_decimal::Decimal;
2125
use serde::{Deserialize, Serialize};
2226
use ustr::Ustr;
@@ -33,7 +37,7 @@ use crate::{
3337
StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
3438
},
3539
orders::OrderError,
36-
types::{Currency, Money, Price, Quantity},
40+
types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
3741
};
3842

3943
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -52,7 +56,7 @@ pub struct MarketToLimitOrder {
5256
impl MarketToLimitOrder {
5357
/// Creates a new [`MarketToLimitOrder`] instance.
5458
#[allow(clippy::too_many_arguments)]
55-
pub fn new(
59+
pub fn new_checked(
5660
trader_id: TraderId,
5761
strategy_id: StrategyId,
5862
instrument_id: InstrumentId,
@@ -75,8 +79,21 @@ impl MarketToLimitOrder {
7579
tags: Option<Vec<Ustr>>,
7680
init_id: UUID4,
7781
ts_init: UnixNanos,
78-
) -> Self {
79-
// TODO: Implement new_checked and check quantity positive, add error docs.
82+
) -> anyhow::Result<Self> {
83+
check_positive_quantity(quantity, "quantity")?;
84+
85+
if let Some(disp) = display_qty {
86+
check_positive_quantity(disp, "display_qty")?;
87+
check_predicate_false(disp > quantity, "`display_qty` may not exceed `quantity`")?;
88+
}
89+
90+
if time_in_force == TimeInForce::Gtd {
91+
check_predicate_false(
92+
expire_time.unwrap_or_default() == 0,
93+
"Condition failed: `expire_time` is required for `GTD` order",
94+
)?;
95+
}
96+
8097
let init_order = OrderInitialized::new(
8198
trader_id,
8299
strategy_id,
@@ -112,13 +129,66 @@ impl MarketToLimitOrder {
112129
exec_spawn_id,
113130
tags,
114131
);
115-
Self {
132+
133+
Ok(Self {
116134
core: OrderCore::new(init_order),
117135
price: None, // Price will be determined on fill
118136
expire_time,
119137
is_post_only: post_only,
120138
display_qty,
121-
}
139+
})
140+
}
141+
142+
#[allow(clippy::too_many_arguments)]
143+
pub fn new(
144+
trader_id: TraderId,
145+
strategy_id: StrategyId,
146+
instrument_id: InstrumentId,
147+
client_order_id: ClientOrderId,
148+
order_side: OrderSide,
149+
quantity: Quantity,
150+
time_in_force: TimeInForce,
151+
expire_time: Option<UnixNanos>,
152+
post_only: bool,
153+
reduce_only: bool,
154+
quote_quantity: bool,
155+
display_qty: Option<Quantity>,
156+
contingency_type: Option<ContingencyType>,
157+
order_list_id: Option<OrderListId>,
158+
linked_order_ids: Option<Vec<ClientOrderId>>,
159+
parent_order_id: Option<ClientOrderId>,
160+
exec_algorithm_id: Option<ExecAlgorithmId>,
161+
exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
162+
exec_spawn_id: Option<ClientOrderId>,
163+
tags: Option<Vec<Ustr>>,
164+
init_id: UUID4,
165+
ts_init: UnixNanos,
166+
) -> Self {
167+
Self::new_checked(
168+
trader_id,
169+
strategy_id,
170+
instrument_id,
171+
client_order_id,
172+
order_side,
173+
quantity,
174+
time_in_force,
175+
expire_time,
176+
post_only,
177+
reduce_only,
178+
quote_quantity,
179+
display_qty,
180+
contingency_type,
181+
order_list_id,
182+
linked_order_ids,
183+
parent_order_id,
184+
exec_algorithm_id,
185+
exec_algorithm_params,
186+
exec_spawn_id,
187+
tags,
188+
init_id,
189+
ts_init,
190+
)
191+
.expect(FAILED)
122192
}
123193
}
124194

@@ -404,7 +474,7 @@ impl Order for MarketToLimitOrder {
404474
}
405475

406476
fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
407-
self.liquidity_side = Some(liquidity_side)
477+
self.liquidity_side = Some(liquidity_side);
408478
}
409479

410480
fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
@@ -444,3 +514,61 @@ impl From<OrderInitialized> for MarketToLimitOrder {
444514
)
445515
}
446516
}
517+
518+
////////////////////////////////////////////////////////////////////////////////
519+
// Tests
520+
////////////////////////////////////////////////////////////////////////////////
521+
#[cfg(test)]
522+
mod tests {
523+
use rstest::rstest;
524+
525+
use crate::{
526+
enums::{OrderSide, OrderType, TimeInForce},
527+
instruments::{CurrencyPair, stubs::*},
528+
orders::builder::OrderTestBuilder,
529+
types::Quantity,
530+
};
531+
532+
#[rstest]
533+
fn ok(audusd_sim: CurrencyPair) {
534+
let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
535+
.instrument_id(audusd_sim.id)
536+
.side(OrderSide::Buy)
537+
.quantity(Quantity::from(1))
538+
.build();
539+
}
540+
541+
#[rstest]
542+
#[should_panic(
543+
expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
544+
)]
545+
fn quantity_zero(audusd_sim: CurrencyPair) {
546+
let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
547+
.instrument_id(audusd_sim.id)
548+
.side(OrderSide::Buy)
549+
.quantity(Quantity::from(0)) // Invalid: zero quantity
550+
.build();
551+
}
552+
553+
#[rstest]
554+
#[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
555+
fn gtd_without_expire(audusd_sim: CurrencyPair) {
556+
let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
557+
.instrument_id(audusd_sim.id)
558+
.side(OrderSide::Buy)
559+
.quantity(Quantity::from(1))
560+
.time_in_force(TimeInForce::Gtd) // Missing expire_time
561+
.build();
562+
}
563+
564+
#[rstest]
565+
#[should_panic(expected = "`display_qty` may not exceed `quantity`")]
566+
fn display_qty_gt_quantity(audusd_sim: CurrencyPair) {
567+
let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
568+
.instrument_id(audusd_sim.id)
569+
.side(OrderSide::Buy)
570+
.quantity(Quantity::from(1))
571+
.display_qty(Quantity::from(2)) // Invalid: display > quantity
572+
.build();
573+
}
574+
}

0 commit comments

Comments
 (0)