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