nautilus_backtest/data_iterator.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//! Static backtest replay using the shared ordered batch iterator.
17
18use nautilus_data::replay::{DataIterator, ReplayPoll};
19use nautilus_model::data::{Data, DataBatch, DataRef};
20
21/// Multi-stream, time-ordered data iterator used by the backtest engine.
22#[derive(Debug, Default)]
23pub struct BacktestDataIterator {
24 iterator: DataIterator,
25}
26
27impl BacktestDataIterator {
28 /// Creates a new empty [`BacktestDataIterator`].
29 #[must_use]
30 pub fn new() -> Self {
31 Self {
32 iterator: DataIterator::new(),
33 }
34 }
35
36 /// Adds (or replaces) a named data stream.
37 ///
38 /// When `append_data` is true the stream gets lower priority on equal replay keys;
39 /// when false (prepend) it wins ties. Named replacements retain their original priority.
40 pub fn add_data(&mut self, name: &str, data: Vec<Data>, append_data: bool) {
41 self.iterator.add_data(name, data, append_data);
42 }
43
44 /// Adds (or replaces) a named typed data stream.
45 ///
46 /// Items are ordered by replay key before insertion. A batch that arrives out of order while
47 /// sharing its backing allocation with another view is copied once, so the shared allocation
48 /// is never reordered.
49 pub fn add_data_batch(&mut self, name: &str, data: DataBatch, append_data: bool) {
50 self.iterator.add_data_batch(name, data, append_data);
51 }
52
53 /// Removes a named data stream.
54 pub fn remove_data(&mut self, name: &str, complete_remove: bool) {
55 self.iterator.remove_data(name, complete_remove);
56 }
57
58 /// Sets the cursor of a named stream to `index` (0-based).
59 pub fn set_index(&mut self, name: &str, index: usize) {
60 self.iterator.set_index(name, index);
61 }
62
63 /// Resets all stream cursors to the beginning.
64 #[expect(
65 clippy::missing_panics_doc,
66 reason = "static replay cannot contain sources or moved book frames"
67 )]
68 pub fn reset_all_cursors(&mut self) {
69 self.iterator
70 .reset_all_cursors()
71 .expect("static replay has no sources or moved book frames");
72 }
73
74 pub(crate) fn peek(&mut self) -> Option<DataRef<'_>> {
75 static_item(self.iterator.peek())
76 }
77
78 pub(crate) fn advance(&mut self) {
79 self.iterator.advance();
80 }
81
82 /// Returns the next market [`Data`] element across all streams in chronological order.
83 #[expect(clippy::should_implement_trait)]
84 pub fn next(&mut self) -> Option<Data> {
85 static_item(self.iterator.next())
86 }
87
88 /// Returns whether all streams have been fully consumed.
89 #[must_use]
90 pub fn is_done(&self) -> bool {
91 self.iterator.is_done()
92 }
93}
94
95fn static_item<T>(poll: anyhow::Result<ReplayPoll<T>>) -> Option<T> {
96 match poll.expect("static replay cannot load or fail") {
97 ReplayPoll::Ready(data) => Some(data),
98 ReplayPoll::Complete => None,
99 ReplayPoll::Pending { .. } => unreachable!("static replay has no pending sources"),
100 }
101}