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::VirtualClock, messages::execution::QueryOrder};
303    use nautilus_core::{DurationNanos, UUID4};
304    use nautilus_execution::models::{
305        fee::{FeeModelAny, MakerTakerFeeModel},
306        latency::{LatencyModelHandle, StaticLatencyModel},
307    };
308    use nautilus_model::{
309        enums::{AccountType, BookType, OmsType},
310        identifiers::{InstrumentId, StrategyId},
311        stubs::TestDefault,
312        types::{Currency, Money},
313    };
314    use rstest::rstest;
315
316    use super::*;
317    use crate::config::SimulatedVenueConfig;
318
319    fn setup_client_with_latency() -> (BacktestExecutionClient, Rc<RefCell<SimulatedExchange>>) {
320        let cache = Rc::new(RefCell::new(Cache::default()));
321        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
322        let latency_model = StaticLatencyModel::new(
323            DurationNanos::default(),
324            DurationNanos::default(),
325            DurationNanos::default(),
326            DurationNanos::default(),
327        );
328        let config = SimulatedVenueConfig::builder()
329            .venue(Venue::new("SIM"))
330            .oms_type(OmsType::Netting)
331            .account_type(AccountType::Margin)
332            .book_type(BookType::L2_MBP)
333            .starting_balances(vec![Money::new(1_000.0, Currency::USD())])
334            .latency_model(LatencyModelHandle::new(latency_model))
335            .fee_model(FeeModelAny::MakerTaker(MakerTakerFeeModel::zero()).into())
336            .build()
337            .unwrap();
338        let exchange = Rc::new(RefCell::new(
339            SimulatedExchange::new(config, cache.clone(), clock.clone()).unwrap(),
340        ));
341        let client = BacktestExecutionClient::new(
342            TraderId::test_default(),
343            AccountId::test_default(),
344            &exchange,
345            cache,
346            clock,
347            None,
348            None,
349        );
350
351        (client, exchange)
352    }
353
354    fn query_order() -> QueryOrder {
355        QueryOrder::new(
356            TraderId::test_default(),
357            None,
358            StrategyId::test_default(),
359            InstrumentId::from("AUD/USD.SIM"),
360            ClientOrderId::from("O-001"),
361            None,
362            UUID4::new(),
363            UnixNanos::default(),
364            None,
365            None,
366        )
367    }
368
369    fn query_account() -> QueryAccount {
370        QueryAccount::new(
371            TraderId::test_default(),
372            None,
373            AccountId::test_default(),
374            UUID4::new(),
375            UnixNanos::default(),
376            None,
377            None,
378        )
379    }
380
381    #[rstest]
382    fn test_new_holds_weak_reference_to_source_exchange() {
383        let (client, exchange) = setup_client_with_latency();
384
385        // The client must not co-own the exchange, otherwise the exchange owning the
386        // client closes an unbreakable cycle.
387        assert_eq!(Rc::strong_count(&exchange), 1);
388
389        let upgraded: Rc<RefCell<SimulatedExchange>> = client
390            .exchange
391            .upgrade()
392            .expect("exchange outlives the client here")
393            .into();
394
395        assert!(Rc::ptr_eq(&upgraded, &exchange));
396    }
397
398    #[rstest]
399    fn test_query_order_is_not_forwarded_to_exchange() {
400        let (client, exchange) = setup_client_with_latency();
401
402        // Hold an immutable exchange borrow across the call: if the client
403        // forwards, send()'s `exchange.borrow_mut()` panics here. This makes the
404        // test bite on a client-only revert rather than being masked by the
405        // exchange-side query guard.
406        let exchange_ref = exchange.borrow();
407        let result = client.query_order(query_order());
408
409        assert!(result.is_ok());
410        assert_eq!(exchange_ref.max_inflight_command_ts(), None);
411    }
412
413    #[rstest]
414    fn test_query_account_is_not_forwarded_to_exchange() {
415        let (client, exchange) = setup_client_with_latency();
416
417        // See test_query_order_is_not_forwarded_to_exchange: the held borrow
418        // makes a forwarding attempt panic before the exchange guard can mask it.
419        let exchange_ref = exchange.borrow();
420        let result = client.query_account(query_account());
421
422        assert!(result.is_ok());
423        assert_eq!(exchange_ref.max_inflight_command_ts(), None);
424    }
425}