1use std::{cell::RefCell, rc::Rc};
19
20#[cfg(feature = "defi")]
21use nautilus_common::messages::defi::{
22 RequestPoolSnapshot, SubscribeBlocks, SubscribePool, SubscribePoolFeeCollects,
23 SubscribePoolFlashEvents, SubscribePoolLiquidityUpdates, SubscribePoolSwaps, UnsubscribeBlocks,
24 UnsubscribePool, UnsubscribePoolFeeCollects, UnsubscribePoolFlashEvents,
25 UnsubscribePoolLiquidityUpdates, UnsubscribePoolSwaps,
26};
27use nautilus_common::{
28 cache::Cache,
29 clients::DataClient,
30 messages::data::{
31 RequestOptionChainReferencePrice, SubscribeBars, SubscribeBookDeltas, SubscribeBookDepth,
32 SubscribeCustomData, SubscribeIndexPrices, SubscribeInstrument, SubscribeInstrumentClose,
33 SubscribeInstrumentStatus, SubscribeInstruments, SubscribeMarkPrices, SubscribeQuotes,
34 SubscribeTrades, UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth,
35 UnsubscribeCustomData, UnsubscribeIndexPrices, UnsubscribeInstrument,
36 UnsubscribeInstrumentClose, UnsubscribeInstrumentStatus, UnsubscribeInstruments,
37 UnsubscribeMarkPrices, UnsubscribeQuotes, UnsubscribeTrades,
38 },
39};
40use nautilus_model::identifiers::{ClientId, Venue};
41
42#[derive(Debug)]
49pub struct BacktestDataClient {
50 pub client_id: ClientId,
51 pub venue: Venue,
52 _cache: Rc<RefCell<Cache>>,
53}
54
55impl BacktestDataClient {
56 #[must_use]
58 pub const fn new(client_id: ClientId, venue: Venue, cache: Rc<RefCell<Cache>>) -> Self {
59 Self {
60 client_id,
61 venue,
62 _cache: cache,
63 }
64 }
65}
66
67#[async_trait::async_trait(?Send)]
68impl DataClient for BacktestDataClient {
69 fn client_id(&self) -> ClientId {
70 self.client_id
71 }
72
73 fn venue(&self) -> Option<Venue> {
74 Some(self.venue)
75 }
76
77 fn start(&mut self) -> anyhow::Result<()> {
78 Ok(())
79 }
80
81 fn stop(&mut self) -> anyhow::Result<()> {
82 Ok(())
83 }
84
85 fn reset(&mut self) -> anyhow::Result<()> {
86 Ok(())
87 }
88
89 fn dispose(&mut self) -> anyhow::Result<()> {
90 Ok(())
91 }
92
93 fn is_connected(&self) -> bool {
94 true
95 }
96
97 fn is_disconnected(&self) -> bool {
98 false
99 }
100
101 fn subscribe(&mut self, _cmd: SubscribeCustomData) -> anyhow::Result<()> {
102 Ok(())
103 }
104
105 fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
106 Ok(())
107 }
108
109 fn subscribe_instrument(&mut self, _cmd: SubscribeInstrument) -> anyhow::Result<()> {
110 Ok(())
111 }
112
113 fn subscribe_book_deltas(&mut self, _cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
114 Ok(())
115 }
116
117 fn subscribe_book_depth(&mut self, _cmd: SubscribeBookDepth) -> anyhow::Result<()> {
118 Ok(())
119 }
120
121 fn subscribe_quotes(&mut self, _cmd: SubscribeQuotes) -> anyhow::Result<()> {
122 Ok(())
123 }
124
125 fn subscribe_trades(&mut self, _cmd: SubscribeTrades) -> anyhow::Result<()> {
126 Ok(())
127 }
128
129 fn subscribe_bars(&mut self, _cmd: SubscribeBars) -> anyhow::Result<()> {
130 Ok(())
131 }
132
133 fn subscribe_mark_prices(&mut self, _cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
134 Ok(())
135 }
136
137 fn subscribe_index_prices(&mut self, _cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
138 Ok(())
139 }
140
141 fn subscribe_instrument_status(
142 &mut self,
143 _cmd: SubscribeInstrumentStatus,
144 ) -> anyhow::Result<()> {
145 Ok(())
146 }
147
148 fn subscribe_instrument_close(&mut self, _cmd: SubscribeInstrumentClose) -> anyhow::Result<()> {
149 Ok(())
150 }
151
152 #[cfg(feature = "defi")]
155 fn subscribe_blocks(&mut self, _cmd: SubscribeBlocks) -> anyhow::Result<()> {
156 Ok(())
157 }
158
159 #[cfg(feature = "defi")]
160 fn subscribe_pool(&mut self, _cmd: SubscribePool) -> anyhow::Result<()> {
161 Ok(())
162 }
163
164 #[cfg(feature = "defi")]
165 fn subscribe_pool_swaps(&mut self, _cmd: SubscribePoolSwaps) -> anyhow::Result<()> {
166 Ok(())
167 }
168
169 #[cfg(feature = "defi")]
170 fn subscribe_pool_liquidity_updates(
171 &mut self,
172 _cmd: SubscribePoolLiquidityUpdates,
173 ) -> anyhow::Result<()> {
174 Ok(())
175 }
176
177 #[cfg(feature = "defi")]
178 fn subscribe_pool_fee_collects(
179 &mut self,
180 _cmd: SubscribePoolFeeCollects,
181 ) -> anyhow::Result<()> {
182 Ok(())
183 }
184
185 #[cfg(feature = "defi")]
186 fn subscribe_pool_flash_events(
187 &mut self,
188 _cmd: SubscribePoolFlashEvents,
189 ) -> anyhow::Result<()> {
190 Ok(())
191 }
192
193 fn unsubscribe(&mut self, _cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
194 Ok(())
195 }
196
197 fn unsubscribe_instruments(&mut self, _cmd: &UnsubscribeInstruments) -> anyhow::Result<()> {
198 Ok(())
199 }
200
201 fn unsubscribe_instrument(&mut self, _cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
202 Ok(())
203 }
204
205 fn unsubscribe_book_deltas(&mut self, _cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
206 Ok(())
207 }
208
209 fn unsubscribe_book_depth(&mut self, _cmd: &UnsubscribeBookDepth) -> anyhow::Result<()> {
210 Ok(())
211 }
212
213 fn unsubscribe_quotes(&mut self, _cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
214 Ok(())
215 }
216
217 fn unsubscribe_trades(&mut self, _cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
218 Ok(())
219 }
220
221 fn unsubscribe_bars(&mut self, _cmd: &UnsubscribeBars) -> anyhow::Result<()> {
222 Ok(())
223 }
224
225 fn unsubscribe_mark_prices(&mut self, _cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
226 Ok(())
227 }
228
229 fn unsubscribe_index_prices(&mut self, _cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
230 Ok(())
231 }
232
233 fn unsubscribe_instrument_status(
234 &mut self,
235 _cmd: &UnsubscribeInstrumentStatus,
236 ) -> anyhow::Result<()> {
237 Ok(())
238 }
239
240 fn unsubscribe_instrument_close(
241 &mut self,
242 _cmd: &UnsubscribeInstrumentClose,
243 ) -> anyhow::Result<()> {
244 Ok(())
245 }
246
247 #[cfg(feature = "defi")]
248 fn unsubscribe_blocks(&mut self, _cmd: &UnsubscribeBlocks) -> anyhow::Result<()> {
249 Ok(())
250 }
251
252 #[cfg(feature = "defi")]
253 fn unsubscribe_pool(&mut self, _cmd: &UnsubscribePool) -> anyhow::Result<()> {
254 Ok(())
255 }
256
257 #[cfg(feature = "defi")]
258 fn unsubscribe_pool_swaps(&mut self, _cmd: &UnsubscribePoolSwaps) -> anyhow::Result<()> {
259 Ok(())
260 }
261
262 #[cfg(feature = "defi")]
263 fn unsubscribe_pool_liquidity_updates(
264 &mut self,
265 _cmd: &UnsubscribePoolLiquidityUpdates,
266 ) -> anyhow::Result<()> {
267 Ok(())
268 }
269
270 #[cfg(feature = "defi")]
271 fn unsubscribe_pool_fee_collects(
272 &mut self,
273 _cmd: &UnsubscribePoolFeeCollects,
274 ) -> anyhow::Result<()> {
275 Ok(())
276 }
277
278 #[cfg(feature = "defi")]
279 fn unsubscribe_pool_flash_events(
280 &mut self,
281 _cmd: &UnsubscribePoolFlashEvents,
282 ) -> anyhow::Result<()> {
283 Ok(())
284 }
285
286 fn request_option_chain_reference_price(
287 &self,
288 _request: RequestOptionChainReferencePrice,
289 ) -> anyhow::Result<()> {
290 anyhow::bail!("backtest data client cannot fetch option-chain reference prices")
291 }
292
293 #[cfg(feature = "defi")]
297 fn request_pool_snapshot(&self, _request: RequestPoolSnapshot) -> anyhow::Result<()> {
298 Ok(())
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use std::sync::{Mutex, MutexGuard};
305
306 use log::{Level, LevelFilter, Log, Metadata, Record};
307 use nautilus_common::messages::data::RequestInstruments;
308 use nautilus_core::{UUID4, UnixNanos};
309 use nautilus_model::{
310 data::BarType,
311 identifiers::{InstrumentId, OptionSeriesId},
312 };
313 use rstest::rstest;
314 use ustr::Ustr;
315
316 use super::*;
317
318 #[rstest::rstest]
319 fn test_bar_list_hooks_accept_every_full_type() {
320 let venue = Venue::from("SIM");
321
322 let mut client = BacktestDataClient::new(
323 ClientId::from("SIM"),
324 venue,
325 Rc::new(RefCell::new(Cache::default())),
326 );
327
328 let command = SubscribeBars::new(
329 [
330 BarType::from("AUD/USD.SIM-1-MINUTE-LAST-EXTERNAL"),
331 BarType::from("AUD/USD.SIM-5-MINUTE-LAST-EXTERNAL"),
332 ],
333 Some(client.client_id),
334 Some(venue),
335 UUID4::new(),
336 UnixNanos::from(17),
337 Some(UUID4::new()),
338 None,
339 );
340
341 let unsubscribe = UnsubscribeBars::new(
342 command.bar_types.clone(),
343 command.client_id,
344 command.venue,
345 UUID4::new(),
346 UnixNanos::from(23),
347 command.correlation_id,
348 command.params.clone(),
349 );
350 assert!(client.subscribe_bars(command).is_ok());
351 assert!(client.unsubscribe_bars(&unsubscribe).is_ok());
352 }
353
354 struct RequestWarnCapture {
355 messages: Mutex<Vec<String>>,
356 }
357
358 impl RequestWarnCapture {
359 fn clear(&self) {
360 self.messages.lock().unwrap().clear();
361 }
362
363 fn messages(&self) -> Vec<String> {
364 self.messages.lock().unwrap().clone()
365 }
366 }
367
368 impl Log for RequestWarnCapture {
369 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
370 metadata.level() == Level::Warn
371 }
372
373 fn log(&self, record: &Record<'_>) {
374 if self.enabled(record.metadata()) {
375 self.messages
376 .lock()
377 .unwrap()
378 .push(record.args().to_string());
379 }
380 }
381
382 fn flush(&self) {}
383 }
384
385 static REQUEST_WARN_CAPTURE: RequestWarnCapture = RequestWarnCapture {
386 messages: Mutex::new(Vec::new()),
387 };
388 static REQUEST_WARN_TEST_LOCK: Mutex<()> = Mutex::new(());
389
390 fn start_request_warn_capture() -> MutexGuard<'static, ()> {
391 let guard = REQUEST_WARN_TEST_LOCK.lock().unwrap();
392 let _ = log::set_logger(&REQUEST_WARN_CAPTURE);
393 log::set_max_level(LevelFilter::Warn);
394 REQUEST_WARN_CAPTURE.clear();
395 guard
396 }
397
398 #[rstest]
399 fn test_request_instruments_returns_unsupported_error() {
400 let _guard = start_request_warn_capture();
401
402 let client_id = ClientId::new("BACKTEST");
403 let venue = Venue::new("BACKTEST");
404 let cache = Rc::new(RefCell::new(Cache::default()));
405 let client = BacktestDataClient::new(client_id, venue, cache);
406
407 let request = RequestInstruments::new(
408 None,
409 None,
410 Some(client_id),
411 Some(venue),
412 UUID4::new(),
413 UnixNanos::default(),
414 None,
415 );
416
417 let result = client.request_instruments(request);
418 assert_eq!(
419 result.unwrap_err().to_string(),
420 "Data client BACKTEST does not support RequestInstruments"
421 );
422 assert!(REQUEST_WARN_CAPTURE.messages().is_empty());
423 }
424 #[rstest]
425 fn test_option_chain_reference_price_is_unsupported() {
426 let client_id = ClientId::new("BACKTEST");
427 let venue = Venue::new("BACKTEST");
428 let cache = Rc::new(RefCell::new(Cache::default()));
429 let client = BacktestDataClient::new(client_id, venue, cache);
430 let series_id = OptionSeriesId::new_derived(
431 venue,
432 Ustr::from("BTC"),
433 Ustr::from("BTC"),
434 UnixNanos::default(),
435 );
436
437 let request = RequestOptionChainReferencePrice::new(
438 series_id,
439 InstrumentId::from("BTC-TEST-50000-C.BACKTEST"),
440 Some(client_id),
441 UUID4::new(),
442 UnixNanos::default(),
443 None,
444 );
445
446 let result = client.request_option_chain_reference_price(request);
447 assert!(result.is_err());
448 let msg = result.unwrap_err().to_string();
449 assert!(msg.contains("backtest data client"));
450 }
451}