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        RequestBars, RequestBookSnapshot, RequestCustomData, RequestForwardPrices,
32        RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars,
33        SubscribeBookDeltas, SubscribeBookDepth10, SubscribeCustomData, SubscribeIndexPrices,
34        SubscribeInstrument, SubscribeInstrumentClose, SubscribeInstrumentStatus,
35        SubscribeInstruments, SubscribeMarkPrices, SubscribeQuotes, SubscribeTrades,
36        UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth10, UnsubscribeCustomData,
37        UnsubscribeIndexPrices, UnsubscribeInstrument, UnsubscribeInstrumentClose,
38        UnsubscribeInstrumentStatus, UnsubscribeInstruments, UnsubscribeMarkPrices,
39        UnsubscribeQuotes, UnsubscribeTrades,
40    },
41};
42use nautilus_model::identifiers::{ClientId, Venue};
43
44/// Data client implementation for backtesting market data operations.
45///
46/// The `BacktestDataClient` provides a data client interface specifically designed
47/// for backtesting environments. It handles market data subscriptions and requests
48/// during backtesting, coordinating with the backtesting engine to provide
49/// historical data replay functionality.
50#[derive(Debug)]
51pub struct BacktestDataClient {
52    pub client_id: ClientId,
53    pub venue: Venue,
54    _cache: Rc<RefCell<Cache>>,
55}
56
57impl BacktestDataClient {
58    /// Creates a new [`BacktestDataClient`] instance.
59    #[must_use]
60    pub const fn new(client_id: ClientId, venue: Venue, cache: Rc<RefCell<Cache>>) -> Self {
61        Self {
62            client_id,
63            venue,
64            _cache: cache,
65        }
66    }
67}
68
69#[async_trait::async_trait(?Send)]
70impl DataClient for BacktestDataClient {
71    fn client_id(&self) -> ClientId {
72        self.client_id
73    }
74
75    fn venue(&self) -> Option<Venue> {
76        Some(self.venue)
77    }
78
79    fn start(&mut self) -> anyhow::Result<()> {
80        Ok(())
81    }
82
83    fn stop(&mut self) -> anyhow::Result<()> {
84        Ok(())
85    }
86
87    fn reset(&mut self) -> anyhow::Result<()> {
88        Ok(())
89    }
90
91    fn dispose(&mut self) -> anyhow::Result<()> {
92        Ok(())
93    }
94
95    fn is_connected(&self) -> bool {
96        true
97    }
98
99    fn is_disconnected(&self) -> bool {
100        false
101    }
102
103    fn subscribe(&mut self, _cmd: SubscribeCustomData) -> anyhow::Result<()> {
104        Ok(())
105    }
106
107    fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
108        Ok(())
109    }
110
111    fn subscribe_instrument(&mut self, _cmd: SubscribeInstrument) -> anyhow::Result<()> {
112        Ok(())
113    }
114
115    fn subscribe_book_deltas(&mut self, _cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
116        Ok(())
117    }
118
119    fn subscribe_book_depth10(&mut self, _cmd: SubscribeBookDepth10) -> anyhow::Result<()> {
120        Ok(())
121    }
122
123    fn subscribe_quotes(&mut self, _cmd: SubscribeQuotes) -> anyhow::Result<()> {
124        Ok(())
125    }
126
127    fn subscribe_trades(&mut self, _cmd: SubscribeTrades) -> anyhow::Result<()> {
128        Ok(())
129    }
130
131    fn subscribe_bars(&mut self, _cmd: SubscribeBars) -> anyhow::Result<()> {
132        Ok(())
133    }
134
135    fn subscribe_mark_prices(&mut self, _cmd: SubscribeMarkPrices) -> anyhow::Result<()> {
136        Ok(())
137    }
138
139    fn subscribe_index_prices(&mut self, _cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
140        Ok(())
141    }
142
143    fn subscribe_instrument_status(
144        &mut self,
145        _cmd: SubscribeInstrumentStatus,
146    ) -> anyhow::Result<()> {
147        Ok(())
148    }
149
150    fn subscribe_instrument_close(&mut self, _cmd: SubscribeInstrumentClose) -> anyhow::Result<()> {
151        Ok(())
152    }
153
154    // DeFi subscriptions/requests are served by replayed data; these silent overrides of the
155    // `DataClient` default stay here because a trait impl cannot be split across modules.
156    #[cfg(feature = "defi")]
157    fn subscribe_blocks(&mut self, _cmd: SubscribeBlocks) -> anyhow::Result<()> {
158        Ok(())
159    }
160
161    #[cfg(feature = "defi")]
162    fn subscribe_pool(&mut self, _cmd: SubscribePool) -> anyhow::Result<()> {
163        Ok(())
164    }
165
166    #[cfg(feature = "defi")]
167    fn subscribe_pool_swaps(&mut self, _cmd: SubscribePoolSwaps) -> anyhow::Result<()> {
168        Ok(())
169    }
170
171    #[cfg(feature = "defi")]
172    fn subscribe_pool_liquidity_updates(
173        &mut self,
174        _cmd: SubscribePoolLiquidityUpdates,
175    ) -> anyhow::Result<()> {
176        Ok(())
177    }
178
179    #[cfg(feature = "defi")]
180    fn subscribe_pool_fee_collects(
181        &mut self,
182        _cmd: SubscribePoolFeeCollects,
183    ) -> anyhow::Result<()> {
184        Ok(())
185    }
186
187    #[cfg(feature = "defi")]
188    fn subscribe_pool_flash_events(
189        &mut self,
190        _cmd: SubscribePoolFlashEvents,
191    ) -> anyhow::Result<()> {
192        Ok(())
193    }
194
195    fn unsubscribe(&mut self, _cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
196        Ok(())
197    }
198
199    fn unsubscribe_instruments(&mut self, _cmd: &UnsubscribeInstruments) -> anyhow::Result<()> {
200        Ok(())
201    }
202
203    fn unsubscribe_instrument(&mut self, _cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
204        Ok(())
205    }
206
207    fn unsubscribe_book_deltas(&mut self, _cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
208        Ok(())
209    }
210
211    fn unsubscribe_book_depth10(&mut self, _cmd: &UnsubscribeBookDepth10) -> anyhow::Result<()> {
212        Ok(())
213    }
214
215    fn unsubscribe_quotes(&mut self, _cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
216        Ok(())
217    }
218
219    fn unsubscribe_trades(&mut self, _cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
220        Ok(())
221    }
222
223    fn unsubscribe_bars(&mut self, _cmd: &UnsubscribeBars) -> anyhow::Result<()> {
224        Ok(())
225    }
226
227    fn unsubscribe_mark_prices(&mut self, _cmd: &UnsubscribeMarkPrices) -> anyhow::Result<()> {
228        Ok(())
229    }
230
231    fn unsubscribe_index_prices(&mut self, _cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
232        Ok(())
233    }
234
235    fn unsubscribe_instrument_status(
236        &mut self,
237        _cmd: &UnsubscribeInstrumentStatus,
238    ) -> anyhow::Result<()> {
239        Ok(())
240    }
241
242    fn unsubscribe_instrument_close(
243        &mut self,
244        _cmd: &UnsubscribeInstrumentClose,
245    ) -> anyhow::Result<()> {
246        Ok(())
247    }
248
249    #[cfg(feature = "defi")]
250    fn unsubscribe_blocks(&mut self, _cmd: &UnsubscribeBlocks) -> anyhow::Result<()> {
251        Ok(())
252    }
253
254    #[cfg(feature = "defi")]
255    fn unsubscribe_pool(&mut self, _cmd: &UnsubscribePool) -> anyhow::Result<()> {
256        Ok(())
257    }
258
259    #[cfg(feature = "defi")]
260    fn unsubscribe_pool_swaps(&mut self, _cmd: &UnsubscribePoolSwaps) -> anyhow::Result<()> {
261        Ok(())
262    }
263
264    #[cfg(feature = "defi")]
265    fn unsubscribe_pool_liquidity_updates(
266        &mut self,
267        _cmd: &UnsubscribePoolLiquidityUpdates,
268    ) -> anyhow::Result<()> {
269        Ok(())
270    }
271
272    #[cfg(feature = "defi")]
273    fn unsubscribe_pool_fee_collects(
274        &mut self,
275        _cmd: &UnsubscribePoolFeeCollects,
276    ) -> anyhow::Result<()> {
277        Ok(())
278    }
279
280    #[cfg(feature = "defi")]
281    fn unsubscribe_pool_flash_events(
282        &mut self,
283        _cmd: &UnsubscribePoolFlashEvents,
284    ) -> anyhow::Result<()> {
285        Ok(())
286    }
287
288    fn request_data(&self, _request: RequestCustomData) -> anyhow::Result<()> {
289        // No-op in backtest: data is replayed by the engine
290        Ok(())
291    }
292
293    fn request_instruments(&self, _request: RequestInstruments) -> anyhow::Result<()> {
294        // No-op in backtest: instruments are pre-loaded
295        Ok(())
296    }
297
298    fn request_instrument(&self, _request: RequestInstrument) -> anyhow::Result<()> {
299        // No-op in backtest: instruments are pre-loaded
300        Ok(())
301    }
302
303    fn request_book_snapshot(&self, _request: RequestBookSnapshot) -> anyhow::Result<()> {
304        // No-op in backtest
305        Ok(())
306    }
307
308    fn request_quotes(&self, _request: RequestQuotes) -> anyhow::Result<()> {
309        // No-op in backtest: quotes are replayed by the engine
310        Ok(())
311    }
312
313    fn request_trades(&self, _request: RequestTrades) -> anyhow::Result<()> {
314        // No-op in backtest: trades are replayed by the engine
315        Ok(())
316    }
317
318    fn request_bars(&self, _request: RequestBars) -> anyhow::Result<()> {
319        // No-op in backtest: bars are replayed by the engine
320        Ok(())
321    }
322
323    fn request_forward_prices(&self, _request: RequestForwardPrices) -> anyhow::Result<()> {
324        // No live ATM source in backtest; return Err so the engine fallback
325        // creates the option-chain manager without an initial ATM price.
326        anyhow::bail!("backtest data client cannot fetch forward prices")
327    }
328
329    #[cfg(feature = "defi")]
330    fn request_pool_snapshot(&self, _request: RequestPoolSnapshot) -> anyhow::Result<()> {
331        Ok(())
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use nautilus_core::{UUID4, UnixNanos};
338    use rstest::rstest;
339    use ustr::Ustr;
340
341    use super::*;
342
343    #[rstest]
344    fn test_request_forward_prices_returns_err_for_engine_fallback() {
345        let client_id = ClientId::new("BACKTEST");
346        let venue = Venue::new("BACKTEST");
347        let cache = Rc::new(RefCell::new(Cache::default()));
348        let client = BacktestDataClient::new(client_id, venue, cache);
349
350        let request = RequestForwardPrices::new(
351            venue,
352            Ustr::from("BTC"),
353            None,
354            Some(client_id),
355            UUID4::new(),
356            UnixNanos::default(),
357            None,
358        );
359
360        let result = client.request_forward_prices(request);
361        assert!(result.is_err());
362        let msg = result.unwrap_err().to_string();
363        assert!(msg.contains("backtest data client"));
364    }
365}