Skip to main content

nautilus_backtest/
data_client.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//! Provides a `BacktestDataClient` implementation for backtesting.
17
18use 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/// Data client implementation for backtesting market data operations.
43///
44/// The `BacktestDataClient` provides a data client interface specifically designed
45/// for backtesting environments. It handles market data subscriptions and requests
46/// during backtesting, coordinating with the backtesting engine to provide
47/// historical data replay functionality.
48#[derive(Debug)]
49pub struct BacktestDataClient {
50    pub client_id: ClientId,
51    pub venue: Venue,
52    _cache: Rc<RefCell<Cache>>,
53}
54
55impl BacktestDataClient {
56    /// Creates a new [`BacktestDataClient`] instance.
57    #[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    // DeFi subscriptions/requests are served by replayed data; these silent overrides of the
153    // `DataClient` default stay here because a trait impl cannot be split across modules.
154    #[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    // Unlike the other request handlers, this stays silent: the engine itself issues this
294    // request when a DeFi subscription arrives before the pool is cached, and the replayed
295    // snapshot completes that flow. The default handler would warn during a successful backtest.
296    #[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}