nautilus_backtest/
exchange.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 `SimulatedExchange` venue for backtesting on historical data.
17
18// Under development
19#![allow(dead_code)]
20#![allow(unused_variables)]
21
22use std::{cell::RefCell, collections::HashMap, rc::Rc};
23
24use nautilus_common::{cache::Cache, msgbus::MessageBus};
25use nautilus_core::{
26    correctness::{check_equal, FAILED},
27    AtomicTime, UnixNanos,
28};
29use nautilus_execution::{client::ExecutionClient, messages::TradingCommand};
30use nautilus_model::{
31    accounts::AccountAny,
32    data::{
33        Bar, Data, InstrumentStatus, OrderBookDelta, OrderBookDeltas, OrderBookDeltas_API,
34        QuoteTick, TradeTick,
35    },
36    enums::{AccountType, BookType, OmsType},
37    identifiers::{InstrumentId, Venue},
38    instruments::InstrumentAny,
39    orderbook::OrderBook,
40    orders::PassiveOrderAny,
41    types::{AccountBalance, Currency, Money, Price},
42};
43use rust_decimal::{prelude::ToPrimitive, Decimal};
44
45use crate::{
46    matching_engine::{config::OrderMatchingEngineConfig, OrderMatchingEngine},
47    models::{fee::FeeModelAny, fill::FillModel, latency::LatencyModel},
48    modules::SimulationModule,
49};
50
51pub struct SimulatedExchange {
52    id: Venue,
53    oms_type: OmsType,
54    account_type: AccountType,
55    starting_balances: Vec<Money>,
56    book_type: BookType,
57    default_leverage: Decimal,
58    exec_client: Option<ExecutionClient>,
59    fee_model: FeeModelAny,
60    fill_model: FillModel,
61    latency_model: LatencyModel,
62    instruments: HashMap<InstrumentId, InstrumentAny>,
63    matching_engines: HashMap<InstrumentId, OrderMatchingEngine>,
64    leverages: HashMap<InstrumentId, Decimal>,
65    modules: Vec<Box<dyn SimulationModule>>,
66    clock: &'static AtomicTime,
67    msgbus: Rc<RefCell<MessageBus>>,
68    cache: Rc<RefCell<Cache>>,
69    frozen_account: bool,
70    bar_execution: bool,
71    reject_stop_orders: bool,
72    support_gtd_orders: bool,
73    support_contingent_orders: bool,
74    use_position_ids: bool,
75    use_random_ids: bool,
76    use_reduce_only: bool,
77    use_message_queue: bool,
78}
79
80impl SimulatedExchange {
81    /// Creates a new [`SimulatedExchange`] instance.
82    #[allow(clippy::too_many_arguments)]
83    pub fn new(
84        venue: Venue,
85        oms_type: OmsType,
86        account_type: AccountType,
87        starting_balances: Vec<Money>,
88        base_currency: Option<Currency>,
89        default_leverage: Decimal,
90        leverages: HashMap<InstrumentId, Decimal>,
91        modules: Vec<Box<dyn SimulationModule>>,
92        msgbus: Rc<RefCell<MessageBus>>, // TODO add portfolio
93        cache: Rc<RefCell<Cache>>,
94        clock: &'static AtomicTime,
95        fill_model: FillModel,
96        fee_model: FeeModelAny,
97        latency_model: LatencyModel,
98        book_type: BookType,
99        frozen_account: Option<bool>,
100        bar_execution: Option<bool>,
101        reject_stop_orders: Option<bool>,
102        support_gtd_orders: Option<bool>,
103        support_contingent_orders: Option<bool>,
104        use_position_ids: Option<bool>,
105        use_random_ids: Option<bool>,
106        use_reduce_only: Option<bool>,
107        use_message_queue: Option<bool>,
108    ) -> anyhow::Result<Self> {
109        if starting_balances.is_empty() {
110            anyhow::bail!("Starting balances must be provided")
111        }
112        if base_currency.is_some() && starting_balances.len() > 1 {
113            anyhow::bail!("single-currency account has multiple starting currencies")
114        }
115        // TODO register and load modules
116        Ok(Self {
117            id: venue,
118            oms_type,
119            account_type,
120            starting_balances,
121            book_type,
122            default_leverage,
123            exec_client: None,
124            fee_model,
125            fill_model,
126            latency_model,
127            instruments: HashMap::new(),
128            matching_engines: HashMap::new(),
129            leverages,
130            modules,
131            clock,
132            msgbus,
133            cache,
134            frozen_account: frozen_account.unwrap_or(false),
135            bar_execution: bar_execution.unwrap_or(true),
136            reject_stop_orders: reject_stop_orders.unwrap_or(true),
137            support_gtd_orders: support_gtd_orders.unwrap_or(true),
138            support_contingent_orders: support_contingent_orders.unwrap_or(true),
139            use_position_ids: use_position_ids.unwrap_or(true),
140            use_random_ids: use_random_ids.unwrap_or(false),
141            use_reduce_only: use_reduce_only.unwrap_or(true),
142            use_message_queue: use_message_queue.unwrap_or(true),
143        })
144    }
145
146    pub fn register_client(&mut self, client: ExecutionClient) {
147        let client_id = client.client_id;
148        self.exec_client = Some(client);
149        log::info!("Registered ExecutionClient: {client_id}");
150    }
151
152    pub fn set_fill_model(&mut self, fill_model: FillModel) {
153        for matching_engine in self.matching_engines.values_mut() {
154            matching_engine.set_fill_model(fill_model.clone());
155            log::info!(
156                "Setting fill model for {} to {}",
157                matching_engine.venue,
158                self.fill_model
159            );
160        }
161        self.fill_model = fill_model;
162    }
163
164    pub fn set_latency_model(&mut self, latency_model: LatencyModel) {
165        self.latency_model = latency_model;
166        log::info!("Setting latency model to {}", self.latency_model);
167    }
168
169    pub fn initialize_account(&mut self) {
170        self.generate_fresh_account_state();
171    }
172
173    pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> {
174        check_equal(
175            instrument.id().venue,
176            self.id,
177            "Venue of instrument id",
178            "Venue of simulated exchange",
179        )
180        .expect(FAILED);
181
182        if self.account_type == AccountType::Cash
183            && (matches!(instrument, InstrumentAny::CryptoPerpetual(_))
184                || matches!(instrument, InstrumentAny::CryptoFuture(_)))
185        {
186            anyhow::bail!("Cash account cannot trade futures or perpetuals")
187        }
188
189        self.instruments.insert(instrument.id(), instrument.clone());
190
191        let matching_engine_config = OrderMatchingEngineConfig::new(
192            self.bar_execution,
193            self.reject_stop_orders,
194            self.support_gtd_orders,
195            self.support_contingent_orders,
196            self.use_position_ids,
197            self.use_random_ids,
198            self.use_reduce_only,
199        );
200        let instrument_id = instrument.id();
201        let matching_engine = OrderMatchingEngine::new(
202            instrument,
203            self.instruments.len() as u32,
204            self.fill_model.clone(),
205            self.fee_model.clone(),
206            self.book_type,
207            self.oms_type,
208            self.account_type,
209            self.clock,
210            Rc::clone(&self.msgbus),
211            Rc::clone(&self.cache),
212            matching_engine_config,
213        );
214        self.matching_engines.insert(instrument_id, matching_engine);
215
216        log::info!("Added instrument {instrument_id} and created matching engine");
217        Ok(())
218    }
219
220    #[must_use]
221    pub fn best_bid_price(&self, instrument_id: InstrumentId) -> Option<Price> {
222        self.matching_engines
223            .get(&instrument_id)
224            .and_then(OrderMatchingEngine::best_bid_price)
225    }
226
227    #[must_use]
228    pub fn best_ask_price(&self, instrument_id: InstrumentId) -> Option<Price> {
229        self.matching_engines
230            .get(&instrument_id)
231            .and_then(OrderMatchingEngine::best_ask_price)
232    }
233
234    pub fn get_book(&self, instrument_id: InstrumentId) -> Option<&OrderBook> {
235        self.matching_engines
236            .get(&instrument_id)
237            .map(OrderMatchingEngine::get_book)
238    }
239
240    #[must_use]
241    pub fn get_matching_engine(&self, instrument_id: InstrumentId) -> Option<&OrderMatchingEngine> {
242        self.matching_engines.get(&instrument_id)
243    }
244
245    #[must_use]
246    pub const fn get_matching_engines(&self) -> &HashMap<InstrumentId, OrderMatchingEngine> {
247        &self.matching_engines
248    }
249
250    #[must_use]
251    pub fn get_books(&self) -> HashMap<InstrumentId, OrderBook> {
252        let mut books = HashMap::new();
253        for (instrument_id, matching_engine) in &self.matching_engines {
254            books.insert(*instrument_id, matching_engine.get_book().clone());
255        }
256        books
257    }
258
259    #[must_use]
260    pub fn get_open_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<PassiveOrderAny> {
261        instrument_id
262            .and_then(|id| {
263                self.matching_engines
264                    .get(&id)
265                    .map(OrderMatchingEngine::get_open_orders)
266            })
267            .unwrap_or_else(|| {
268                self.matching_engines
269                    .values()
270                    .flat_map(OrderMatchingEngine::get_open_orders)
271                    .collect()
272            })
273    }
274
275    #[must_use]
276    pub fn get_open_bid_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<PassiveOrderAny> {
277        instrument_id
278            .and_then(|id| {
279                self.matching_engines
280                    .get(&id)
281                    .map(|engine| engine.get_open_bid_orders().to_vec())
282            })
283            .unwrap_or_else(|| {
284                self.matching_engines
285                    .values()
286                    .flat_map(|engine| engine.get_open_bid_orders().to_vec())
287                    .collect()
288            })
289    }
290
291    #[must_use]
292    pub fn get_open_ask_orders(&self, instrument_id: Option<InstrumentId>) -> Vec<PassiveOrderAny> {
293        instrument_id
294            .and_then(|id| {
295                self.matching_engines
296                    .get(&id)
297                    .map(|engine| engine.get_open_ask_orders().to_vec())
298            })
299            .unwrap_or_else(|| {
300                self.matching_engines
301                    .values()
302                    .flat_map(|engine| engine.get_open_ask_orders().to_vec())
303                    .collect()
304            })
305    }
306
307    #[must_use]
308    pub fn get_account(&self) -> Option<AccountAny> {
309        self.exec_client
310            .as_ref()
311            .map(|client| client.get_account().unwrap())
312    }
313
314    pub fn adjust_account(&mut self, adjustment: Money) {
315        if self.frozen_account {
316            // Nothing to adjust
317            return;
318        }
319
320        if let Some(exec_client) = &self.exec_client {
321            let venue = exec_client.venue;
322            println!("Adjusting account for venue {venue}");
323            if let Some(account) = self.cache.borrow().account_for_venue(&venue) {
324                match account.balance(Some(adjustment.currency)) {
325                    Some(balance) => {
326                        let mut current_balance = *balance;
327                        current_balance.total += adjustment;
328                        current_balance.free += adjustment;
329
330                        let margins = match account {
331                            AccountAny::Margin(margin_account) => margin_account.margins.clone(),
332                            _ => HashMap::new(),
333                        };
334
335                        if let Some(exec_client) = &self.exec_client {
336                            exec_client
337                                .generate_account_state(
338                                    vec![current_balance],
339                                    margins.values().copied().collect(),
340                                    true,
341                                    self.clock.get_time_ns(),
342                                )
343                                .unwrap();
344                        }
345                    }
346                    None => {
347                        log::error!(
348                            "Cannot adjust account: no balance for currency {}",
349                            adjustment.currency
350                        );
351                    }
352                }
353            } else {
354                log::error!("Cannot adjust account: no account for venue {venue}");
355            }
356        }
357    }
358
359    pub fn send(&self, _command: TradingCommand) {
360        todo!("send")
361    }
362
363    pub fn generate_inflight_command(&self, _command: TradingCommand) {
364        todo!("generate inflight command")
365    }
366
367    pub fn process_order_book_delta(&mut self, delta: OrderBookDelta) {
368        for module in &self.modules {
369            module.pre_process(Data::Delta(delta));
370        }
371
372        if !self.matching_engines.contains_key(&delta.instrument_id) {
373            let instrument = {
374                let cache = self.cache.as_ref().borrow();
375                cache.instrument(&delta.instrument_id).cloned()
376            };
377
378            if let Some(instrument) = instrument {
379                self.add_instrument(instrument).unwrap();
380            } else {
381                panic!(
382                    "No matching engine found for instrument {}",
383                    delta.instrument_id
384                );
385            }
386        }
387
388        if let Some(matching_engine) = self.matching_engines.get_mut(&delta.instrument_id) {
389            matching_engine.process_order_book_delta(&delta);
390        } else {
391            panic!("Matching engine should be initialized");
392        }
393    }
394
395    pub fn process_order_book_deltas(&mut self, deltas: OrderBookDeltas) {
396        for module in &self.modules {
397            module.pre_process(Data::Deltas(OrderBookDeltas_API::new(deltas.clone())));
398        }
399
400        if !self.matching_engines.contains_key(&deltas.instrument_id) {
401            let instrument = {
402                let cache = self.cache.as_ref().borrow();
403                cache.instrument(&deltas.instrument_id).cloned()
404            };
405
406            if let Some(instrument) = instrument {
407                self.add_instrument(instrument).unwrap();
408            } else {
409                panic!(
410                    "No matching engine found for instrument {}",
411                    deltas.instrument_id
412                );
413            }
414        }
415
416        if let Some(matching_engine) = self.matching_engines.get_mut(&deltas.instrument_id) {
417            matching_engine.process_order_book_deltas(&deltas);
418        } else {
419            panic!("Matching engine should be initialized");
420        }
421    }
422
423    pub fn process_quote_tick(&mut self, quote: &QuoteTick) {
424        for module in &self.modules {
425            module.pre_process(Data::Quote(quote.to_owned()));
426        }
427
428        if !self.matching_engines.contains_key(&quote.instrument_id) {
429            let instrument = {
430                let cache = self.cache.as_ref().borrow();
431                cache.instrument(&quote.instrument_id).cloned()
432            };
433
434            if let Some(instrument) = instrument {
435                self.add_instrument(instrument).unwrap();
436            } else {
437                panic!(
438                    "No matching engine found for instrument {}",
439                    quote.instrument_id
440                );
441            }
442        }
443
444        if let Some(matching_engine) = self.matching_engines.get_mut(&quote.instrument_id) {
445            matching_engine.process_quote_tick(quote);
446        } else {
447            panic!("Matching engine should be initialized");
448        }
449    }
450
451    pub fn process_trade_tick(&mut self, trade: &TradeTick) {
452        for module in &self.modules {
453            module.pre_process(Data::Trade(trade.to_owned()));
454        }
455
456        if !self.matching_engines.contains_key(&trade.instrument_id) {
457            let instrument = {
458                let cache = self.cache.as_ref().borrow();
459                cache.instrument(&trade.instrument_id).cloned()
460            };
461
462            if let Some(instrument) = instrument {
463                self.add_instrument(instrument).unwrap();
464            } else {
465                panic!(
466                    "No matching engine found for instrument {}",
467                    trade.instrument_id
468                );
469            }
470        }
471
472        if let Some(matching_engine) = self.matching_engines.get_mut(&trade.instrument_id) {
473            matching_engine.process_trade_tick(trade);
474        } else {
475            panic!("Matching engine should be initialized");
476        }
477    }
478
479    pub fn process_bar(&mut self, bar: Bar) {
480        for module in &self.modules {
481            module.pre_process(Data::Bar(bar));
482        }
483
484        if !self.matching_engines.contains_key(&bar.instrument_id()) {
485            let instrument = {
486                let cache = self.cache.as_ref().borrow();
487                cache.instrument(&bar.instrument_id()).cloned()
488            };
489
490            if let Some(instrument) = instrument {
491                self.add_instrument(instrument).unwrap();
492            } else {
493                panic!(
494                    "No matching engine found for instrument {}",
495                    bar.instrument_id()
496                );
497            }
498        }
499
500        if let Some(matching_engine) = self.matching_engines.get_mut(&bar.instrument_id()) {
501            matching_engine.process_bar(&bar);
502        } else {
503            panic!("Matching engine should be initialized");
504        }
505    }
506
507    pub fn process_instrument_status(&mut self, status: InstrumentStatus) {
508        // TODO add module preprocessing
509
510        if !self.matching_engines.contains_key(&status.instrument_id) {
511            let instrument = {
512                let cache = self.cache.as_ref().borrow();
513                cache.instrument(&status.instrument_id).cloned()
514            };
515
516            if let Some(instrument) = instrument {
517                self.add_instrument(instrument).unwrap();
518            } else {
519                panic!(
520                    "No matching engine found for instrument {}",
521                    status.instrument_id
522                );
523            }
524        }
525
526        if let Some(matching_engine) = self.matching_engines.get_mut(&status.instrument_id) {
527            matching_engine.process_status(status.action);
528        } else {
529            panic!("Matching engine should be initialized");
530        }
531    }
532
533    pub fn process(&mut self, _ts_now: UnixNanos) {
534        todo!("process")
535    }
536
537    pub fn reset(&mut self) {
538        for module in &self.modules {
539            module.reset();
540        }
541
542        self.generate_fresh_account_state();
543
544        for matching_engine in self.matching_engines.values_mut() {
545            matching_engine.reset();
546        }
547
548        // TODO Clear the inflight and message queues
549        log::info!("Resetting exchange state");
550    }
551
552    pub fn process_trading_command(&mut self, command: TradingCommand) {
553        if let Some(matching_engine) = self.matching_engines.get_mut(&command.instrument_id()) {
554            let account_id = if let Some(exec_client) = &self.exec_client {
555                exec_client.account_id
556            } else {
557                panic!("Execution client should be initialized");
558            };
559            match command {
560                TradingCommand::SubmitOrder(mut command) => {
561                    matching_engine.process_order(&mut command.order, account_id);
562                }
563                TradingCommand::ModifyOrder(ref command) => {
564                    matching_engine.process_modify(command, account_id);
565                }
566                TradingCommand::CancelOrder(ref command) => {
567                    matching_engine.process_cancel(command, account_id);
568                }
569                TradingCommand::CancelAllOrders(ref command) => {
570                    matching_engine.process_cancel_all(command, account_id);
571                }
572                TradingCommand::BatchCancelOrders(ref command) => {
573                    matching_engine.process_batch_cancel(command, account_id);
574                }
575                TradingCommand::QueryOrder(ref command) => {
576                    matching_engine.process_query_order(command, account_id);
577                }
578                TradingCommand::SubmitOrderList(mut command) => {
579                    for order in &mut command.order_list.orders {
580                        matching_engine.process_order(order, account_id);
581                    }
582                }
583            }
584        } else {
585            panic!("Matching engine should be initialized");
586        }
587    }
588
589    pub fn generate_fresh_account_state(&self) {
590        let balances: Vec<AccountBalance> = self
591            .starting_balances
592            .iter()
593            .map(|money| AccountBalance::new(*money, Money::zero(money.currency), *money))
594            .collect();
595
596        if let Some(exec_client) = &self.exec_client {
597            exec_client
598                .generate_account_state(balances, vec![], true, self.clock.get_time_ns())
599                .unwrap();
600        }
601
602        // Set leverages
603        if let Some(AccountAny::Margin(mut margin_account)) = self.get_account() {
604            margin_account.set_default_leverage(self.default_leverage.to_f64().unwrap());
605
606            // Set instrument specific leverages
607            for (instrument_id, leverage) in &self.leverages {
608                margin_account.set_leverage(*instrument_id, leverage.to_f64().unwrap());
609            }
610        }
611    }
612}
613
614////////////////////////////////////////////////////////////////////////////////
615// Tests
616////////////////////////////////////////////////////////////////////////////////
617#[cfg(test)]
618mod tests {
619    use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::LazyLock};
620
621    use nautilus_common::{
622        cache::Cache,
623        msgbus::{
624            stubs::{get_message_saving_handler, get_saved_messages},
625            MessageBus,
626        },
627    };
628    use nautilus_core::{AtomicTime, UnixNanos, UUID4};
629    use nautilus_execution::client::ExecutionClient;
630    use nautilus_model::{
631        accounts::{AccountAny, MarginAccount},
632        data::{
633            Bar, BarType, BookOrder, InstrumentStatus, OrderBookDelta, OrderBookDeltas, QuoteTick,
634            TradeTick,
635        },
636        enums::{
637            AccountType, AggressorSide, BookAction, BookType, MarketStatus, MarketStatusAction,
638            OmsType, OrderSide,
639        },
640        events::AccountState,
641        identifiers::{AccountId, ClientId, TradeId, TraderId, Venue},
642        instruments::{stubs::crypto_perpetual_ethusdt, CryptoPerpetual, InstrumentAny},
643        types::{AccountBalance, Currency, Money, Price, Quantity},
644    };
645    use rstest::rstest;
646    use ustr::Ustr;
647
648    use crate::{
649        exchange::SimulatedExchange,
650        models::{
651            fee::{FeeModelAny, MakerTakerFeeModel},
652            fill::FillModel,
653            latency::LatencyModel,
654        },
655    };
656
657    static ATOMIC_TIME: LazyLock<AtomicTime> =
658        LazyLock::new(|| AtomicTime::new(true, UnixNanos::default()));
659
660    fn get_exchange(
661        venue: Venue,
662        account_type: AccountType,
663        book_type: BookType,
664        msgbus: Option<Rc<RefCell<MessageBus>>>,
665        cache: Option<Rc<RefCell<Cache>>>,
666    ) -> SimulatedExchange {
667        let msgbus = msgbus.unwrap_or(Rc::new(RefCell::new(MessageBus::default())));
668        let cache = cache.unwrap_or(Rc::new(RefCell::new(Cache::default())));
669
670        let mut exchange = SimulatedExchange::new(
671            venue,
672            OmsType::Netting,
673            account_type,
674            vec![Money::new(1000.0, Currency::USD())],
675            None,
676            1.into(),
677            HashMap::new(),
678            vec![],
679            msgbus.clone(),
680            cache.clone(),
681            &ATOMIC_TIME,
682            FillModel::default(),
683            FeeModelAny::MakerTaker(MakerTakerFeeModel),
684            LatencyModel,
685            book_type,
686            None,
687            None,
688            None,
689            None,
690            None,
691            None,
692            None,
693            None,
694            None,
695        )
696        .unwrap();
697
698        let execution_client = ExecutionClient::new(
699            TraderId::default(),
700            ClientId::default(),
701            venue,
702            OmsType::Netting,
703            AccountId::default(),
704            account_type,
705            None,
706            &ATOMIC_TIME,
707            cache,
708            msgbus,
709        );
710        exchange.register_client(execution_client);
711
712        exchange
713    }
714
715    #[rstest]
716    #[should_panic(
717        expected = r#"Condition failed: 'Venue of instrument id' value of BINANCE was not equal to 'Venue of simulated exchange' value of SIM"#
718    )]
719    fn test_venue_mismatch_between_exchange_and_instrument(
720        crypto_perpetual_ethusdt: CryptoPerpetual,
721    ) {
722        let mut exchange: SimulatedExchange = get_exchange(
723            Venue::new("SIM"),
724            AccountType::Margin,
725            BookType::L1_MBP,
726            None,
727            None,
728        );
729        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
730        exchange.add_instrument(instrument).unwrap();
731    }
732
733    #[rstest]
734    #[should_panic(expected = "Cash account cannot trade futures or perpetuals")]
735    fn test_cash_account_trading_futures_or_perpetuals(crypto_perpetual_ethusdt: CryptoPerpetual) {
736        let mut exchange: SimulatedExchange = get_exchange(
737            Venue::new("BINANCE"),
738            AccountType::Cash,
739            BookType::L1_MBP,
740            None,
741            None,
742        );
743        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
744        exchange.add_instrument(instrument).unwrap();
745    }
746
747    #[rstest]
748    fn test_exchange_process_quote_tick(crypto_perpetual_ethusdt: CryptoPerpetual) {
749        let mut exchange: SimulatedExchange = get_exchange(
750            Venue::new("BINANCE"),
751            AccountType::Margin,
752            BookType::L1_MBP,
753            None,
754            None,
755        );
756        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
757
758        // register instrument
759        exchange.add_instrument(instrument).unwrap();
760
761        // process tick
762        let quote_tick = QuoteTick::new(
763            crypto_perpetual_ethusdt.id,
764            Price::from("1000"),
765            Price::from("1001"),
766            Quantity::from(1),
767            Quantity::from(1),
768            UnixNanos::default(),
769            UnixNanos::default(),
770        );
771        exchange.process_quote_tick(&quote_tick);
772
773        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
774        assert_eq!(best_bid_price, Some(Price::from("1000")));
775        let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
776        assert_eq!(best_ask_price, Some(Price::from("1001")));
777    }
778
779    #[rstest]
780    fn test_exchange_process_trade_tick(crypto_perpetual_ethusdt: CryptoPerpetual) {
781        let mut exchange: SimulatedExchange = get_exchange(
782            Venue::new("BINANCE"),
783            AccountType::Margin,
784            BookType::L1_MBP,
785            None,
786            None,
787        );
788        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
789
790        // register instrument
791        exchange.add_instrument(instrument).unwrap();
792
793        // process tick
794        let trade_tick = TradeTick::new(
795            crypto_perpetual_ethusdt.id,
796            Price::from("1000"),
797            Quantity::from(1),
798            AggressorSide::Buyer,
799            TradeId::from("1"),
800            UnixNanos::default(),
801            UnixNanos::default(),
802        );
803        exchange.process_trade_tick(&trade_tick);
804
805        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
806        assert_eq!(best_bid_price, Some(Price::from("1000")));
807        let best_ask = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
808        assert_eq!(best_ask, Some(Price::from("1000")));
809    }
810
811    #[rstest]
812    fn test_exchange_process_bar_last_bar_spec(crypto_perpetual_ethusdt: CryptoPerpetual) {
813        let mut exchange: SimulatedExchange = get_exchange(
814            Venue::new("BINANCE"),
815            AccountType::Margin,
816            BookType::L1_MBP,
817            None,
818            None,
819        );
820        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
821
822        // register instrument
823        exchange.add_instrument(instrument).unwrap();
824
825        // process bar
826        let bar = Bar::new(
827            BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
828            Price::from("1500.00"),
829            Price::from("1505.00"),
830            Price::from("1490.00"),
831            Price::from("1502.00"),
832            Quantity::from(100),
833            UnixNanos::default(),
834            UnixNanos::default(),
835        );
836        exchange.process_bar(bar);
837
838        // this will be processed as ticks so both bid and ask will be the same as close of the bar
839        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
840        assert_eq!(best_bid_price, Some(Price::from("1502.00")));
841        let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
842        assert_eq!(best_ask_price, Some(Price::from("1502.00")));
843    }
844
845    #[rstest]
846    fn test_exchange_process_bar_bid_ask_bar_spec(crypto_perpetual_ethusdt: CryptoPerpetual) {
847        let mut exchange: SimulatedExchange = get_exchange(
848            Venue::new("BINANCE"),
849            AccountType::Margin,
850            BookType::L1_MBP,
851            None,
852            None,
853        );
854        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
855
856        // register instrument
857        exchange.add_instrument(instrument).unwrap();
858
859        // create both bid and ask based bars
860        // add +1 on ask to make sure it is different from bid
861        let bar_bid = Bar::new(
862            BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-BID-EXTERNAL"),
863            Price::from("1500.00"),
864            Price::from("1505.00"),
865            Price::from("1490.00"),
866            Price::from("1502.00"),
867            Quantity::from(100),
868            UnixNanos::from(1),
869            UnixNanos::from(1),
870        );
871        let bar_ask = Bar::new(
872            BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-ASK-EXTERNAL"),
873            Price::from("1501.00"),
874            Price::from("1506.00"),
875            Price::from("1491.00"),
876            Price::from("1503.00"),
877            Quantity::from(100),
878            UnixNanos::from(1),
879            UnixNanos::from(1),
880        );
881
882        // process them
883        exchange.process_bar(bar_bid);
884        exchange.process_bar(bar_ask);
885
886        // current bid and ask prices will be the corresponding close of the ask and bid bar
887        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
888        assert_eq!(best_bid_price, Some(Price::from("1502.00")));
889        let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
890        assert_eq!(best_ask_price, Some(Price::from("1503.00")));
891    }
892
893    #[rstest]
894    fn test_exchange_process_orderbook_delta(crypto_perpetual_ethusdt: CryptoPerpetual) {
895        let mut exchange: SimulatedExchange = get_exchange(
896            Venue::new("BINANCE"),
897            AccountType::Margin,
898            BookType::L2_MBP,
899            None,
900            None,
901        );
902        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
903
904        // register instrument
905        exchange.add_instrument(instrument).unwrap();
906
907        // create order book delta at both bid and ask with incremented ts init and sequence
908        let delta_buy = OrderBookDelta::new(
909            crypto_perpetual_ethusdt.id,
910            BookAction::Add,
911            BookOrder::new(OrderSide::Buy, Price::from("1000.00"), Quantity::from(1), 1),
912            0,
913            0,
914            UnixNanos::from(1),
915            UnixNanos::from(1),
916        );
917        let delta_sell = OrderBookDelta::new(
918            crypto_perpetual_ethusdt.id,
919            BookAction::Add,
920            BookOrder::new(
921                OrderSide::Sell,
922                Price::from("1001.00"),
923                Quantity::from(1),
924                1,
925            ),
926            0,
927            1,
928            UnixNanos::from(2),
929            UnixNanos::from(2),
930        );
931
932        // process both deltas
933        exchange.process_order_book_delta(delta_buy);
934        exchange.process_order_book_delta(delta_sell);
935
936        let book = exchange.get_book(crypto_perpetual_ethusdt.id).unwrap();
937        assert_eq!(book.count, 2);
938        assert_eq!(book.sequence, 1);
939        assert_eq!(book.ts_last, UnixNanos::from(2));
940        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
941        assert_eq!(best_bid_price, Some(Price::from("1000.00")));
942        let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
943        assert_eq!(best_ask_price, Some(Price::from("1001.00")));
944    }
945
946    #[rstest]
947    fn test_exchange_process_orderbook_deltas(crypto_perpetual_ethusdt: CryptoPerpetual) {
948        let mut exchange: SimulatedExchange = get_exchange(
949            Venue::new("BINANCE"),
950            AccountType::Margin,
951            BookType::L2_MBP,
952            None,
953            None,
954        );
955        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
956
957        // register instrument
958        exchange.add_instrument(instrument).unwrap();
959
960        // create two sell order book deltas with same timestamps and higher sequence
961        let delta_sell_1 = OrderBookDelta::new(
962            crypto_perpetual_ethusdt.id,
963            BookAction::Add,
964            BookOrder::new(
965                OrderSide::Sell,
966                Price::from("1000.00"),
967                Quantity::from(3),
968                1,
969            ),
970            0,
971            0,
972            UnixNanos::from(1),
973            UnixNanos::from(1),
974        );
975        let delta_sell_2 = OrderBookDelta::new(
976            crypto_perpetual_ethusdt.id,
977            BookAction::Add,
978            BookOrder::new(
979                OrderSide::Sell,
980                Price::from("1001.00"),
981                Quantity::from(1),
982                1,
983            ),
984            0,
985            1,
986            UnixNanos::from(1),
987            UnixNanos::from(1),
988        );
989        let orderbook_deltas = OrderBookDeltas::new(
990            crypto_perpetual_ethusdt.id,
991            vec![delta_sell_1, delta_sell_2],
992        );
993
994        // process both deltas
995        exchange.process_order_book_deltas(orderbook_deltas);
996
997        let book = exchange.get_book(crypto_perpetual_ethusdt.id).unwrap();
998        assert_eq!(book.count, 2);
999        assert_eq!(book.sequence, 1);
1000        assert_eq!(book.ts_last, UnixNanos::from(1));
1001        let best_bid_price = exchange.best_bid_price(crypto_perpetual_ethusdt.id);
1002        // no bid orders in orderbook deltas
1003        assert_eq!(best_bid_price, None);
1004        let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
1005        // best ask price is the first order in orderbook deltas
1006        assert_eq!(best_ask_price, Some(Price::from("1000.00")));
1007    }
1008
1009    #[rstest]
1010    fn test_exchange_process_instrument_status(crypto_perpetual_ethusdt: CryptoPerpetual) {
1011        let mut exchange: SimulatedExchange = get_exchange(
1012            Venue::new("BINANCE"),
1013            AccountType::Margin,
1014            BookType::L2_MBP,
1015            None,
1016            None,
1017        );
1018        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt);
1019
1020        // register instrument
1021        exchange.add_instrument(instrument).unwrap();
1022
1023        let instrument_status = InstrumentStatus::new(
1024            crypto_perpetual_ethusdt.id,
1025            MarketStatusAction::Close, // close the market
1026            UnixNanos::from(1),
1027            UnixNanos::from(1),
1028            None,
1029            None,
1030            None,
1031            None,
1032            None,
1033        );
1034
1035        exchange.process_instrument_status(instrument_status);
1036
1037        let matching_engine = exchange
1038            .get_matching_engine(crypto_perpetual_ethusdt.id)
1039            .unwrap();
1040        assert_eq!(matching_engine.market_status, MarketStatus::Closed);
1041    }
1042
1043    #[rstest]
1044    fn test_accounting() {
1045        let account_type = AccountType::Margin;
1046        let mut msgbus = MessageBus::default();
1047        let mut cache = Cache::default();
1048        let handler = get_message_saving_handler::<AccountState>(None);
1049        msgbus.register(Ustr::from("Portfolio.update_account"), handler.clone());
1050        let margin_account = MarginAccount::new(
1051            AccountState::new(
1052                AccountId::from("SIM-001"),
1053                account_type,
1054                vec![AccountBalance::new(
1055                    Money::from("1000 USD"),
1056                    Money::from("0 USD"),
1057                    Money::from("1000 USD"),
1058                )],
1059                vec![],
1060                false,
1061                UUID4::default(),
1062                UnixNanos::default(),
1063                UnixNanos::default(),
1064                None,
1065            ),
1066            false,
1067        );
1068        let () = cache
1069            .add_account(AccountAny::Margin(margin_account))
1070            .unwrap();
1071        // build indexes
1072        cache.build_index();
1073
1074        let mut exchange = get_exchange(
1075            Venue::new("SIM"),
1076            account_type,
1077            BookType::L2_MBP,
1078            Some(Rc::new(RefCell::new(msgbus))),
1079            Some(Rc::new(RefCell::new(cache))),
1080        );
1081        exchange.initialize_account();
1082
1083        // Test adjust account, increase balance by 500 USD
1084        exchange.adjust_account(Money::from("500 USD"));
1085
1086        // Check if we received two messages, one for initial account state and one for adjusted account state
1087        let messages = get_saved_messages::<AccountState>(handler);
1088        assert_eq!(messages.len(), 2);
1089        let account_state_first = messages.first().unwrap();
1090        let account_state_second = messages.last().unwrap();
1091
1092        assert_eq!(account_state_first.balances.len(), 1);
1093        let current_balance = account_state_first.balances[0];
1094        assert_eq!(current_balance.free, Money::new(1000.0, Currency::USD()));
1095        assert_eq!(current_balance.locked, Money::new(0.0, Currency::USD()));
1096        assert_eq!(current_balance.total, Money::new(1000.0, Currency::USD()));
1097
1098        assert_eq!(account_state_second.balances.len(), 1);
1099        let current_balance = account_state_second.balances[0];
1100        assert_eq!(current_balance.free, Money::new(1500.0, Currency::USD()));
1101        assert_eq!(current_balance.locked, Money::new(0.0, Currency::USD()));
1102        assert_eq!(current_balance.total, Money::new(1500.0, Currency::USD()));
1103    }
1104}