1#![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 #[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>>, 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 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 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("e.instrument_id) {
430 let instrument = {
431 let cache = self.cache.as_ref().borrow();
432 cache.instrument("e.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("e.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 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 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 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 for (instrument_id, leverage) in &self.leverages {
609 margin_account.set_leverage(*instrument_id, leverage.to_f64().unwrap());
610 }
611 }
612 }
613}
614
615#[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 exchange.add_instrument(instrument).unwrap();
761
762 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("e_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 exchange.add_instrument(instrument).unwrap();
793
794 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 exchange.add_instrument(instrument).unwrap();
825
826 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 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 exchange.add_instrument(instrument).unwrap();
859
860 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 exchange.process_bar(bar_bid);
885 exchange.process_bar(bar_ask);
886
887 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 exchange.add_instrument(instrument).unwrap();
907
908 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 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 exchange.add_instrument(instrument).unwrap();
960
961 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 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 assert_eq!(best_bid_price, None);
1005 let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
1006 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 exchange.add_instrument(instrument).unwrap();
1023
1024 let instrument_status = InstrumentStatus::new(
1025 crypto_perpetual_ethusdt.id,
1026 MarketStatusAction::Close, 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 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 exchange.adjust_account(Money::from("500 USD"));
1086
1087 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}