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, Serialize, Deserialize, Builder)]
40#[serde(tag = "type")]
41#[cfg_attr(any(test, feature = "stubs"), builder(default))]
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)]
310mod tests {
311 use nautilus_core::UnixNanos;
312 use rstest::rstest;
313 use ustr::Ustr;
314
315 use super::*;
316 use crate::{
317 events::order::stubs::*,
318 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
319 stubs::TestDefault,
320 };
321
322 fn create_test_order_rejected() -> OrderRejected {
323 OrderRejected::new(
324 TraderId::from("TRADER-001"),
325 StrategyId::from("EMA-CROSS"),
326 InstrumentId::from("EURUSD.SIM"),
327 ClientOrderId::from("O-19700101-000000-001-001-1"),
328 AccountId::from("SIM-001"),
329 Ustr::from("INSUFFICIENT_MARGIN"),
330 Default::default(),
331 UnixNanos::from(1_000_000_000),
332 UnixNanos::from(2_000_000_000),
333 false,
334 false,
335 )
336 }
337
338 #[rstest]
339 fn test_order_rejected_new() {
340 let order_rejected = create_test_order_rejected();
341
342 assert_eq!(order_rejected.trader_id, TraderId::from("TRADER-001"));
343 assert_eq!(order_rejected.strategy_id, StrategyId::from("EMA-CROSS"));
344 assert_eq!(
345 order_rejected.instrument_id,
346 InstrumentId::from("EURUSD.SIM")
347 );
348 assert_eq!(
349 order_rejected.client_order_id,
350 ClientOrderId::from("O-19700101-000000-001-001-1")
351 );
352 assert_eq!(order_rejected.account_id, AccountId::from("SIM-001"));
353 assert_eq!(order_rejected.reason, Ustr::from("INSUFFICIENT_MARGIN"));
354 assert_eq!(order_rejected.ts_event, UnixNanos::from(1_000_000_000));
355 assert_eq!(order_rejected.ts_init, UnixNanos::from(2_000_000_000));
356 assert_eq!(order_rejected.reconciliation, 0);
357 assert_eq!(order_rejected.due_post_only, 0);
358 }
359
360 #[rstest]
361 fn test_order_rejected_new_with_reconciliation() {
362 let order_rejected = OrderRejected::new(
363 TraderId::from("TRADER-001"),
364 StrategyId::from("EMA-CROSS"),
365 InstrumentId::from("EURUSD.SIM"),
366 ClientOrderId::from("O-19700101-000000-001-001-1"),
367 AccountId::from("SIM-001"),
368 Ustr::from("INVALID_PRICE"),
369 Default::default(),
370 UnixNanos::from(1_000_000_000),
371 UnixNanos::from(2_000_000_000),
372 true,
373 false,
374 );
375
376 assert_eq!(order_rejected.reconciliation, 1);
377 }
378
379 #[rstest]
380 fn test_order_rejected_clone() {
381 let order_rejected1 = create_test_order_rejected();
382 let order_rejected2 = order_rejected1;
383
384 assert_eq!(order_rejected1, order_rejected2);
385 }
386
387 #[rstest]
388 fn test_order_rejected_debug() {
389 let order_rejected = create_test_order_rejected();
390 let debug_str = format!("{order_rejected:?}");
391
392 assert!(debug_str.contains("OrderRejected"));
393 assert!(debug_str.contains("TRADER-001"));
394 assert!(debug_str.contains("EMA-CROSS"));
395 assert!(debug_str.contains("EURUSD.SIM"));
396 assert!(debug_str.contains("O-19700101-000000-001-001-1"));
397 assert!(debug_str.contains("INSUFFICIENT_MARGIN"));
398 }
399
400 #[rstest]
401 fn test_order_rejected_display(order_rejected_insufficient_margin: OrderRejected) {
402 let display = format!("{order_rejected_insufficient_margin}");
403 assert_eq!(
404 display,
405 "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
406 account_id=SIM-001, reason='INSUFFICIENT_MARGIN', ts_event=0)"
407 );
408 }
409
410 #[rstest]
411 fn test_order_rejected_partial_eq() {
412 let order_rejected1 = create_test_order_rejected();
413 let mut order_rejected2 = create_test_order_rejected();
414 order_rejected2.event_id = order_rejected1.event_id; let mut order_rejected3 = create_test_order_rejected();
416 order_rejected3.reason = Ustr::from("INVALID_ORDER");
417
418 assert_eq!(order_rejected1, order_rejected2);
419 assert_ne!(order_rejected1, order_rejected3);
420 }
421
422 #[rstest]
423 fn test_order_rejected_default() {
424 let order_rejected = OrderRejected::default();
425
426 assert_eq!(order_rejected.trader_id, TraderId::test_default());
427 assert_eq!(order_rejected.strategy_id, StrategyId::test_default());
428 assert_eq!(order_rejected.instrument_id, InstrumentId::test_default());
429 assert_eq!(
430 order_rejected.client_order_id,
431 ClientOrderId::test_default()
432 );
433 assert_eq!(order_rejected.account_id, AccountId::test_default());
434 assert_eq!(order_rejected.reconciliation, 0);
435 assert_eq!(order_rejected.due_post_only, 0);
436 }
437
438 #[rstest]
439 fn test_order_rejected_order_event_trait() {
440 let order_rejected = create_test_order_rejected();
441
442 assert_eq!(order_rejected.id(), order_rejected.event_id);
443 assert_eq!(order_rejected.kind(), "OrderRejected");
444 assert_eq!(order_rejected.order_type(), None);
445 assert_eq!(order_rejected.order_side(), None);
446 assert_eq!(order_rejected.trader_id(), TraderId::from("TRADER-001"));
447 assert_eq!(order_rejected.strategy_id(), StrategyId::from("EMA-CROSS"));
448 assert_eq!(
449 order_rejected.instrument_id(),
450 InstrumentId::from("EURUSD.SIM")
451 );
452 assert_eq!(order_rejected.trade_id(), None);
453 assert_eq!(order_rejected.currency(), None);
454 assert_eq!(
455 order_rejected.client_order_id(),
456 ClientOrderId::from("O-19700101-000000-001-001-1")
457 );
458 assert_eq!(
459 order_rejected.reason(),
460 Some(Ustr::from("INSUFFICIENT_MARGIN"))
461 );
462 assert_eq!(order_rejected.quantity(), None);
463 assert_eq!(order_rejected.time_in_force(), None);
464 assert_eq!(order_rejected.liquidity_side(), None);
465 assert_eq!(order_rejected.post_only(), None);
466 assert_eq!(order_rejected.reduce_only(), None);
467 assert_eq!(order_rejected.quote_quantity(), None);
468 assert!(!order_rejected.reconciliation());
469 assert_eq!(order_rejected.venue_order_id(), None);
470 assert_eq!(
471 order_rejected.account_id(),
472 Some(AccountId::from("SIM-001"))
473 );
474 assert_eq!(order_rejected.position_id(), None);
475 assert_eq!(order_rejected.commission(), None);
476 assert_eq!(order_rejected.ts_event(), UnixNanos::from(1_000_000_000));
477 assert_eq!(order_rejected.ts_init(), UnixNanos::from(2_000_000_000));
478 }
479
480 #[rstest]
481 fn test_order_rejected_different_reasons() {
482 let mut insufficient_margin = create_test_order_rejected();
483 insufficient_margin.reason = Ustr::from("INSUFFICIENT_MARGIN");
484
485 let mut invalid_price = create_test_order_rejected();
486 invalid_price.reason = Ustr::from("INVALID_PRICE");
487
488 let mut market_closed = create_test_order_rejected();
489 market_closed.reason = Ustr::from("MARKET_CLOSED");
490
491 assert_ne!(insufficient_margin, invalid_price);
492 assert_ne!(invalid_price, market_closed);
493 assert_eq!(
494 insufficient_margin.reason,
495 Ustr::from("INSUFFICIENT_MARGIN")
496 );
497 assert_eq!(invalid_price.reason, Ustr::from("INVALID_PRICE"));
498 assert_eq!(market_closed.reason, Ustr::from("MARKET_CLOSED"));
499 }
500
501 #[rstest]
502 fn test_order_rejected_timestamps() {
503 let order_rejected = create_test_order_rejected();
504
505 assert_eq!(order_rejected.ts_event, UnixNanos::from(1_000_000_000));
506 assert_eq!(order_rejected.ts_init, UnixNanos::from(2_000_000_000));
507 assert!(order_rejected.ts_event < order_rejected.ts_init);
508 }
509
510 #[rstest]
511 fn test_order_rejected_serialization() {
512 let original = create_test_order_rejected();
513
514 let json = serde_json::to_string(&original).unwrap();
515 let deserialized: OrderRejected = serde_json::from_str(&json).unwrap();
516
517 assert_eq!(original, deserialized);
518 }
519
520 #[rstest]
521 fn test_order_rejected_different_accounts() {
522 let mut live_account = create_test_order_rejected();
523 live_account.account_id = AccountId::from("LIVE-001");
524
525 let mut sim_account = create_test_order_rejected();
526 sim_account.account_id = AccountId::from("SIM-001");
527
528 assert_ne!(live_account, sim_account);
529 assert_eq!(live_account.account_id, AccountId::from("LIVE-001"));
530 assert_eq!(sim_account.account_id, AccountId::from("SIM-001"));
531 }
532
533 #[rstest]
534 fn test_order_rejected_different_instruments() {
535 let mut btc_order = create_test_order_rejected();
536 btc_order.instrument_id = InstrumentId::from("BTCUSD.COINBASE");
537
538 let mut eth_order = create_test_order_rejected();
539 eth_order.instrument_id = InstrumentId::from("ETHUSD.COINBASE");
540
541 assert_ne!(btc_order, eth_order);
542 assert_eq!(
543 btc_order.instrument_id,
544 InstrumentId::from("BTCUSD.COINBASE")
545 );
546 assert_eq!(
547 eth_order.instrument_id,
548 InstrumentId::from("ETHUSD.COINBASE")
549 );
550 }
551
552 #[rstest]
553 fn test_order_rejected_with_due_post_only() {
554 let order_rejected = OrderRejected::new(
555 TraderId::from("TRADER-001"),
556 StrategyId::from("EMA-CROSS"),
557 InstrumentId::from("EURUSD.SIM"),
558 ClientOrderId::from("O-19700101-000000-001-001-1"),
559 AccountId::from("SIM-001"),
560 Ustr::from("POST_ONLY_WOULD_EXECUTE"),
561 Default::default(),
562 UnixNanos::from(1_000_000_000),
563 UnixNanos::from(2_000_000_000),
564 false,
565 true,
566 );
567
568 assert_eq!(order_rejected.due_post_only, 1);
569 assert_eq!(order_rejected.reason, Ustr::from("POST_ONLY_WOULD_EXECUTE"));
570 }
571}