1use std::{num::NonZeroUsize, time::Duration};
17
18use ahash::{AHashMap, AHashSet};
19use chrono::Duration as ChronoDuration;
20use nautilus_common::{
21 actor::{DataActor, DataActorCore},
22 config::ConfigError,
23 enums::LogColor,
24 log_info, nautilus_actor,
25 timer::TimeEvent,
26};
27use nautilus_model::{
28 data::{
29 Bar, FundingRateUpdate, IndexPriceUpdate, InstrumentClose, InstrumentStatus,
30 MarkPriceUpdate, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick,
31 option_chain::OptionGreeks,
32 },
33 identifiers::InstrumentId,
34 instruments::InstrumentAny,
35 orderbook::OrderBook,
36};
37
38use super::config::DataTesterConfig;
39
40#[derive(Debug)]
49pub struct DataTester {
50 pub(super) core: DataActorCore,
51 pub(super) config: DataTesterConfig,
52 pub(super) books: AHashMap<InstrumentId, OrderBook>,
53}
54
55nautilus_actor!(DataTester);
56
57impl DataActor for DataTester {
58 #[expect(
59 clippy::too_many_lines,
60 reason = "startup subscribes to each configured data scenario explicitly"
61 )]
62 fn on_start(&mut self) -> anyhow::Result<()> {
63 let instrument_ids = self.config.instrument_ids.clone();
64 let client_id = self.config.client_id;
65 let subscribe_params = self.config.subscribe_params.clone();
66 let request_params = self.config.request_params.clone();
67 let stats_interval_secs = self.config.stats_interval_secs;
68
69 if self.config.request_instruments {
71 let mut venues = AHashSet::new();
72 for instrument_id in &instrument_ids {
73 venues.insert(instrument_id.venue);
74 }
75
76 for venue in venues {
77 let _ = self.request_instruments(
78 Some(venue),
79 None,
80 None,
81 client_id,
82 request_params.clone(),
83 );
84 }
85 }
86
87 for instrument_id in instrument_ids {
89 if self.config.subscribe_instrument {
90 self.subscribe_instrument(instrument_id, client_id, subscribe_params.clone());
91 }
92
93 if self.config.subscribe_book_deltas {
94 self.subscribe_book_deltas(
95 instrument_id,
96 self.config.book_type,
97 None,
98 client_id,
99 self.config.manage_book,
100 subscribe_params.clone(),
101 );
102
103 if self.config.manage_book {
104 let book = OrderBook::new(instrument_id, self.config.book_type);
105 self.books.insert(instrument_id, book);
106 }
107 }
108
109 if self.config.subscribe_book_at_interval {
110 self.subscribe_book_at_interval(
111 instrument_id,
112 self.config.book_type,
113 self.config
114 .book_depth
115 .map(|depth| {
116 NonZeroUsize::new(depth).ok_or_else(|| {
117 ConfigError::range("book_depth", "must be positive, was 0")
118 })
119 })
120 .transpose()?,
121 NonZeroUsize::new(self.config.book_interval_ms).ok_or_else(|| {
122 ConfigError::range("book_interval_ms", "must be positive, was 0")
123 })?,
124 client_id,
125 subscribe_params.clone(),
126 );
127 }
128
129 if self.config.subscribe_book_depth {
130 self.subscribe_book_depth10(
131 instrument_id,
132 self.config.book_type,
133 client_id,
134 self.config.manage_book,
135 subscribe_params.clone(),
136 );
137 }
138
139 if self.config.subscribe_quotes {
140 self.subscribe_quotes(instrument_id, client_id, subscribe_params.clone());
141 }
142
143 if self.config.subscribe_trades {
144 self.subscribe_trades(instrument_id, client_id, subscribe_params.clone());
145 }
146
147 if self.config.subscribe_mark_prices {
148 self.subscribe_mark_prices(instrument_id, client_id, subscribe_params.clone());
149 }
150
151 if self.config.subscribe_index_prices {
152 self.subscribe_index_prices(instrument_id, client_id, subscribe_params.clone());
153 }
154
155 if self.config.subscribe_funding_rates {
156 self.subscribe_funding_rates(instrument_id, client_id, subscribe_params.clone());
157 }
158
159 if self.config.subscribe_instrument_status {
160 self.subscribe_instrument_status(
161 instrument_id,
162 client_id,
163 subscribe_params.clone(),
164 );
165 }
166
167 if self.config.subscribe_instrument_close {
168 self.subscribe_instrument_close(instrument_id, client_id, subscribe_params.clone());
169 }
170
171 if self.config.subscribe_option_greeks {
172 self.subscribe_option_greeks(instrument_id, client_id, subscribe_params.clone());
173 }
174
175 if self.config.request_quotes {
177 let start = self.clock().utc_now() - ChronoDuration::hours(1);
178
179 if let Err(e) = self.request_quotes(
180 instrument_id,
181 Some(start),
182 None,
183 None,
184 client_id,
185 request_params.clone(),
186 ) {
187 log::error!("Failed to request quotes for {instrument_id}: {e}");
188 }
189 }
190
191 if self.config.request_book_snapshot {
193 let _ = self.request_book_snapshot(
194 instrument_id,
195 self.config
196 .book_depth
197 .map(|depth| {
198 NonZeroUsize::new(depth).ok_or_else(|| {
199 ConfigError::range("book_depth", "must be positive, was 0")
200 })
201 })
202 .transpose()?,
203 client_id,
204 request_params.clone(),
205 );
206 }
207
208 if self.config.request_trades {
212 let start = self.clock().utc_now() - ChronoDuration::hours(1);
213
214 if let Err(e) = self.request_trades(
215 instrument_id,
216 Some(start),
217 None,
218 None,
219 client_id,
220 request_params.clone(),
221 ) {
222 log::error!("Failed to request trades for {instrument_id}: {e}");
223 }
224 }
225
226 if self.config.request_funding_rates {
228 let start = self.clock().utc_now() - ChronoDuration::days(7);
229
230 if let Err(e) = self.request_funding_rates(
231 instrument_id,
232 Some(start),
233 None,
234 None,
235 client_id,
236 request_params.clone(),
237 ) {
238 log::error!("Failed to request funding rates for {instrument_id}: {e}");
239 }
240 }
241 }
242
243 if let Some(bar_types) = self.config.bar_types.clone() {
245 for bar_type in bar_types {
246 if self.config.subscribe_bars {
247 self.subscribe_bars(bar_type, client_id, subscribe_params.clone());
248 }
249
250 if self.config.request_bars {
252 let start = self.clock().utc_now() - ChronoDuration::hours(1);
253
254 if let Err(e) = self.request_bars(
255 bar_type,
256 Some(start),
257 None,
258 None,
259 client_id,
260 request_params.clone(),
261 ) {
262 log::error!("Failed to request bars for {bar_type}: {e}");
263 }
264 }
265 }
266 }
267
268 if stats_interval_secs > 0 {
270 self.clock().set_timer(
271 "STATS-TIMER",
272 Duration::from_secs(stats_interval_secs),
273 None,
274 None,
275 None,
276 Some(true),
277 Some(false),
278 )?;
279 }
280
281 Ok(())
282 }
283
284 fn on_stop(&mut self) -> anyhow::Result<()> {
285 if !self.config.can_unsubscribe {
286 return Ok(());
287 }
288
289 let instrument_ids = self.config.instrument_ids.clone();
290 let client_id = self.config.client_id;
291 let subscribe_params = self.config.subscribe_params.clone();
292
293 for instrument_id in instrument_ids {
294 if self.config.subscribe_instrument {
295 self.unsubscribe_instrument(instrument_id, client_id, subscribe_params.clone());
296 }
297
298 if self.config.subscribe_book_deltas {
299 self.unsubscribe_book_deltas(instrument_id, client_id, subscribe_params.clone());
300 }
301
302 if self.config.subscribe_book_at_interval {
303 self.unsubscribe_book_at_interval(
304 instrument_id,
305 NonZeroUsize::new(self.config.book_interval_ms).ok_or_else(|| {
306 ConfigError::range("book_interval_ms", "must be positive, was 0")
307 })?,
308 client_id,
309 subscribe_params.clone(),
310 );
311 }
312
313 if self.config.subscribe_book_depth {
314 self.unsubscribe_book_depth10(instrument_id, client_id, subscribe_params.clone());
315 }
316
317 if self.config.subscribe_quotes {
318 self.unsubscribe_quotes(instrument_id, client_id, subscribe_params.clone());
319 }
320
321 if self.config.subscribe_trades {
322 self.unsubscribe_trades(instrument_id, client_id, subscribe_params.clone());
323 }
324
325 if self.config.subscribe_mark_prices {
326 self.unsubscribe_mark_prices(instrument_id, client_id, subscribe_params.clone());
327 }
328
329 if self.config.subscribe_index_prices {
330 self.unsubscribe_index_prices(instrument_id, client_id, subscribe_params.clone());
331 }
332
333 if self.config.subscribe_funding_rates {
334 self.unsubscribe_funding_rates(instrument_id, client_id, subscribe_params.clone());
335 }
336
337 if self.config.subscribe_instrument_status {
338 self.unsubscribe_instrument_status(
339 instrument_id,
340 client_id,
341 subscribe_params.clone(),
342 );
343 }
344
345 if self.config.subscribe_instrument_close {
346 self.unsubscribe_instrument_close(
347 instrument_id,
348 client_id,
349 subscribe_params.clone(),
350 );
351 }
352
353 if self.config.subscribe_option_greeks {
354 self.unsubscribe_option_greeks(instrument_id, client_id, subscribe_params.clone());
355 }
356 }
357
358 if let Some(bar_types) = self.config.bar_types.clone() {
359 for bar_type in bar_types {
360 if self.config.subscribe_bars {
361 self.unsubscribe_bars(bar_type, client_id, subscribe_params.clone());
362 }
363 }
364 }
365
366 Ok(())
367 }
368
369 fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
370 Ok(())
372 }
373
374 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
375 if self.config.log_data {
376 log_info!("{instrument:?}", color = LogColor::Cyan);
377 }
378 Ok(())
379 }
380
381 fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
382 if self.config.log_data {
383 let levels = self.config.book_levels_to_print;
384 let instrument_id = book.instrument_id;
385 let book_str = book.pprint(levels, None);
386 log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
387 }
388
389 Ok(())
390 }
391
392 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
393 if self.config.manage_book {
394 if let Some(book) = self.books.get_mut(&deltas.instrument_id) {
395 book.apply_deltas(deltas)?;
396
397 if self.config.log_data {
398 let levels = self.config.book_levels_to_print;
399 let instrument_id = deltas.instrument_id;
400 let book_str = book.pprint(levels, None);
401 log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
402 }
403 }
404 } else if self.config.log_data {
405 log_info!("{deltas:?}", color = LogColor::Cyan);
406 }
407 Ok(())
408 }
409
410 fn on_book_depth(&mut self, depth: &OrderBookDepth10) -> anyhow::Result<()> {
411 if self.config.log_data {
412 log_info!("{depth:?}", color = LogColor::Cyan);
413 }
414 Ok(())
415 }
416
417 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
418 if self.config.log_data {
419 log_info!("{quote:?}", color = LogColor::Cyan);
420 }
421 Ok(())
422 }
423
424 fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
425 if self.config.log_data {
426 log_info!("{trade:?}", color = LogColor::Cyan);
427 }
428 Ok(())
429 }
430
431 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
432 if self.config.log_data {
433 log_info!("{bar:?}", color = LogColor::Cyan);
434 }
435 Ok(())
436 }
437
438 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
439 if self.config.log_data {
440 log_info!("{mark_price:?}", color = LogColor::Cyan);
441 }
442 Ok(())
443 }
444
445 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
446 if self.config.log_data {
447 log_info!("{index_price:?}", color = LogColor::Cyan);
448 }
449 Ok(())
450 }
451
452 fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
453 if self.config.log_data {
454 log_info!("{funding_rate:?}", color = LogColor::Cyan);
455 }
456 Ok(())
457 }
458
459 fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
460 if self.config.log_data {
461 log_info!("{data:?}", color = LogColor::Cyan);
462 }
463 Ok(())
464 }
465
466 fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
467 if self.config.log_data {
468 log_info!("{update:?}", color = LogColor::Cyan);
469 }
470 Ok(())
471 }
472
473 fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
474 if self.config.log_data {
475 log_info!("{greeks:?}", color = LogColor::Cyan);
476 }
477 Ok(())
478 }
479
480 fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
481 if self.config.log_data {
482 log_info!(
483 "Received {} historical trades",
484 trades.len(),
485 color = LogColor::Cyan
486 );
487
488 for trade in trades.iter().take(5) {
489 log_info!(" {trade:?}", color = LogColor::Cyan);
490 }
491
492 if trades.len() > 5 {
493 log_info!(
494 " ... and {} more trades",
495 trades.len() - 5,
496 color = LogColor::Cyan
497 );
498 }
499 }
500 Ok(())
501 }
502
503 fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
504 if self.config.log_data {
505 log_info!(
506 "Received {} historical quotes",
507 quotes.len(),
508 color = LogColor::Cyan
509 );
510
511 for quote in quotes.iter().take(5) {
512 log_info!(" {quote:?}", color = LogColor::Cyan);
513 }
514
515 if quotes.len() > 5 {
516 log_info!(
517 " ... and {} more quotes",
518 quotes.len() - 5,
519 color = LogColor::Cyan
520 );
521 }
522 }
523 Ok(())
524 }
525
526 fn on_historical_funding_rates(
527 &mut self,
528 funding_rates: &[FundingRateUpdate],
529 ) -> anyhow::Result<()> {
530 if self.config.log_data {
531 log_info!(
532 "Received {} historical funding rates",
533 funding_rates.len(),
534 color = LogColor::Cyan
535 );
536
537 for rate in funding_rates.iter().take(5) {
538 log_info!(" {rate:?}", color = LogColor::Cyan);
539 }
540
541 if funding_rates.len() > 5 {
542 log_info!(
543 " ... and {} more funding rates",
544 funding_rates.len() - 5,
545 color = LogColor::Cyan
546 );
547 }
548 }
549 Ok(())
550 }
551
552 fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
553 if self.config.log_data {
554 log_info!(
555 "Received {} historical bars",
556 bars.len(),
557 color = LogColor::Cyan
558 );
559
560 for bar in bars.iter().take(5) {
561 log_info!(" {bar:?}", color = LogColor::Cyan);
562 }
563
564 if bars.len() > 5 {
565 log_info!(
566 " ... and {} more bars",
567 bars.len() - 5,
568 color = LogColor::Cyan
569 );
570 }
571 }
572 Ok(())
573 }
574}
575
576impl DataTester {
577 #[must_use]
579 pub fn new(config: DataTesterConfig) -> Self {
580 Self {
581 core: DataActorCore::new(config.base.clone()),
582 config,
583 books: AHashMap::new(),
584 }
585 }
586}