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::identifiers::{InstrumentId, OptionSeriesId};
310    use rstest::rstest;
311    use ustr::Ustr;
312
313    use super::*;
314
315    struct RequestWarnCapture {
316        messages: Mutex<Vec<String>>,
317    }
318
319    impl RequestWarnCapture {
320        fn clear(&self) {
321            self.messages.lock().unwrap().clear();
322        }
323
324        fn messages(&self) -> Vec<String> {
325            self.messages.lock().unwrap().clone()
326        }
327    }
328
329    impl Log for RequestWarnCapture {
330        fn enabled(&self, metadata: &Metadata<'_>) -> bool {
331            metadata.level() == Level::Warn
332        }
333
334        fn log(&self, record: &Record<'_>) {
335            if self.enabled(record.metadata()) {
336                self.messages
337                    .lock()
338                    .unwrap()
339                    .push(record.args().to_string());
340            }
341        }
342
343        fn flush(&self) {}
344    }
345
346    static REQUEST_WARN_CAPTURE: RequestWarnCapture = RequestWarnCapture {
347        messages: Mutex::new(Vec::new()),
348    };
349    static REQUEST_WARN_TEST_LOCK: Mutex<()> = Mutex::new(());
350
351    fn start_request_warn_capture() -> MutexGuard<'static, ()> {
352        let guard = REQUEST_WARN_TEST_LOCK.lock().unwrap();
353        let _ = log::set_logger(&REQUEST_WARN_CAPTURE);
354        log::set_max_level(LevelFilter::Warn);
355        REQUEST_WARN_CAPTURE.clear();
356        guard
357    }
358
359    #[rstest]
360    fn test_request_instruments_logs_not_implemented_warning() {
361        let _guard = start_request_warn_capture();
362
363        let client_id = ClientId::new("BACKTEST");
364        let venue = Venue::new("BACKTEST");
365        let cache = Rc::new(RefCell::new(Cache::default()));
366        let client = BacktestDataClient::new(client_id, venue, cache);
367
368        let request = RequestInstruments::new(
369            None,
370            None,
371            Some(client_id),
372            Some(venue),
373            UUID4::new(),
374            UnixNanos::default(),
375            None,
376        );
377
378        let result = client.request_instruments(request);
379
380        assert!(result.is_ok());
381        assert!(
382            REQUEST_WARN_CAPTURE
383                .messages()
384                .iter()
385                .any(|message| message.contains("RequestInstruments")
386                    && message.contains("handler not implemented")),
387        );
388    }
389    #[rstest]
390    fn test_option_chain_reference_price_is_unsupported() {
391        let client_id = ClientId::new("BACKTEST");
392        let venue = Venue::new("BACKTEST");
393        let cache = Rc::new(RefCell::new(Cache::default()));
394        let client = BacktestDataClient::new(client_id, venue, cache);
395        let series_id = OptionSeriesId::new(
396            venue,
397            Ustr::from("BTC"),
398            Ustr::from("BTC"),
399            UnixNanos::default(),
400        );
401
402        let request = RequestOptionChainReferencePrice::new(
403            series_id,
404            InstrumentId::from("BTC-TEST-50000-C.BACKTEST"),
405            Some(client_id),
406            UUID4::new(),
407            UnixNanos::default(),
408            None,
409        );
410
411        let result = client.request_option_chain_reference_price(request);
412        assert!(result.is_err());
413        let msg = result.unwrap_err().to_string();
414        assert!(msg.contains("backtest data client"));
415    }
416}