1use std::{collections::HashMap, fmt::Display, hash::Hash};
19
20use derive_builder::Builder;
21use nautilus_core::{UnixNanos, serialization::Serializable};
22use serde::{Deserialize, Serialize};
23use ustr::Ustr;
24
25use super::HasTsInit;
26use crate::{enums::MarketStatusAction, identifiers::InstrumentId};
27
28#[repr(C)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Builder)]
31#[serde(tag = "type")]
32#[cfg_attr(
33 feature = "python",
34 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
35)]
36#[cfg_attr(
37 feature = "python",
38 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
39)]
40pub struct InstrumentStatus {
41 pub instrument_id: InstrumentId,
43 pub action: MarketStatusAction,
45 pub ts_event: UnixNanos,
47 pub ts_init: UnixNanos,
49 pub reason: Option<Ustr>,
51 pub trading_event: Option<Ustr>,
53 pub is_trading: Option<bool>,
55 pub is_quoting: Option<bool>,
57 pub is_short_sell_restricted: Option<bool>,
59}
60
61impl InstrumentStatus {
62 #[expect(clippy::too_many_arguments)]
64 #[must_use]
65 pub fn new(
66 instrument_id: InstrumentId,
67 action: MarketStatusAction,
68 ts_event: UnixNanos,
69 ts_init: UnixNanos,
70 reason: Option<Ustr>,
71 trading_event: Option<Ustr>,
72 is_trading: Option<bool>,
73 is_quoting: Option<bool>,
74 is_short_sell_restricted: Option<bool>,
75 ) -> Self {
76 Self {
77 instrument_id,
78 action,
79 ts_event,
80 ts_init,
81 reason,
82 trading_event,
83 is_trading,
84 is_quoting,
85 is_short_sell_restricted,
86 }
87 }
88
89 #[must_use]
91 pub fn get_metadata(instrument_id: &InstrumentId) -> HashMap<String, String> {
92 let mut metadata = HashMap::new();
93 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
94 metadata
95 }
96}
97
98impl Display for InstrumentStatus {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(
102 f,
103 "{},{},{},{}",
104 self.instrument_id, self.action, self.ts_event, self.ts_init,
105 )
106 }
107}
108
109impl Serializable for InstrumentStatus {}
110
111impl HasTsInit for InstrumentStatus {
112 fn ts_init(&self) -> UnixNanos {
113 self.ts_init
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use std::{
120 collections::hash_map::DefaultHasher,
121 hash::{Hash, Hasher},
122 };
123
124 use rstest::rstest;
125 use ustr::Ustr;
126
127 use super::*;
128 use crate::data::stubs::stub_instrument_status;
129
130 fn create_test_instrument_status() -> InstrumentStatus {
131 InstrumentStatus::new(
132 InstrumentId::from("EURUSD.SIM"),
133 MarketStatusAction::Trading,
134 UnixNanos::from(1_000_000_000),
135 UnixNanos::from(2_000_000_000),
136 Some(Ustr::from("Normal trading")),
137 Some(Ustr::from("MARKET_OPEN")),
138 Some(true),
139 Some(true),
140 Some(false),
141 )
142 }
143
144 fn create_test_instrument_status_minimal() -> InstrumentStatus {
145 InstrumentStatus::new(
146 InstrumentId::from("GBPUSD.SIM"),
147 MarketStatusAction::PreOpen,
148 UnixNanos::from(500_000_000),
149 UnixNanos::from(1_000_000_000),
150 None,
151 None,
152 None,
153 None,
154 None,
155 )
156 }
157
158 #[rstest]
159 fn test_instrument_status_new() {
160 let status = create_test_instrument_status();
161
162 assert_eq!(status.instrument_id, InstrumentId::from("EURUSD.SIM"));
163 assert_eq!(status.action, MarketStatusAction::Trading);
164 assert_eq!(status.ts_event, UnixNanos::from(1_000_000_000));
165 assert_eq!(status.ts_init, UnixNanos::from(2_000_000_000));
166 assert_eq!(status.reason, Some(Ustr::from("Normal trading")));
167 assert_eq!(status.trading_event, Some(Ustr::from("MARKET_OPEN")));
168 assert_eq!(status.is_trading, Some(true));
169 assert_eq!(status.is_quoting, Some(true));
170 assert_eq!(status.is_short_sell_restricted, Some(false));
171 }
172
173 #[rstest]
174 fn test_instrument_status_new_minimal() {
175 let status = create_test_instrument_status_minimal();
176
177 assert_eq!(status.instrument_id, InstrumentId::from("GBPUSD.SIM"));
178 assert_eq!(status.action, MarketStatusAction::PreOpen);
179 assert_eq!(status.ts_event, UnixNanos::from(500_000_000));
180 assert_eq!(status.ts_init, UnixNanos::from(1_000_000_000));
181 assert_eq!(status.reason, None);
182 assert_eq!(status.trading_event, None);
183 assert_eq!(status.is_trading, None);
184 assert_eq!(status.is_quoting, None);
185 assert_eq!(status.is_short_sell_restricted, None);
186 }
187
188 #[rstest]
189 fn test_instrument_status_builder() {
190 let status = InstrumentStatusBuilder::default()
191 .instrument_id(InstrumentId::from("BTCUSD.CRYPTO"))
192 .action(MarketStatusAction::Halt)
193 .ts_event(UnixNanos::from(3_000_000_000))
194 .ts_init(UnixNanos::from(4_000_000_000))
195 .reason(Some(Ustr::from("Technical issue")))
196 .trading_event(Some(Ustr::from("HALT_REQUESTED")))
197 .is_trading(Some(false))
198 .is_quoting(Some(false))
199 .is_short_sell_restricted(Some(true))
200 .build()
201 .unwrap();
202
203 assert_eq!(status.instrument_id, InstrumentId::from("BTCUSD.CRYPTO"));
204 assert_eq!(status.action, MarketStatusAction::Halt);
205 assert_eq!(status.ts_event, UnixNanos::from(3_000_000_000));
206 assert_eq!(status.ts_init, UnixNanos::from(4_000_000_000));
207 assert_eq!(status.reason, Some(Ustr::from("Technical issue")));
208 assert_eq!(status.trading_event, Some(Ustr::from("HALT_REQUESTED")));
209 assert_eq!(status.is_trading, Some(false));
210 assert_eq!(status.is_quoting, Some(false));
211 assert_eq!(status.is_short_sell_restricted, Some(true));
212 }
213
214 #[rstest]
215 fn test_instrument_status_builder_minimal() {
216 let status = InstrumentStatusBuilder::default()
217 .instrument_id(InstrumentId::from("AAPL.XNAS"))
218 .action(MarketStatusAction::Close)
219 .ts_event(UnixNanos::from(1_500_000_000))
220 .ts_init(UnixNanos::from(2_500_000_000))
221 .reason(None)
222 .trading_event(None)
223 .is_trading(None)
224 .is_quoting(None)
225 .is_short_sell_restricted(None)
226 .build()
227 .unwrap();
228
229 assert_eq!(status.instrument_id, InstrumentId::from("AAPL.XNAS"));
230 assert_eq!(status.action, MarketStatusAction::Close);
231 assert_eq!(status.ts_event, UnixNanos::from(1_500_000_000));
232 assert_eq!(status.ts_init, UnixNanos::from(2_500_000_000));
233 assert_eq!(status.reason, None);
234 assert_eq!(status.trading_event, None);
235 assert_eq!(status.is_trading, None);
236 assert_eq!(status.is_quoting, None);
237 assert_eq!(status.is_short_sell_restricted, None);
238 }
239
240 #[rstest]
241 #[case(MarketStatusAction::None)]
242 #[case(MarketStatusAction::PreOpen)]
243 #[case(MarketStatusAction::PreCross)]
244 #[case(MarketStatusAction::Quoting)]
245 #[case(MarketStatusAction::Cross)]
246 #[case(MarketStatusAction::Rotation)]
247 #[case(MarketStatusAction::NewPriceIndication)]
248 #[case(MarketStatusAction::Trading)]
249 #[case(MarketStatusAction::Halt)]
250 #[case(MarketStatusAction::Pause)]
251 #[case(MarketStatusAction::Suspend)]
252 #[case(MarketStatusAction::PreClose)]
253 #[case(MarketStatusAction::Close)]
254 #[case(MarketStatusAction::PostClose)]
255 #[case(MarketStatusAction::ShortSellRestrictionChange)]
256 #[case(MarketStatusAction::NotAvailableForTrading)]
257 fn test_instrument_status_with_all_actions(#[case] action: MarketStatusAction) {
258 let status = InstrumentStatus::new(
259 InstrumentId::from("TEST.SIM"),
260 action,
261 UnixNanos::from(1_000_000_000),
262 UnixNanos::from(2_000_000_000),
263 None,
264 None,
265 None,
266 None,
267 None,
268 );
269
270 assert_eq!(status.action, action);
271 }
272
273 #[rstest]
274 fn test_get_metadata() {
275 let instrument_id = InstrumentId::from("EURUSD.SIM");
276 let metadata = InstrumentStatus::get_metadata(&instrument_id);
277
278 assert_eq!(metadata.len(), 1);
279 assert_eq!(
280 metadata.get("instrument_id"),
281 Some(&"EURUSD.SIM".to_string())
282 );
283 }
284
285 #[rstest]
286 fn test_get_metadata_different_instruments() {
287 let eur_metadata = InstrumentStatus::get_metadata(&InstrumentId::from("EURUSD.SIM"));
288 let gbp_metadata = InstrumentStatus::get_metadata(&InstrumentId::from("GBPUSD.SIM"));
289
290 assert_eq!(
291 eur_metadata.get("instrument_id"),
292 Some(&"EURUSD.SIM".to_string())
293 );
294 assert_eq!(
295 gbp_metadata.get("instrument_id"),
296 Some(&"GBPUSD.SIM".to_string())
297 );
298 assert_ne!(eur_metadata, gbp_metadata);
299 }
300
301 #[rstest]
302 fn test_instrument_status_partial_eq() {
303 let status1 = create_test_instrument_status();
304 let status2 = create_test_instrument_status();
305 let status3 = create_test_instrument_status_minimal();
306
307 assert_eq!(status1, status2);
308 assert_ne!(status1, status3);
309 }
310
311 #[rstest]
312 fn test_instrument_status_partial_eq_different_fields() {
313 let status1 = create_test_instrument_status();
314 let mut status2 = create_test_instrument_status();
315 status2.action = MarketStatusAction::Halt;
316
317 let mut status3 = create_test_instrument_status();
318 status3.is_trading = Some(false);
319
320 let mut status4 = create_test_instrument_status();
321 status4.reason = Some(Ustr::from("Different reason"));
322
323 assert_ne!(status1, status2);
324 assert_ne!(status1, status3);
325 assert_ne!(status1, status4);
326 }
327
328 #[rstest]
329 fn test_instrument_status_eq_consistency() {
330 let status1 = create_test_instrument_status();
331 let status2 = create_test_instrument_status();
332
333 assert_eq!(status1, status2);
334 assert_eq!(status2, status1); assert_eq!(status1, status1); }
337
338 #[rstest]
339 fn test_instrument_status_hash() {
340 let status1 = create_test_instrument_status();
341 let status2 = create_test_instrument_status();
342
343 let mut hasher1 = DefaultHasher::new();
344 let mut hasher2 = DefaultHasher::new();
345
346 status1.hash(&mut hasher1);
347 status2.hash(&mut hasher2);
348
349 assert_eq!(hasher1.finish(), hasher2.finish());
350 }
351
352 #[rstest]
353 fn test_instrument_status_hash_different_objects() {
354 let status1 = create_test_instrument_status();
355 let status2 = create_test_instrument_status_minimal();
356
357 let mut hasher1 = DefaultHasher::new();
358 let mut hasher2 = DefaultHasher::new();
359
360 status1.hash(&mut hasher1);
361 status2.hash(&mut hasher2);
362
363 assert_ne!(hasher1.finish(), hasher2.finish());
364 }
365
366 #[rstest]
367 fn test_instrument_status_clone() {
368 let status1 = create_test_instrument_status();
369 let status2 = status1;
370
371 assert_eq!(status1, status2);
372 assert_eq!(status1.instrument_id, status2.instrument_id);
373 assert_eq!(status1.action, status2.action);
374 assert_eq!(status1.ts_event, status2.ts_event);
375 assert_eq!(status1.ts_init, status2.ts_init);
376 assert_eq!(status1.reason, status2.reason);
377 assert_eq!(status1.trading_event, status2.trading_event);
378 assert_eq!(status1.is_trading, status2.is_trading);
379 assert_eq!(status1.is_quoting, status2.is_quoting);
380 assert_eq!(
381 status1.is_short_sell_restricted,
382 status2.is_short_sell_restricted
383 );
384 }
385
386 #[rstest]
387 fn test_instrument_status_debug() {
388 let status = create_test_instrument_status();
389 let debug_str = format!("{status:?}");
390
391 assert!(debug_str.contains("InstrumentStatus"));
392 assert!(debug_str.contains("EURUSD.SIM"));
393 assert!(debug_str.contains("Trading"));
394 assert!(debug_str.contains("Normal trading"));
395 assert!(debug_str.contains("MARKET_OPEN"));
396 }
397
398 #[rstest]
399 fn test_instrument_status_copy() {
400 let status1 = create_test_instrument_status();
401 let status2 = status1; assert_eq!(status1, status2);
404 assert_eq!(status1.instrument_id, status2.instrument_id);
405 assert_eq!(status1.action, status2.action);
406 }
407
408 #[rstest]
409 fn test_instrument_status_has_ts_init() {
410 let status = create_test_instrument_status();
411 assert_eq!(status.ts_init(), UnixNanos::from(2_000_000_000));
412 }
413
414 #[rstest]
415 fn test_instrument_status_has_ts_init_different_values() {
416 let status1 = create_test_instrument_status();
417 let status2 = create_test_instrument_status_minimal();
418
419 assert_eq!(status1.ts_init(), UnixNanos::from(2_000_000_000));
420 assert_eq!(status2.ts_init(), UnixNanos::from(1_000_000_000));
421 assert_ne!(status1.ts_init(), status2.ts_init());
422 }
423
424 #[rstest]
425 fn test_instrument_status_display() {
426 let status = create_test_instrument_status();
427 let display_str = format!("{status}");
428
429 assert!(display_str.contains("EURUSD.SIM"));
430 assert!(display_str.contains("TRADING"));
431 assert!(display_str.contains("1000000000"));
432 assert!(display_str.contains("2000000000"));
433 }
434
435 #[rstest]
436 fn test_instrument_status_display_format() {
437 let status = create_test_instrument_status();
438 let expected = "EURUSD.SIM,TRADING,1000000000,2000000000";
439
440 assert_eq!(format!("{status}"), expected);
441 }
442
443 #[rstest]
444 fn test_instrument_status_display_different_actions() {
445 let halt_status = InstrumentStatus::new(
446 InstrumentId::from("TEST.SIM"),
447 MarketStatusAction::Halt,
448 UnixNanos::from(1_000_000_000),
449 UnixNanos::from(2_000_000_000),
450 None,
451 None,
452 None,
453 None,
454 None,
455 );
456
457 let display_str = format!("{halt_status}");
458 assert!(display_str.contains("HALT"));
459 }
460
461 #[rstest]
462 fn test_instrument_status_serialization() {
463 let status = create_test_instrument_status();
464
465 let json = serde_json::to_string(&status).unwrap();
467 let deserialized: InstrumentStatus = serde_json::from_str(&json).unwrap();
468
469 assert_eq!(status, deserialized);
470 }
471
472 #[rstest]
473 fn test_instrument_status_serialization_with_optional_fields() {
474 let status = create_test_instrument_status_minimal();
475
476 let json = serde_json::to_string(&status).unwrap();
478 let deserialized: InstrumentStatus = serde_json::from_str(&json).unwrap();
479
480 assert_eq!(status, deserialized);
481 assert_eq!(deserialized.reason, None);
482 assert_eq!(deserialized.trading_event, None);
483 assert_eq!(deserialized.is_trading, None);
484 assert_eq!(deserialized.is_quoting, None);
485 assert_eq!(deserialized.is_short_sell_restricted, None);
486 }
487
488 #[rstest]
489 fn test_instrument_status_with_trading_flags() {
490 let status = InstrumentStatus::new(
491 InstrumentId::from("TEST.SIM"),
492 MarketStatusAction::Trading,
493 UnixNanos::from(1_000_000_000),
494 UnixNanos::from(2_000_000_000),
495 None,
496 None,
497 Some(true),
498 Some(true),
499 Some(false),
500 );
501
502 assert_eq!(status.is_trading, Some(true));
503 assert_eq!(status.is_quoting, Some(true));
504 assert_eq!(status.is_short_sell_restricted, Some(false));
505 }
506
507 #[rstest]
508 fn test_instrument_status_with_halt_flags() {
509 let status = InstrumentStatus::new(
510 InstrumentId::from("TEST.SIM"),
511 MarketStatusAction::Halt,
512 UnixNanos::from(1_000_000_000),
513 UnixNanos::from(2_000_000_000),
514 Some(Ustr::from("System maintenance")),
515 Some(Ustr::from("HALT_SYSTEM")),
516 Some(false),
517 Some(false),
518 Some(true),
519 );
520
521 assert_eq!(status.action, MarketStatusAction::Halt);
522 assert_eq!(status.is_trading, Some(false));
523 assert_eq!(status.is_quoting, Some(false));
524 assert_eq!(status.is_short_sell_restricted, Some(true));
525 assert_eq!(status.reason, Some(Ustr::from("System maintenance")));
526 assert_eq!(status.trading_event, Some(Ustr::from("HALT_SYSTEM")));
527 }
528
529 #[rstest]
530 fn test_instrument_status_with_short_sell_restriction() {
531 let status = InstrumentStatus::new(
532 InstrumentId::from("TEST.SIM"),
533 MarketStatusAction::ShortSellRestrictionChange,
534 UnixNanos::from(1_000_000_000),
535 UnixNanos::from(2_000_000_000),
536 Some(Ustr::from("Circuit breaker triggered")),
537 Some(Ustr::from("SSR_ACTIVATED")),
538 Some(true),
539 Some(true),
540 Some(true),
541 );
542
543 assert_eq!(
544 status.action,
545 MarketStatusAction::ShortSellRestrictionChange
546 );
547 assert_eq!(status.is_short_sell_restricted, Some(true));
548 assert_eq!(status.reason, Some(Ustr::from("Circuit breaker triggered")));
549 assert_eq!(status.trading_event, Some(Ustr::from("SSR_ACTIVATED")));
550 }
551
552 #[rstest]
553 fn test_instrument_status_with_mixed_optional_fields() {
554 let status = InstrumentStatus::new(
555 InstrumentId::from("TEST.SIM"),
556 MarketStatusAction::Quoting,
557 UnixNanos::from(1_000_000_000),
558 UnixNanos::from(2_000_000_000),
559 Some(Ustr::from("Pre-market")),
560 None,
561 Some(false),
562 Some(true),
563 None,
564 );
565
566 assert_eq!(status.reason, Some(Ustr::from("Pre-market")));
567 assert_eq!(status.trading_event, None);
568 assert_eq!(status.is_trading, Some(false));
569 assert_eq!(status.is_quoting, Some(true));
570 assert_eq!(status.is_short_sell_restricted, None);
571 }
572
573 #[rstest]
574 fn test_instrument_status_with_empty_reason() {
575 let status = InstrumentStatus::new(
576 InstrumentId::from("TEST.SIM"),
577 MarketStatusAction::Trading,
578 UnixNanos::from(1_000_000_000),
579 UnixNanos::from(2_000_000_000),
580 Some(Ustr::from("")),
581 None,
582 None,
583 None,
584 None,
585 );
586
587 assert_eq!(status.reason, Some(Ustr::from("")));
588 }
589
590 #[rstest]
591 fn test_instrument_status_with_long_reason() {
592 let long_reason = "This is a very long reason that explains in detail why the market status has changed and includes multiple sentences to test the handling of longer text strings.";
593 let status = InstrumentStatus::new(
594 InstrumentId::from("TEST.SIM"),
595 MarketStatusAction::Suspend,
596 UnixNanos::from(1_000_000_000),
597 UnixNanos::from(2_000_000_000),
598 Some(Ustr::from(long_reason)),
599 None,
600 None,
601 None,
602 None,
603 );
604
605 assert_eq!(status.reason, Some(Ustr::from(long_reason)));
606 }
607
608 #[rstest]
609 fn test_instrument_status_with_zero_timestamps() {
610 let status = InstrumentStatus::new(
611 InstrumentId::from("TEST.SIM"),
612 MarketStatusAction::None,
613 UnixNanos::from(0),
614 UnixNanos::from(0),
615 None,
616 None,
617 None,
618 None,
619 None,
620 );
621
622 assert_eq!(status.ts_event, UnixNanos::from(0));
623 assert_eq!(status.ts_init, UnixNanos::from(0));
624 }
625
626 #[rstest]
627 fn test_instrument_status_with_max_timestamps() {
628 let status = InstrumentStatus::new(
629 InstrumentId::from("TEST.SIM"),
630 MarketStatusAction::Trading,
631 UnixNanos::from(u64::MAX),
632 UnixNanos::from(u64::MAX),
633 None,
634 None,
635 None,
636 None,
637 None,
638 );
639
640 assert_eq!(status.ts_event, UnixNanos::from(u64::MAX));
641 assert_eq!(status.ts_init, UnixNanos::from(u64::MAX));
642 }
643
644 #[rstest]
645 fn test_to_string(stub_instrument_status: InstrumentStatus) {
646 assert_eq!(stub_instrument_status.to_string(), "MSFT.XNAS,TRADING,1,2");
647 }
648
649 #[rstest]
650 fn test_data_from_instrument_status(stub_instrument_status: InstrumentStatus) {
651 let data: crate::data::Data = stub_instrument_status.into();
652 assert!(matches!(data, crate::data::Data::InstrumentStatus(_)));
653 assert_eq!(data.instrument_id(), stub_instrument_status.instrument_id);
654 }
655
656 #[rstest]
657 fn test_data_has_ts_init_for_instrument_status(stub_instrument_status: InstrumentStatus) {
658 let data: crate::data::Data = stub_instrument_status.into();
659 assert_eq!(data.ts_init(), stub_instrument_status.ts_init);
660 }
661
662 #[rstest]
663 fn test_try_from_data_instrument_status(stub_instrument_status: InstrumentStatus) {
664 let data: crate::data::Data = stub_instrument_status.into();
665 let extracted: InstrumentStatus = InstrumentStatus::try_from(data).unwrap();
666 assert_eq!(extracted, stub_instrument_status);
667 }
668
669 #[rstest]
670 fn test_try_from_data_instrument_status_wrong_variant(
671 stub_instrument_status: InstrumentStatus,
672 ) {
673 let data = crate::data::Data::InstrumentClose(crate::data::close::InstrumentClose::new(
674 stub_instrument_status.instrument_id,
675 crate::types::Price::new(100.0, 2),
676 crate::enums::InstrumentCloseType::EndOfSession,
677 stub_instrument_status.ts_event,
678 stub_instrument_status.ts_init,
679 ));
680 assert!(InstrumentStatus::try_from(data).is_err());
681 }
682
683 #[rstest]
684 fn test_data_serde_roundtrip_instrument_status(stub_instrument_status: InstrumentStatus) {
685 let data: crate::data::Data = stub_instrument_status.into();
686 let json = serde_json::to_string(&data).unwrap();
687 let roundtrip: crate::data::Data = serde_json::from_str(&json).unwrap();
688 match roundtrip {
689 crate::data::Data::InstrumentStatus(s) => assert_eq!(s, stub_instrument_status),
690 _ => panic!("unexpected variant"),
691 }
692 }
693
694 #[rstest]
695 fn test_data_clone_instrument_status(stub_instrument_status: InstrumentStatus) {
696 let data: crate::data::Data = stub_instrument_status.into();
697 let cloned = data.clone();
698 assert_eq!(data, cloned);
699 }
700
701 #[cfg(feature = "ffi")]
702 #[rstest]
703 fn test_data_ffi_try_from_instrument_status_errors(stub_instrument_status: InstrumentStatus) {
704 let data: crate::data::Data = stub_instrument_status.into();
705 assert!(crate::data::DataFFI::try_from(data).is_err());
706 }
707}