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