1use 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#[derive(Debug, Clone)]
44pub struct DataTesterConfig {
45 pub base: DataActorConfig,
47 pub instrument_ids: Vec<InstrumentId>,
49 pub client_id: Option<ClientId>,
51 pub bar_types: Option<Vec<BarType>>,
53 pub subscribe_book_deltas: bool,
55 pub subscribe_book_depth: bool,
57 pub subscribe_book_at_interval: bool,
59 pub subscribe_quotes: bool,
61 pub subscribe_trades: bool,
63 pub subscribe_mark_prices: bool,
65 pub subscribe_index_prices: bool,
67 pub subscribe_funding_rates: bool,
69 pub subscribe_bars: bool,
71 pub subscribe_instrument: bool,
73 pub subscribe_instrument_status: bool,
75 pub subscribe_instrument_close: bool,
77 pub can_unsubscribe: bool,
80 pub request_instruments: bool,
82 pub request_quotes: bool,
85 pub request_trades: bool,
88 pub request_bars: bool,
91 pub book_type: BookType,
94 pub book_depth: Option<NonZeroUsize>,
96 pub book_interval_ms: NonZeroUsize,
99 pub book_levels_to_print: usize,
101 pub manage_book: bool,
103 pub log_data: bool,
105 pub stats_interval_secs: u64,
107}
108
109impl DataTesterConfig {
110 #[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#[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 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 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 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 }
435
436 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 }
448 }
449
450 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 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 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 #[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("e);
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("e);
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}