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// Under development
17#![allow(dead_code)]
18#![allow(unused_variables)]
19
20//! Provides a `BacktestExecutionClient` implementation for backtesting.
21
22use std::{cell::RefCell, fmt::Debug, rc::Rc};
23
24use async_trait::async_trait;
25use nautilus_common::{
26    cache::Cache,
27    clock::Clock,
28    messages::execution::{
29        BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
30        SubmitOrder, SubmitOrderList, TradingCommand,
31    },
32};
33use nautilus_core::{SharedCell, UnixNanos, WeakCell};
34use nautilus_execution::client::{ExecutionClient, base::ExecutionClientCore};
35use nautilus_model::{
36    accounts::AccountAny,
37    enums::OmsType,
38    identifiers::{AccountId, ClientId, TraderId, Venue},
39    orders::Order,
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    exchange: WeakCell<SimulatedExchange>,
55    clock: Rc<RefCell<dyn Clock>>,
56    is_connected: bool,
57    routing: bool,
58    frozen_account: bool,
59}
60
61impl Debug for BacktestExecutionClient {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct(stringify!(BacktestExecutionClient))
64            .field("client_id", &self.core.client_id)
65            .field("routing", &self.routing)
66            .finish()
67    }
68}
69
70impl BacktestExecutionClient {
71    #[allow(clippy::too_many_arguments)]
72    pub fn new(
73        trader_id: TraderId,
74        account_id: AccountId,
75        exchange: Rc<RefCell<SimulatedExchange>>,
76        cache: Rc<RefCell<Cache>>,
77        clock: Rc<RefCell<dyn Clock>>,
78        routing: Option<bool>,
79        frozen_account: Option<bool>,
80    ) -> Self {
81        let routing = routing.unwrap_or(false);
82        let frozen_account = frozen_account.unwrap_or(false);
83        let exchange_shared: SharedCell<SimulatedExchange> = SharedCell::from(exchange.clone());
84        let exchange_id = exchange_shared.borrow().id;
85        let core_client = ExecutionClientCore::new(
86            trader_id,
87            ClientId::from(exchange_id.as_str()),
88            Venue::from(exchange_id.as_str()),
89            exchange.borrow().oms_type,
90            account_id,
91            exchange.borrow().account_type,
92            exchange.borrow().base_currency,
93            clock.clone(),
94            cache,
95        );
96
97        if !frozen_account {
98            // TODO Register calculated account
99        }
100
101        Self {
102            exchange: exchange_shared.downgrade(),
103            clock,
104            core: core_client,
105            is_connected: false,
106            routing,
107            frozen_account,
108        }
109    }
110}
111
112#[async_trait(?Send)]
113impl ExecutionClient for BacktestExecutionClient {
114    fn is_connected(&self) -> bool {
115        self.is_connected
116    }
117
118    fn client_id(&self) -> ClientId {
119        self.core.client_id
120    }
121
122    fn account_id(&self) -> AccountId {
123        self.core.account_id
124    }
125
126    fn venue(&self) -> Venue {
127        self.core.venue
128    }
129
130    fn oms_type(&self) -> OmsType {
131        self.core.oms_type
132    }
133
134    fn get_account(&self) -> Option<AccountAny> {
135        self.core.get_account()
136    }
137
138    fn generate_account_state(
139        &self,
140        balances: Vec<AccountBalance>,
141        margins: Vec<MarginBalance>,
142        reported: bool,
143        ts_event: UnixNanos,
144    ) -> anyhow::Result<()> {
145        self.core
146            .generate_account_state(balances, margins, reported, ts_event)
147    }
148
149    fn start(&mut self) -> anyhow::Result<()> {
150        self.is_connected = true;
151        log::info!("Backtest execution client started");
152        Ok(())
153    }
154
155    fn stop(&mut self) -> anyhow::Result<()> {
156        self.is_connected = false;
157        log::info!("Backtest execution client stopped");
158        Ok(())
159    }
160
161    fn submit_order(&self, cmd: &SubmitOrder) -> anyhow::Result<()> {
162        self.core.generate_order_submitted(
163            cmd.strategy_id,
164            cmd.instrument_id,
165            cmd.client_order_id(),
166            self.clock.borrow().timestamp_ns(),
167        );
168
169        if let Some(exchange) = self.exchange.upgrade() {
170            exchange
171                .borrow_mut()
172                .send(TradingCommand::SubmitOrder(cmd.clone()));
173        } else {
174            log::error!("submit_order: SimulatedExchange has been dropped");
175        }
176        Ok(())
177    }
178
179    fn submit_order_list(&self, cmd: &SubmitOrderList) -> anyhow::Result<()> {
180        for order in &cmd.order_list.orders {
181            self.core.generate_order_submitted(
182                cmd.strategy_id,
183                order.instrument_id(),
184                order.client_order_id(),
185                self.clock.borrow().timestamp_ns(),
186            );
187        }
188
189        if let Some(exchange) = self.exchange.upgrade() {
190            exchange
191                .borrow_mut()
192                .send(TradingCommand::SubmitOrderList(cmd.clone()));
193        } else {
194            log::error!("submit_order_list: SimulatedExchange has been dropped");
195        }
196        Ok(())
197    }
198
199    fn modify_order(&self, cmd: &ModifyOrder) -> anyhow::Result<()> {
200        if let Some(exchange) = self.exchange.upgrade() {
201            exchange
202                .borrow_mut()
203                .send(TradingCommand::ModifyOrder(cmd.clone()));
204        } else {
205            log::error!("modify_order: SimulatedExchange has been dropped");
206        }
207        Ok(())
208    }
209
210    fn cancel_order(&self, cmd: &CancelOrder) -> anyhow::Result<()> {
211        if let Some(exchange) = self.exchange.upgrade() {
212            exchange
213                .borrow_mut()
214                .send(TradingCommand::CancelOrder(cmd.clone()));
215        } else {
216            log::error!("cancel_order: SimulatedExchange has been dropped");
217        }
218        Ok(())
219    }
220
221    fn cancel_all_orders(&self, cmd: &CancelAllOrders) -> anyhow::Result<()> {
222        if let Some(exchange) = self.exchange.upgrade() {
223            exchange
224                .borrow_mut()
225                .send(TradingCommand::CancelAllOrders(cmd.clone()));
226        } else {
227            log::error!("cancel_all_orders: SimulatedExchange has been dropped");
228        }
229        Ok(())
230    }
231
232    fn batch_cancel_orders(&self, cmd: &BatchCancelOrders) -> anyhow::Result<()> {
233        if let Some(exchange) = self.exchange.upgrade() {
234            exchange
235                .borrow_mut()
236                .send(TradingCommand::BatchCancelOrders(cmd.clone()));
237        } else {
238            log::error!("batch_cancel_orders: SimulatedExchange has been dropped");
239        }
240        Ok(())
241    }
242
243    fn query_account(&self, cmd: &QueryAccount) -> anyhow::Result<()> {
244        if let Some(exchange) = self.exchange.upgrade() {
245            exchange
246                .borrow_mut()
247                .send(TradingCommand::QueryAccount(cmd.clone()));
248        } else {
249            log::error!("query_account: SimulatedExchange has been dropped");
250        }
251        Ok(())
252    }
253
254    fn query_order(&self, cmd: &QueryOrder) -> anyhow::Result<()> {
255        if let Some(exchange) = self.exchange.upgrade() {
256            exchange
257                .borrow_mut()
258                .send(TradingCommand::QueryOrder(cmd.clone()));
259        } else {
260            log::error!("query_order: SimulatedExchange has been dropped");
261        }
262        Ok(())
263    }
264}