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