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