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 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 #[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>>, 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 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 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("e.instrument_id) {
429 let instrument = {
430 let cache = self.cache.as_ref().borrow();
431 cache.instrument("e.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("e.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 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 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 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 for (instrument_id, leverage) in &self.leverages {
608 margin_account.set_leverage(*instrument_id, leverage.to_f64().unwrap());
609 }
610 }
611 }
612}
613
614#[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 exchange.add_instrument(instrument).unwrap();
760
761 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("e_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 exchange.add_instrument(instrument).unwrap();
792
793 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 exchange.add_instrument(instrument).unwrap();
824
825 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 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 exchange.add_instrument(instrument).unwrap();
858
859 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 exchange.process_bar(bar_bid);
884 exchange.process_bar(bar_ask);
885
886 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 exchange.add_instrument(instrument).unwrap();
906
907 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 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 exchange.add_instrument(instrument).unwrap();
959
960 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 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 assert_eq!(best_bid_price, None);
1004 let best_ask_price = exchange.best_ask_price(crypto_perpetual_ethusdt.id);
1005 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 exchange.add_instrument(instrument).unwrap();
1022
1023 let instrument_status = InstrumentStatus::new(
1024 crypto_perpetual_ethusdt.id,
1025 MarketStatusAction::Close, 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 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 exchange.adjust_account(Money::from("500 USD"));
1085
1086 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}