Skip to main content

nautilus_backtest/
execution_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 `BacktestExecutionClient` implementation for backtesting.
17
18use std::{cell::RefCell, fmt::Debug, rc::Rc};
19
20use async_trait::async_trait;
21use nautilus_common::{
22    cache::Cache,
23    clients::ExecutionClient,
24    clock::Clock,
25    factories::OrderEventFactory,
26    messages::execution::{
27        BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
28        QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
29    },
30    msgbus::{self, MessagingSwitchboard},
31};
32use nautilus_core::{Params, UnixNanos, WeakCell};
33use nautilus_execution::client::core::ExecutionClientCore;
34use nautilus_model::{
35    accounts::AccountAny,
36    enums::OmsType,
37    events::OrderEventAny,
38    identifiers::{AccountId, ClientId, ClientOrderId, TraderId, Venue},
39    orders::OrderAny,
40    types::{AccountBalance, MarginBalance},
41};
42
43use crate::exchange::SimulatedExchange;
44
45/// Execution client implementation for backtesting trading operations.
46///
47/// The `BacktestExecutionClient` provides an execution client interface for
48/// backtesting environments, handling order management and trade execution
49/// through simulated exchanges. It processes trading commands and coordinates
50/// with the simulation infrastructure to provide realistic execution behavior.
51#[derive(Clone)]
52pub struct BacktestExecutionClient {
53    core: ExecutionClientCore,
54    factory: OrderEventFactory,
55    cache: Rc<RefCell<Cache>>,
56    clock: Rc<RefCell<dyn Clock>>,
57    exchange: WeakCell<SimulatedExchange>,
58    /// Buffered order events for deferred processing.
59    ///
60    /// Events like `OrderSubmitted` cannot be sent synchronously through
61    /// the msgbus during `submit_order` because the exec engine holds a
62    /// borrow via its `execute` handler. Instead, events are buffered here
63    /// and drained by the engine after the execute borrow is released.
64    queued_events: Rc<RefCell<Vec<OrderEventAny>>>,
65    routing: bool,
66    _frozen_account: bool,
67}
68
69impl Debug for BacktestExecutionClient {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct(stringify!(BacktestExecutionClient))
72            .field("client_id", &self.core.client_id)
73            .field("routing", &self.routing)
74            .finish_non_exhaustive()
75    }
76}
77
78impl BacktestExecutionClient {
79    /// Creates a new [`BacktestExecutionClient`] instance.
80    #[must_use]
81    pub fn new(
82        trader_id: TraderId,
83        account_id: AccountId,
84        exchange: &Rc<RefCell<SimulatedExchange>>,
85        cache: Rc<RefCell<Cache>>,
86        clock: Rc<RefCell<dyn Clock>>,
87        routing: Option<bool>,
88        frozen_account: Option<bool>,
89    ) -> Self {
90        let routing = routing.unwrap_or(false);
91        let frozen_account = frozen_account.unwrap_or(false);
92        let exchange_id = exchange.borrow().id;
93        let account_type = exchange.borrow().account_type;
94        let base_currency = exchange.borrow().base_currency;
95
96        let core = ExecutionClientCore::new(
97            trader_id,
98            ClientId::from(exchange_id.as_str()),
99            Venue::from(exchange_id.as_str()),
100            exchange.borrow().oms_type,
101            account_id,
102            account_type,
103            base_currency,
104            cache.clone(),
105        );
106
107        let factory = OrderEventFactory::new(trader_id, account_id, account_type, base_currency);
108
109        Self {
110            core,
111            factory,
112            exchange: WeakCell::from(Rc::downgrade(exchange)),
113            cache,
114            clock,
115            queued_events: Rc::new(RefCell::new(Vec::new())),
116            routing,
117            _frozen_account: frozen_account,
118        }
119    }
120
121    fn get_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<OrderAny> {
122        Ok(self.cache.borrow().try_order_owned(&client_order_id)?)
123    }
124
125    /// Drain buffered order events, sending each to the exec engine.
126    pub fn drain_queued_events(&self) {
127        let events: Vec<OrderEventAny> = self.queued_events.borrow_mut().drain(..).collect();
128        let endpoint = MessagingSwitchboard::exec_engine_process();
129        for event in events {
130            msgbus::send_order_event(endpoint, event);
131        }
132    }
133
134    pub(crate) fn order_event_handler(&self) -> Rc<dyn Fn(OrderEventAny)> {
135        let queued_events = Rc::clone(&self.queued_events);
136        Rc::new(move |event| queued_events.borrow_mut().push(event))
137    }
138}
139
140#[async_trait(?Send)]
141impl ExecutionClient for BacktestExecutionClient {
142    fn is_connected(&self) -> bool {
143        self.core.is_connected()
144    }
145
146    fn client_id(&self) -> ClientId {
147        self.core.client_id
148    }
149
150    fn account_id(&self) -> AccountId {
151        self.core.account_id
152    }
153
154    fn venue(&self) -> Venue {
155        self.core.venue
156    }
157
158    fn oms_type(&self) -> OmsType {
159        self.core.oms_type
160    }
161
162    fn get_account(&self) -> Option<AccountAny> {
163        self.cache.borrow().account_owned(&self.core.account_id)
164    }
165
166    fn generate_account_state(
167        &self,
168        balances: Vec<AccountBalance>,
169        margins: Vec<MarginBalance>,
170        reported: bool,
171        ts_event: UnixNanos,
172        info: Option<Params>,
173    ) -> anyhow::Result<()> {
174        let ts_init = self.clock.borrow().timestamp_ns();
175        let state = self
176            .factory
177            .generate_account_state(balances, margins, reported, ts_event, ts_init, info);
178        let endpoint = MessagingSwitchboard::portfolio_update_account();
179        msgbus::send_account_state(endpoint, &state);
180        Ok(())
181    }
182
183    fn start(&mut self) -> anyhow::Result<()> {
184        self.core.set_connected();
185        log::info!("Backtest execution client started");
186        Ok(())
187    }
188
189    fn stop(&mut self) -> anyhow::Result<()> {
190        self.core.set_disconnected();
191        log::info!("Backtest execution client stopped");
192        Ok(())
193    }
194
195    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
196        // Buffer the OrderSubmitted event for deferred processing to avoid
197        // RefCell re-entrancy (exec_engine holds a borrow during execute)
198        let order = self.get_order(cmd.client_order_id)?;
199        let ts_init = self.clock.borrow().timestamp_ns();
200        let event = self.factory.generate_order_submitted(&order, ts_init);
201        self.queued_events.borrow_mut().push(event);
202
203        if let Some(exchange) = self.exchange.upgrade() {
204            exchange.borrow_mut().send(TradingCommand::SubmitOrder(cmd));
205        } else {
206            log::error!("submit_order: SimulatedExchange has been dropped");
207        }
208        Ok(())
209    }
210
211    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
212        let ts_init = self.clock.borrow().timestamp_ns();
213
214        let orders: Vec<OrderAny> = self
215            .cache
216            .borrow()
217            .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
218
219        // Buffer events for deferred processing
220        let mut queued = self.queued_events.borrow_mut();
221
222        for order in &orders {
223            let event = self.factory.generate_order_submitted(order, ts_init);
224            queued.push(event);
225        }
226        drop(queued);
227
228        if let Some(exchange) = self.exchange.upgrade() {
229            exchange
230                .borrow_mut()
231                .send(TradingCommand::SubmitOrderList(cmd));
232        } else {
233            log::error!("submit_order_list: SimulatedExchange has been dropped");
234        }
235        Ok(())
236    }
237
238    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
239        if let Some(exchange) = self.exchange.upgrade() {
240            exchange.borrow_mut().send(TradingCommand::ModifyOrder(cmd));
241        } else {
242            log::error!("modify_order: SimulatedExchange has been dropped");
243        }
244        Ok(())
245    }
246
247    fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
248        if let Some(exchange) = self.exchange.upgrade() {
249            exchange
250                .borrow_mut()
251                .send(TradingCommand::ModifyOrders(cmd));
252        } else {
253            log::error!("batch_modify_orders: SimulatedExchange has been dropped");
254        }
255        Ok(())
256    }
257
258    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
259        if let Some(exchange) = self.exchange.upgrade() {
260            exchange.borrow_mut().send(TradingCommand::CancelOrder(cmd));
261        } else {
262            log::error!("cancel_order: SimulatedExchange has been dropped");
263        }
264        Ok(())
265    }
266
267    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
268        if let Some(exchange) = self.exchange.upgrade() {
269            exchange
270                .borrow_mut()
271                .send(TradingCommand::CancelAllOrders(cmd));
272        } else {
273            log::error!("cancel_all_orders: SimulatedExchange has been dropped");
274        }
275        Ok(())
276    }
277
278    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
279        if let Some(exchange) = self.exchange.upgrade() {
280            exchange
281                .borrow_mut()
282                .send(TradingCommand::CancelOrders(cmd));
283        } else {
284            log::error!("batch_cancel_orders: SimulatedExchange has been dropped");
285        }
286        Ok(())
287    }
288
289    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
290        log::warn!("Backtest execution client does not support account queries: {cmd}");
291        Ok(())
292    }
293
294    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
295        log::warn!("Backtest execution client does not support order queries: {cmd}");
296        Ok(())
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use nautilus_common::{clock::TestClock, messages::execution::QueryOrder};
303    use nautilus_core::UUID4;
304    use nautilus_execution::models::latency::{LatencyModelHandle, StaticLatencyModel};
305    use nautilus_model::{
306        enums::{AccountType, BookType, OmsType},
307        identifiers::{InstrumentId, StrategyId},
308        stubs::TestDefault,
309        types::{Currency, Money},
310    };
311    use rstest::rstest;
312
313    use super::*;
314    use crate::config::SimulatedVenueConfig;
315
316    fn setup_client_with_latency() -> (BacktestExecutionClient, Rc<RefCell<SimulatedExchange>>) {
317        let cache = Rc::new(RefCell::new(Cache::default()));
318        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
319        let latency_model = StaticLatencyModel::new(
320            UnixNanos::default(),
321            UnixNanos::default(),
322            UnixNanos::default(),
323            UnixNanos::default(),
324        );
325        let config = SimulatedVenueConfig::builder()
326            .venue(Venue::new("SIM"))
327            .oms_type(OmsType::Netting)
328            .account_type(AccountType::Margin)
329            .book_type(BookType::L2_MBP)
330            .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
331            .latency_model(LatencyModelHandle::new(latency_model))
332            .build()
333            .unwrap();
334        let exchange = Rc::new(RefCell::new(
335            SimulatedExchange::new(config, cache.clone(), clock.clone()).unwrap(),
336        ));
337        let client = BacktestExecutionClient::new(
338            TraderId::test_default(),
339            AccountId::test_default(),
340            &exchange,
341            cache,
342            clock,
343            None,
344            None,
345        );
346
347        (client, exchange)
348    }
349
350    fn query_order() -> QueryOrder {
351        QueryOrder::new(
352            TraderId::test_default(),
353            None,
354            StrategyId::test_default(),
355            InstrumentId::from("AUD/USD.SIM"),
356            ClientOrderId::from("O-001"),
357            None,
358            UUID4::new(),
359            UnixNanos::default(),
360            None,
361            None,
362        )
363    }
364
365    fn query_account() -> QueryAccount {
366        QueryAccount::new(
367            TraderId::test_default(),
368            None,
369            AccountId::test_default(),
370            UUID4::new(),
371            UnixNanos::default(),
372            None,
373            None,
374        )
375    }
376
377    #[rstest]
378    fn test_new_holds_weak_reference_to_source_exchange() {
379        let (client, exchange) = setup_client_with_latency();
380
381        // The client must not co-own the exchange, otherwise the exchange owning the
382        // client closes an unbreakable cycle.
383        assert_eq!(Rc::strong_count(&exchange), 1);
384
385        let upgraded: Rc<RefCell<SimulatedExchange>> = client
386            .exchange
387            .upgrade()
388            .expect("exchange outlives the client here")
389            .into();
390
391        assert!(Rc::ptr_eq(&upgraded, &exchange));
392    }
393
394    #[rstest]
395    fn test_query_order_is_not_forwarded_to_exchange() {
396        let (client, exchange) = setup_client_with_latency();
397
398        // Hold an immutable exchange borrow across the call: if the client
399        // forwards, send()'s `exchange.borrow_mut()` panics here. This makes the
400        // test bite on a client-only revert rather than being masked by the
401        // exchange-side query guard.
402        let exchange_ref = exchange.borrow();
403        let result = client.query_order(query_order());
404
405        assert!(result.is_ok());
406        assert_eq!(exchange_ref.max_inflight_command_ts(), None);
407    }
408
409    #[rstest]
410    fn test_query_account_is_not_forwarded_to_exchange() {
411        let (client, exchange) = setup_client_with_latency();
412
413        // See test_query_order_is_not_forwarded_to_exchange: the held borrow
414        // makes a forwarding attempt panic before the exchange guard can mask it.
415        let exchange_ref = exchange.borrow();
416        let result = client.query_account(query_account());
417
418        assert!(result.is_ok());
419        assert_eq!(exchange_ref.max_inflight_command_ts(), None);
420    }
421}