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