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 #[allow(clippy::too_many_arguments)]
64 pub fn new(
65 instrument_id: InstrumentId,
66 action: MarketStatusAction,
67 ts_event: UnixNanos,
68 ts_init: UnixNanos,
69 reason: Option<Ustr>,
70 trading_event: Option<Ustr>,
71 is_trading: Option<bool>,
72 is_quoting: Option<bool>,
73 is_short_sell_restricted: Option<bool>,
74 ) -> Self {
75 Self {
76 instrument_id,
77 action,
78 ts_event,
79 ts_init,
80 reason,
81 trading_event,
82 is_trading,
83 is_quoting,
84 is_short_sell_restricted,
85 }
86 }
87
88 #[must_use]
90 pub fn get_metadata(instrument_id: &InstrumentId) -> HashMap<String, String> {
91 let mut metadata = HashMap::new();
92 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
93 metadata
94 }
95}
96
97impl Display for InstrumentStatus {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(
101 f,
102 "{},{},{},{}",
103 self.instrument_id, self.action, self.ts_event, self.ts_init,
104 )
105 }
106}
107
108impl Serializable for InstrumentStatus {}
109
110impl HasTsInit for InstrumentStatus {
111 fn ts_init(&self) -> UnixNanos {
112 self.ts_init
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use std::{
119 collections::hash_map::DefaultHasher,
120 hash::{Hash, Hasher},
121 };
122
123 use rstest::rstest;
124 use ustr::Ustr;
125
126 use super::*;
127 use crate::data::stubs::stub_instrument_status;
128
129 fn create_test_instrument_status() -> InstrumentStatus {
130 InstrumentStatus::new(
131 InstrumentId::from("EURUSD.SIM"),
132 MarketStatusAction::Trading,
133 UnixNanos::from(1_000_000_000),
134 UnixNanos::from(2_000_000_000),
135 Some(Ustr::from("Normal trading")),
136 Some(Ustr::from("MARKET_OPEN")),
137 Some(true),
138 Some(true),
139 Some(false),
140 )
141 }
142
143 fn create_test_instrument_status_minimal() -> InstrumentStatus {
144 InstrumentStatus::new(
145 InstrumentId::from("GBPUSD.SIM"),
146 MarketStatusAction::PreOpen,
147 UnixNanos::from(500_000_000),
148 UnixNanos::from(1_000_000_000),
149 None,
150 None,
151 None,
152 None,
153 None,
154 )
155 }
156
157 #[rstest]
158 fn test_instrument_status_new() {
159 let status = create_test_instrument_status();
160
161 assert_eq!(status.instrument_id, InstrumentId::from("EURUSD.SIM"));
162 assert_eq!(status.action, MarketStatusAction::Trading);
163 assert_eq!(status.ts_event, UnixNanos::from(1_000_000_000));
164 assert_eq!(status.ts_init, UnixNanos::from(2_000_000_000));
165 assert_eq!(status.reason, Some(Ustr::from("Normal trading")));
166 assert_eq!(status.trading_event, Some(Ustr::from("MARKET_OPEN")));
167 assert_eq!(status.is_trading, Some(true));
168 assert_eq!(status.is_quoting, Some(true));
169 assert_eq!(status.is_short_sell_restricted, Some(false));
170 }
171
172 #[rstest]
173 fn test_instrument_status_new_minimal() {
174 let status = create_test_instrument_status_minimal();
175
176 assert_eq!(status.instrument_id, InstrumentId::from("GBPUSD.SIM"));
177 assert_eq!(status.action, MarketStatusAction::PreOpen);
178 assert_eq!(status.ts_event, UnixNanos::from(500_000_000));
179 assert_eq!(status.ts_init, UnixNanos::from(1_000_000_000));
180 assert_eq!(status.reason, None);
181 assert_eq!(status.trading_event, None);
182 assert_eq!(status.is_trading, None);
183 assert_eq!(status.is_quoting, None);
184 assert_eq!(status.is_short_sell_restricted, None);
185 }
186
187 #[rstest]
188 fn test_instrument_status_builder() {
189 let status = InstrumentStatusBuilder::default()
190 .instrument_id(InstrumentId::from("BTCUSD.CRYPTO"))
191 .action(MarketStatusAction::Halt)
192 .ts_event(UnixNanos::from(3_000_000_000))
193 .ts_init(UnixNanos::from(4_000_000_000))
194 .reason(Some(Ustr::from("Technical issue")))
195 .trading_event(Some(Ustr::from("HALT_REQUESTED")))
196 .is_trading(Some(false))
197 .is_quoting(Some(false))
198 .is_short_sell_restricted(Some(true))
199 .build()
200 .unwrap();
201
202 assert_eq!(status.instrument_id, InstrumentId::from("BTCUSD.CRYPTO"));
203 assert_eq!(status.action, MarketStatusAction::Halt);
204 assert_eq!(status.ts_event, UnixNanos::from(3_000_000_000));
205 assert_eq!(status.ts_init, UnixNanos::from(4_000_000_000));
206 assert_eq!(status.reason, Some(Ustr::from("Technical issue")));
207 assert_eq!(status.trading_event, Some(Ustr::from("HALT_REQUESTED")));
208 assert_eq!(status.is_trading, Some(false));
209 assert_eq!(status.is_quoting, Some(false));
210 assert_eq!(status.is_short_sell_restricted, Some(true));
211 }
212
213 #[rstest]
214 fn test_instrument_status_builder_minimal() {
215 let status = InstrumentStatusBuilder::default()
216 .instrument_id(InstrumentId::from("AAPL.XNAS"))
217 .action(MarketStatusAction::Close)
218 .ts_event(UnixNanos::from(1_500_000_000))
219 .ts_init(UnixNanos::from(2_500_000_000))
220 .reason(None)
221 .trading_event(None)
222 .is_trading(None)
223 .is_quoting(None)
224 .is_short_sell_restricted(None)
225 .build()
226 .unwrap();
227
228 assert_eq!(status.instrument_id, InstrumentId::from("AAPL.XNAS"));
229 assert_eq!(status.action, MarketStatusAction::Close);
230 assert_eq!(status.ts_event, UnixNanos::from(1_500_000_000));
231 assert_eq!(status.ts_init, UnixNanos::from(2_500_000_000));
232 assert_eq!(status.reason, None);
233 assert_eq!(status.trading_event, None);
234 assert_eq!(status.is_trading, None);
235 assert_eq!(status.is_quoting, None);
236 assert_eq!(status.is_short_sell_restricted, None);
237 }
238
239 #[rstest]
240 #[case(MarketStatusAction::None)]
241 #[case(MarketStatusAction::PreOpen)]
242 #[case(MarketStatusAction::PreCross)]
243 #[case(MarketStatusAction::Quoting)]
244 #[case(MarketStatusAction::Cross)]
245 #[case(MarketStatusAction::Rotation)]
246 #[case(MarketStatusAction::NewPriceIndication)]
247 #[case(MarketStatusAction::Trading)]
248 #[case(MarketStatusAction::Halt)]
249 #[case(MarketStatusAction::Pause)]
250 #[case(MarketStatusAction::Suspend)]
251 #[case(MarketStatusAction::PreClose)]
252 #[case(MarketStatusAction::Close)]
253 #[case(MarketStatusAction::PostClose)]
254 #[case(MarketStatusAction::ShortSellRestrictionChange)]
255 #[case(MarketStatusAction::NotAvailableForTrading)]
256 fn test_instrument_status_with_all_actions(#[case] action: MarketStatusAction) {
257 let status = InstrumentStatus::new(
258 InstrumentId::from("TEST.SIM"),
259 action,
260 UnixNanos::from(1_000_000_000),
261 UnixNanos::from(2_000_000_000),
262 None,
263 None,
264 None,
265 None,
266 None,
267 );
268
269 assert_eq!(status.action, action);
270 }
271
272 #[rstest]
273 fn test_get_metadata() {
274 let instrument_id = InstrumentId::from("EURUSD.SIM");
275 let metadata = InstrumentStatus::get_metadata(&instrument_id);
276
277 assert_eq!(metadata.len(), 1);
278 assert_eq!(
279 metadata.get("instrument_id"),
280 Some(&"EURUSD.SIM".to_string())
281 );
282 }
283
284 #[rstest]
285 fn test_get_metadata_different_instruments() {
286 let eur_metadata = InstrumentStatus::get_metadata(&InstrumentId::from("EURUSD.SIM"));
287 let gbp_metadata = InstrumentStatus::get_metadata(&InstrumentId::from("GBPUSD.SIM"));
288
289 assert_eq!(
290 eur_metadata.get("instrument_id"),
291 Some(&"EURUSD.SIM".to_string())
292 );
293 assert_eq!(
294 gbp_metadata.get("instrument_id"),
295 Some(&"GBPUSD.SIM".to_string())
296 );
297 assert_ne!(eur_metadata, gbp_metadata);
298 }
299
300 #[rstest]
301 fn test_instrument_status_partial_eq() {
302 let status1 = create_test_instrument_status();
303 let status2 = create_test_instrument_status();
304 let status3 = create_test_instrument_status_minimal();
305
306 assert_eq!(status1, status2);
307 assert_ne!(status1, status3);
308 }
309
310 #[rstest]
311 fn test_instrument_status_partial_eq_different_fields() {
312 let status1 = create_test_instrument_status();
313 let mut status2 = create_test_instrument_status();
314 status2.action = MarketStatusAction::Halt;
315
316 let mut status3 = create_test_instrument_status();
317 status3.is_trading = Some(false);
318
319 let mut status4 = create_test_instrument_status();
320 status4.reason = Some(Ustr::from("Different reason"));
321
322 assert_ne!(status1, status2);
323 assert_ne!(status1, status3);
324 assert_ne!(status1, status4);
325 }
326
327 #[rstest]
328 fn test_instrument_status_eq_consistency() {
329 let status1 = create_test_instrument_status();
330 let status2 = create_test_instrument_status();
331
332 assert_eq!(status1, status2);
333 assert_eq!(status2, status1); assert_eq!(status1, status1); }
336
337 #[rstest]
338 fn test_instrument_status_hash() {
339 let status1 = create_test_instrument_status();
340 let status2 = create_test_instrument_status();
341
342 let mut hasher1 = DefaultHasher::new();
343 let mut hasher2 = DefaultHasher::new();
344
345 status1.hash(&mut hasher1);
346 status2.hash(&mut hasher2);
347
348 assert_eq!(hasher1.finish(), hasher2.finish());
349 }
350
351 #[rstest]
352 fn test_instrument_status_hash_different_objects() {
353 let status1 = create_test_instrument_status();
354 let status2 = create_test_instrument_status_minimal();
355
356 let mut hasher1 = DefaultHasher::new();
357 let mut hasher2 = DefaultHasher::new();
358
359 status1.hash(&mut hasher1);
360 status2.hash(&mut hasher2);
361
362 assert_ne!(hasher1.finish(), hasher2.finish());
363 }
364
365 #[rstest]
366 fn test_instrument_status_clone() {
367 let status1 = create_test_instrument_status();
368 let status2 = status1;
369
370 assert_eq!(status1, status2);
371 assert_eq!(status1.instrument_id, status2.instrument_id);
372 assert_eq!(status1.action, status2.action);
373 assert_eq!(status1.ts_event, status2.ts_event);
374 assert_eq!(status1.ts_init, status2.ts_init);
375 assert_eq!(status1.reason, status2.reason);
376 assert_eq!(status1.trading_event, status2.trading_event);
377 assert_eq!(status1.is_trading, status2.is_trading);
378 assert_eq!(status1.is_quoting, status2.is_quoting);
379 assert_eq!(
380 status1.is_short_sell_restricted,
381 status2.is_short_sell_restricted
382 );
383 }
384
385 #[rstest]
386 fn test_instrument_status_debug() {
387 let status = create_test_instrument_status();
388 let debug_str = format!("{status:?}");
389
390 assert!(debug_str.contains("InstrumentStatus"));
391 assert!(debug_str.contains("EURUSD.SIM"));
392 assert!(debug_str.contains("Trading"));
393 assert!(debug_str.contains("Normal trading"));
394 assert!(debug_str.contains("MARKET_OPEN"));
395 }
396
397 #[rstest]
398 fn test_instrument_status_copy() {
399 let status1 = create_test_instrument_status();
400 let status2 = status1; assert_eq!(status1, status2);
403 assert_eq!(status1.instrument_id, status2.instrument_id);
404 assert_eq!(status1.action, status2.action);
405 }
406
407 #[rstest]
408 fn test_instrument_status_has_ts_init() {
409 let status = create_test_instrument_status();
410 assert_eq!(status.ts_init(), UnixNanos::from(2_000_000_000));
411 }
412
413 #[rstest]
414 fn test_instrument_status_has_ts_init_different_values() {
415 let status1 = create_test_instrument_status();
416 let status2 = create_test_instrument_status_minimal();
417
418 assert_eq!(status1.ts_init(), UnixNanos::from(2_000_000_000));
419 assert_eq!(status2.ts_init(), UnixNanos::from(1_000_000_000));
420 assert_ne!(status1.ts_init(), status2.ts_init());
421 }
422
423 #[rstest]
424 fn test_instrument_status_display() {
425 let status = create_test_instrument_status();
426 let display_str = format!("{status}");
427
428 assert!(display_str.contains("EURUSD.SIM"));
429 assert!(display_str.contains("TRADING"));
430 assert!(display_str.contains("1000000000"));
431 assert!(display_str.contains("2000000000"));
432 }
433
434 #[rstest]
435 fn test_instrument_status_display_format() {
436 let status = create_test_instrument_status();
437 let expected = "EURUSD.SIM,TRADING,1000000000,2000000000";
438
439 assert_eq!(format!("{status}"), expected);
440 }
441
442 #[rstest]
443 fn test_instrument_status_display_different_actions() {
444 let halt_status = InstrumentStatus::new(
445 InstrumentId::from("TEST.SIM"),
446 MarketStatusAction::Halt,
447 UnixNanos::from(1_000_000_000),
448 UnixNanos::from(2_000_000_000),
449 None,
450 None,
451 None,
452 None,
453 None,
454 );
455
456 let display_str = format!("{halt_status}");
457 assert!(display_str.contains("HALT"));
458 }
459
460 #[rstest]
461 fn test_instrument_status_serialization() {
462 let status = create_test_instrument_status();
463
464 let json = serde_json::to_string(&status).unwrap();
466 let deserialized: InstrumentStatus = serde_json::from_str(&json).unwrap();
467
468 assert_eq!(status, deserialized);
469 }
470
471 #[rstest]
472 fn test_instrument_status_serialization_with_optional_fields() {
473 let status = create_test_instrument_status_minimal();
474
475 let json = serde_json::to_string(&status).unwrap();
477 let deserialized: InstrumentStatus = serde_json::from_str(&json).unwrap();
478
479 assert_eq!(status, deserialized);
480 assert_eq!(deserialized.reason, None);
481 assert_eq!(deserialized.trading_event, None);
482 assert_eq!(deserialized.is_trading, None);
483 assert_eq!(deserialized.is_quoting, None);
484 assert_eq!(deserialized.is_short_sell_restricted, None);
485 }
486
487 #[rstest]
488 fn test_instrument_status_with_trading_flags() {
489 let status = InstrumentStatus::new(
490 InstrumentId::from("TEST.SIM"),
491 MarketStatusAction::Trading,
492 UnixNanos::from(1_000_000_000),
493 UnixNanos::from(2_000_000_000),
494 None,
495 None,
496 Some(true),
497 Some(true),
498 Some(false),
499 );
500
501 assert_eq!(status.is_trading, Some(true));
502 assert_eq!(status.is_quoting, Some(true));
503 assert_eq!(status.is_short_sell_restricted, Some(false));
504 }
505
506 #[rstest]
507 fn test_instrument_status_with_halt_flags() {
508 let status = InstrumentStatus::new(
509 InstrumentId::from("TEST.SIM"),
510 MarketStatusAction::Halt,
511 UnixNanos::from(1_000_000_000),
512 UnixNanos::from(2_000_000_000),
513 Some(Ustr::from("System maintenance")),
514 Some(Ustr::from("HALT_SYSTEM")),
515 Some(false),
516 Some(false),
517 Some(true),
518 );
519
520 assert_eq!(status.action, MarketStatusAction::Halt);
521 assert_eq!(status.is_trading, Some(false));
522 assert_eq!(status.is_quoting, Some(false));
523 assert_eq!(status.is_short_sell_restricted, Some(true));
524 assert_eq!(status.reason, Some(Ustr::from("System maintenance")));
525 assert_eq!(status.trading_event, Some(Ustr::from("HALT_SYSTEM")));
526 }
527
528 #[rstest]
529 fn test_instrument_status_with_short_sell_restriction() {
530 let status = InstrumentStatus::new(
531 InstrumentId::from("TEST.SIM"),
532 MarketStatusAction::ShortSellRestrictionChange,
533 UnixNanos::from(1_000_000_000),
534 UnixNanos::from(2_000_000_000),
535 Some(Ustr::from("Circuit breaker triggered")),
536 Some(Ustr::from("SSR_ACTIVATED")),
537 Some(true),
538 Some(true),
539 Some(true),
540 );
541
542 assert_eq!(
543 status.action,
544 MarketStatusAction::ShortSellRestrictionChange
545 );
546 assert_eq!(status.is_short_sell_restricted, Some(true));
547 assert_eq!(status.reason, Some(Ustr::from("Circuit breaker triggered")));
548 assert_eq!(status.trading_event, Some(Ustr::from("SSR_ACTIVATED")));
549 }
550
551 #[rstest]
552 fn test_instrument_status_with_mixed_optional_fields() {
553 let status = InstrumentStatus::new(
554 InstrumentId::from("TEST.SIM"),
555 MarketStatusAction::Quoting,
556 UnixNanos::from(1_000_000_000),
557 UnixNanos::from(2_000_000_000),
558 Some(Ustr::from("Pre-market")),
559 None,
560 Some(false),
561 Some(true),
562 None,
563 );
564
565 assert_eq!(status.reason, Some(Ustr::from("Pre-market")));
566 assert_eq!(status.trading_event, None);
567 assert_eq!(status.is_trading, Some(false));
568 assert_eq!(status.is_quoting, Some(true));
569 assert_eq!(status.is_short_sell_restricted, None);
570 }
571
572 #[rstest]
573 fn test_instrument_status_with_empty_reason() {
574 let status = InstrumentStatus::new(
575 InstrumentId::from("TEST.SIM"),
576 MarketStatusAction::Trading,
577 UnixNanos::from(1_000_000_000),
578 UnixNanos::from(2_000_000_000),
579 Some(Ustr::from("")),
580 None,
581 None,
582 None,
583 None,
584 );
585
586 assert_eq!(status.reason, Some(Ustr::from("")));
587 }
588
589 #[rstest]
590 fn test_instrument_status_with_long_reason() {
591 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.";
592 let status = InstrumentStatus::new(
593 InstrumentId::from("TEST.SIM"),
594 MarketStatusAction::Suspend,
595 UnixNanos::from(1_000_000_000),
596 UnixNanos::from(2_000_000_000),
597 Some(Ustr::from(long_reason)),
598 None,
599 None,
600 None,
601 None,
602 );
603
604 assert_eq!(status.reason, Some(Ustr::from(long_reason)));
605 }
606
607 #[rstest]
608 fn test_instrument_status_with_zero_timestamps() {
609 let status = InstrumentStatus::new(
610 InstrumentId::from("TEST.SIM"),
611 MarketStatusAction::None,
612 UnixNanos::from(0),
613 UnixNanos::from(0),
614 None,
615 None,
616 None,
617 None,
618 None,
619 );
620
621 assert_eq!(status.ts_event, UnixNanos::from(0));
622 assert_eq!(status.ts_init, UnixNanos::from(0));
623 }
624
625 #[rstest]
626 fn test_instrument_status_with_max_timestamps() {
627 let status = InstrumentStatus::new(
628 InstrumentId::from("TEST.SIM"),
629 MarketStatusAction::Trading,
630 UnixNanos::from(u64::MAX),
631 UnixNanos::from(u64::MAX),
632 None,
633 None,
634 None,
635 None,
636 None,
637 );
638
639 assert_eq!(status.ts_event, UnixNanos::from(u64::MAX));
640 assert_eq!(status.ts_init, UnixNanos::from(u64::MAX));
641 }
642
643 #[rstest]
644 fn test_to_string(stub_instrument_status: InstrumentStatus) {
645 assert_eq!(stub_instrument_status.to_string(), "MSFT.XNAS,TRADING,1,2");
646 }
647}