nautilus_testkit/testers/
data.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Data tester actor for live testing market data subscriptions.
17
18use std::{
19    num::NonZeroUsize,
20    ops::{Deref, DerefMut},
21    time::Duration,
22};
23
24use ahash::{AHashMap, AHashSet};
25use nautilus_common::{
26    actor::{DataActor, DataActorConfig, DataActorCore},
27    enums::LogColor,
28    log_info,
29    timer::TimeEvent,
30};
31use nautilus_model::{
32    data::{
33        Bar, FundingRateUpdate, IndexPriceUpdate, InstrumentClose, InstrumentStatus,
34        MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick, bar::BarType,
35    },
36    enums::BookType,
37    identifiers::{ClientId, InstrumentId},
38    instruments::InstrumentAny,
39    orderbook::OrderBook,
40};
41
42/// Configuration for the data tester actor.
43#[derive(Debug, Clone)]
44pub struct DataTesterConfig {
45    /// Base data actor configuration.
46    pub base: DataActorConfig,
47    /// Instrument IDs to subscribe to.
48    pub instrument_ids: Vec<InstrumentId>,
49    /// Client ID to use for subscriptions.
50    pub client_id: Option<ClientId>,
51    /// Bar types to subscribe to.
52    pub bar_types: Option<Vec<BarType>>,
53    /// Whether to subscribe to order book deltas.
54    pub subscribe_book_deltas: bool,
55    /// Whether to subscribe to order book depth snapshots.
56    pub subscribe_book_depth: bool,
57    /// Whether to subscribe to order book at interval.
58    pub subscribe_book_at_interval: bool,
59    /// Whether to subscribe to quotes.
60    pub subscribe_quotes: bool,
61    /// Whether to subscribe to trades.
62    pub subscribe_trades: bool,
63    /// Whether to subscribe to mark prices.
64    pub subscribe_mark_prices: bool,
65    /// Whether to subscribe to index prices.
66    pub subscribe_index_prices: bool,
67    /// Whether to subscribe to funding rates.
68    pub subscribe_funding_rates: bool,
69    /// Whether to subscribe to bars.
70    pub subscribe_bars: bool,
71    /// Whether to subscribe to instrument updates.
72    pub subscribe_instrument: bool,
73    /// Whether to subscribe to instrument status.
74    pub subscribe_instrument_status: bool,
75    /// Whether to subscribe to instrument close.
76    pub subscribe_instrument_close: bool,
77    // TODO: Support subscribe_params when we have a type-safe way to pass arbitrary params
78    /// Whether unsubscribe is supported on stop.
79    pub can_unsubscribe: bool,
80    /// Whether to request instruments on start.
81    pub request_instruments: bool,
82    // TODO: Support request_quotes when historical data requests are available
83    /// Whether to request historical quotes (not yet implemented).
84    pub request_quotes: bool,
85    // TODO: Support request_trades when historical data requests are available
86    /// Whether to request historical trades (not yet implemented).
87    pub request_trades: bool,
88    // TODO: Support request_bars when historical data requests are available
89    /// Whether to request historical bars (not yet implemented).
90    pub request_bars: bool,
91    // TODO: Support requests_start_delta when we implement historical data requests
92    /// Book type for order book subscriptions.
93    pub book_type: BookType,
94    /// Order book depth for subscriptions.
95    pub book_depth: Option<NonZeroUsize>,
96    // TODO: Support book_group_size when order book grouping is implemented
97    /// Order book interval in milliseconds for at_interval subscriptions.
98    pub book_interval_ms: NonZeroUsize,
99    /// Number of order book levels to print when logging.
100    pub book_levels_to_print: usize,
101    /// Whether to manage local order book from deltas.
102    pub manage_book: bool,
103    /// Whether to log received data.
104    pub log_data: bool,
105    /// Stats logging interval in seconds (0 to disable).
106    pub stats_interval_secs: u64,
107}
108
109impl DataTesterConfig {
110    /// Creates a new [`DataTesterConfig`] instance with minimal settings.
111    ///
112    /// For subscribing to quotes and trades on specified instruments.
113    ///
114    /// # Panics
115    ///
116    /// Panics if `NonZeroUsize::new(1000)` fails (which should never happen).
117    #[must_use]
118    pub fn new(
119        client_id: ClientId,
120        instrument_ids: Vec<InstrumentId>,
121        subscribe_quotes: bool,
122        subscribe_trades: bool,
123    ) -> Self {
124        Self {
125            base: DataActorConfig::default(),
126            instrument_ids,
127            client_id: Some(client_id),
128            bar_types: None,
129            subscribe_book_deltas: false,
130            subscribe_book_depth: false,
131            subscribe_book_at_interval: false,
132            subscribe_quotes,
133            subscribe_trades,
134            subscribe_mark_prices: false,
135            subscribe_index_prices: false,
136            subscribe_funding_rates: false,
137            subscribe_bars: false,
138            subscribe_instrument: false,
139            subscribe_instrument_status: false,
140            subscribe_instrument_close: false,
141            can_unsubscribe: true,
142            request_instruments: false,
143            request_quotes: false,
144            request_trades: false,
145            request_bars: false,
146            book_type: BookType::L2_MBP,
147            book_depth: None,
148            book_interval_ms: NonZeroUsize::new(1000).unwrap(),
149            book_levels_to_print: 10,
150            manage_book: false,
151            log_data: true,
152            stats_interval_secs: 5,
153        }
154    }
155
156    #[must_use]
157    pub fn with_log_data(mut self, log_data: bool) -> Self {
158        self.log_data = log_data;
159        self
160    }
161
162    #[must_use]
163    pub fn with_subscribe_book_deltas(mut self, subscribe: bool) -> Self {
164        self.subscribe_book_deltas = subscribe;
165        self
166    }
167
168    #[must_use]
169    pub fn with_subscribe_book_depth(mut self, subscribe: bool) -> Self {
170        self.subscribe_book_depth = subscribe;
171        self
172    }
173
174    #[must_use]
175    pub fn with_subscribe_book_at_interval(mut self, subscribe: bool) -> Self {
176        self.subscribe_book_at_interval = subscribe;
177        self
178    }
179
180    #[must_use]
181    pub fn with_subscribe_mark_prices(mut self, subscribe: bool) -> Self {
182        self.subscribe_mark_prices = subscribe;
183        self
184    }
185
186    #[must_use]
187    pub fn with_subscribe_index_prices(mut self, subscribe: bool) -> Self {
188        self.subscribe_index_prices = subscribe;
189        self
190    }
191
192    #[must_use]
193    pub fn with_subscribe_funding_rates(mut self, subscribe: bool) -> Self {
194        self.subscribe_funding_rates = subscribe;
195        self
196    }
197
198    #[must_use]
199    pub fn with_subscribe_bars(mut self, subscribe: bool) -> Self {
200        self.subscribe_bars = subscribe;
201        self
202    }
203
204    #[must_use]
205    pub fn with_bar_types(mut self, bar_types: Vec<BarType>) -> Self {
206        self.bar_types = Some(bar_types);
207        self
208    }
209
210    #[must_use]
211    pub fn with_subscribe_instrument(mut self, subscribe: bool) -> Self {
212        self.subscribe_instrument = subscribe;
213        self
214    }
215
216    #[must_use]
217    pub fn with_subscribe_instrument_status(mut self, subscribe: bool) -> Self {
218        self.subscribe_instrument_status = subscribe;
219        self
220    }
221
222    #[must_use]
223    pub fn with_subscribe_instrument_close(mut self, subscribe: bool) -> Self {
224        self.subscribe_instrument_close = subscribe;
225        self
226    }
227
228    #[must_use]
229    pub fn with_book_type(mut self, book_type: BookType) -> Self {
230        self.book_type = book_type;
231        self
232    }
233
234    #[must_use]
235    pub fn with_book_depth(mut self, depth: Option<NonZeroUsize>) -> Self {
236        self.book_depth = depth;
237        self
238    }
239
240    #[must_use]
241    pub fn with_book_interval_ms(mut self, interval_ms: NonZeroUsize) -> Self {
242        self.book_interval_ms = interval_ms;
243        self
244    }
245
246    #[must_use]
247    pub fn with_manage_book(mut self, manage: bool) -> Self {
248        self.manage_book = manage;
249        self
250    }
251
252    #[must_use]
253    pub fn with_request_instruments(mut self, request: bool) -> Self {
254        self.request_instruments = request;
255        self
256    }
257
258    #[must_use]
259    pub fn with_can_unsubscribe(mut self, can_unsubscribe: bool) -> Self {
260        self.can_unsubscribe = can_unsubscribe;
261        self
262    }
263
264    #[must_use]
265    pub fn with_stats_interval_secs(mut self, interval_secs: u64) -> Self {
266        self.stats_interval_secs = interval_secs;
267        self
268    }
269}
270
271impl Default for DataTesterConfig {
272    fn default() -> Self {
273        Self {
274            base: DataActorConfig::default(),
275            instrument_ids: Vec::new(),
276            client_id: None,
277            bar_types: None,
278            subscribe_book_deltas: false,
279            subscribe_book_depth: false,
280            subscribe_book_at_interval: false,
281            subscribe_quotes: false,
282            subscribe_trades: false,
283            subscribe_mark_prices: false,
284            subscribe_index_prices: false,
285            subscribe_funding_rates: false,
286            subscribe_bars: false,
287            subscribe_instrument: false,
288            subscribe_instrument_status: false,
289            subscribe_instrument_close: false,
290            can_unsubscribe: true,
291            request_instruments: false,
292            request_quotes: false,
293            request_trades: false,
294            request_bars: false,
295            book_type: BookType::L2_MBP,
296            book_depth: None,
297            book_interval_ms: NonZeroUsize::new(1000).unwrap(),
298            book_levels_to_print: 10,
299            manage_book: false,
300            log_data: true,
301            stats_interval_secs: 5,
302        }
303    }
304}
305
306/// A data tester actor for live testing market data subscriptions.
307///
308/// Subscribes to configured data types for specified instruments and logs
309/// received data to demonstrate the data flow. Useful for testing adapters
310/// and validating data connectivity.
311///
312/// This actor provides equivalent functionality to the Python `DataTester`
313/// in the test kit.
314#[derive(Debug)]
315pub struct DataTester {
316    core: DataActorCore,
317    config: DataTesterConfig,
318    books: AHashMap<InstrumentId, OrderBook>,
319}
320
321impl Deref for DataTester {
322    type Target = DataActorCore;
323
324    fn deref(&self) -> &Self::Target {
325        &self.core
326    }
327}
328
329impl DerefMut for DataTester {
330    fn deref_mut(&mut self) -> &mut Self::Target {
331        &mut self.core
332    }
333}
334
335impl DataActor for DataTester {
336    fn on_start(&mut self) -> anyhow::Result<()> {
337        let instrument_ids = self.config.instrument_ids.clone();
338        let client_id = self.config.client_id;
339        let stats_interval_secs = self.config.stats_interval_secs;
340
341        // Request instruments if configured
342        if self.config.request_instruments {
343            let mut venues = AHashSet::new();
344            for instrument_id in &instrument_ids {
345                venues.insert(instrument_id.venue);
346            }
347
348            for venue in venues {
349                let _ = self.request_instruments(Some(venue), None, None, client_id, None);
350            }
351        }
352
353        // Subscribe to data for each instrument
354        for instrument_id in instrument_ids {
355            if self.config.subscribe_instrument {
356                self.subscribe_instrument(instrument_id, client_id, None);
357            }
358
359            if self.config.subscribe_book_deltas {
360                self.subscribe_book_deltas(
361                    instrument_id,
362                    self.config.book_type,
363                    None,
364                    client_id,
365                    self.config.manage_book,
366                    None,
367                );
368
369                if self.config.manage_book {
370                    let book = OrderBook::new(instrument_id, self.config.book_type);
371                    self.books.insert(instrument_id, book);
372                }
373            }
374
375            if self.config.subscribe_book_at_interval {
376                self.subscribe_book_at_interval(
377                    instrument_id,
378                    self.config.book_type,
379                    self.config.book_depth,
380                    self.config.book_interval_ms,
381                    client_id,
382                    None,
383                );
384            }
385
386            // TODO: Support subscribe_book_depth when the method is available
387            // if self.config.subscribe_book_depth {
388            //     self.subscribe_book_depth(
389            //         instrument_id,
390            //         self.config.book_type,
391            //         self.config.book_depth,
392            //         client_id,
393            //         None,
394            //     );
395            // }
396
397            if self.config.subscribe_quotes {
398                self.subscribe_quotes(instrument_id, client_id, None);
399            }
400
401            if self.config.subscribe_trades {
402                self.subscribe_trades(instrument_id, client_id, None);
403            }
404
405            if self.config.subscribe_mark_prices {
406                self.subscribe_mark_prices(instrument_id, client_id, None);
407            }
408
409            if self.config.subscribe_index_prices {
410                self.subscribe_index_prices(instrument_id, client_id, None);
411            }
412
413            if self.config.subscribe_funding_rates {
414                self.subscribe_funding_rates(instrument_id, client_id, None);
415            }
416
417            if self.config.subscribe_instrument_status {
418                self.subscribe_instrument_status(instrument_id, client_id, None);
419            }
420
421            if self.config.subscribe_instrument_close {
422                self.subscribe_instrument_close(instrument_id, client_id, None);
423            }
424
425            // TODO: Implement historical data requests
426            // if self.config.request_quotes {
427            //     self.request_quote_ticks(...);
428            // }
429
430            // TODO: Implement historical data requests
431            // if self.config.request_trades {
432            //     self.request_trade_ticks(...);
433            // }
434        }
435
436        // Subscribe to bars
437        if let Some(bar_types) = self.config.bar_types.clone() {
438            for bar_type in bar_types {
439                if self.config.subscribe_bars {
440                    self.subscribe_bars(bar_type, client_id, None);
441                }
442
443                // TODO: Implement historical data requests
444                // if self.config.request_bars {
445                //     self.request_bars(...);
446                // }
447            }
448        }
449
450        // Set up stats timer
451        if stats_interval_secs > 0 {
452            self.clock().set_timer(
453                "STATS-TIMER",
454                Duration::from_secs(stats_interval_secs),
455                None,
456                None,
457                None,
458                Some(true),
459                Some(false),
460            )?;
461        }
462
463        Ok(())
464    }
465
466    fn on_stop(&mut self) -> anyhow::Result<()> {
467        if !self.config.can_unsubscribe {
468            return Ok(());
469        }
470
471        let instrument_ids = self.config.instrument_ids.clone();
472        let client_id = self.config.client_id;
473
474        for instrument_id in instrument_ids {
475            if self.config.subscribe_instrument {
476                self.unsubscribe_instrument(instrument_id, client_id, None);
477            }
478
479            if self.config.subscribe_book_deltas {
480                self.unsubscribe_book_deltas(instrument_id, client_id, None);
481            }
482
483            if self.config.subscribe_book_at_interval {
484                self.unsubscribe_book_at_interval(
485                    instrument_id,
486                    self.config.book_interval_ms,
487                    client_id,
488                    None,
489                );
490            }
491
492            // TODO: Support unsubscribe_book_depth when the method is available
493            // if self.config.subscribe_book_depth {
494            //     self.unsubscribe_book_depth(instrument_id, client_id, None);
495            // }
496
497            if self.config.subscribe_quotes {
498                self.unsubscribe_quotes(instrument_id, client_id, None);
499            }
500
501            if self.config.subscribe_trades {
502                self.unsubscribe_trades(instrument_id, client_id, None);
503            }
504
505            if self.config.subscribe_mark_prices {
506                self.unsubscribe_mark_prices(instrument_id, client_id, None);
507            }
508
509            if self.config.subscribe_index_prices {
510                self.unsubscribe_index_prices(instrument_id, client_id, None);
511            }
512
513            if self.config.subscribe_funding_rates {
514                self.unsubscribe_funding_rates(instrument_id, client_id, None);
515            }
516
517            if self.config.subscribe_instrument_status {
518                self.unsubscribe_instrument_status(instrument_id, client_id, None);
519            }
520
521            if self.config.subscribe_instrument_close {
522                self.unsubscribe_instrument_close(instrument_id, client_id, None);
523            }
524        }
525
526        if let Some(bar_types) = self.config.bar_types.clone() {
527            for bar_type in bar_types {
528                if self.config.subscribe_bars {
529                    self.unsubscribe_bars(bar_type, client_id, None);
530                }
531            }
532        }
533
534        Ok(())
535    }
536
537    fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
538        // Timer events are used by the actor but don't require specific handling
539        Ok(())
540    }
541
542    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
543        if self.config.log_data {
544            log_info!("Received {instrument:?}", color = LogColor::Cyan);
545        }
546        Ok(())
547    }
548
549    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
550        if self.config.manage_book {
551            if let Some(book) = self.books.get_mut(&deltas.instrument_id) {
552                book.apply_deltas(deltas)?;
553
554                if self.config.log_data {
555                    let levels = self.config.book_levels_to_print;
556                    let instrument_id = deltas.instrument_id;
557                    let book_str = book.pprint(levels, None);
558                    log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
559                }
560            }
561        } else if self.config.log_data {
562            log_info!("Received {deltas:?}", color = LogColor::Cyan);
563        }
564        Ok(())
565    }
566
567    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
568        if self.config.log_data {
569            log_info!("Received {quote:?}", color = LogColor::Cyan);
570        }
571        Ok(())
572    }
573
574    fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
575        if self.config.log_data {
576            log_info!("Received {trade:?}", color = LogColor::Cyan);
577        }
578        Ok(())
579    }
580
581    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
582        if self.config.log_data {
583            log_info!("Received {bar:?}", color = LogColor::Cyan);
584        }
585        Ok(())
586    }
587
588    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
589        if self.config.log_data {
590            log_info!("Received {mark_price:?}", color = LogColor::Cyan);
591        }
592        Ok(())
593    }
594
595    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
596        if self.config.log_data {
597            log_info!("Received {index_price:?}", color = LogColor::Cyan);
598        }
599        Ok(())
600    }
601
602    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
603        if self.config.log_data {
604            log_info!("Received {funding_rate:?}", color = LogColor::Cyan);
605        }
606        Ok(())
607    }
608
609    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
610        if self.config.log_data {
611            log_info!("Received {data:?}", color = LogColor::Cyan);
612        }
613        Ok(())
614    }
615
616    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
617        if self.config.log_data {
618            log_info!("Received {update:?}", color = LogColor::Cyan);
619        }
620        Ok(())
621    }
622}
623
624impl DataTester {
625    /// Creates a new [`DataTester`] instance.
626    #[must_use]
627    pub fn new(config: DataTesterConfig) -> Self {
628        Self {
629            core: DataActorCore::new(config.base.clone()),
630            config,
631            books: AHashMap::new(),
632        }
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use nautilus_core::UnixNanos;
639    use nautilus_model::{
640        data::OrderBookDelta,
641        enums::{InstrumentCloseType, MarketStatusAction},
642        identifiers::Symbol,
643        instruments::CurrencyPair,
644        types::{Currency, Price, Quantity},
645    };
646    use rstest::*;
647    use rust_decimal::Decimal;
648
649    use super::*;
650
651    #[fixture]
652    fn config() -> DataTesterConfig {
653        let client_id = ClientId::new("TEST");
654        let instrument_ids = vec![
655            InstrumentId::from("BTC-USDT.TEST"),
656            InstrumentId::from("ETH-USDT.TEST"),
657        ];
658        DataTesterConfig::new(client_id, instrument_ids, true, true)
659    }
660
661    #[rstest]
662    fn test_config_creation() {
663        let client_id = ClientId::new("TEST");
664        let instrument_ids = vec![InstrumentId::from("BTC-USDT.TEST")];
665        let config = DataTesterConfig::new(client_id, instrument_ids.clone(), true, false);
666
667        assert_eq!(config.client_id, Some(client_id));
668        assert_eq!(config.instrument_ids, instrument_ids);
669        assert!(config.subscribe_quotes);
670        assert!(!config.subscribe_trades);
671        assert!(config.log_data);
672        assert_eq!(config.stats_interval_secs, 5);
673    }
674
675    #[rstest]
676    fn test_config_default() {
677        let config = DataTesterConfig::default();
678
679        assert_eq!(config.client_id, None);
680        assert!(config.instrument_ids.is_empty());
681        assert!(!config.subscribe_quotes);
682        assert!(!config.subscribe_trades);
683        assert!(!config.subscribe_bars);
684        assert!(config.can_unsubscribe);
685        assert!(config.log_data);
686    }
687
688    #[rstest]
689    fn test_actor_creation(config: DataTesterConfig) {
690        let actor = DataTester::new(config);
691
692        assert_eq!(actor.config.client_id, Some(ClientId::new("TEST")));
693        assert_eq!(actor.config.instrument_ids.len(), 2);
694    }
695
696    #[rstest]
697    fn test_on_quote_with_logging_enabled(config: DataTesterConfig) {
698        let mut actor = DataTester::new(config);
699
700        let quote = QuoteTick::default();
701        let result = actor.on_quote(&quote);
702
703        assert!(result.is_ok());
704    }
705
706    #[rstest]
707    fn test_on_quote_with_logging_disabled(mut config: DataTesterConfig) {
708        config.log_data = false;
709        let mut actor = DataTester::new(config);
710
711        let quote = QuoteTick::default();
712        let result = actor.on_quote(&quote);
713
714        assert!(result.is_ok());
715    }
716
717    #[rstest]
718    fn test_on_trade(config: DataTesterConfig) {
719        let mut actor = DataTester::new(config);
720
721        let trade = TradeTick::default();
722        let result = actor.on_trade(&trade);
723
724        assert!(result.is_ok());
725    }
726
727    #[rstest]
728    fn test_on_bar(config: DataTesterConfig) {
729        let mut actor = DataTester::new(config);
730
731        let bar = Bar::default();
732        let result = actor.on_bar(&bar);
733
734        assert!(result.is_ok());
735    }
736
737    #[rstest]
738    fn test_on_instrument(config: DataTesterConfig) {
739        let mut actor = DataTester::new(config);
740
741        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
742        let instrument = CurrencyPair::new(
743            instrument_id,
744            Symbol::from("BTC/USDT"),
745            Currency::USD(),
746            Currency::USD(),
747            4,
748            3,
749            Price::from("0.0001"),
750            Quantity::from("0.001"),
751            None,
752            None,
753            None,
754            None,
755            None,
756            None,
757            None,
758            None,
759            None,
760            None,
761            None,
762            None,
763            UnixNanos::default(),
764            UnixNanos::default(),
765        );
766        let result = actor.on_instrument(&InstrumentAny::CurrencyPair(instrument));
767
768        assert!(result.is_ok());
769    }
770
771    #[rstest]
772    fn test_on_book_deltas_without_managed_book(config: DataTesterConfig) {
773        let mut actor = DataTester::new(config);
774
775        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
776        let delta =
777            OrderBookDelta::clear(instrument_id, 0, UnixNanos::default(), UnixNanos::default());
778        let deltas = OrderBookDeltas::new(instrument_id, vec![delta]);
779        let result = actor.on_book_deltas(&deltas);
780
781        assert!(result.is_ok());
782    }
783
784    #[rstest]
785    fn test_on_mark_price(config: DataTesterConfig) {
786        let mut actor = DataTester::new(config);
787
788        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
789        let price = Price::from("50000.0");
790        let mark_price = MarkPriceUpdate::new(
791            instrument_id,
792            price,
793            UnixNanos::default(),
794            UnixNanos::default(),
795        );
796        let result = actor.on_mark_price(&mark_price);
797
798        assert!(result.is_ok());
799    }
800
801    #[rstest]
802    fn test_on_index_price(config: DataTesterConfig) {
803        let mut actor = DataTester::new(config);
804
805        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
806        let price = Price::from("50000.0");
807        let index_price = IndexPriceUpdate::new(
808            instrument_id,
809            price,
810            UnixNanos::default(),
811            UnixNanos::default(),
812        );
813        let result = actor.on_index_price(&index_price);
814
815        assert!(result.is_ok());
816    }
817
818    #[rstest]
819    fn test_on_funding_rate(config: DataTesterConfig) {
820        let mut actor = DataTester::new(config);
821
822        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
823        let funding_rate = FundingRateUpdate::new(
824            instrument_id,
825            Decimal::new(1, 4),
826            None,
827            UnixNanos::default(),
828            UnixNanos::default(),
829        );
830        let result = actor.on_funding_rate(&funding_rate);
831
832        assert!(result.is_ok());
833    }
834
835    #[rstest]
836    fn test_on_instrument_status(config: DataTesterConfig) {
837        let mut actor = DataTester::new(config);
838
839        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
840        let status = InstrumentStatus::new(
841            instrument_id,
842            MarketStatusAction::Trading,
843            UnixNanos::default(),
844            UnixNanos::default(),
845            None,
846            None,
847            None,
848            None,
849            None,
850        );
851        let result = actor.on_instrument_status(&status);
852
853        assert!(result.is_ok());
854    }
855
856    #[rstest]
857    fn test_on_instrument_close(config: DataTesterConfig) {
858        let mut actor = DataTester::new(config);
859
860        let instrument_id = InstrumentId::from("BTC-USDT.TEST");
861        let price = Price::from("50000.0");
862        let close = InstrumentClose::new(
863            instrument_id,
864            price,
865            InstrumentCloseType::EndOfSession,
866            UnixNanos::default(),
867            UnixNanos::default(),
868        );
869        let result = actor.on_instrument_close(&close);
870
871        assert!(result.is_ok());
872    }
873
874    #[rstest]
875    fn test_on_time_event(config: DataTesterConfig) {
876        let mut actor = DataTester::new(config);
877
878        let event = TimeEvent::new(
879            "TEST".into(),
880            Default::default(),
881            UnixNanos::default(),
882            UnixNanos::default(),
883        );
884        let result = actor.on_time_event(&event);
885
886        assert!(result.is_ok());
887    }
888
889    #[rstest]
890    fn test_config_with_all_subscriptions_enabled(mut config: DataTesterConfig) {
891        config.subscribe_book_deltas = true;
892        config.subscribe_book_at_interval = true;
893        config.subscribe_bars = true;
894        config.subscribe_mark_prices = true;
895        config.subscribe_index_prices = true;
896        config.subscribe_funding_rates = true;
897        config.subscribe_instrument = true;
898        config.subscribe_instrument_status = true;
899        config.subscribe_instrument_close = true;
900
901        let actor = DataTester::new(config);
902
903        assert!(actor.config.subscribe_book_deltas);
904        assert!(actor.config.subscribe_book_at_interval);
905        assert!(actor.config.subscribe_bars);
906        assert!(actor.config.subscribe_mark_prices);
907        assert!(actor.config.subscribe_index_prices);
908        assert!(actor.config.subscribe_funding_rates);
909        assert!(actor.config.subscribe_instrument);
910        assert!(actor.config.subscribe_instrument_status);
911        assert!(actor.config.subscribe_instrument_close);
912    }
913
914    #[rstest]
915    fn test_config_with_book_management(mut config: DataTesterConfig) {
916        config.manage_book = true;
917        config.book_levels_to_print = 5;
918
919        let actor = DataTester::new(config);
920
921        assert!(actor.config.manage_book);
922        assert_eq!(actor.config.book_levels_to_print, 5);
923        assert!(actor.books.is_empty());
924    }
925
926    #[rstest]
927    fn test_config_with_custom_stats_interval(mut config: DataTesterConfig) {
928        config.stats_interval_secs = 10;
929
930        let actor = DataTester::new(config);
931
932        assert_eq!(actor.config.stats_interval_secs, 10);
933    }
934
935    #[rstest]
936    fn test_config_with_unsubscribe_disabled(mut config: DataTesterConfig) {
937        config.can_unsubscribe = false;
938
939        let actor = DataTester::new(config);
940
941        assert!(!actor.config.can_unsubscribe);
942    }
943}