1use std::fmt::{Debug, Display};
17
18use derive_builder::Builder;
19use nautilus_core::{UUID4, UnixNanos, serialization::from_bool_as_u8};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22use ustr::Ustr;
23
24use crate::{
25 enums::{
26 ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType,
27 TriggerType,
28 },
29 events::OrderEvent,
30 identifiers::{
31 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
32 StrategyId, TradeId, TraderId, VenueOrderId,
33 },
34 types::{Currency, Money, Price, Quantity},
35};
36
37#[repr(C)]
39#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)]
40#[builder(default)]
41#[serde(tag = "type")]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct OrderRejected {
47 pub trader_id: TraderId,
49 pub strategy_id: StrategyId,
51 pub instrument_id: InstrumentId,
53 pub client_order_id: ClientOrderId,
55 pub account_id: AccountId,
57 pub reason: Ustr,
59 pub event_id: UUID4,
61 pub ts_event: UnixNanos,
63 pub ts_init: UnixNanos,
65 #[serde(deserialize_with = "from_bool_as_u8")]
67 pub reconciliation: u8, #[serde(default, deserialize_with = "from_bool_as_u8")]
70 pub due_post_only: u8, }
72
73impl OrderRejected {
74 #[allow(clippy::too_many_arguments)]
76 pub fn new(
77 trader_id: TraderId,
78 strategy_id: StrategyId,
79 instrument_id: InstrumentId,
80 client_order_id: ClientOrderId,
81 account_id: AccountId,
82 reason: Ustr,
83 event_id: UUID4,
84 ts_event: UnixNanos,
85 ts_init: UnixNanos,
86 reconciliation: bool,
87 due_post_only: bool,
88 ) -> Self {
89 Self {
90 trader_id,
91 strategy_id,
92 instrument_id,
93 client_order_id,
94 account_id,
95 reason,
96 event_id,
97 ts_event,
98 ts_init,
99 reconciliation: u8::from(reconciliation),
100 due_post_only: u8::from(due_post_only),
101 }
102 }
103}
104
105impl Debug for OrderRejected {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(
108 f,
109 "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})",
110 stringify!(OrderRejected),
111 self.trader_id,
112 self.strategy_id,
113 self.instrument_id,
114 self.client_order_id,
115 self.account_id,
116 self.reason,
117 self.event_id,
118 self.ts_event,
119 self.ts_init
120 )
121 }
122}
123
124impl Display for OrderRejected {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(
127 f,
128 "{}(instrument_id={}, client_order_id={}, account_id={}, reason='{}', ts_event={})",
129 stringify!(OrderRejected),
130 self.instrument_id,
131 self.client_order_id,
132 self.account_id,
133 self.reason,
134 self.ts_event
135 )
136 }
137}
138
139impl OrderEvent for OrderRejected {
140 fn id(&self) -> UUID4 {
141 self.event_id
142 }
143
144 fn kind(&self) -> &str {
145 stringify!(OrderRejected)
146 }
147
148 fn order_type(&self) -> Option<OrderType> {
149 None
150 }
151
152 fn order_side(&self) -> Option<OrderSide> {
153 None
154 }
155
156 fn trader_id(&self) -> TraderId {
157 self.trader_id
158 }
159
160 fn strategy_id(&self) -> StrategyId {
161 self.strategy_id
162 }
163
164 fn instrument_id(&self) -> InstrumentId {
165 self.instrument_id
166 }
167
168 fn trade_id(&self) -> Option<TradeId> {
169 None
170 }
171
172 fn currency(&self) -> Option<Currency> {
173 None
174 }
175
176 fn client_order_id(&self) -> ClientOrderId {
177 self.client_order_id
178 }
179
180 fn reason(&self) -> Option<Ustr> {
181 Some(self.reason)
182 }
183
184 fn quantity(&self) -> Option<Quantity> {
185 None
186 }
187
188 fn time_in_force(&self) -> Option<TimeInForce> {
189 None
190 }
191
192 fn liquidity_side(&self) -> Option<LiquiditySide> {
193 None
194 }
195
196 fn post_only(&self) -> Option<bool> {
197 None
198 }
199
200 fn reduce_only(&self) -> Option<bool> {
201 None
202 }
203
204 fn quote_quantity(&self) -> Option<bool> {
205 None
206 }
207
208 fn reconciliation(&self) -> bool {
209 false
210 }
211
212 fn price(&self) -> Option<Price> {
213 None
214 }
215
216 fn last_px(&self) -> Option<Price> {
217 None
218 }
219
220 fn last_qty(&self) -> Option<Quantity> {
221 None
222 }
223
224 fn trigger_price(&self) -> Option<Price> {
225 None
226 }
227
228 fn trigger_type(&self) -> Option<TriggerType> {
229 None
230 }
231
232 fn limit_offset(&self) -> Option<Decimal> {
233 None
234 }
235
236 fn trailing_offset(&self) -> Option<Decimal> {
237 None
238 }
239
240 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
241 None
242 }
243
244 fn expire_time(&self) -> Option<UnixNanos> {
245 None
246 }
247
248 fn display_qty(&self) -> Option<Quantity> {
249 None
250 }
251
252 fn emulation_trigger(&self) -> Option<TriggerType> {
253 None
254 }
255
256 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
257 None
258 }
259
260 fn contingency_type(&self) -> Option<ContingencyType> {
261 None
262 }
263
264 fn order_list_id(&self) -> Option<OrderListId> {
265 None
266 }
267
268 fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
269 None
270 }
271
272 fn parent_order_id(&self) -> Option<ClientOrderId> {
273 None
274 }
275
276 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
277 None
278 }
279
280 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
281 None
282 }
283
284 fn venue_order_id(&self) -> Option<VenueOrderId> {
285 None
286 }
287
288 fn account_id(&self) -> Option<AccountId> {
289 Some(self.account_id)
290 }
291
292 fn position_id(&self) -> Option<PositionId> {
293 None
294 }
295
296 fn commission(&self) -> Option<Money> {
297 None
298 }
299
300 fn ts_event(&self) -> UnixNanos {
301 self.ts_event
302 }
303
304 fn ts_init(&self) -> UnixNanos {
305 self.ts_init
306 }
307}
308
309#[cfg(test)]
313mod tests {
314 use nautilus_core::UnixNanos;
315 use rstest::rstest;
316 use ustr::Ustr;
317
318 use super::*;
319 use crate::{
320 events::order::stubs::*,
321 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
322 };
323
324 fn create_test_order_rejected() -> OrderRejected {
325 OrderRejected::new(
326 TraderId::from("TRADER-001"),
327 StrategyId::from("EMA-CROSS"),
328 InstrumentId::from("EURUSD.SIM"),
329 ClientOrderId::from("O-19700101-000000-001-001-1"),
330 AccountId::from("SIM-001"),
331 Ustr::from("INSUFFICIENT_MARGIN"),
332 Default::default(),
333 UnixNanos::from(1_000_000_000),
334 UnixNanos::from(2_000_000_000),
335 false,
336 false,
337 )
338 }
339
340 #[rstest]
341 fn test_order_rejected_new() {
342 let order_rejected = create_test_order_rejected();
343
344 assert_eq!(order_rejected.trader_id, TraderId::from("TRADER-001"));
345 assert_eq!(order_rejected.strategy_id, StrategyId::from("EMA-CROSS"));
346 assert_eq!(
347 order_rejected.instrument_id,
348 InstrumentId::from("EURUSD.SIM")
349 );
350 assert_eq!(
351 order_rejected.client_order_id,
352 ClientOrderId::from("O-19700101-000000-001-001-1")
353 );
354 assert_eq!(order_rejected.account_id, AccountId::from("SIM-001"));
355 assert_eq!(order_rejected.reason, Ustr::from("INSUFFICIENT_MARGIN"));
356 assert_eq!(order_rejected.ts_event, UnixNanos::from(1_000_000_000));
357 assert_eq!(order_rejected.ts_init, UnixNanos::from(2_000_000_000));
358 assert_eq!(order_rejected.reconciliation, 0);
359 assert_eq!(order_rejected.due_post_only, 0);
360 }
361
362 #[rstest]
363 fn test_order_rejected_new_with_reconciliation() {
364 let order_rejected = OrderRejected::new(
365 TraderId::from("TRADER-001"),
366 StrategyId::from("EMA-CROSS"),
367 InstrumentId::from("EURUSD.SIM"),
368 ClientOrderId::from("O-19700101-000000-001-001-1"),
369 AccountId::from("SIM-001"),
370 Ustr::from("INVALID_PRICE"),
371 Default::default(),
372 UnixNanos::from(1_000_000_000),
373 UnixNanos::from(2_000_000_000),
374 true,
375 false,
376 );
377
378 assert_eq!(order_rejected.reconciliation, 1);
379 }
380
381 #[rstest]
382 fn test_order_rejected_clone() {
383 let order_rejected1 = create_test_order_rejected();
384 let order_rejected2 = order_rejected1;
385
386 assert_eq!(order_rejected1, order_rejected2);
387 }
388
389 #[rstest]
390 fn test_order_rejected_debug() {
391 let order_rejected = create_test_order_rejected();
392 let debug_str = format!("{order_rejected:?}");
393
394 assert!(debug_str.contains("OrderRejected"));
395 assert!(debug_str.contains("TRADER-001"));
396 assert!(debug_str.contains("EMA-CROSS"));
397 assert!(debug_str.contains("EURUSD.SIM"));
398 assert!(debug_str.contains("O-19700101-000000-001-001-1"));
399 assert!(debug_str.contains("INSUFFICIENT_MARGIN"));
400 }
401
402 #[rstest]
403 fn test_order_rejected_display(order_rejected_insufficient_margin: OrderRejected) {
404 let display = format!("{order_rejected_insufficient_margin}");
405 assert_eq!(
406 display,
407 "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
408 account_id=SIM-001, reason='INSUFFICIENT_MARGIN', ts_event=0)"
409 );
410 }
411
412 #[rstest]
413 fn test_order_rejected_partial_eq() {
414 let order_rejected1 = create_test_order_rejected();
415 let mut order_rejected2 = create_test_order_rejected();
416 order_rejected2.event_id = order_rejected1.event_id; let mut order_rejected3 = create_test_order_rejected();
418 order_rejected3.reason = Ustr::from("INVALID_ORDER");
419
420 assert_eq!(order_rejected1, order_rejected2);
421 assert_ne!(order_rejected1, order_rejected3);
422 }
423
424 #[rstest]
425 fn test_order_rejected_default() {
426 let order_rejected = OrderRejected::default();
427
428 assert_eq!(order_rejected.trader_id, TraderId::default());
429 assert_eq!(order_rejected.strategy_id, StrategyId::default());
430 assert_eq!(order_rejected.instrument_id, InstrumentId::default());
431 assert_eq!(order_rejected.client_order_id, ClientOrderId::default());
432 assert_eq!(order_rejected.account_id, AccountId::default());
433 assert_eq!(order_rejected.reconciliation, 0);
434 assert_eq!(order_rejected.due_post_only, 0);
435 }
436
437 #[rstest]
438 fn test_order_rejected_order_event_trait() {
439 let order_rejected = create_test_order_rejected();
440
441 assert_eq!(order_rejected.id(), order_rejected.event_id);
442 assert_eq!(order_rejected.kind(), "OrderRejected");
443 assert_eq!(order_rejected.order_type(), None);
444 assert_eq!(order_rejected.order_side(), None);
445 assert_eq!(order_rejected.trader_id(), TraderId::from("TRADER-001"));
446 assert_eq!(order_rejected.strategy_id(), StrategyId::from("EMA-CROSS"));
447 assert_eq!(
448 order_rejected.instrument_id(),
449 InstrumentId::from("EURUSD.SIM")
450 );
451 assert_eq!(order_rejected.trade_id(), None);
452 assert_eq!(order_rejected.currency(), None);
453 assert_eq!(
454 order_rejected.client_order_id(),
455 ClientOrderId::from("O-19700101-000000-001-001-1")
456 );
457 assert_eq!(
458 order_rejected.reason(),
459 Some(Ustr::from("INSUFFICIENT_MARGIN"))
460 );
461 assert_eq!(order_rejected.quantity(), None);
462 assert_eq!(order_rejected.time_in_force(), None);
463 assert_eq!(order_rejected.liquidity_side(), None);
464 assert_eq!(order_rejected.post_only(), None);
465 assert_eq!(order_rejected.reduce_only(), None);
466 assert_eq!(order_rejected.quote_quantity(), None);
467 assert!(!order_rejected.reconciliation());
468 assert_eq!(order_rejected.venue_order_id(), None);
469 assert_eq!(
470 order_rejected.account_id(),
471 Some(AccountId::from("SIM-001"))
472 );
473 assert_eq!(order_rejected.position_id(), None);
474 assert_eq!(order_rejected.commission(), None);
475 assert_eq!(order_rejected.ts_event(), UnixNanos::from(1_000_000_000));
476 assert_eq!(order_rejected.ts_init(), UnixNanos::from(2_000_000_000));
477 }
478
479 #[rstest]
480 fn test_order_rejected_different_reasons() {
481 let mut insufficient_margin = create_test_order_rejected();
482 insufficient_margin.reason = Ustr::from("INSUFFICIENT_MARGIN");
483
484 let mut invalid_price = create_test_order_rejected();
485 invalid_price.reason = Ustr::from("INVALID_PRICE");
486
487 let mut market_closed = create_test_order_rejected();
488 market_closed.reason = Ustr::from("MARKET_CLOSED");
489
490 assert_ne!(insufficient_margin, invalid_price);
491 assert_ne!(invalid_price, market_closed);
492 assert_eq!(
493 insufficient_margin.reason,
494 Ustr::from("INSUFFICIENT_MARGIN")
495 );
496 assert_eq!(invalid_price.reason, Ustr::from("INVALID_PRICE"));
497 assert_eq!(market_closed.reason, Ustr::from("MARKET_CLOSED"));
498 }
499
500 #[rstest]
501 fn test_order_rejected_timestamps() {
502 let order_rejected = create_test_order_rejected();
503
504 assert_eq!(order_rejected.ts_event, UnixNanos::from(1_000_000_000));
505 assert_eq!(order_rejected.ts_init, UnixNanos::from(2_000_000_000));
506 assert!(order_rejected.ts_event < order_rejected.ts_init);
507 }
508
509 #[rstest]
510 fn test_order_rejected_serialization() {
511 let original = create_test_order_rejected();
512
513 let json = serde_json::to_string(&original).unwrap();
514 let deserialized: OrderRejected = serde_json::from_str(&json).unwrap();
515
516 assert_eq!(original, deserialized);
517 }
518
519 #[rstest]
520 fn test_order_rejected_different_accounts() {
521 let mut live_account = create_test_order_rejected();
522 live_account.account_id = AccountId::from("LIVE-001");
523
524 let mut sim_account = create_test_order_rejected();
525 sim_account.account_id = AccountId::from("SIM-001");
526
527 assert_ne!(live_account, sim_account);
528 assert_eq!(live_account.account_id, AccountId::from("LIVE-001"));
529 assert_eq!(sim_account.account_id, AccountId::from("SIM-001"));
530 }
531
532 #[rstest]
533 fn test_order_rejected_different_instruments() {
534 let mut btc_order = create_test_order_rejected();
535 btc_order.instrument_id = InstrumentId::from("BTCUSD.COINBASE");
536
537 let mut eth_order = create_test_order_rejected();
538 eth_order.instrument_id = InstrumentId::from("ETHUSD.COINBASE");
539
540 assert_ne!(btc_order, eth_order);
541 assert_eq!(
542 btc_order.instrument_id,
543 InstrumentId::from("BTCUSD.COINBASE")
544 );
545 assert_eq!(
546 eth_order.instrument_id,
547 InstrumentId::from("ETHUSD.COINBASE")
548 );
549 }
550
551 #[rstest]
552 fn test_order_rejected_with_due_post_only() {
553 let order_rejected = OrderRejected::new(
554 TraderId::from("TRADER-001"),
555 StrategyId::from("EMA-CROSS"),
556 InstrumentId::from("EURUSD.SIM"),
557 ClientOrderId::from("O-19700101-000000-001-001-1"),
558 AccountId::from("SIM-001"),
559 Ustr::from("POST_ONLY_WOULD_EXECUTE"),
560 Default::default(),
561 UnixNanos::from(1_000_000_000),
562 UnixNanos::from(2_000_000_000),
563 false,
564 true,
565 );
566
567 assert_eq!(order_rejected.due_post_only, 1);
568 assert_eq!(order_rejected.reason, Ustr::from("POST_ONLY_WOULD_EXECUTE"));
569 }
570}