1use 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#[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 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 #[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 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 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 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 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 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 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}