Skip to main content

nautilus_model/data/
status.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! An `InstrumentStatus` data type representing a change in an instrument market status.
17
18use 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/// Represents an event that indicates a change in an instrument market status.
29#[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    /// The instrument ID for the status change.
42    pub instrument_id: InstrumentId,
43    /// The instrument market status action.
44    pub action: MarketStatusAction,
45    /// UNIX timestamp (nanoseconds) when the status event occurred.
46    pub ts_event: UnixNanos,
47    /// UNIX timestamp (nanoseconds) when the instance was created.
48    pub ts_init: UnixNanos,
49    /// Additional details about the cause of the status change.
50    pub reason: Option<Ustr>,
51    /// Further information about the status change (if provided).
52    pub trading_event: Option<Ustr>,
53    /// The state of trading in the instrument.
54    pub is_trading: Option<bool>,
55    /// The state of quoting in the instrument.
56    pub is_quoting: Option<bool>,
57    /// The state of short sell restrictions for the instrument (if applicable).
58    pub is_short_sell_restricted: Option<bool>,
59}
60
61impl InstrumentStatus {
62    /// Creates a new [`InstrumentStatus`] instance.
63    #[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    /// Returns the metadata for the type, for use with serialization formats.
90    #[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
98// TODO: Revisit this
99impl 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); // Symmetry
335        assert_eq!(status1, status1); // Reflexivity
336    }
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; // Copy, not clone
402
403        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        // Test serde JSON serialization
466        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        // Test serde JSON serialization with None values
477        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}