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