1use 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#[derive(Debug, Clone)]
45pub struct DataTesterConfig {
46 pub base: DataActorConfig,
48 pub instrument_ids: Vec<InstrumentId>,
50 pub client_id: Option<ClientId>,
52 pub bar_types: Option<Vec<BarType>>,
54 pub subscribe_book_deltas: bool,
56 pub subscribe_book_depth: bool,
58 pub subscribe_book_at_interval: bool,
60 pub subscribe_quotes: bool,
62 pub subscribe_trades: bool,
64 pub subscribe_mark_prices: bool,
66 pub subscribe_index_prices: bool,
68 pub subscribe_funding_rates: bool,
70 pub subscribe_bars: bool,
72 pub subscribe_instrument: bool,
74 pub subscribe_instrument_status: bool,
76 pub subscribe_instrument_close: bool,
78 pub can_unsubscribe: bool,
81 pub request_instruments: bool,
83 pub request_quotes: bool,
86 pub request_trades: bool,
89 pub request_bars: bool,
91 pub request_book_snapshot: bool,
93 pub book_type: BookType,
96 pub book_depth: Option<NonZeroUsize>,
98 pub book_interval_ms: NonZeroUsize,
101 pub book_levels_to_print: usize,
103 pub manage_book: bool,
105 pub log_data: bool,
107 pub stats_interval_secs: u64,
109}
110
111impl DataTesterConfig {
112 #[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#[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 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 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 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 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, None, client_id,
467 None, ) {
469 log::error!("Failed to request trades for {instrument_id}: {e}");
470 }
471 }
472
473 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 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 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, None, client_id,
500 None, ) {
502 log::error!("Failed to request bars for {bar_type}: {e}");
503 }
504 }
505 }
506 }
507
508 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 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 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 #[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("e);
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("e);
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}