1use std::{cell::RefCell, collections::HashMap, rc::Rc};
19
20use config::RiskEngineConfig;
21use nautilus_common::{
22 cache::Cache,
23 clock::Clock,
24 logging::{CMD, EVT, RECV},
25 msgbus::MessageBus,
26 throttler::Throttler,
27};
28use nautilus_core::UUID4;
29use nautilus_execution::messages::{ModifyOrder, SubmitOrder, SubmitOrderList, TradingCommand};
30use nautilus_model::{
31 accounts::{Account, AccountAny},
32 enums::{InstrumentClass, OrderSide, OrderStatus, TradingState},
33 events::{OrderDenied, OrderEventAny, OrderModifyRejected},
34 identifiers::InstrumentId,
35 instruments::InstrumentAny,
36 orders::{OrderAny, OrderList},
37 types::{Currency, Money, Price, Quantity},
38};
39use nautilus_portfolio::Portfolio;
40use rust_decimal::{Decimal, prelude::ToPrimitive};
41use ustr::Ustr;
42
43pub mod config;
44
45type SubmitOrderFn = Box<dyn Fn(SubmitOrder)>;
46type ModifyOrderFn = Box<dyn Fn(ModifyOrder)>;
47
48pub struct RiskEngine {
49 clock: Rc<RefCell<dyn Clock>>,
50 cache: Rc<RefCell<Cache>>,
51 msgbus: Rc<RefCell<MessageBus>>,
52 portfolio: Portfolio,
53 pub throttled_submit_order: Throttler<SubmitOrder, SubmitOrderFn>,
54 pub throttled_modify_order: Throttler<ModifyOrder, ModifyOrderFn>,
55 max_notional_per_order: HashMap<InstrumentId, Decimal>,
56 trading_state: TradingState,
57 config: RiskEngineConfig,
58}
59
60impl RiskEngine {
61 pub fn new(
62 config: RiskEngineConfig,
63 portfolio: Portfolio,
64 clock: Rc<RefCell<dyn Clock>>,
65 cache: Rc<RefCell<Cache>>,
66 msgbus: Rc<RefCell<MessageBus>>,
67 ) -> Self {
68 let throttled_submit_order = Self::create_submit_order_throttler(
69 &config,
70 clock.clone(),
71 cache.clone(),
72 msgbus.clone(),
73 );
74
75 let throttled_modify_order = Self::create_modify_order_throttler(
76 &config,
77 clock.clone(),
78 cache.clone(),
79 msgbus.clone(),
80 );
81
82 Self {
83 clock,
84 cache,
85 msgbus,
86 portfolio,
87 throttled_submit_order,
88 throttled_modify_order,
89 max_notional_per_order: HashMap::new(),
90 trading_state: TradingState::Active,
91 config,
92 }
93 }
94
95 fn create_submit_order_throttler(
96 config: &RiskEngineConfig,
97 clock: Rc<RefCell<dyn Clock>>,
98 cache: Rc<RefCell<Cache>>,
99 msgbus: Rc<RefCell<MessageBus>>,
100 ) -> Throttler<SubmitOrder, SubmitOrderFn> {
101 let success_handler = {
102 let msgbus = msgbus.clone();
103 Box::new(move |submit_order: SubmitOrder| {
104 msgbus.borrow_mut().send(
105 &Ustr::from("ExecEngine.execute"),
106 &TradingCommand::SubmitOrder(submit_order),
107 );
108 }) as Box<dyn Fn(SubmitOrder)>
109 };
110
111 let failure_handler = {
112 let msgbus = msgbus;
113 let cache = cache;
114 let clock = clock.clone();
115 Box::new(move |submit_order: SubmitOrder| {
116 let reason = "REJECTED BY THROTTLER";
117 log::warn!(
118 "SubmitOrder for {} DENIED: {}",
119 submit_order.client_order_id,
120 reason
121 );
122
123 Self::handle_submit_order_cache(&cache, &submit_order);
124
125 let denied = Self::create_order_denied(&submit_order, reason, &clock);
126
127 msgbus
128 .borrow_mut()
129 .send(&Ustr::from("ExecEngine.process"), &denied);
130 }) as Box<dyn Fn(SubmitOrder)>
131 };
132
133 Throttler::new(
134 config.max_order_submit.clone(),
135 clock,
136 "ORDER_SUBMIT_THROTTLER".to_string(),
137 success_handler,
138 Some(failure_handler),
139 )
140 }
141
142 fn create_modify_order_throttler(
143 config: &RiskEngineConfig,
144 clock: Rc<RefCell<dyn Clock>>,
145 cache: Rc<RefCell<Cache>>,
146 msgbus: Rc<RefCell<MessageBus>>,
147 ) -> Throttler<ModifyOrder, ModifyOrderFn> {
148 let success_handler = {
149 let msgbus = msgbus.clone();
150 Box::new(move |order: ModifyOrder| {
151 msgbus.borrow_mut().send(
152 &Ustr::from("ExecEngine.execute"),
153 &TradingCommand::ModifyOrder(order),
154 );
155 }) as Box<dyn Fn(ModifyOrder)>
156 };
157
158 let failure_handler = {
159 let msgbus = msgbus;
160 let cache = cache;
161 let clock = clock.clone();
162 Box::new(move |order: ModifyOrder| {
163 let reason = "Exceeded MAX_ORDER_MODIFY_RATE";
164 log::warn!(
165 "SubmitOrder for {} DENIED: {}",
166 order.client_order_id,
167 reason
168 );
169
170 let order = match Self::get_existing_order(&cache, &order) {
171 Some(order) => order,
172 None => return,
173 };
174
175 let rejected = Self::create_modify_rejected(&order, reason, &clock);
176
177 msgbus
178 .borrow_mut()
179 .send(&Ustr::from("ExecEngine.process"), &rejected);
180 }) as Box<dyn Fn(ModifyOrder)>
181 };
182
183 Throttler::new(
184 config.max_order_modify.clone(),
185 clock,
186 "ORDER_MODIFY_THROTTLER".to_string(),
187 success_handler,
188 Some(failure_handler),
189 )
190 }
191
192 fn handle_submit_order_cache(cache: &Rc<RefCell<Cache>>, submit_order: &SubmitOrder) {
193 let mut cache = cache.borrow_mut();
194 if !cache.order_exists(&submit_order.client_order_id) {
195 cache
196 .add_order(submit_order.order.clone(), None, None, false)
197 .map_err(|e| {
198 log::error!("Cannot add order to cache: {e}");
199 })
200 .unwrap();
201 }
202 }
203
204 fn get_existing_order(cache: &Rc<RefCell<Cache>>, order: &ModifyOrder) -> Option<OrderAny> {
205 let cache = cache.borrow();
206 if let Some(order) = cache.order(&order.client_order_id) {
207 Some(order.clone())
208 } else {
209 log::error!(
210 "Order with command.client_order_id: {} not found",
211 order.client_order_id
212 );
213 None
214 }
215 }
216
217 fn create_order_denied(
218 submit_order: &SubmitOrder,
219 reason: &str,
220 clock: &Rc<RefCell<dyn Clock>>,
221 ) -> OrderEventAny {
222 let timestamp = clock.borrow().timestamp_ns();
223 OrderEventAny::Denied(OrderDenied::new(
224 submit_order.trader_id,
225 submit_order.strategy_id,
226 submit_order.instrument_id,
227 submit_order.client_order_id,
228 reason.into(),
229 UUID4::new(),
230 timestamp,
231 timestamp,
232 ))
233 }
234
235 fn create_modify_rejected(
236 order: &OrderAny,
237 reason: &str,
238 clock: &Rc<RefCell<dyn Clock>>,
239 ) -> OrderEventAny {
240 let timestamp = clock.borrow().timestamp_ns();
241 OrderEventAny::ModifyRejected(OrderModifyRejected::new(
242 order.trader_id(),
243 order.strategy_id(),
244 order.instrument_id(),
245 order.client_order_id(),
246 reason.into(),
247 UUID4::new(),
248 timestamp,
249 timestamp,
250 false,
251 order.venue_order_id(),
252 None,
253 ))
254 }
255
256 pub fn execute(&mut self, command: TradingCommand) {
259 self.handle_command(command);
261 }
262
263 pub fn process(&mut self, event: OrderEventAny) {
264 self.handle_event(event);
266 }
267
268 pub fn set_trading_state(&mut self, state: TradingState) {
269 if state == self.trading_state {
270 log::warn!("No change to trading state: already set to {state:?}");
271 return;
272 }
273
274 self.trading_state = state;
275
276 let _ts_now = self.clock.borrow().timestamp_ns();
277
278 self.msgbus
282 .borrow_mut()
283 .publish(&Ustr::from("events.risk"), &"message"); log::info!("Trading state set to {state:?}");
286 }
287
288 pub fn set_max_notional_per_order(&mut self, instrument_id: InstrumentId, new_value: Decimal) {
289 self.max_notional_per_order.insert(instrument_id, new_value);
290
291 let new_value_str = new_value.to_string();
292 log::info!("Set MAX_NOTIONAL_PER_ORDER: {instrument_id} {new_value_str}");
293 }
294
295 fn handle_command(&mut self, command: TradingCommand) {
299 if self.config.debug {
300 log::debug!("{}{} {:?}", CMD, RECV, command);
301 }
302
303 match command {
304 TradingCommand::SubmitOrder(submit_order) => self.handle_submit_order(submit_order),
305 TradingCommand::SubmitOrderList(submit_order_list) => {
306 self.handle_submit_order_list(submit_order_list);
307 }
308 TradingCommand::ModifyOrder(modify_order) => self.handle_modify_order(modify_order),
309 _ => {
310 log::error!("Cannot handle command: {command}");
311 }
312 }
313 }
314
315 fn handle_submit_order(&self, command: SubmitOrder) {
316 if self.config.bypass {
317 self.send_to_execution(TradingCommand::SubmitOrder(command));
318 return;
319 }
320
321 let order = &command.order;
322 if let Some(position_id) = command.position_id {
323 if order.is_reduce_only() {
324 let position_exists = {
325 let cache = self.cache.borrow();
326 cache
327 .position(&position_id)
328 .map(|pos| (pos.side, pos.quantity))
329 };
330
331 if let Some((pos_side, pos_quantity)) = position_exists {
332 if !order.would_reduce_only(pos_side, pos_quantity) {
333 self.deny_command(
334 TradingCommand::SubmitOrder(command),
335 &format!("Reduce only order would increase position {position_id}"),
336 );
337 return; }
339 } else {
340 self.deny_command(
341 TradingCommand::SubmitOrder(command),
342 &format!("Position {position_id} not found for reduce-only order"),
343 );
344 return;
345 }
346 }
347 }
348
349 let instrument_exists = {
350 let cache = self.cache.borrow();
351 cache.instrument(&order.instrument_id()).cloned()
352 };
353
354 let instrument = if let Some(instrument) = instrument_exists {
355 instrument
356 } else {
357 self.deny_command(
358 TradingCommand::SubmitOrder(command.clone()),
359 &format!("Instrument for {} not found", command.instrument_id),
360 );
361 return; };
363
364 if !self.check_order(instrument.clone(), order.clone()) {
368 return; }
370
371 if !self.check_orders_risk(instrument.clone(), Vec::from([order.clone()])) {
372 return; }
374
375 self.execution_gateway(instrument, TradingCommand::SubmitOrder(command.clone()));
376 }
377
378 fn handle_submit_order_list(&self, command: SubmitOrderList) {
379 if self.config.bypass {
380 self.send_to_execution(TradingCommand::SubmitOrderList(command));
381 return;
382 }
383
384 let instrument_exists = {
385 let cache = self.cache.borrow();
386 cache.instrument(&command.instrument_id).cloned()
387 };
388
389 let instrument = if let Some(instrument) = instrument_exists {
390 instrument
391 } else {
392 self.deny_command(
393 TradingCommand::SubmitOrderList(command.clone()),
394 &format!("no instrument found for {}", command.instrument_id),
395 );
396 return; };
398
399 for order in command.order_list.orders.clone() {
403 if !self.check_order(instrument.clone(), order) {
404 return; }
406 }
407
408 if !self.check_orders_risk(instrument.clone(), command.order_list.clone().orders) {
409 self.deny_order_list(
410 command.order_list.clone(),
411 &format!("OrderList {} DENIED", command.order_list.id),
412 );
413 return; }
415
416 self.execution_gateway(instrument, TradingCommand::SubmitOrderList(command));
417 }
418
419 fn handle_modify_order(&self, command: ModifyOrder) {
420 let order_exists = {
424 let cache = self.cache.borrow();
425 cache.order(&command.client_order_id).cloned()
426 };
427
428 let order = if let Some(order) = order_exists {
429 order
430 } else {
431 log::error!(
432 "ModifyOrder DENIED: Order with command.client_order_id: {} not found",
433 command.client_order_id
434 );
435 return;
436 };
437
438 if order.is_closed() {
439 self.reject_modify_order(
440 order,
441 &format!(
442 "Order with command.client_order_id: {} already closed",
443 command.client_order_id
444 ),
445 );
446 return;
447 } else if order.status() == OrderStatus::PendingCancel {
448 self.reject_modify_order(
449 order,
450 &format!(
451 "Order with command.client_order_id: {} is already pending cancel",
452 command.client_order_id
453 ),
454 );
455 return;
456 }
457
458 let maybe_instrument = {
460 let cache = self.cache.borrow();
461 cache.instrument(&command.instrument_id).cloned()
462 };
463
464 let instrument = if let Some(instrument) = maybe_instrument {
465 instrument
466 } else {
467 self.reject_modify_order(
468 order,
469 &format!("no instrument found for {}", command.instrument_id),
470 );
471 return; };
473
474 let mut risk_msg = self.check_price(&instrument, command.price);
476 if let Some(risk_msg) = risk_msg {
477 self.reject_modify_order(order, &risk_msg);
478 return; }
480
481 risk_msg = self.check_price(&instrument, command.trigger_price);
483 if let Some(risk_msg) = risk_msg {
484 self.reject_modify_order(order, &risk_msg);
485 return; }
487
488 risk_msg = self.check_quantity(&instrument, command.quantity);
490 if let Some(risk_msg) = risk_msg {
491 self.reject_modify_order(order, &risk_msg);
492 return; }
494
495 match self.trading_state {
497 TradingState::Halted => {
498 self.reject_modify_order(order, "TradingState is HALTED: Cannot modify order");
499 return; }
501 TradingState::Reducing => {
502 if let Some(quantity) = command.quantity {
503 if quantity > order.quantity()
504 && ((order.is_buy() && self.portfolio.is_net_long(&instrument.id()))
505 || (order.is_sell() && self.portfolio.is_net_short(&instrument.id())))
506 {
507 self.reject_modify_order(
508 order,
509 &format!(
510 "TradingState is REDUCING and update will increase exposure {}",
511 instrument.id()
512 ),
513 );
514 return; }
516 }
517 }
518 _ => {}
519 }
520
521 self.throttled_modify_order.send(command);
522 }
523
524 fn check_order(&self, instrument: InstrumentAny, order: OrderAny) -> bool {
527 if !self.check_order_price(instrument.clone(), order.clone())
531 || !self.check_order_quantity(instrument, order)
532 {
533 return false; }
535
536 true
537 }
538
539 fn check_order_price(&self, instrument: InstrumentAny, order: OrderAny) -> bool {
540 if order.price().is_some() {
544 let risk_msg = self.check_price(&instrument, order.price());
545 if let Some(risk_msg) = risk_msg {
546 self.deny_order(order, &risk_msg);
547 return false; }
549 }
550
551 if order.trigger_price().is_some() {
555 let risk_msg = self.check_price(&instrument, order.trigger_price());
556 if let Some(risk_msg) = risk_msg {
557 self.deny_order(order, &risk_msg);
558 return false; }
560 }
561
562 true
563 }
564
565 fn check_order_quantity(&self, instrument: InstrumentAny, order: OrderAny) -> bool {
566 let risk_msg = self.check_quantity(&instrument, Some(order.quantity()));
567 if let Some(risk_msg) = risk_msg {
568 self.deny_order(order, &risk_msg);
569 return false; }
571
572 true
573 }
574
575 fn check_orders_risk(&self, instrument: InstrumentAny, orders: Vec<OrderAny>) -> bool {
576 let mut last_px: Option<Price> = None;
580 let mut max_notional: Option<Money> = None;
581
582 let max_notional_setting = self.max_notional_per_order.get(&instrument.id());
584 if let Some(max_notional_setting_val) = max_notional_setting.copied() {
585 max_notional = Some(Money::new(
586 max_notional_setting_val
587 .to_f64()
588 .expect("Invalid decimal conversion"),
589 instrument.quote_currency(),
590 ));
591 }
592
593 let account_exists = {
595 let cache = self.cache.borrow();
596 cache.account_for_venue(&instrument.id().venue).cloned()
597 };
598
599 let account = if let Some(account) = account_exists {
600 account
601 } else {
602 log::debug!("Cannot find account for venue {}", instrument.id().venue);
603 return true; };
605 let cash_account = match account {
606 AccountAny::Cash(cash_account) => cash_account,
607 AccountAny::Margin(_) => return true, };
609 let free = cash_account.balance_free(Some(instrument.quote_currency()));
610 if self.config.debug {
611 log::debug!("Free cash: {:?}", free);
612 }
613
614 let mut cum_notional_buy: Option<Money> = None;
615 let mut cum_notional_sell: Option<Money> = None;
616 let mut base_currency: Option<Currency> = None;
617 for order in &orders {
618 last_px = match order {
620 OrderAny::Market(_) | OrderAny::MarketToLimit(_) => {
621 if last_px.is_none() {
622 let cache = self.cache.borrow();
623 if let Some(last_quote) = cache.quote(&instrument.id()) {
624 match order.order_side() {
625 OrderSide::Buy => Some(last_quote.ask_price),
626 OrderSide::Sell => Some(last_quote.bid_price),
627 _ => panic!("Invalid order side"),
628 }
629 } else {
630 let cache = self.cache.borrow();
631 let last_trade = cache.trade(&instrument.id());
632
633 if let Some(last_trade) = last_trade {
634 Some(last_trade.price)
635 } else {
636 log::warn!(
637 "Cannot check MARKET order risk: no prices for {}",
638 instrument.id()
639 );
640 continue;
641 }
642 }
643 } else {
644 last_px
645 }
646 }
647 OrderAny::StopMarket(_) | OrderAny::MarketIfTouched(_) => order.trigger_price(),
648 OrderAny::TrailingStopMarket(_) | OrderAny::TrailingStopLimit(_) => {
649 if let Some(trigger_price) = order.trigger_price() {
650 Some(trigger_price)
651 } else {
652 log::warn!(
653 "Cannot check {} order risk: no trigger price was set", order.order_type()
655 );
656 continue;
657 }
658 }
659 _ => order.price(),
660 };
661
662 let last_px = if let Some(px) = last_px {
663 px
664 } else {
665 log::error!("Cannot check order risk: no price available");
666 continue;
667 };
668
669 let notional =
670 instrument.calculate_notional_value(order.quantity(), last_px, Some(true));
671
672 if self.config.debug {
673 log::debug!("Notional: {:?}", notional);
674 }
675
676 if let Some(max_notional_value) = max_notional {
678 if notional > max_notional_value {
679 self.deny_order(
680 order.clone(),
681 &format!(
682 "NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional={max_notional_value:?}, notional={notional:?}"
683 ),
684 );
685 return false; }
687 }
688
689 if let Some(min_notional) = instrument.min_notional() {
691 if notional.currency == min_notional.currency && notional < min_notional {
692 self.deny_order(
693 order.clone(),
694 &format!(
695 "NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT: min_notional={min_notional:?}, notional={notional:?}"
696 ),
697 );
698 return false; }
700 }
701
702 if let Some(max_notional) = instrument.max_notional() {
704 if notional.currency == max_notional.currency && notional > max_notional {
705 self.deny_order(
706 order.clone(),
707 &format!(
708 "NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT: max_notional={max_notional:?}, notional={notional:?}"
709 ),
710 );
711 return false; }
713 }
714
715 let notional = instrument.calculate_notional_value(order.quantity(), last_px, None);
717 let order_balance_impact = match order.order_side() {
718 OrderSide::Buy => Money::from_raw(-notional.raw, notional.currency),
719 OrderSide::Sell => Money::from_raw(notional.raw, notional.currency),
720 OrderSide::NoOrderSide => {
721 panic!("invalid `OrderSide`, was {}", order.order_side());
722 }
723 };
724
725 if self.config.debug {
726 log::debug!("Balance impact: {}", order_balance_impact);
727 }
728
729 if let Some(free_val) = free {
730 if (free_val.as_decimal() + order_balance_impact.as_decimal()) < Decimal::ZERO {
731 self.deny_order(
732 order.clone(),
733 &format!(
734 "NOTIONAL_EXCEEDS_FREE_BALANCE: free={free_val:?}, notional={notional:?}"
735 ),
736 );
737 return false;
738 }
739 }
740
741 if base_currency.is_none() {
742 base_currency = instrument.base_currency();
743 }
744 if order.is_buy() {
745 match cum_notional_buy.as_mut() {
746 Some(cum_notional_buy_val) => {
747 cum_notional_buy_val.raw += -order_balance_impact.raw;
748 }
749 None => {
750 cum_notional_buy = Some(Money::from_raw(
751 -order_balance_impact.raw,
752 order_balance_impact.currency,
753 ));
754 }
755 }
756
757 if self.config.debug {
758 log::debug!("Cumulative notional BUY: {:?}", cum_notional_buy);
759 }
760
761 if let (Some(free), Some(cum_notional_buy)) = (free, cum_notional_buy) {
762 if cum_notional_buy > free {
763 self.deny_order(order.clone(), &format!("CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_buy}"));
764 return false; }
766 }
767 } else if order.is_sell() {
768 if cash_account.base_currency.is_some() {
769 match cum_notional_sell.as_mut() {
770 Some(cum_notional_buy_val) => {
771 cum_notional_buy_val.raw += order_balance_impact.raw;
772 }
773 None => {
774 cum_notional_sell = Some(Money::from_raw(
775 order_balance_impact.raw,
776 order_balance_impact.currency,
777 ));
778 }
779 }
780 if self.config.debug {
781 log::debug!("Cumulative notional SELL: {:?}", cum_notional_sell);
782 }
783
784 if let (Some(free), Some(cum_notional_sell)) = (free, cum_notional_sell) {
785 if cum_notional_sell > free {
786 self.deny_order(order.clone(), &format!("CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_sell}"));
787 return false; }
789 }
790 }
791 else if let Some(base_currency) = base_currency {
793 let cash_value = Money::from_raw(
794 order
795 .quantity()
796 .raw
797 .try_into()
798 .map_err(|e| log::error!("Unable to convert Quantity to f64: {}", e))
799 .unwrap(),
800 base_currency,
801 );
802
803 if self.config.debug {
804 log::debug!("Cash value: {:?}", cash_value);
805 log::debug!(
806 "Total: {:?}",
807 cash_account.balance_total(Some(base_currency))
808 );
809 log::debug!(
810 "Locked: {:?}",
811 cash_account.balance_locked(Some(base_currency))
812 );
813 log::debug!("Free: {:?}", cash_account.balance_free(Some(base_currency)));
814 }
815
816 match cum_notional_sell {
817 Some(mut cum_notional_sell) => {
818 cum_notional_sell.raw += cash_value.raw;
819 }
820 None => cum_notional_sell = Some(cash_value),
821 }
822
823 if self.config.debug {
824 log::debug!("Cumulative notional SELL: {:?}", cum_notional_sell);
825 }
826 if let (Some(free), Some(cum_notional_sell)) = (free, cum_notional_sell) {
827 if cum_notional_sell.raw > free.raw {
828 self.deny_order(order.clone(), &format!("CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_sell}"));
829 return false; }
831 }
832 }
833 }
834 }
835
836 true }
839
840 fn check_price(&self, instrument: &InstrumentAny, price: Option<Price>) -> Option<String> {
841 let price_val = price?;
842
843 if price_val.precision > instrument.price_precision() {
844 return Some(format!(
845 "price {} invalid (precision {} > {})",
846 price_val,
847 price_val.precision,
848 instrument.price_precision()
849 ));
850 }
851
852 if instrument.instrument_class() != InstrumentClass::Option && price_val.raw <= 0 {
853 return Some(format!("price {price_val} invalid (<= 0)"));
854 }
855
856 None
857 }
858
859 fn check_quantity(
860 &self,
861 instrument: &InstrumentAny,
862 quantity: Option<Quantity>,
863 ) -> Option<String> {
864 let quantity_val = quantity?;
865
866 if quantity_val.precision > instrument.size_precision() {
868 return Some(format!(
869 "quantity {} invalid (precision {} > {})",
870 quantity_val,
871 quantity_val.precision,
872 instrument.size_precision()
873 ));
874 }
875
876 if let Some(max_quantity) = instrument.max_quantity() {
878 if quantity_val > max_quantity {
879 return Some(format!(
880 "quantity {quantity_val} invalid (> maximum trade size of {max_quantity})"
881 ));
882 }
883 }
884
885 if let Some(min_quantity) = instrument.min_quantity() {
887 if quantity_val < min_quantity {
888 return Some(format!(
889 "quantity {quantity_val} invalid (< minimum trade size of {min_quantity})"
890 ));
891 }
892 }
893
894 None
895 }
896
897 fn deny_command(&self, command: TradingCommand, reason: &str) {
900 match command {
901 TradingCommand::SubmitOrder(submit_order) => {
902 self.deny_order(submit_order.order, reason);
903 }
904 TradingCommand::SubmitOrderList(submit_order_list) => {
905 self.deny_order_list(submit_order_list.order_list, reason);
906 }
907 _ => {
908 panic!("Cannot deny command {command}");
909 }
910 }
911 }
912
913 fn deny_order(&self, order: OrderAny, reason: &str) {
914 log::warn!(
915 "SubmitOrder for {} DENIED: {}",
916 order.client_order_id(),
917 reason
918 );
919
920 if order.status() != OrderStatus::Initialized {
921 return;
922 }
923
924 let mut cache = self.cache.borrow_mut();
925 if !cache.order_exists(&order.client_order_id()) {
926 cache
927 .add_order(order.clone(), None, None, false)
928 .map_err(|e| {
929 log::error!("Cannot add order to cache: {e}");
930 })
931 .unwrap();
932 }
933
934 let denied = OrderEventAny::Denied(OrderDenied::new(
935 order.trader_id(),
936 order.strategy_id(),
937 order.instrument_id(),
938 order.client_order_id(),
939 reason.into(),
940 UUID4::new(),
941 self.clock.borrow().timestamp_ns(),
942 self.clock.borrow().timestamp_ns(),
943 ));
944
945 self.msgbus
946 .borrow_mut()
947 .send(&Ustr::from("ExecEngine.process"), &denied);
948 }
949
950 fn deny_order_list(&self, order_list: OrderList, reason: &str) {
951 for order in order_list.orders {
952 if !order.is_closed() {
953 self.deny_order(order, reason);
954 }
955 }
956 }
957
958 fn reject_modify_order(&self, order: OrderAny, reason: &str) {
959 let ts_event = self.clock.borrow().timestamp_ns();
960 let denied = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
961 order.trader_id(),
962 order.strategy_id(),
963 order.instrument_id(),
964 order.client_order_id(),
965 reason.into(),
966 UUID4::new(),
967 ts_event,
968 ts_event,
969 false,
970 order.venue_order_id(),
971 order.account_id(),
972 ));
973
974 self.msgbus
975 .borrow_mut()
976 .send(&Ustr::from("ExecEngine.process"), &denied);
977 }
978
979 fn execution_gateway(&self, instrument: InstrumentAny, command: TradingCommand) {
982 match self.trading_state {
983 TradingState::Halted => match command {
984 TradingCommand::SubmitOrder(submit_order) => {
985 self.deny_order(submit_order.order, "TradingState::HALTED");
986 }
987 TradingCommand::SubmitOrderList(submit_order_list) => {
988 self.deny_order_list(submit_order_list.order_list, "TradingState::HALTED");
989 }
990 _ => {}
991 },
992 TradingState::Reducing => match command {
993 TradingCommand::SubmitOrder(submit_order) => {
994 let order = submit_order.order;
995 if order.is_buy() && self.portfolio.is_net_long(&instrument.id()) {
996 self.deny_order(
997 order,
998 &format!(
999 "BUY when TradingState::REDUCING and LONG {}",
1000 instrument.id()
1001 ),
1002 );
1003 } else if order.is_sell() && self.portfolio.is_net_short(&instrument.id()) {
1004 self.deny_order(
1005 order,
1006 &format!(
1007 "SELL when TradingState::REDUCING and SHORT {}",
1008 instrument.id()
1009 ),
1010 );
1011 return;
1012 }
1013 }
1014 TradingCommand::SubmitOrderList(submit_order_list) => {
1015 let order_list = submit_order_list.order_list;
1016 for order in &order_list.orders {
1017 if order.is_buy() && self.portfolio.is_net_long(&instrument.id()) {
1018 self.deny_order_list(
1019 order_list,
1020 &format!(
1021 "BUY when TradingState::REDUCING and LONG {}",
1022 instrument.id()
1023 ),
1024 );
1025 return;
1026 } else if order.is_sell() && self.portfolio.is_net_short(&instrument.id()) {
1027 self.deny_order_list(
1028 order_list,
1029 &format!(
1030 "SELL when TradingState::REDUCING and SHORT {}",
1031 instrument.id()
1032 ),
1033 );
1034 return;
1035 }
1036 }
1037 }
1038 _ => {}
1039 },
1040 TradingState::Active => match command {
1041 TradingCommand::SubmitOrder(submit_order) => {
1042 self.throttled_submit_order.send(submit_order);
1043 }
1044 TradingCommand::SubmitOrderList(_submit_order_list) => {
1045 todo!("NOT IMPLEMENTED");
1046 }
1047 _ => {}
1048 },
1049 }
1050 }
1051
1052 fn send_to_execution(&self, command: TradingCommand) {
1053 self.msgbus
1054 .borrow_mut()
1055 .send(&Ustr::from("ExecEngine.execute"), &command);
1056 }
1057
1058 fn handle_event(&mut self, event: OrderEventAny) {
1059 if self.config.debug {
1062 log::debug!("{}{} {event:?}", RECV, EVT);
1063 }
1064 }
1065}
1066
1067#[cfg(test)]
1071mod tests {
1072 use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
1073
1074 use nautilus_common::{
1075 cache::Cache,
1076 clock::TestClock,
1077 msgbus::{
1078 MessageBus,
1079 handler::ShareableMessageHandler,
1080 stubs::{get_message_saving_handler, get_saved_messages},
1081 },
1082 throttler::RateLimit,
1083 };
1084 use nautilus_core::{UUID4, UnixNanos};
1085 use nautilus_execution::{
1086 engine::{ExecutionEngine, config::ExecutionEngineConfig},
1087 messages::{ModifyOrder, SubmitOrder, SubmitOrderList, TradingCommand},
1088 };
1089 use nautilus_model::{
1090 accounts::{
1091 AccountAny,
1092 stubs::{cash_account, margin_account},
1093 },
1094 data::{QuoteTick, stubs::quote_audusd},
1095 enums::{AccountType, LiquiditySide, OrderSide, OrderType, TradingState},
1096 events::{
1097 AccountState, OrderAccepted, OrderDenied, OrderEventAny, OrderEventType, OrderFilled,
1098 OrderSubmitted, account::stubs::cash_account_state_million_usd,
1099 },
1100 identifiers::{
1101 AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId,
1102 Symbol, TradeId, TraderId, VenueOrderId,
1103 stubs::{
1104 account_id, client_id_binance, client_order_id, strategy_id_ema_cross, trader_id,
1105 uuid4, venue_order_id,
1106 },
1107 },
1108 instruments::{
1109 CryptoPerpetual, CurrencyPair, InstrumentAny,
1110 stubs::{audusd_sim, crypto_perpetual_ethusdt, xbtusd_bitmex},
1111 },
1112 orders::{OrderAny, OrderList, OrderTestBuilder},
1113 types::{AccountBalance, Currency, Money, Price, Quantity, fixed::FIXED_PRECISION},
1114 };
1115 use nautilus_portfolio::Portfolio;
1116 use rstest::{fixture, rstest};
1117 use rust_decimal::{Decimal, prelude::FromPrimitive};
1118 use ustr::Ustr;
1119
1120 use super::{RiskEngine, config::RiskEngineConfig};
1121
1122 #[fixture]
1123 fn msgbus() -> MessageBus {
1124 MessageBus::default()
1125 }
1126
1127 #[fixture]
1128 fn process_order_event_handler() -> ShareableMessageHandler {
1129 get_message_saving_handler::<OrderEventAny>(Some(Ustr::from("ExecEngine.process")))
1130 }
1131
1132 #[fixture]
1133 fn execute_order_event_handler() -> ShareableMessageHandler {
1134 get_message_saving_handler::<TradingCommand>(Some(Ustr::from("ExecEngine.execute")))
1135 }
1136
1137 #[fixture]
1138 fn simple_cache() -> Cache {
1139 Cache::new(None, None)
1140 }
1141
1142 #[fixture]
1143 fn clock() -> TestClock {
1144 TestClock::new()
1145 }
1146
1147 #[fixture]
1148 fn max_order_submit() -> RateLimit {
1149 RateLimit::new(10, 1)
1150 }
1151
1152 #[fixture]
1153 fn max_order_modify() -> RateLimit {
1154 RateLimit::new(5, 1)
1155 }
1156
1157 #[fixture]
1158 fn max_notional_per_order() -> HashMap<InstrumentId, Decimal> {
1159 HashMap::new()
1160 }
1161
1162 #[fixture]
1164 fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
1165 OrderTestBuilder::new(OrderType::Market)
1166 .instrument_id(instrument_eth_usdt.id())
1167 .side(OrderSide::Buy)
1168 .quantity(Quantity::from("1"))
1169 .build()
1170 }
1171
1172 #[fixture]
1174 fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
1175 OrderTestBuilder::new(OrderType::Market)
1176 .instrument_id(instrument_eth_usdt.id())
1177 .side(OrderSide::Sell)
1178 .quantity(Quantity::from("1"))
1179 .build()
1180 }
1181
1182 #[fixture]
1183 fn get_stub_submit_order(
1184 trader_id: TraderId,
1185 client_id_binance: ClientId,
1186 strategy_id_ema_cross: StrategyId,
1187 client_order_id: ClientOrderId,
1188 venue_order_id: VenueOrderId,
1189 instrument_eth_usdt: InstrumentAny,
1190 ) -> SubmitOrder {
1191 SubmitOrder::new(
1192 trader_id,
1193 client_id_binance,
1194 strategy_id_ema_cross,
1195 instrument_eth_usdt.id(),
1196 client_order_id,
1197 venue_order_id,
1198 market_order_buy(instrument_eth_usdt),
1199 None,
1200 None,
1201 UUID4::new(),
1202 UnixNanos::from(10),
1203 )
1204 .unwrap()
1205 }
1206
1207 #[fixture]
1208 fn config_fixture(
1209 max_order_submit: RateLimit,
1210 max_order_modify: RateLimit,
1211 max_notional_per_order: HashMap<InstrumentId, Decimal>,
1212 ) -> RiskEngineConfig {
1213 RiskEngineConfig {
1214 debug: true,
1215 bypass: false,
1216 max_order_submit,
1217 max_order_modify,
1218 max_notional_per_order,
1219 }
1220 }
1221
1222 #[fixture]
1223 pub fn bitmex_cash_account_state_multi() -> AccountState {
1224 let btc_account_balance = AccountBalance::new(
1225 Money::from("10 BTC"),
1226 Money::from("0 BTC"),
1227 Money::from("10 BTC"),
1228 );
1229 let eth_account_balance = AccountBalance::new(
1230 Money::from("20 ETH"),
1231 Money::from("0 ETH"),
1232 Money::from("20 ETH"),
1233 );
1234 AccountState::new(
1235 AccountId::from("BITMEX-001"),
1236 AccountType::Cash,
1237 vec![btc_account_balance, eth_account_balance],
1238 vec![],
1239 true,
1240 uuid4(),
1241 0.into(),
1242 0.into(),
1243 None, )
1245 }
1246
1247 fn get_process_order_event_handler_messages(
1248 event_handler: ShareableMessageHandler,
1249 ) -> Vec<OrderEventAny> {
1250 get_saved_messages::<OrderEventAny>(event_handler)
1251 }
1252
1253 fn get_execute_order_event_handler_messages(
1254 event_handler: ShareableMessageHandler,
1255 ) -> Vec<TradingCommand> {
1256 get_saved_messages::<TradingCommand>(event_handler)
1257 }
1258
1259 #[fixture]
1260 fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
1261 InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
1262 }
1263
1264 #[fixture]
1265 fn instrument_xbtusd_bitmex(xbtusd_bitmex: CryptoPerpetual) -> InstrumentAny {
1266 InstrumentAny::CryptoPerpetual(xbtusd_bitmex)
1267 }
1268
1269 #[fixture]
1270 fn instrument_audusd(audusd_sim: CurrencyPair) -> InstrumentAny {
1271 InstrumentAny::CurrencyPair(audusd_sim)
1272 }
1273
1274 #[fixture]
1275 pub fn instrument_xbtusd_with_high_size_precision() -> InstrumentAny {
1276 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
1277 InstrumentId::from("BTCUSDT.BITMEX"),
1278 Symbol::from("XBTUSD"),
1279 Currency::BTC(),
1280 Currency::USD(),
1281 Currency::BTC(),
1282 true,
1283 1,
1284 2,
1285 Price::from("0.5"),
1286 Quantity::from("0.01"),
1287 None,
1288 None,
1289 None,
1290 None,
1291 Some(Money::from("10000000 USD")),
1292 Some(Money::from("1 USD")),
1293 Some(Price::from("10000000")),
1294 Some(Price::from("0.01")),
1295 Some(Decimal::from_str("0.01").unwrap()),
1296 Some(Decimal::from_str("0.0035").unwrap()),
1297 Some(Decimal::from_str("-0.00025").unwrap()),
1298 Some(Decimal::from_str("0.00075").unwrap()),
1299 UnixNanos::default(),
1300 UnixNanos::default(),
1301 ))
1302 }
1303
1304 fn get_risk_engine(
1306 msgbus: Rc<RefCell<MessageBus>>,
1307 cache: Option<Rc<RefCell<Cache>>>,
1308 config: Option<RiskEngineConfig>,
1309 clock: Option<Rc<RefCell<TestClock>>>,
1310 bypass: bool,
1311 ) -> RiskEngine {
1312 let cache = cache.unwrap_or(Rc::new(RefCell::new(Cache::default())));
1313 let config = config.unwrap_or(RiskEngineConfig {
1314 debug: true,
1315 bypass,
1316 max_order_submit: RateLimit::new(10, 1000),
1317 max_order_modify: RateLimit::new(5, 1000),
1318 max_notional_per_order: HashMap::new(),
1319 });
1320 let clock = clock.unwrap_or(Rc::new(RefCell::new(TestClock::new())));
1321 let portfolio = Portfolio::new(msgbus.clone(), cache.clone(), clock.clone(), None);
1322 RiskEngine::new(config, portfolio, clock, cache, msgbus)
1323 }
1324
1325 fn get_exec_engine(
1326 msgbus: Rc<RefCell<MessageBus>>,
1327 cache: Option<Rc<RefCell<Cache>>>,
1328 clock: Option<Rc<RefCell<TestClock>>>,
1329 config: Option<ExecutionEngineConfig>,
1330 ) -> ExecutionEngine {
1331 let cache = cache.unwrap_or(Rc::new(RefCell::new(Cache::default())));
1332 let clock = clock.unwrap_or(Rc::new(RefCell::new(TestClock::new())));
1333 ExecutionEngine::new(clock, cache, msgbus, config)
1334 }
1335
1336 fn order_submitted(order: &OrderAny) -> OrderSubmitted {
1337 OrderSubmitted::new(
1338 order.trader_id(),
1339 order.strategy_id(),
1340 order.instrument_id(),
1341 order.client_order_id(),
1342 order.account_id().unwrap_or(account_id()),
1343 UUID4::new(),
1344 0.into(),
1345 0.into(),
1346 )
1347 }
1348
1349 fn order_accepted(order: &OrderAny, venue_order_id: Option<VenueOrderId>) -> OrderAccepted {
1350 OrderAccepted::new(
1351 order.trader_id(),
1352 order.strategy_id(),
1353 order.instrument_id(),
1354 order.client_order_id(),
1355 venue_order_id.unwrap_or_default(),
1356 order.account_id().unwrap_or_default(),
1357 UUID4::new(),
1358 0.into(),
1359 0.into(),
1360 false,
1361 )
1362 }
1363
1364 fn order_filled(
1365 order: &OrderAny,
1366 instrument: &InstrumentAny,
1367 strategy_id: Option<StrategyId>,
1368 account_id: Option<AccountId>,
1369 venue_order_id: Option<VenueOrderId>,
1370 trade_id: Option<TradeId>,
1371 last_qty: Option<Quantity>,
1372 last_px: Option<Price>,
1373 liquidity_side: Option<LiquiditySide>,
1374 account: Option<AccountAny>,
1375 ts_filled_ns: Option<UnixNanos>,
1376 ) -> OrderFilled {
1377 let strategy_id = strategy_id.unwrap_or(order.strategy_id());
1378 let account_id = account_id.unwrap_or(order.account_id().unwrap_or_default());
1379 let venue_order_id = venue_order_id.unwrap_or(order.venue_order_id().unwrap_or_default());
1380 let trade_id =
1381 trade_id.unwrap_or(order.client_order_id().as_str().replace('O', "E").into());
1382 let last_qty = last_qty.unwrap_or(order.quantity());
1383 let last_px = last_px.unwrap_or(order.price().unwrap_or_default());
1384 let liquidity_side = liquidity_side.unwrap_or(LiquiditySide::Taker);
1385 let ts_filled_ns = ts_filled_ns.unwrap_or(0.into());
1386 let account = account.unwrap_or(AccountAny::Cash(cash_account(
1387 cash_account_state_million_usd("1000000 USD", "0 USD", "1000000 USD"),
1388 )));
1389
1390 let commission = account
1391 .calculate_commission(
1392 instrument.clone(),
1393 order.quantity(),
1394 last_px,
1395 liquidity_side,
1396 None,
1397 )
1398 .unwrap();
1399
1400 OrderFilled::new(
1401 trader_id(),
1402 strategy_id,
1403 instrument.id(),
1404 order.client_order_id(),
1405 venue_order_id,
1406 account_id,
1407 trade_id,
1408 order.order_side(),
1409 order.order_type(),
1410 last_qty,
1411 last_px,
1412 instrument.quote_currency(),
1413 liquidity_side,
1414 UUID4::new(),
1415 ts_filled_ns,
1416 0.into(),
1417 false,
1418 None,
1419 Some(commission),
1420 )
1421 }
1422
1423 #[rstest]
1425 fn test_bypass_config_risk_engine(msgbus: MessageBus) {
1426 let risk_engine = get_risk_engine(
1427 Rc::new(RefCell::new(msgbus)),
1428 None,
1429 None,
1430 None,
1431 true, );
1433
1434 assert!(risk_engine.config.bypass);
1435 }
1436
1437 #[rstest]
1438 fn test_trading_state_after_instantiation_returns_active(msgbus: MessageBus) {
1439 let risk_engine = get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1440
1441 assert_eq!(risk_engine.trading_state, TradingState::Active);
1442 }
1443
1444 #[rstest]
1445 fn test_set_trading_state_when_no_change_logs_warning(msgbus: MessageBus) {
1446 let mut risk_engine =
1447 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1448
1449 risk_engine.set_trading_state(TradingState::Active);
1450
1451 assert_eq!(risk_engine.trading_state, TradingState::Active);
1452 }
1453
1454 #[rstest]
1455 fn test_set_trading_state_changes_value_and_publishes_event(msgbus: MessageBus) {
1456 let mut risk_engine =
1457 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1458
1459 risk_engine.set_trading_state(TradingState::Halted);
1460
1461 assert_eq!(risk_engine.trading_state, TradingState::Halted);
1462 }
1463
1464 #[rstest]
1465 fn test_max_order_submit_rate_when_no_risk_config_returns_10_per_second(msgbus: MessageBus) {
1466 let risk_engine = get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1467
1468 assert_eq!(risk_engine.config.max_order_submit.limit, 10);
1469 assert_eq!(risk_engine.config.max_order_submit.interval_ns, 1000);
1470 }
1471
1472 #[rstest]
1473 fn test_max_order_modify_rate_when_no_risk_config_returns_5_per_second(msgbus: MessageBus) {
1474 let risk_engine = get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1475
1476 assert_eq!(risk_engine.config.max_order_modify.limit, 5);
1477 assert_eq!(risk_engine.config.max_order_modify.interval_ns, 1000);
1478 }
1479
1480 #[rstest]
1481 fn test_max_notionals_per_order_when_no_risk_config_returns_empty_hashmap(msgbus: MessageBus) {
1482 let risk_engine = get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1483
1484 assert_eq!(risk_engine.max_notional_per_order, HashMap::new());
1485 }
1486
1487 #[rstest]
1488 fn test_set_max_notional_per_order_changes_setting(
1489 msgbus: MessageBus,
1490 instrument_audusd: InstrumentAny,
1491 ) {
1492 let mut risk_engine =
1493 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1494
1495 risk_engine
1496 .set_max_notional_per_order(instrument_audusd.id(), Decimal::from_i64(100000).unwrap());
1497
1498 let mut expected = HashMap::new();
1499 expected.insert(instrument_audusd.id(), Decimal::from_i64(100000).unwrap());
1500 assert_eq!(risk_engine.max_notional_per_order, expected);
1501 }
1502
1503 #[rstest]
1504 fn test_given_random_command_then_logs_and_continues(
1505 msgbus: MessageBus,
1506 strategy_id_ema_cross: StrategyId,
1507 client_id_binance: ClientId,
1508 trader_id: TraderId,
1509 client_order_id: ClientOrderId,
1510 instrument_audusd: InstrumentAny,
1511 venue_order_id: VenueOrderId,
1512 ) {
1513 let mut risk_engine =
1514 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1515
1516 let order = OrderTestBuilder::new(OrderType::Limit)
1517 .instrument_id(instrument_audusd.id())
1518 .side(OrderSide::Buy)
1519 .price(Price::from_raw(100, 0))
1520 .quantity(Quantity::from("1000"))
1521 .build();
1522
1523 let submit_order = SubmitOrder::new(
1524 trader_id,
1525 client_id_binance,
1526 strategy_id_ema_cross,
1527 instrument_audusd.id(),
1528 client_order_id,
1529 venue_order_id,
1530 order,
1531 None,
1532 None,
1533 UUID4::new(),
1534 risk_engine.clock.borrow().timestamp_ns(),
1535 )
1536 .unwrap();
1537
1538 let random_command = TradingCommand::SubmitOrder(submit_order);
1539
1540 risk_engine.execute(random_command);
1541 }
1542
1543 #[rstest]
1544 fn test_given_random_event_then_logs_and_continues(
1545 msgbus: MessageBus,
1546 instrument_audusd: InstrumentAny,
1547 ) {
1548 let mut risk_engine =
1549 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, false);
1550
1551 let order = OrderTestBuilder::new(OrderType::Limit)
1552 .instrument_id(instrument_audusd.id())
1553 .side(OrderSide::Buy)
1554 .price(Price::from_raw(100, 0))
1555 .quantity(Quantity::from("1000"))
1556 .build();
1557
1558 let random_event = OrderEventAny::Denied(OrderDenied::new(
1559 order.trader_id(),
1560 order.strategy_id(),
1561 order.instrument_id(),
1562 order.client_order_id(),
1563 Ustr::from("DENIED"),
1564 UUID4::new(),
1565 risk_engine.clock.borrow().timestamp_ns(),
1566 risk_engine.clock.borrow().timestamp_ns(),
1567 ));
1568
1569 risk_engine.process(random_event);
1570 }
1571
1572 #[rstest]
1574 fn test_submit_order_with_default_settings_then_sends_to_client(
1575 mut msgbus: MessageBus,
1576 strategy_id_ema_cross: StrategyId,
1577 client_id_binance: ClientId,
1578 trader_id: TraderId,
1579 client_order_id: ClientOrderId,
1580 instrument_audusd: InstrumentAny,
1581 venue_order_id: VenueOrderId,
1582 process_order_event_handler: ShareableMessageHandler,
1583 execute_order_event_handler: ShareableMessageHandler,
1584 cash_account_state_million_usd: AccountState,
1585 quote_audusd: QuoteTick,
1586 mut simple_cache: Cache,
1587 ) {
1588 msgbus.register(
1589 msgbus.switchboard.exec_engine_process,
1590 process_order_event_handler,
1591 );
1592
1593 msgbus.register(
1594 msgbus.switchboard.exec_engine_execute,
1595 execute_order_event_handler.clone(),
1596 );
1597
1598 simple_cache
1599 .add_account(AccountAny::Cash(cash_account(
1600 cash_account_state_million_usd,
1601 )))
1602 .unwrap();
1603
1604 simple_cache
1605 .add_instrument(instrument_audusd.clone())
1606 .unwrap();
1607
1608 simple_cache.add_quote(quote_audusd).unwrap();
1609
1610 let mut risk_engine = get_risk_engine(
1611 Rc::new(RefCell::new(msgbus)),
1612 Some(Rc::new(RefCell::new(simple_cache))),
1613 None,
1614 None,
1615 false,
1616 );
1617 let order = OrderTestBuilder::new(OrderType::Limit)
1618 .instrument_id(instrument_audusd.id())
1619 .side(OrderSide::Buy)
1620 .price(Price::from_raw(100, 0))
1621 .quantity(Quantity::from("1000"))
1622 .build();
1623
1624 let submit_order = SubmitOrder::new(
1625 trader_id,
1626 client_id_binance,
1627 strategy_id_ema_cross,
1628 instrument_audusd.id(),
1629 client_order_id,
1630 venue_order_id,
1631 order,
1632 None,
1633 None,
1634 UUID4::new(),
1635 risk_engine.clock.borrow().timestamp_ns(),
1636 )
1637 .unwrap();
1638
1639 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
1640 let saved_execute_messages =
1641 get_execute_order_event_handler_messages(execute_order_event_handler);
1642 assert_eq!(saved_execute_messages.len(), 1);
1643 assert_eq!(
1644 saved_execute_messages.first().unwrap().instrument_id(),
1645 instrument_audusd.id()
1646 );
1647 }
1648
1649 #[rstest]
1650 fn test_submit_order_when_risk_bypassed_sends_to_execution_engine(
1651 mut msgbus: MessageBus,
1652 strategy_id_ema_cross: StrategyId,
1653 client_id_binance: ClientId,
1654 trader_id: TraderId,
1655 client_order_id: ClientOrderId,
1656 instrument_audusd: InstrumentAny,
1657 venue_order_id: VenueOrderId,
1658 process_order_event_handler: ShareableMessageHandler,
1659 execute_order_event_handler: ShareableMessageHandler,
1660 ) {
1661 msgbus.register(
1662 msgbus.switchboard.exec_engine_process,
1663 process_order_event_handler,
1664 );
1665
1666 msgbus.register(
1667 msgbus.switchboard.exec_engine_execute,
1668 execute_order_event_handler.clone(),
1669 );
1670
1671 let mut risk_engine =
1672 get_risk_engine(Rc::new(RefCell::new(msgbus)), None, None, None, true);
1673
1674 let order = OrderTestBuilder::new(OrderType::Limit)
1676 .instrument_id(instrument_audusd.id())
1677 .side(OrderSide::Buy)
1678 .price(Price::from_raw(100, 0))
1679 .quantity(Quantity::from("1000"))
1680 .build();
1681
1682 let submit_order = SubmitOrder::new(
1683 trader_id,
1684 client_id_binance,
1685 strategy_id_ema_cross,
1686 instrument_audusd.id(),
1687 client_order_id,
1688 venue_order_id,
1689 order,
1690 None,
1691 None,
1692 UUID4::new(),
1693 risk_engine.clock.borrow().timestamp_ns(),
1694 )
1695 .unwrap();
1696
1697 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
1698
1699 let saved_execute_messages =
1700 get_execute_order_event_handler_messages(execute_order_event_handler);
1701 assert_eq!(saved_execute_messages.len(), 1);
1702 assert_eq!(
1703 saved_execute_messages.first().unwrap().instrument_id(),
1704 instrument_audusd.id()
1705 );
1706 }
1707
1708 #[rstest]
1709 fn test_submit_reduce_only_order_when_position_already_closed_then_denies(
1710 mut msgbus: MessageBus,
1711 strategy_id_ema_cross: StrategyId,
1712 client_id_binance: ClientId,
1713 trader_id: TraderId,
1714 client_order_id: ClientOrderId,
1715 instrument_audusd: InstrumentAny,
1716 venue_order_id: VenueOrderId,
1717 process_order_event_handler: ShareableMessageHandler,
1718 execute_order_event_handler: ShareableMessageHandler,
1719 clock: TestClock,
1720 simple_cache: Cache,
1721 ) {
1722 msgbus.register(
1723 msgbus.switchboard.exec_engine_process,
1724 process_order_event_handler,
1725 );
1726
1727 msgbus.register(
1728 msgbus.switchboard.exec_engine_execute,
1729 execute_order_event_handler.clone(),
1730 );
1731
1732 let msgbus = Rc::new(RefCell::new(msgbus));
1733 let clock = Rc::new(RefCell::new(clock));
1734 let simple_cache = Rc::new(RefCell::new(simple_cache));
1735
1736 let mut risk_engine = get_risk_engine(
1737 msgbus.clone(),
1738 Some(simple_cache.clone()),
1739 None,
1740 Some(clock.clone()),
1741 true,
1742 );
1743 let mut exec_engine = get_exec_engine(msgbus, Some(simple_cache), Some(clock), None);
1744
1745 let order1 = OrderTestBuilder::new(OrderType::Market)
1746 .instrument_id(instrument_audusd.id())
1747 .side(OrderSide::Buy)
1748 .quantity(Quantity::from("1000"))
1749 .build();
1750
1751 let order2 = OrderTestBuilder::new(OrderType::Market)
1752 .instrument_id(instrument_audusd.id())
1753 .side(OrderSide::Sell)
1754 .quantity(Quantity::from("1000"))
1755 .reduce_only(true)
1756 .build();
1757
1758 let order3 = OrderTestBuilder::new(OrderType::Market)
1759 .instrument_id(instrument_audusd.id())
1760 .side(OrderSide::Sell)
1761 .quantity(Quantity::from("1000"))
1762 .reduce_only(true)
1763 .build();
1764
1765 let submit_order1 = SubmitOrder::new(
1766 trader_id,
1767 client_id_binance,
1768 strategy_id_ema_cross,
1769 instrument_audusd.id(),
1770 client_order_id,
1771 venue_order_id,
1772 order1.clone(),
1773 None,
1774 None,
1775 UUID4::new(),
1776 risk_engine.clock.borrow().timestamp_ns(),
1777 )
1778 .unwrap();
1779
1780 let order_submitted_event = OrderEventAny::Submitted(order_submitted(&order1));
1781 let order_accepted_event = OrderEventAny::Accepted(order_accepted(&order1, None));
1782 let order_filled_event = OrderEventAny::Filled(order_filled(
1783 &order1,
1784 &instrument_audusd,
1785 None,
1786 None,
1787 None,
1788 None,
1789 None,
1790 None,
1791 None,
1792 None,
1793 None,
1794 ));
1795
1796 risk_engine.execute(TradingCommand::SubmitOrder(submit_order1));
1797 exec_engine.process(&order_submitted_event);
1798 exec_engine.process(&order_accepted_event);
1799 exec_engine.process(&order_filled_event);
1800
1801 let submit_order2 = SubmitOrder::new(
1802 trader_id,
1803 client_id_binance,
1804 strategy_id_ema_cross,
1805 instrument_audusd.id(),
1806 client_order_id,
1807 venue_order_id,
1808 order2.clone(),
1809 None,
1810 None,
1811 UUID4::new(),
1812 risk_engine.clock.borrow().timestamp_ns(),
1813 )
1814 .unwrap();
1815
1816 risk_engine.execute(TradingCommand::SubmitOrder(submit_order2));
1817 exec_engine.process(&OrderEventAny::Submitted(order_submitted(&order2)));
1818 exec_engine.process(&OrderEventAny::Filled(order_filled(
1819 &order2,
1820 &instrument_audusd,
1821 None,
1822 None,
1823 None,
1824 None,
1825 None,
1826 None,
1827 None,
1828 None,
1829 None,
1830 )));
1831
1832 let submit_order3 = SubmitOrder::new(
1833 trader_id,
1834 client_id_binance,
1835 strategy_id_ema_cross,
1836 instrument_audusd.id(),
1837 client_order_id,
1838 venue_order_id,
1839 order3,
1840 None,
1841 None,
1842 UUID4::new(),
1843 risk_engine.clock.borrow().timestamp_ns(),
1844 )
1845 .unwrap();
1846
1847 risk_engine.execute(TradingCommand::SubmitOrder(submit_order3));
1849
1850 let saved_execute_messages =
1856 get_execute_order_event_handler_messages(execute_order_event_handler);
1857 assert_eq!(saved_execute_messages.len(), 3);
1858 assert_eq!(
1859 saved_execute_messages.first().unwrap().instrument_id(),
1860 instrument_audusd.id()
1861 );
1862 }
1863
1864 #[rstest]
1865 fn test_submit_reduce_only_order_when_position_would_be_increased_then_denies(
1866 mut msgbus: MessageBus,
1867 strategy_id_ema_cross: StrategyId,
1868 client_id_binance: ClientId,
1869 trader_id: TraderId,
1870 client_order_id: ClientOrderId,
1871 instrument_audusd: InstrumentAny,
1872 venue_order_id: VenueOrderId,
1873 process_order_event_handler: ShareableMessageHandler,
1874 execute_order_event_handler: ShareableMessageHandler,
1875 clock: TestClock,
1876 simple_cache: Cache,
1877 ) {
1878 msgbus.register(
1879 msgbus.switchboard.exec_engine_process,
1880 process_order_event_handler,
1881 );
1882
1883 msgbus.register(
1884 msgbus.switchboard.exec_engine_execute,
1885 execute_order_event_handler.clone(),
1886 );
1887
1888 let msgbus = Rc::new(RefCell::new(msgbus));
1889 let clock = Rc::new(RefCell::new(clock));
1890 let simple_cache = Rc::new(RefCell::new(simple_cache));
1891
1892 let mut risk_engine = get_risk_engine(
1893 msgbus.clone(),
1894 Some(simple_cache.clone()),
1895 None,
1896 Some(clock.clone()),
1897 true,
1898 );
1899 let mut exec_engine = get_exec_engine(msgbus, Some(simple_cache), Some(clock), None);
1900
1901 let order1 = OrderTestBuilder::new(OrderType::Market)
1902 .instrument_id(instrument_audusd.id())
1903 .side(OrderSide::Buy)
1904 .quantity(Quantity::from("1000"))
1905 .build();
1906
1907 let order2 = OrderTestBuilder::new(OrderType::Market)
1908 .instrument_id(instrument_audusd.id())
1909 .side(OrderSide::Sell)
1910 .quantity(Quantity::from("2000"))
1911 .reduce_only(true)
1912 .build();
1913
1914 let submit_order1 = SubmitOrder::new(
1915 trader_id,
1916 client_id_binance,
1917 strategy_id_ema_cross,
1918 instrument_audusd.id(),
1919 client_order_id,
1920 venue_order_id,
1921 order1.clone(),
1922 None,
1923 None,
1924 UUID4::new(),
1925 risk_engine.clock.borrow().timestamp_ns(),
1926 )
1927 .unwrap();
1928
1929 let order_submitted_event = OrderEventAny::Submitted(order_submitted(&order1));
1930 let order_accepted_event = OrderEventAny::Accepted(order_accepted(&order1, None));
1931 let order_filled_event = OrderEventAny::Filled(order_filled(
1932 &order1,
1933 &instrument_audusd,
1934 None,
1935 None,
1936 None,
1937 None,
1938 None,
1939 None,
1940 None,
1941 None,
1942 None,
1943 ));
1944
1945 risk_engine.execute(TradingCommand::SubmitOrder(submit_order1));
1946 exec_engine.process(&order_submitted_event);
1947 exec_engine.process(&order_accepted_event);
1948 exec_engine.process(&order_filled_event);
1949
1950 let submit_order2 = SubmitOrder::new(
1951 trader_id,
1952 client_id_binance,
1953 strategy_id_ema_cross,
1954 instrument_audusd.id(),
1955 client_order_id,
1956 venue_order_id,
1957 order2.clone(),
1958 None,
1959 None,
1960 UUID4::new(),
1961 risk_engine.clock.borrow().timestamp_ns(),
1962 )
1963 .unwrap();
1964
1965 risk_engine.execute(TradingCommand::SubmitOrder(submit_order2));
1967 exec_engine.process(&OrderEventAny::Submitted(order_submitted(&order2)));
1968 exec_engine.process(&OrderEventAny::Accepted(order_accepted(&order2, None)));
1969 exec_engine.process(&OrderEventAny::Filled(order_filled(
1970 &order2,
1971 &instrument_audusd,
1972 None,
1973 None,
1974 None,
1975 None,
1976 None,
1977 None,
1978 None,
1979 None,
1980 None,
1981 )));
1982
1983 let saved_execute_messages =
1988 get_execute_order_event_handler_messages(execute_order_event_handler);
1989 assert_eq!(saved_execute_messages.len(), 2);
1990 assert_eq!(
1991 saved_execute_messages.first().unwrap().instrument_id(),
1992 instrument_audusd.id()
1993 );
1994 }
1995
1996 #[rstest]
1997 fn test_submit_order_reduce_only_order_with_custom_position_id_not_open_then_denies(
1998 mut msgbus: MessageBus,
1999 strategy_id_ema_cross: StrategyId,
2000 client_id_binance: ClientId,
2001 trader_id: TraderId,
2002 client_order_id: ClientOrderId,
2003 instrument_audusd: InstrumentAny,
2004 venue_order_id: VenueOrderId,
2005 process_order_event_handler: ShareableMessageHandler,
2006 cash_account_state_million_usd: AccountState,
2007 quote_audusd: QuoteTick,
2008 mut simple_cache: Cache,
2009 ) {
2010 msgbus.register(
2011 msgbus.switchboard.exec_engine_process,
2012 process_order_event_handler.clone(),
2013 );
2014
2015 simple_cache
2016 .add_account(AccountAny::Cash(cash_account(
2017 cash_account_state_million_usd,
2018 )))
2019 .unwrap();
2020
2021 simple_cache
2022 .add_instrument(instrument_audusd.clone())
2023 .unwrap();
2024
2025 simple_cache.add_quote(quote_audusd).unwrap();
2026
2027 let mut risk_engine = get_risk_engine(
2028 Rc::new(RefCell::new(msgbus)),
2029 Some(Rc::new(RefCell::new(simple_cache))),
2030 None,
2031 None,
2032 false,
2033 );
2034
2035 let order = OrderTestBuilder::new(OrderType::Limit)
2036 .instrument_id(instrument_audusd.id())
2037 .side(OrderSide::Buy)
2038 .price(Price::from_raw(100, 0))
2039 .quantity(Quantity::from("1000"))
2040 .reduce_only(true)
2041 .build();
2042
2043 let submit_order = SubmitOrder::new(
2044 trader_id,
2045 client_id_binance,
2046 strategy_id_ema_cross,
2047 instrument_audusd.id(),
2048 client_order_id,
2049 venue_order_id,
2050 order,
2051 None,
2052 Some(PositionId::new("CUSTOM-001")), UUID4::new(),
2054 risk_engine.clock.borrow().timestamp_ns(),
2055 )
2056 .unwrap();
2057
2058 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2059 let saved_process_messages =
2060 get_process_order_event_handler_messages(process_order_event_handler);
2061 assert_eq!(saved_process_messages.len(), 1);
2062
2063 assert_eq!(
2064 saved_process_messages.first().unwrap().event_type(),
2065 OrderEventType::Denied
2066 );
2067 assert_eq!(
2068 saved_process_messages.first().unwrap().message().unwrap(),
2069 Ustr::from("Position CUSTOM-001 not found for reduce-only order")
2070 );
2071 }
2072
2073 #[rstest]
2074 fn test_submit_order_when_instrument_not_in_cache_then_denies(
2075 mut msgbus: MessageBus,
2076 strategy_id_ema_cross: StrategyId,
2077 client_id_binance: ClientId,
2078 trader_id: TraderId,
2079 client_order_id: ClientOrderId,
2080 instrument_audusd: InstrumentAny,
2081 venue_order_id: VenueOrderId,
2082 process_order_event_handler: ShareableMessageHandler,
2083 cash_account_state_million_usd: AccountState,
2084 quote_audusd: QuoteTick,
2085 mut simple_cache: Cache,
2086 ) {
2087 msgbus.register(
2088 msgbus.switchboard.exec_engine_process,
2089 process_order_event_handler.clone(),
2090 );
2091
2092 simple_cache
2093 .add_account(AccountAny::Cash(cash_account(
2094 cash_account_state_million_usd,
2095 )))
2096 .unwrap();
2097
2098 simple_cache.add_quote(quote_audusd).unwrap();
2099
2100 let mut risk_engine = get_risk_engine(
2101 Rc::new(RefCell::new(msgbus)),
2102 Some(Rc::new(RefCell::new(simple_cache))),
2103 None,
2104 None,
2105 false,
2106 );
2107 let order = OrderTestBuilder::new(OrderType::Limit)
2108 .instrument_id(instrument_audusd.id())
2109 .side(OrderSide::Buy)
2110 .price(Price::from_raw(100, 0))
2111 .quantity(Quantity::from("1000"))
2112 .build();
2113
2114 let submit_order = SubmitOrder::new(
2115 trader_id,
2116 client_id_binance,
2117 strategy_id_ema_cross,
2118 instrument_audusd.id(),
2119 client_order_id,
2120 venue_order_id,
2121 order,
2122 None,
2123 None,
2124 UUID4::new(),
2125 risk_engine.clock.borrow().timestamp_ns(),
2126 )
2127 .unwrap();
2128
2129 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2130 let saved_process_messages =
2131 get_process_order_event_handler_messages(process_order_event_handler);
2132 assert_eq!(saved_process_messages.len(), 1);
2133
2134 assert_eq!(
2135 saved_process_messages.first().unwrap().event_type(),
2136 OrderEventType::Denied
2137 );
2138 assert_eq!(
2139 saved_process_messages.first().unwrap().message().unwrap(),
2140 Ustr::from("Instrument for AUD/USD.SIM not found")
2141 );
2142 }
2143
2144 #[rstest]
2145 fn test_submit_order_when_invalid_price_precision_then_denies(
2146 mut msgbus: MessageBus,
2147 strategy_id_ema_cross: StrategyId,
2148 client_id_binance: ClientId,
2149 trader_id: TraderId,
2150 client_order_id: ClientOrderId,
2151 instrument_audusd: InstrumentAny,
2152 venue_order_id: VenueOrderId,
2153 process_order_event_handler: ShareableMessageHandler,
2154 cash_account_state_million_usd: AccountState,
2155 quote_audusd: QuoteTick,
2156 mut simple_cache: Cache,
2157 ) {
2158 msgbus.register(
2159 msgbus.switchboard.exec_engine_process,
2160 process_order_event_handler.clone(),
2161 );
2162
2163 simple_cache
2164 .add_instrument(instrument_audusd.clone())
2165 .unwrap();
2166
2167 simple_cache
2168 .add_account(AccountAny::Cash(cash_account(
2169 cash_account_state_million_usd,
2170 )))
2171 .unwrap();
2172
2173 simple_cache.add_quote(quote_audusd).unwrap();
2174
2175 let mut risk_engine = get_risk_engine(
2176 Rc::new(RefCell::new(msgbus)),
2177 Some(Rc::new(RefCell::new(simple_cache))),
2178 None,
2179 None,
2180 false,
2181 );
2182 let order = OrderTestBuilder::new(OrderType::Limit)
2183 .instrument_id(instrument_audusd.id())
2184 .side(OrderSide::Buy)
2185 .price(Price::from_raw(1_000_000_000_000, FIXED_PRECISION)) .quantity(Quantity::from("1000"))
2187 .build();
2188
2189 let submit_order = SubmitOrder::new(
2190 trader_id,
2191 client_id_binance,
2192 strategy_id_ema_cross,
2193 instrument_audusd.id(),
2194 client_order_id,
2195 venue_order_id,
2196 order,
2197 None,
2198 None,
2199 UUID4::new(),
2200 risk_engine.clock.borrow().timestamp_ns(),
2201 )
2202 .unwrap();
2203
2204 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2205 let saved_process_messages =
2206 get_process_order_event_handler_messages(process_order_event_handler);
2207 assert_eq!(saved_process_messages.len(), 1);
2208
2209 assert_eq!(
2210 saved_process_messages.first().unwrap().event_type(),
2211 OrderEventType::Denied
2212 );
2213 assert!(
2214 saved_process_messages
2215 .first()
2216 .unwrap()
2217 .message()
2218 .unwrap()
2219 .contains(&format!("invalid (precision {FIXED_PRECISION} > 5)"))
2220 );
2221 }
2222
2223 #[rstest]
2224 fn test_submit_order_when_invalid_negative_price_and_not_option_then_denies(
2225 mut msgbus: MessageBus,
2226 strategy_id_ema_cross: StrategyId,
2227 client_id_binance: ClientId,
2228 trader_id: TraderId,
2229 client_order_id: ClientOrderId,
2230 instrument_audusd: InstrumentAny,
2231 venue_order_id: VenueOrderId,
2232 process_order_event_handler: ShareableMessageHandler,
2233 cash_account_state_million_usd: AccountState,
2234 quote_audusd: QuoteTick,
2235 mut simple_cache: Cache,
2236 ) {
2237 msgbus.register(
2238 msgbus.switchboard.exec_engine_process,
2239 process_order_event_handler.clone(),
2240 );
2241
2242 simple_cache
2243 .add_instrument(instrument_audusd.clone())
2244 .unwrap();
2245
2246 simple_cache
2247 .add_account(AccountAny::Cash(cash_account(
2248 cash_account_state_million_usd,
2249 )))
2250 .unwrap();
2251
2252 simple_cache.add_quote(quote_audusd).unwrap();
2253
2254 let mut risk_engine = get_risk_engine(
2255 Rc::new(RefCell::new(msgbus)),
2256 Some(Rc::new(RefCell::new(simple_cache))),
2257 None,
2258 None,
2259 false,
2260 );
2261 let order = OrderTestBuilder::new(OrderType::Limit)
2262 .instrument_id(instrument_audusd.id())
2263 .side(OrderSide::Buy)
2264 .price(Price::from_raw(-1, 1)) .quantity(Quantity::from("1000"))
2266 .build();
2267
2268 let submit_order = SubmitOrder::new(
2269 trader_id,
2270 client_id_binance,
2271 strategy_id_ema_cross,
2272 instrument_audusd.id(),
2273 client_order_id,
2274 venue_order_id,
2275 order,
2276 None,
2277 None,
2278 UUID4::new(),
2279 risk_engine.clock.borrow().timestamp_ns(),
2280 )
2281 .unwrap();
2282
2283 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2284 let saved_process_messages =
2285 get_process_order_event_handler_messages(process_order_event_handler);
2286 assert_eq!(saved_process_messages.len(), 1);
2287
2288 assert_eq!(
2289 saved_process_messages.first().unwrap().event_type(),
2290 OrderEventType::Denied
2291 );
2292 assert_eq!(
2293 saved_process_messages.first().unwrap().message().unwrap(),
2294 Ustr::from("price -0.0 invalid (<= 0)") );
2296 }
2297
2298 #[rstest]
2299 fn test_submit_order_when_invalid_trigger_price_then_denies(
2300 mut msgbus: MessageBus,
2301 strategy_id_ema_cross: StrategyId,
2302 client_id_binance: ClientId,
2303 trader_id: TraderId,
2304 client_order_id: ClientOrderId,
2305 instrument_audusd: InstrumentAny,
2306 venue_order_id: VenueOrderId,
2307 process_order_event_handler: ShareableMessageHandler,
2308 cash_account_state_million_usd: AccountState,
2309 quote_audusd: QuoteTick,
2310 mut simple_cache: Cache,
2311 ) {
2312 msgbus.register(
2313 msgbus.switchboard.exec_engine_process,
2314 process_order_event_handler.clone(),
2315 );
2316
2317 simple_cache
2318 .add_instrument(instrument_audusd.clone())
2319 .unwrap();
2320
2321 simple_cache
2322 .add_account(AccountAny::Cash(cash_account(
2323 cash_account_state_million_usd,
2324 )))
2325 .unwrap();
2326
2327 simple_cache.add_quote(quote_audusd).unwrap();
2328
2329 let mut risk_engine = get_risk_engine(
2330 Rc::new(RefCell::new(msgbus)),
2331 Some(Rc::new(RefCell::new(simple_cache))),
2332 None,
2333 None,
2334 false,
2335 );
2336 let order = OrderTestBuilder::new(OrderType::StopLimit)
2337 .instrument_id(instrument_audusd.id())
2338 .side(OrderSide::Buy)
2339 .quantity(Quantity::from_str("1000").unwrap())
2340 .price(Price::from_raw(1, 1))
2341 .trigger_price(Price::from_raw(1_000_000_000_000_000, FIXED_PRECISION)) .build();
2343
2344 let submit_order = SubmitOrder::new(
2345 trader_id,
2346 client_id_binance,
2347 strategy_id_ema_cross,
2348 instrument_audusd.id(),
2349 client_order_id,
2350 venue_order_id,
2351 order,
2352 None,
2353 None,
2354 UUID4::new(),
2355 risk_engine.clock.borrow().timestamp_ns(),
2356 )
2357 .unwrap();
2358
2359 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2360 let saved_process_messages =
2361 get_process_order_event_handler_messages(process_order_event_handler);
2362 assert_eq!(saved_process_messages.len(), 1);
2363
2364 assert_eq!(
2365 saved_process_messages.first().unwrap().event_type(),
2366 OrderEventType::Denied
2367 );
2368 }
2375
2376 #[rstest]
2377 fn test_submit_order_when_invalid_quantity_precision_then_denies(
2378 mut msgbus: MessageBus,
2379 strategy_id_ema_cross: StrategyId,
2380 client_id_binance: ClientId,
2381 trader_id: TraderId,
2382 client_order_id: ClientOrderId,
2383 instrument_audusd: InstrumentAny,
2384 venue_order_id: VenueOrderId,
2385 process_order_event_handler: ShareableMessageHandler,
2386 cash_account_state_million_usd: AccountState,
2387 quote_audusd: QuoteTick,
2388 mut simple_cache: Cache,
2389 ) {
2390 msgbus.register(
2391 msgbus.switchboard.exec_engine_process,
2392 process_order_event_handler.clone(),
2393 );
2394
2395 simple_cache
2396 .add_instrument(instrument_audusd.clone())
2397 .unwrap();
2398
2399 simple_cache
2400 .add_account(AccountAny::Cash(cash_account(
2401 cash_account_state_million_usd,
2402 )))
2403 .unwrap();
2404
2405 simple_cache.add_quote(quote_audusd).unwrap();
2406
2407 let mut risk_engine = get_risk_engine(
2408 Rc::new(RefCell::new(msgbus)),
2409 Some(Rc::new(RefCell::new(simple_cache))),
2410 None,
2411 None,
2412 false,
2413 );
2414 let order = OrderTestBuilder::new(OrderType::Market)
2415 .instrument_id(instrument_audusd.id())
2416 .side(OrderSide::Buy)
2417 .quantity(Quantity::from_str("0.1").unwrap())
2418 .build();
2419
2420 let submit_order = SubmitOrder::new(
2421 trader_id,
2422 client_id_binance,
2423 strategy_id_ema_cross,
2424 instrument_audusd.id(),
2425 client_order_id,
2426 venue_order_id,
2427 order,
2428 None,
2429 None,
2430 UUID4::new(),
2431 risk_engine.clock.borrow().timestamp_ns(),
2432 )
2433 .unwrap();
2434
2435 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2436 let saved_process_messages =
2437 get_process_order_event_handler_messages(process_order_event_handler);
2438 assert_eq!(saved_process_messages.len(), 1);
2439
2440 assert_eq!(
2441 saved_process_messages.first().unwrap().event_type(),
2442 OrderEventType::Denied
2443 );
2444 assert_eq!(
2445 saved_process_messages.first().unwrap().message().unwrap(),
2446 Ustr::from("quantity 0.1 invalid (precision 1 > 0)")
2447 );
2448 }
2449
2450 #[rstest]
2451 fn test_submit_order_when_invalid_quantity_exceeds_maximum_then_denies(
2452 mut msgbus: MessageBus,
2453 strategy_id_ema_cross: StrategyId,
2454 client_id_binance: ClientId,
2455 trader_id: TraderId,
2456 client_order_id: ClientOrderId,
2457 instrument_audusd: InstrumentAny,
2458 venue_order_id: VenueOrderId,
2459 process_order_event_handler: ShareableMessageHandler,
2460 cash_account_state_million_usd: AccountState,
2461 quote_audusd: QuoteTick,
2462 mut simple_cache: Cache,
2463 ) {
2464 msgbus.register(
2465 msgbus.switchboard.exec_engine_process,
2466 process_order_event_handler.clone(),
2467 );
2468
2469 simple_cache
2470 .add_instrument(instrument_audusd.clone())
2471 .unwrap();
2472
2473 simple_cache
2474 .add_account(AccountAny::Cash(cash_account(
2475 cash_account_state_million_usd,
2476 )))
2477 .unwrap();
2478
2479 simple_cache.add_quote(quote_audusd).unwrap();
2480
2481 let mut risk_engine = get_risk_engine(
2482 Rc::new(RefCell::new(msgbus)),
2483 Some(Rc::new(RefCell::new(simple_cache))),
2484 None,
2485 None,
2486 false,
2487 );
2488 let order = OrderTestBuilder::new(OrderType::Market)
2489 .instrument_id(instrument_audusd.id())
2490 .side(OrderSide::Buy)
2491 .quantity(Quantity::from_str("100000000").unwrap())
2492 .build();
2493
2494 let submit_order = SubmitOrder::new(
2495 trader_id,
2496 client_id_binance,
2497 strategy_id_ema_cross,
2498 instrument_audusd.id(),
2499 client_order_id,
2500 venue_order_id,
2501 order,
2502 None,
2503 None,
2504 UUID4::new(),
2505 risk_engine.clock.borrow().timestamp_ns(),
2506 )
2507 .unwrap();
2508
2509 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2510 let saved_process_messages =
2511 get_process_order_event_handler_messages(process_order_event_handler);
2512 assert_eq!(saved_process_messages.len(), 1);
2513
2514 assert_eq!(
2515 saved_process_messages.first().unwrap().event_type(),
2516 OrderEventType::Denied
2517 );
2518 assert_eq!(
2519 saved_process_messages.first().unwrap().message().unwrap(),
2520 Ustr::from("quantity 100000000 invalid (> maximum trade size of 1000000)")
2521 );
2522 }
2523
2524 #[rstest]
2525 fn test_submit_order_when_invalid_quantity_less_than_minimum_then_denies(
2526 mut msgbus: MessageBus,
2527 strategy_id_ema_cross: StrategyId,
2528 client_id_binance: ClientId,
2529 trader_id: TraderId,
2530 client_order_id: ClientOrderId,
2531 instrument_audusd: InstrumentAny,
2532 venue_order_id: VenueOrderId,
2533 process_order_event_handler: ShareableMessageHandler,
2534 cash_account_state_million_usd: AccountState,
2535 quote_audusd: QuoteTick,
2536 mut simple_cache: Cache,
2537 ) {
2538 msgbus.register(
2539 msgbus.switchboard.exec_engine_process,
2540 process_order_event_handler.clone(),
2541 );
2542
2543 simple_cache
2544 .add_instrument(instrument_audusd.clone())
2545 .unwrap();
2546
2547 simple_cache
2548 .add_account(AccountAny::Cash(cash_account(
2549 cash_account_state_million_usd,
2550 )))
2551 .unwrap();
2552
2553 simple_cache.add_quote(quote_audusd).unwrap();
2554
2555 let mut risk_engine = get_risk_engine(
2556 Rc::new(RefCell::new(msgbus)),
2557 Some(Rc::new(RefCell::new(simple_cache))),
2558 None,
2559 None,
2560 false,
2561 );
2562 let order = OrderTestBuilder::new(OrderType::Market)
2563 .instrument_id(instrument_audusd.id())
2564 .side(OrderSide::Buy)
2565 .quantity(Quantity::from_str("1").unwrap())
2566 .build();
2567
2568 let submit_order = SubmitOrder::new(
2569 trader_id,
2570 client_id_binance,
2571 strategy_id_ema_cross,
2572 instrument_audusd.id(),
2573 client_order_id,
2574 venue_order_id,
2575 order,
2576 None,
2577 None,
2578 UUID4::new(),
2579 risk_engine.clock.borrow().timestamp_ns(),
2580 )
2581 .unwrap();
2582
2583 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2584 let saved_process_messages =
2585 get_process_order_event_handler_messages(process_order_event_handler);
2586 assert_eq!(saved_process_messages.len(), 1);
2587
2588 assert_eq!(
2589 saved_process_messages.first().unwrap().event_type(),
2590 OrderEventType::Denied
2591 );
2592 assert_eq!(
2593 saved_process_messages.first().unwrap().message().unwrap(),
2594 Ustr::from("quantity 1 invalid (< minimum trade size of 100)")
2595 );
2596 }
2597
2598 #[rstest]
2599 fn test_submit_order_when_market_order_and_no_market_then_logs_warning(
2600 mut msgbus: MessageBus,
2601 strategy_id_ema_cross: StrategyId,
2602 client_id_binance: ClientId,
2603 trader_id: TraderId,
2604 client_order_id: ClientOrderId,
2605 instrument_audusd: InstrumentAny,
2606 venue_order_id: VenueOrderId,
2607 execute_order_event_handler: ShareableMessageHandler,
2608 cash_account_state_million_usd: AccountState,
2609 quote_audusd: QuoteTick,
2610 mut simple_cache: Cache,
2611 ) {
2612 msgbus.register(
2613 msgbus.switchboard.exec_engine_execute,
2614 execute_order_event_handler.clone(),
2615 );
2616
2617 simple_cache
2618 .add_instrument(instrument_audusd.clone())
2619 .unwrap();
2620
2621 simple_cache
2622 .add_account(AccountAny::Cash(cash_account(
2623 cash_account_state_million_usd,
2624 )))
2625 .unwrap();
2626
2627 simple_cache.add_quote(quote_audusd).unwrap();
2628
2629 let mut risk_engine = get_risk_engine(
2630 Rc::new(RefCell::new(msgbus)),
2631 Some(Rc::new(RefCell::new(simple_cache))),
2632 None,
2633 None,
2634 false,
2635 );
2636 risk_engine.set_max_notional_per_order(
2637 instrument_audusd.id(),
2638 Decimal::from_i32(10000000).unwrap(),
2639 );
2640
2641 let order = OrderTestBuilder::new(OrderType::Market)
2642 .instrument_id(instrument_audusd.id())
2643 .side(OrderSide::Buy)
2644 .quantity(Quantity::from_str("100").unwrap())
2645 .build();
2646
2647 let submit_order = SubmitOrder::new(
2648 trader_id,
2649 client_id_binance,
2650 strategy_id_ema_cross,
2651 instrument_audusd.id(),
2652 client_order_id,
2653 venue_order_id,
2654 order,
2655 None,
2656 None,
2657 UUID4::new(),
2658 risk_engine.clock.borrow().timestamp_ns(),
2659 )
2660 .unwrap();
2661
2662 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2663
2664 let saved_execute_messages =
2665 get_execute_order_event_handler_messages(execute_order_event_handler);
2666 assert_eq!(saved_execute_messages.len(), 1);
2667 assert_eq!(
2668 saved_execute_messages.first().unwrap().instrument_id(),
2669 instrument_audusd.id()
2670 );
2671 }
2672
2673 #[rstest]
2674 fn test_submit_order_when_less_than_min_notional_for_instrument_then_denies(
2675 mut msgbus: MessageBus,
2676 strategy_id_ema_cross: StrategyId,
2677 client_id_binance: ClientId,
2678 trader_id: TraderId,
2679 client_order_id: ClientOrderId,
2680 instrument_xbtusd_with_high_size_precision: InstrumentAny,
2681 venue_order_id: VenueOrderId,
2682 process_order_event_handler: ShareableMessageHandler,
2683 execute_order_event_handler: ShareableMessageHandler,
2684 bitmex_cash_account_state_multi: AccountState,
2685 mut simple_cache: Cache,
2686 ) {
2687 msgbus.register(
2688 msgbus.switchboard.exec_engine_process,
2689 process_order_event_handler.clone(),
2690 );
2691
2692 msgbus.register(
2693 msgbus.switchboard.exec_engine_execute,
2694 execute_order_event_handler,
2695 );
2696
2697 simple_cache
2698 .add_instrument(instrument_xbtusd_with_high_size_precision.clone())
2699 .unwrap();
2700
2701 simple_cache
2702 .add_account(AccountAny::Cash(cash_account(
2703 bitmex_cash_account_state_multi,
2704 )))
2705 .unwrap();
2706
2707 let quote = QuoteTick::new(
2708 instrument_xbtusd_with_high_size_precision.id(),
2709 Price::from("0.075000"),
2710 Price::from("0.075005"),
2711 Quantity::from("50000"),
2712 Quantity::from("50000"),
2713 UnixNanos::default(),
2714 UnixNanos::default(),
2715 );
2716
2717 simple_cache.add_quote(quote).unwrap();
2718
2719 let mut risk_engine = get_risk_engine(
2720 Rc::new(RefCell::new(msgbus)),
2721 Some(Rc::new(RefCell::new(simple_cache))),
2722 None,
2723 None,
2724 false,
2725 );
2726
2727 let order = OrderTestBuilder::new(OrderType::Market)
2728 .instrument_id(instrument_xbtusd_with_high_size_precision.id())
2729 .side(OrderSide::Buy)
2730 .quantity(Quantity::from_str("0.9").unwrap())
2731 .build();
2732
2733 let submit_order = SubmitOrder::new(
2734 trader_id,
2735 client_id_binance,
2736 strategy_id_ema_cross,
2737 instrument_xbtusd_with_high_size_precision.id(),
2738 client_order_id,
2739 venue_order_id,
2740 order,
2741 None,
2742 None,
2743 UUID4::new(),
2744 risk_engine.clock.borrow().timestamp_ns(),
2745 )
2746 .unwrap();
2747
2748 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2749
2750 let saved_process_messages =
2751 get_process_order_event_handler_messages(process_order_event_handler);
2752 assert_eq!(saved_process_messages.len(), 1);
2753
2754 assert_eq!(
2755 saved_process_messages.first().unwrap().event_type(),
2756 OrderEventType::Denied
2757 );
2758 assert_eq!(
2759 saved_process_messages.first().unwrap().message().unwrap(),
2760 Ustr::from(
2761 "NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT: min_notional=Money(1.00, USD), notional=Money(0.90, USD)"
2762 )
2763 );
2764 }
2765
2766 #[rstest]
2767 fn test_submit_order_when_greater_than_max_notional_for_instrument_then_denies(
2768 mut msgbus: MessageBus,
2769 strategy_id_ema_cross: StrategyId,
2770 client_id_binance: ClientId,
2771 trader_id: TraderId,
2772 client_order_id: ClientOrderId,
2773 instrument_xbtusd_bitmex: InstrumentAny,
2774 venue_order_id: VenueOrderId,
2775 process_order_event_handler: ShareableMessageHandler,
2776 bitmex_cash_account_state_multi: AccountState,
2777 mut simple_cache: Cache,
2778 ) {
2779 msgbus.register(
2780 msgbus.switchboard.exec_engine_process,
2781 process_order_event_handler.clone(),
2782 );
2783
2784 simple_cache
2785 .add_instrument(instrument_xbtusd_bitmex.clone())
2786 .unwrap();
2787
2788 simple_cache
2789 .add_account(AccountAny::Cash(cash_account(
2790 bitmex_cash_account_state_multi,
2791 )))
2792 .unwrap();
2793
2794 let quote = QuoteTick::new(
2795 instrument_xbtusd_bitmex.id(),
2796 Price::from("7.5000"),
2797 Price::from("7.5005"),
2798 Quantity::from("50000"),
2799 Quantity::from("50000"),
2800 UnixNanos::default(),
2801 UnixNanos::default(),
2802 );
2803
2804 simple_cache.add_quote(quote).unwrap();
2805
2806 let mut risk_engine = get_risk_engine(
2807 Rc::new(RefCell::new(msgbus)),
2808 Some(Rc::new(RefCell::new(simple_cache))),
2809 None,
2810 None,
2811 false,
2812 );
2813 risk_engine.set_max_notional_per_order(
2814 instrument_xbtusd_bitmex.id(),
2815 Decimal::from_i64(100000000).unwrap(),
2816 );
2817
2818 let order = OrderTestBuilder::new(OrderType::Market)
2819 .instrument_id(instrument_xbtusd_bitmex.id())
2820 .side(OrderSide::Buy)
2821 .quantity(Quantity::from_str("10000001").unwrap())
2822 .build();
2823
2824 let submit_order = SubmitOrder::new(
2825 trader_id,
2826 client_id_binance,
2827 strategy_id_ema_cross,
2828 instrument_xbtusd_bitmex.id(),
2829 client_order_id,
2830 venue_order_id,
2831 order,
2832 None,
2833 None,
2834 UUID4::new(),
2835 risk_engine.clock.borrow().timestamp_ns(),
2836 )
2837 .unwrap();
2838
2839 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2840 let saved_process_messages =
2841 get_process_order_event_handler_messages(process_order_event_handler);
2842 assert_eq!(saved_process_messages.len(), 1);
2843
2844 assert_eq!(
2845 saved_process_messages.first().unwrap().event_type(),
2846 OrderEventType::Denied
2847 );
2848 assert_eq!(
2849 saved_process_messages.first().unwrap().message().unwrap(),
2850 Ustr::from(
2851 "NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT: max_notional=Money(10000000.00, USD), notional=Money(10000001.00, USD)"
2852 )
2853 );
2854 }
2855
2856 #[rstest]
2857 fn test_submit_order_when_buy_market_order_and_over_max_notional_then_denies(
2858 mut msgbus: MessageBus,
2859 strategy_id_ema_cross: StrategyId,
2860 client_id_binance: ClientId,
2861 trader_id: TraderId,
2862 client_order_id: ClientOrderId,
2863 instrument_audusd: InstrumentAny,
2864 venue_order_id: VenueOrderId,
2865 process_order_event_handler: ShareableMessageHandler,
2866 cash_account_state_million_usd: AccountState,
2867 mut simple_cache: Cache,
2868 ) {
2869 msgbus.register(
2870 msgbus.switchboard.exec_engine_process,
2871 process_order_event_handler.clone(),
2872 );
2873
2874 simple_cache
2875 .add_instrument(instrument_audusd.clone())
2876 .unwrap();
2877
2878 simple_cache
2879 .add_account(AccountAny::Cash(cash_account(
2880 cash_account_state_million_usd,
2881 )))
2882 .unwrap();
2883
2884 let quote = QuoteTick::new(
2885 instrument_audusd.id(),
2886 Price::from("0.75000"),
2887 Price::from("0.75005"),
2888 Quantity::from("500000"),
2889 Quantity::from("500000"),
2890 UnixNanos::default(),
2891 UnixNanos::default(),
2892 );
2893
2894 simple_cache.add_quote(quote).unwrap();
2895
2896 let mut risk_engine = get_risk_engine(
2897 Rc::new(RefCell::new(msgbus)),
2898 Some(Rc::new(RefCell::new(simple_cache))),
2899 None,
2900 None,
2901 false,
2902 );
2903 risk_engine
2904 .set_max_notional_per_order(instrument_audusd.id(), Decimal::from_i64(100000).unwrap());
2905
2906 let order = OrderTestBuilder::new(OrderType::Market)
2907 .instrument_id(instrument_audusd.id())
2908 .side(OrderSide::Buy)
2909 .quantity(Quantity::from_str("1000000").unwrap())
2910 .build();
2911
2912 let submit_order = SubmitOrder::new(
2913 trader_id,
2914 client_id_binance,
2915 strategy_id_ema_cross,
2916 instrument_audusd.id(),
2917 client_order_id,
2918 venue_order_id,
2919 order,
2920 None,
2921 None,
2922 UUID4::new(),
2923 risk_engine.clock.borrow().timestamp_ns(),
2924 )
2925 .unwrap();
2926
2927 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
2928 let saved_process_messages =
2929 get_process_order_event_handler_messages(process_order_event_handler);
2930 assert_eq!(saved_process_messages.len(), 1);
2931
2932 assert_eq!(
2933 saved_process_messages.first().unwrap().event_type(),
2934 OrderEventType::Denied
2935 );
2936 assert_eq!(
2937 saved_process_messages.first().unwrap().message().unwrap(),
2938 Ustr::from(
2939 "NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional=Money(100000.00, USD), notional=Money(750050.00, USD)"
2940 )
2941 );
2942 }
2943
2944 #[rstest]
2945 fn test_submit_order_when_sell_market_order_and_over_max_notional_then_denies(
2946 mut msgbus: MessageBus,
2947 strategy_id_ema_cross: StrategyId,
2948 client_id_binance: ClientId,
2949 trader_id: TraderId,
2950 client_order_id: ClientOrderId,
2951 instrument_audusd: InstrumentAny,
2952 venue_order_id: VenueOrderId,
2953 process_order_event_handler: ShareableMessageHandler,
2954 cash_account_state_million_usd: AccountState,
2955 mut simple_cache: Cache,
2956 ) {
2957 msgbus.register(
2958 msgbus.switchboard.exec_engine_process,
2959 process_order_event_handler.clone(),
2960 );
2961
2962 simple_cache
2963 .add_instrument(instrument_audusd.clone())
2964 .unwrap();
2965
2966 simple_cache
2967 .add_account(AccountAny::Cash(cash_account(
2968 cash_account_state_million_usd,
2969 )))
2970 .unwrap();
2971
2972 let quote = QuoteTick::new(
2973 instrument_audusd.id(),
2974 Price::from("0.75000"),
2975 Price::from("0.75005"),
2976 Quantity::from("500000"),
2977 Quantity::from("500000"),
2978 UnixNanos::default(),
2979 UnixNanos::default(),
2980 );
2981
2982 simple_cache.add_quote(quote).unwrap();
2983
2984 let mut risk_engine = get_risk_engine(
2985 Rc::new(RefCell::new(msgbus)),
2986 Some(Rc::new(RefCell::new(simple_cache))),
2987 None,
2988 None,
2989 false,
2990 );
2991 risk_engine
2992 .set_max_notional_per_order(instrument_audusd.id(), Decimal::from_i64(100000).unwrap());
2993
2994 let order = OrderTestBuilder::new(OrderType::Market)
2995 .instrument_id(instrument_audusd.id())
2996 .side(OrderSide::Sell)
2997 .quantity(Quantity::from_str("1000000").unwrap())
2998 .build();
2999
3000 let submit_order = SubmitOrder::new(
3001 trader_id,
3002 client_id_binance,
3003 strategy_id_ema_cross,
3004 instrument_audusd.id(),
3005 client_order_id,
3006 venue_order_id,
3007 order,
3008 None,
3009 None,
3010 UUID4::new(),
3011 risk_engine.clock.borrow().timestamp_ns(),
3012 )
3013 .unwrap();
3014
3015 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3016 let saved_process_messages =
3017 get_process_order_event_handler_messages(process_order_event_handler);
3018 assert_eq!(saved_process_messages.len(), 1);
3019
3020 assert_eq!(
3021 saved_process_messages.first().unwrap().event_type(),
3022 OrderEventType::Denied
3023 );
3024 assert_eq!(
3025 saved_process_messages.first().unwrap().message().unwrap(),
3026 Ustr::from(
3027 "NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional=Money(100000.00, USD), notional=Money(750000.00, USD)"
3028 )
3029 );
3030 }
3031
3032 #[rstest]
3033 fn test_submit_order_when_market_order_and_over_free_balance_then_denies(
3034 mut msgbus: MessageBus,
3035 strategy_id_ema_cross: StrategyId,
3036 client_id_binance: ClientId,
3037 trader_id: TraderId,
3038 client_order_id: ClientOrderId,
3039 instrument_audusd: InstrumentAny,
3040 venue_order_id: VenueOrderId,
3041 process_order_event_handler: ShareableMessageHandler,
3042 cash_account_state_million_usd: AccountState,
3043 quote_audusd: QuoteTick,
3044 mut simple_cache: Cache,
3045 ) {
3046 msgbus.register(
3047 msgbus.switchboard.exec_engine_process,
3048 process_order_event_handler.clone(),
3049 );
3050
3051 simple_cache
3052 .add_instrument(instrument_audusd.clone())
3053 .unwrap();
3054
3055 simple_cache
3056 .add_account(AccountAny::Cash(cash_account(
3057 cash_account_state_million_usd,
3058 )))
3059 .unwrap();
3060
3061 simple_cache.add_quote(quote_audusd).unwrap();
3062
3063 let mut risk_engine = get_risk_engine(
3064 Rc::new(RefCell::new(msgbus)),
3065 Some(Rc::new(RefCell::new(simple_cache))),
3066 None,
3067 None,
3068 false,
3069 );
3070 let order = OrderTestBuilder::new(OrderType::Market)
3071 .instrument_id(instrument_audusd.id())
3072 .side(OrderSide::Buy)
3073 .quantity(Quantity::from_str("100000").unwrap())
3074 .build();
3075
3076 let submit_order = SubmitOrder::new(
3077 trader_id,
3078 client_id_binance,
3079 strategy_id_ema_cross,
3080 instrument_audusd.id(),
3081 client_order_id,
3082 venue_order_id,
3083 order,
3084 None,
3085 None,
3086 UUID4::new(),
3087 risk_engine.clock.borrow().timestamp_ns(),
3088 )
3089 .unwrap();
3090
3091 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3092 let saved_process_messages =
3093 get_process_order_event_handler_messages(process_order_event_handler);
3094 assert_eq!(saved_process_messages.len(), 1);
3095
3096 assert_eq!(
3097 saved_process_messages.first().unwrap().event_type(),
3098 OrderEventType::Denied
3099 );
3100 assert_eq!(
3101 saved_process_messages.first().unwrap().message().unwrap(),
3102 Ustr::from(
3103 "NOTIONAL_EXCEEDS_FREE_BALANCE: free=Money(1000000.00, USD), notional=Money(10100000.00, USD)"
3104 )
3105 );
3106 }
3107
3108 #[rstest]
3109 fn test_submit_order_list_buys_when_over_free_balance_then_denies(
3110 mut msgbus: MessageBus,
3111 strategy_id_ema_cross: StrategyId,
3112 client_id_binance: ClientId,
3113 trader_id: TraderId,
3114 client_order_id: ClientOrderId,
3115 instrument_audusd: InstrumentAny,
3116 venue_order_id: VenueOrderId,
3117 process_order_event_handler: ShareableMessageHandler,
3118 cash_account_state_million_usd: AccountState,
3119 quote_audusd: QuoteTick,
3120 mut simple_cache: Cache,
3121 ) {
3122 msgbus.register(
3123 msgbus.switchboard.exec_engine_process,
3124 process_order_event_handler.clone(),
3125 );
3126
3127 simple_cache
3128 .add_instrument(instrument_audusd.clone())
3129 .unwrap();
3130
3131 simple_cache
3132 .add_account(AccountAny::Cash(cash_account(
3133 cash_account_state_million_usd,
3134 )))
3135 .unwrap();
3136
3137 simple_cache.add_quote(quote_audusd).unwrap();
3138
3139 let mut risk_engine = get_risk_engine(
3140 Rc::new(RefCell::new(msgbus)),
3141 Some(Rc::new(RefCell::new(simple_cache))),
3142 None,
3143 None,
3144 false,
3145 );
3146 let order1 = OrderTestBuilder::new(OrderType::Market)
3147 .instrument_id(instrument_audusd.id())
3148 .side(OrderSide::Buy)
3149 .quantity(Quantity::from_str("4920").unwrap())
3150 .build();
3151
3152 let order2 = OrderTestBuilder::new(OrderType::Market)
3153 .instrument_id(instrument_audusd.id())
3154 .side(OrderSide::Buy)
3155 .quantity(Quantity::from_str("5653").unwrap()) .build();
3157
3158 let order_list = OrderList::new(
3159 OrderListId::new("1"),
3160 instrument_audusd.id(),
3161 StrategyId::new("S-001"),
3162 vec![order1, order2],
3163 risk_engine.clock.borrow().timestamp_ns(),
3164 );
3165
3166 let submit_order = SubmitOrderList::new(
3167 trader_id,
3168 client_id_binance,
3169 strategy_id_ema_cross,
3170 instrument_audusd.id(),
3171 client_order_id,
3172 venue_order_id,
3173 order_list,
3174 None,
3175 None,
3176 UUID4::new(),
3177 risk_engine.clock.borrow().timestamp_ns(),
3178 )
3179 .unwrap();
3180
3181 risk_engine.execute(TradingCommand::SubmitOrderList(submit_order));
3182 let saved_process_messages =
3183 get_process_order_event_handler_messages(process_order_event_handler);
3184
3185 assert_eq!(saved_process_messages.len(), 3);
3186
3187 for event in &saved_process_messages {
3188 assert_eq!(event.event_type(), OrderEventType::Denied);
3189 }
3190
3191 assert_eq!(
3193 saved_process_messages.first().unwrap().message().unwrap(),
3194 Ustr::from(
3195 "CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free=1000000.00 USD, cum_notional=1067873.00 USD"
3196 )
3197 );
3198 }
3199
3200 #[rstest]
3201 fn test_submit_order_list_sells_when_over_free_balance_then_denies(
3202 mut msgbus: MessageBus,
3203 strategy_id_ema_cross: StrategyId,
3204 client_id_binance: ClientId,
3205 trader_id: TraderId,
3206 client_order_id: ClientOrderId,
3207 instrument_audusd: InstrumentAny,
3208 venue_order_id: VenueOrderId,
3209 process_order_event_handler: ShareableMessageHandler,
3210 cash_account_state_million_usd: AccountState,
3211 quote_audusd: QuoteTick,
3212 mut simple_cache: Cache,
3213 ) {
3214 msgbus.register(
3215 msgbus.switchboard.exec_engine_process,
3216 process_order_event_handler.clone(),
3217 );
3218
3219 simple_cache
3220 .add_instrument(instrument_audusd.clone())
3221 .unwrap();
3222
3223 simple_cache
3224 .add_account(AccountAny::Cash(cash_account(
3225 cash_account_state_million_usd,
3226 )))
3227 .unwrap();
3228
3229 simple_cache.add_quote(quote_audusd).unwrap();
3230
3231 let mut risk_engine = get_risk_engine(
3232 Rc::new(RefCell::new(msgbus)),
3233 Some(Rc::new(RefCell::new(simple_cache))),
3234 None,
3235 None,
3236 false,
3237 );
3238 let order1 = OrderTestBuilder::new(OrderType::Market)
3239 .instrument_id(instrument_audusd.id())
3240 .side(OrderSide::Sell)
3241 .quantity(Quantity::from_str("4920").unwrap())
3242 .build();
3243
3244 let order2 = OrderTestBuilder::new(OrderType::Market)
3245 .instrument_id(instrument_audusd.id())
3246 .side(OrderSide::Sell)
3247 .quantity(Quantity::from_str("5653").unwrap()) .build();
3249
3250 let order_list = OrderList::new(
3251 OrderListId::new("1"),
3252 instrument_audusd.id(),
3253 StrategyId::new("S-001"),
3254 vec![order1, order2],
3255 risk_engine.clock.borrow().timestamp_ns(),
3256 );
3257
3258 let submit_order = SubmitOrderList::new(
3259 trader_id,
3260 client_id_binance,
3261 strategy_id_ema_cross,
3262 instrument_audusd.id(),
3263 client_order_id,
3264 venue_order_id,
3265 order_list,
3266 None,
3267 None,
3268 UUID4::new(),
3269 risk_engine.clock.borrow().timestamp_ns(),
3270 )
3271 .unwrap();
3272
3273 risk_engine.execute(TradingCommand::SubmitOrderList(submit_order));
3274 let saved_process_messages =
3275 get_process_order_event_handler_messages(process_order_event_handler);
3276
3277 assert_eq!(saved_process_messages.len(), 3);
3278
3279 for event in &saved_process_messages {
3280 assert_eq!(event.event_type(), OrderEventType::Denied);
3281 }
3282
3283 assert_eq!(
3285 saved_process_messages.first().unwrap().message().unwrap(),
3286 Ustr::from(
3287 "CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free=1000000.00 USD, cum_notional=1057300.00 USD"
3288 )
3289 );
3290 }
3291
3292 #[rstest]
3294 fn test_submit_order_list_sells_when_multi_currency_cash_account_over_cumulative_notional() {}
3295
3296 #[rstest]
3297 fn test_submit_order_when_reducing_and_buy_order_adds_then_denies(
3298 mut msgbus: MessageBus,
3299 strategy_id_ema_cross: StrategyId,
3300 client_id_binance: ClientId,
3301 trader_id: TraderId,
3302 client_order_id: ClientOrderId,
3303 instrument_xbtusd_bitmex: InstrumentAny,
3304 venue_order_id: VenueOrderId,
3305 process_order_event_handler: ShareableMessageHandler,
3306 execute_order_event_handler: ShareableMessageHandler,
3307 bitmex_cash_account_state_multi: AccountState,
3308 mut simple_cache: Cache,
3309 ) {
3310 msgbus.register(
3311 msgbus.switchboard.exec_engine_process,
3312 process_order_event_handler,
3313 );
3314
3315 msgbus.register(
3316 msgbus.switchboard.exec_engine_execute,
3317 execute_order_event_handler.clone(),
3318 );
3319
3320 simple_cache
3321 .add_instrument(instrument_xbtusd_bitmex.clone())
3322 .unwrap();
3323
3324 simple_cache
3325 .add_account(AccountAny::Cash(cash_account(
3326 bitmex_cash_account_state_multi,
3327 )))
3328 .unwrap();
3329
3330 let quote = QuoteTick::new(
3331 instrument_xbtusd_bitmex.id(),
3332 Price::from("0.075000"),
3333 Price::from("0.075005"),
3334 Quantity::from("50000"),
3335 Quantity::from("50000"),
3336 UnixNanos::default(),
3337 UnixNanos::default(),
3338 );
3339
3340 simple_cache.add_quote(quote).unwrap();
3341
3342 let mut risk_engine = get_risk_engine(
3343 Rc::new(RefCell::new(msgbus)),
3344 Some(Rc::new(RefCell::new(simple_cache))),
3345 None,
3346 None,
3347 false,
3348 );
3349
3350 risk_engine.set_max_notional_per_order(
3351 instrument_xbtusd_bitmex.id(),
3352 Decimal::from_str("10000").unwrap(),
3353 );
3354
3355 let order1 = OrderTestBuilder::new(OrderType::Market)
3356 .instrument_id(instrument_xbtusd_bitmex.id())
3357 .side(OrderSide::Buy)
3358 .quantity(Quantity::from_str("100").unwrap())
3359 .build();
3360
3361 let submit_order1 = SubmitOrder::new(
3362 trader_id,
3363 client_id_binance,
3364 strategy_id_ema_cross,
3365 instrument_xbtusd_bitmex.id(),
3366 client_order_id,
3367 venue_order_id,
3368 order1,
3369 None,
3370 None,
3371 UUID4::new(),
3372 risk_engine.clock.borrow().timestamp_ns(),
3373 )
3374 .unwrap();
3375
3376 risk_engine.execute(TradingCommand::SubmitOrder(submit_order1));
3377 risk_engine.set_trading_state(TradingState::Reducing);
3378
3379 let order2 = OrderTestBuilder::new(OrderType::Market)
3380 .instrument_id(instrument_xbtusd_bitmex.id())
3381 .side(OrderSide::Buy)
3382 .quantity(Quantity::from_str("100").unwrap())
3383 .build();
3384
3385 let submit_order2 = SubmitOrder::new(
3386 trader_id,
3387 client_id_binance,
3388 strategy_id_ema_cross,
3389 instrument_xbtusd_bitmex.id(),
3390 client_order_id,
3391 venue_order_id,
3392 order2,
3393 None,
3394 None,
3395 UUID4::new(),
3396 risk_engine.clock.borrow().timestamp_ns(),
3397 )
3398 .unwrap();
3399
3400 risk_engine.execute(TradingCommand::SubmitOrder(submit_order2));
3401
3402 let saved_execute_messages =
3403 get_execute_order_event_handler_messages(execute_order_event_handler);
3404 assert_eq!(saved_execute_messages.len(), 1);
3405
3406 }
3421
3422 #[rstest]
3423 fn test_submit_order_when_reducing_and_sell_order_adds_then_denies(
3424 mut msgbus: MessageBus,
3425 strategy_id_ema_cross: StrategyId,
3426 client_id_binance: ClientId,
3427 trader_id: TraderId,
3428 client_order_id: ClientOrderId,
3429 instrument_xbtusd_bitmex: InstrumentAny,
3430 venue_order_id: VenueOrderId,
3431 process_order_event_handler: ShareableMessageHandler,
3432 execute_order_event_handler: ShareableMessageHandler,
3433 bitmex_cash_account_state_multi: AccountState,
3434 mut simple_cache: Cache,
3435 ) {
3436 msgbus.register(
3437 msgbus.switchboard.exec_engine_process,
3438 process_order_event_handler,
3439 );
3440
3441 msgbus.register(
3442 msgbus.switchboard.exec_engine_execute,
3443 execute_order_event_handler.clone(),
3444 );
3445
3446 simple_cache
3447 .add_instrument(instrument_xbtusd_bitmex.clone())
3448 .unwrap();
3449
3450 simple_cache
3451 .add_account(AccountAny::Cash(cash_account(
3452 bitmex_cash_account_state_multi,
3453 )))
3454 .unwrap();
3455
3456 let quote = QuoteTick::new(
3457 instrument_xbtusd_bitmex.id(),
3458 Price::from("0.075000"),
3459 Price::from("0.075005"),
3460 Quantity::from("50000"),
3461 Quantity::from("50000"),
3462 UnixNanos::default(),
3463 UnixNanos::default(),
3464 );
3465
3466 simple_cache.add_quote(quote).unwrap();
3467
3468 let mut risk_engine = get_risk_engine(
3469 Rc::new(RefCell::new(msgbus)),
3470 Some(Rc::new(RefCell::new(simple_cache))),
3471 None,
3472 None,
3473 false,
3474 );
3475
3476 risk_engine.set_max_notional_per_order(
3477 instrument_xbtusd_bitmex.id(),
3478 Decimal::from_str("10000").unwrap(),
3479 );
3480
3481 let order1 = OrderTestBuilder::new(OrderType::Market)
3482 .instrument_id(instrument_xbtusd_bitmex.id())
3483 .side(OrderSide::Sell)
3484 .quantity(Quantity::from_str("100").unwrap())
3485 .build();
3486
3487 let submit_order1 = SubmitOrder::new(
3488 trader_id,
3489 client_id_binance,
3490 strategy_id_ema_cross,
3491 instrument_xbtusd_bitmex.id(),
3492 client_order_id,
3493 venue_order_id,
3494 order1,
3495 None,
3496 None,
3497 UUID4::new(),
3498 risk_engine.clock.borrow().timestamp_ns(),
3499 )
3500 .unwrap();
3501
3502 risk_engine.execute(TradingCommand::SubmitOrder(submit_order1));
3503 risk_engine.set_trading_state(TradingState::Reducing);
3504
3505 let order2 = OrderTestBuilder::new(OrderType::Market)
3506 .instrument_id(instrument_xbtusd_bitmex.id())
3507 .side(OrderSide::Sell)
3508 .quantity(Quantity::from_str("100").unwrap())
3509 .build();
3510
3511 let submit_order2 = SubmitOrder::new(
3512 trader_id,
3513 client_id_binance,
3514 strategy_id_ema_cross,
3515 instrument_xbtusd_bitmex.id(),
3516 client_order_id,
3517 venue_order_id,
3518 order2,
3519 None,
3520 None,
3521 UUID4::new(),
3522 risk_engine.clock.borrow().timestamp_ns(),
3523 )
3524 .unwrap();
3525
3526 risk_engine.execute(TradingCommand::SubmitOrder(submit_order2));
3527 let saved_execute_messages =
3528 get_execute_order_event_handler_messages(execute_order_event_handler);
3529 assert_eq!(saved_execute_messages.len(), 1);
3530
3531 }
3546
3547 #[rstest]
3548 fn test_submit_order_when_trading_halted_then_denies_order(
3549 mut msgbus: MessageBus,
3550 strategy_id_ema_cross: StrategyId,
3551 client_id_binance: ClientId,
3552 trader_id: TraderId,
3553 client_order_id: ClientOrderId,
3554 instrument_eth_usdt: InstrumentAny,
3555 venue_order_id: VenueOrderId,
3556 process_order_event_handler: ShareableMessageHandler,
3557 mut simple_cache: Cache,
3558 ) {
3559 msgbus.register(
3560 msgbus.switchboard.exec_engine_process,
3561 process_order_event_handler.clone(),
3562 );
3563
3564 simple_cache
3565 .add_instrument(instrument_eth_usdt.clone())
3566 .unwrap();
3567
3568 let mut risk_engine = get_risk_engine(
3569 Rc::new(RefCell::new(msgbus)),
3570 Some(Rc::new(RefCell::new(simple_cache))),
3571 None,
3572 None,
3573 false,
3574 );
3575 let order = OrderTestBuilder::new(OrderType::Market)
3576 .instrument_id(instrument_eth_usdt.id())
3577 .side(OrderSide::Buy)
3578 .quantity(Quantity::from_str("100").unwrap())
3579 .build();
3580
3581 let submit_order = SubmitOrder::new(
3582 trader_id,
3583 client_id_binance,
3584 strategy_id_ema_cross,
3585 order.instrument_id(),
3586 client_order_id,
3587 venue_order_id,
3588 order,
3589 None,
3590 None,
3591 UUID4::new(),
3592 risk_engine.clock.borrow().timestamp_ns(),
3593 )
3594 .unwrap();
3595
3596 risk_engine.set_trading_state(TradingState::Halted);
3597
3598 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3599
3600 let saved_messages = get_process_order_event_handler_messages(process_order_event_handler);
3602 assert_eq!(saved_messages.len(), 1);
3603 let first_message = saved_messages.first().unwrap();
3604 assert_eq!(first_message.event_type(), OrderEventType::Denied);
3605 assert_eq!(
3606 first_message.message().unwrap(),
3607 Ustr::from("TradingState::HALTED")
3608 );
3609 }
3610
3611 #[rstest]
3612 fn test_submit_order_beyond_rate_limit_then_denies_order(
3613 mut msgbus: MessageBus,
3614 strategy_id_ema_cross: StrategyId,
3615 client_id_binance: ClientId,
3616 trader_id: TraderId,
3617 client_order_id: ClientOrderId,
3618 instrument_audusd: InstrumentAny,
3619 venue_order_id: VenueOrderId,
3620 process_order_event_handler: ShareableMessageHandler,
3621 cash_account_state_million_usd: AccountState,
3622 mut simple_cache: Cache,
3623 ) {
3624 msgbus.register(
3625 msgbus.switchboard.exec_engine_process,
3626 process_order_event_handler.clone(),
3627 );
3628
3629 simple_cache
3630 .add_instrument(instrument_audusd.clone())
3631 .unwrap();
3632
3633 simple_cache
3634 .add_account(AccountAny::Cash(cash_account(
3635 cash_account_state_million_usd,
3636 )))
3637 .unwrap();
3638
3639 let mut risk_engine = get_risk_engine(
3640 Rc::new(RefCell::new(msgbus)),
3641 Some(Rc::new(RefCell::new(simple_cache))),
3642 None,
3643 None,
3644 false,
3645 );
3646 for _i in 0..11 {
3647 let order = OrderTestBuilder::new(OrderType::Market)
3648 .instrument_id(instrument_audusd.id())
3649 .side(OrderSide::Buy)
3650 .quantity(Quantity::from_str("100").unwrap())
3651 .build();
3652
3653 let submit_order = SubmitOrder::new(
3654 trader_id,
3655 client_id_binance,
3656 strategy_id_ema_cross,
3657 order.instrument_id(),
3658 client_order_id,
3659 venue_order_id,
3660 order.clone(),
3661 None,
3662 None,
3663 UUID4::new(),
3664 risk_engine.clock.borrow().timestamp_ns(),
3665 )
3666 .unwrap();
3667
3668 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3669 }
3670
3671 assert_eq!(risk_engine.throttled_submit_order.used(), 1.0);
3672
3673 let saved_process_messages =
3675 get_process_order_event_handler_messages(process_order_event_handler);
3676 assert_eq!(saved_process_messages.len(), 1);
3677 let first_message = saved_process_messages.first().unwrap();
3678 assert_eq!(first_message.event_type(), OrderEventType::Denied);
3679 assert_eq!(
3680 first_message.message().unwrap(),
3681 Ustr::from("REJECTED BY THROTTLER")
3682 );
3683 }
3684
3685 #[rstest]
3686 fn test_submit_order_list_when_trading_halted_then_denies_orders(
3687 mut msgbus: MessageBus,
3688 strategy_id_ema_cross: StrategyId,
3689 client_id_binance: ClientId,
3690 trader_id: TraderId,
3691 client_order_id: ClientOrderId,
3692 instrument_audusd: InstrumentAny,
3693 venue_order_id: VenueOrderId,
3694 process_order_event_handler: ShareableMessageHandler,
3695 cash_account_state_million_usd: AccountState,
3696 mut simple_cache: Cache,
3697 ) {
3698 msgbus.register(
3699 msgbus.switchboard.exec_engine_process,
3700 process_order_event_handler.clone(),
3701 );
3702
3703 simple_cache
3704 .add_instrument(instrument_audusd.clone())
3705 .unwrap();
3706
3707 simple_cache
3708 .add_account(AccountAny::Cash(cash_account(
3709 cash_account_state_million_usd,
3710 )))
3711 .unwrap();
3712
3713 let mut risk_engine = get_risk_engine(
3714 Rc::new(RefCell::new(msgbus)),
3715 Some(Rc::new(RefCell::new(simple_cache))),
3716 None,
3717 None,
3718 false,
3719 );
3720 let entry = OrderTestBuilder::new(OrderType::Market)
3721 .instrument_id(instrument_audusd.id())
3722 .side(OrderSide::Buy)
3723 .quantity(Quantity::from_str("100").unwrap())
3724 .build();
3725
3726 let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
3727 .instrument_id(instrument_audusd.id())
3728 .side(OrderSide::Buy)
3729 .quantity(Quantity::from_str("100").unwrap())
3730 .trigger_price(Price::from_raw(1, 1))
3731 .build();
3732
3733 let take_profit = OrderTestBuilder::new(OrderType::Limit)
3734 .instrument_id(instrument_audusd.id())
3735 .side(OrderSide::Buy)
3736 .quantity(Quantity::from_str("100").unwrap())
3737 .price(Price::from_raw(11, 2))
3738 .build();
3739
3740 let bracket = OrderList::new(
3741 OrderListId::new("1"),
3742 instrument_audusd.id(),
3743 StrategyId::new("S-001"),
3744 vec![entry, stop_loss, take_profit],
3745 risk_engine.clock.borrow().timestamp_ns(),
3746 );
3747
3748 let submit_bracket = SubmitOrderList::new(
3749 trader_id,
3750 client_id_binance,
3751 strategy_id_ema_cross,
3752 bracket.instrument_id,
3753 client_order_id,
3754 venue_order_id,
3755 bracket,
3756 None,
3757 None,
3758 UUID4::new(),
3759 risk_engine.clock.borrow().timestamp_ns(),
3760 )
3761 .unwrap();
3762
3763 risk_engine.set_trading_state(TradingState::Halted);
3764 risk_engine.execute(TradingCommand::SubmitOrderList(submit_bracket));
3765
3766 let saved_process_messages =
3768 get_process_order_event_handler_messages(process_order_event_handler);
3769 assert_eq!(saved_process_messages.len(), 3);
3770
3771 for event in &saved_process_messages {
3772 assert_eq!(event.event_type(), OrderEventType::Denied);
3773 assert_eq!(event.message().unwrap(), Ustr::from("TradingState::HALTED"));
3774 }
3775 }
3776
3777 #[ignore] #[rstest]
3779 fn test_submit_order_list_buys_when_trading_reducing_then_denies_orders(
3780 mut msgbus: MessageBus,
3781 strategy_id_ema_cross: StrategyId,
3782 client_id_binance: ClientId,
3783 trader_id: TraderId,
3784 client_order_id: ClientOrderId,
3785 instrument_xbtusd_bitmex: InstrumentAny,
3786 venue_order_id: VenueOrderId,
3787 process_order_event_handler: ShareableMessageHandler,
3788 execute_order_event_handler: ShareableMessageHandler,
3789 bitmex_cash_account_state_multi: AccountState,
3790 mut simple_cache: Cache,
3791 ) {
3792 msgbus.register(
3793 msgbus.switchboard.exec_engine_process,
3794 process_order_event_handler,
3795 );
3796
3797 msgbus.register(
3798 msgbus.switchboard.exec_engine_execute,
3799 execute_order_event_handler.clone(),
3800 );
3801
3802 simple_cache
3803 .add_instrument(instrument_xbtusd_bitmex.clone())
3804 .unwrap();
3805
3806 simple_cache
3807 .add_account(AccountAny::Cash(cash_account(
3808 bitmex_cash_account_state_multi,
3809 )))
3810 .unwrap();
3811
3812 let quote = QuoteTick::new(
3813 instrument_xbtusd_bitmex.id(),
3814 Price::from("0.075000"),
3815 Price::from("0.075005"),
3816 Quantity::from("50000"),
3817 Quantity::from("50000"),
3818 UnixNanos::default(),
3819 UnixNanos::default(),
3820 );
3821
3822 simple_cache.add_quote(quote).unwrap();
3823
3824 let mut risk_engine = get_risk_engine(
3825 Rc::new(RefCell::new(msgbus)),
3826 Some(Rc::new(RefCell::new(simple_cache))),
3827 None,
3828 None,
3829 false,
3830 );
3831
3832 risk_engine.set_max_notional_per_order(
3833 instrument_xbtusd_bitmex.id(),
3834 Decimal::from_str("10000").unwrap(),
3835 );
3836
3837 let long = OrderTestBuilder::new(OrderType::Market)
3838 .instrument_id(instrument_xbtusd_bitmex.id())
3839 .side(OrderSide::Buy)
3840 .quantity(Quantity::from_str("100").unwrap())
3841 .build();
3842
3843 let submit_order = SubmitOrder::new(
3844 trader_id,
3845 client_id_binance,
3846 strategy_id_ema_cross,
3847 instrument_xbtusd_bitmex.id(),
3848 client_order_id,
3849 venue_order_id,
3850 long,
3851 None,
3852 None,
3853 UUID4::new(),
3854 risk_engine.clock.borrow().timestamp_ns(),
3855 )
3856 .unwrap();
3857
3858 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3859 risk_engine.set_trading_state(TradingState::Reducing);
3860
3861 let entry = OrderTestBuilder::new(OrderType::Market)
3862 .instrument_id(instrument_xbtusd_bitmex.id())
3863 .side(OrderSide::Buy)
3864 .quantity(Quantity::from_str("100").unwrap())
3865 .build();
3866
3867 let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
3868 .instrument_id(instrument_xbtusd_bitmex.id())
3869 .side(OrderSide::Buy)
3870 .quantity(Quantity::from_str("100").unwrap())
3871 .trigger_price(Price::from_raw(11, 1))
3872 .build();
3873
3874 let bracket = OrderList::new(
3883 OrderListId::new("1"),
3884 instrument_xbtusd_bitmex.id(),
3885 StrategyId::new("S-001"),
3886 vec![entry, stop_loss],
3887 risk_engine.clock.borrow().timestamp_ns(),
3888 );
3889
3890 let submit_order_list = SubmitOrderList::new(
3891 trader_id,
3892 client_id_binance,
3893 strategy_id_ema_cross,
3894 instrument_xbtusd_bitmex.id(),
3895 client_order_id,
3896 venue_order_id,
3897 bracket,
3898 None,
3899 None,
3900 UUID4::new(),
3901 risk_engine.clock.borrow().timestamp_ns(),
3902 )
3903 .unwrap();
3904
3905 risk_engine.execute(TradingCommand::SubmitOrderList(submit_order_list));
3906
3907 let saved_execute_messages =
3908 get_execute_order_event_handler_messages(execute_order_event_handler);
3909 assert_eq!(saved_execute_messages.len(), 1);
3910 }
3911
3912 #[ignore] #[rstest]
3914 fn test_submit_order_list_sells_when_trading_reducing_then_denies_orders(
3915 mut msgbus: MessageBus,
3916 strategy_id_ema_cross: StrategyId,
3917 client_id_binance: ClientId,
3918 trader_id: TraderId,
3919 client_order_id: ClientOrderId,
3920 instrument_xbtusd_bitmex: InstrumentAny,
3921 venue_order_id: VenueOrderId,
3922 process_order_event_handler: ShareableMessageHandler,
3923 execute_order_event_handler: ShareableMessageHandler,
3924 bitmex_cash_account_state_multi: AccountState,
3925 mut simple_cache: Cache,
3926 ) {
3927 msgbus.register(
3928 msgbus.switchboard.exec_engine_process,
3929 process_order_event_handler,
3930 );
3931
3932 msgbus.register(
3933 msgbus.switchboard.exec_engine_execute,
3934 execute_order_event_handler.clone(),
3935 );
3936
3937 simple_cache
3938 .add_instrument(instrument_xbtusd_bitmex.clone())
3939 .unwrap();
3940
3941 simple_cache
3942 .add_account(AccountAny::Cash(cash_account(
3943 bitmex_cash_account_state_multi,
3944 )))
3945 .unwrap();
3946
3947 let quote = QuoteTick::new(
3948 instrument_xbtusd_bitmex.id(),
3949 Price::from("0.075000"),
3950 Price::from("0.075005"),
3951 Quantity::from("50000"),
3952 Quantity::from("50000"),
3953 UnixNanos::default(),
3954 UnixNanos::default(),
3955 );
3956
3957 simple_cache.add_quote(quote).unwrap();
3958
3959 let mut risk_engine = get_risk_engine(
3960 Rc::new(RefCell::new(msgbus)),
3961 Some(Rc::new(RefCell::new(simple_cache))),
3962 None,
3963 None,
3964 false,
3965 );
3966
3967 risk_engine.set_max_notional_per_order(
3968 instrument_xbtusd_bitmex.id(),
3969 Decimal::from_str("10000").unwrap(),
3970 );
3971
3972 let short = OrderTestBuilder::new(OrderType::Market)
3973 .instrument_id(instrument_xbtusd_bitmex.id())
3974 .side(OrderSide::Sell)
3975 .quantity(Quantity::from_str("100").unwrap())
3976 .build();
3977
3978 let submit_order = SubmitOrder::new(
3979 trader_id,
3980 client_id_binance,
3981 strategy_id_ema_cross,
3982 instrument_xbtusd_bitmex.id(),
3983 client_order_id,
3984 venue_order_id,
3985 short,
3986 None,
3987 None,
3988 UUID4::new(),
3989 risk_engine.clock.borrow().timestamp_ns(),
3990 )
3991 .unwrap();
3992
3993 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
3994 risk_engine.set_trading_state(TradingState::Reducing);
3995
3996 let entry = OrderTestBuilder::new(OrderType::Market)
3997 .instrument_id(instrument_xbtusd_bitmex.id())
3998 .side(OrderSide::Sell)
3999 .quantity(Quantity::from_str("100").unwrap())
4000 .build();
4001
4002 let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
4003 .instrument_id(instrument_xbtusd_bitmex.id())
4004 .side(OrderSide::Sell)
4005 .quantity(Quantity::from_str("100").unwrap())
4006 .trigger_price(Price::from_raw(11, 1))
4007 .build();
4008
4009 let take_profit = OrderTestBuilder::new(OrderType::Limit)
4010 .instrument_id(instrument_xbtusd_bitmex.id())
4011 .side(OrderSide::Sell)
4012 .quantity(Quantity::from_str("100").unwrap())
4013 .price(Price::from_raw(12, 1))
4014 .build();
4015
4016 let bracket = OrderList::new(
4017 OrderListId::new("1"),
4018 instrument_xbtusd_bitmex.id(),
4019 StrategyId::new("S-001"),
4020 vec![entry, stop_loss, take_profit],
4021 risk_engine.clock.borrow().timestamp_ns(),
4022 );
4023
4024 let submit_order_list = SubmitOrderList::new(
4025 trader_id,
4026 client_id_binance,
4027 strategy_id_ema_cross,
4028 instrument_xbtusd_bitmex.id(),
4029 client_order_id,
4030 venue_order_id,
4031 bracket,
4032 None,
4033 None,
4034 UUID4::new(),
4035 risk_engine.clock.borrow().timestamp_ns(),
4036 )
4037 .unwrap();
4038
4039 risk_engine.execute(TradingCommand::SubmitOrderList(submit_order_list));
4040
4041 let saved_execute_messages =
4042 get_execute_order_event_handler_messages(execute_order_event_handler);
4043 assert_eq!(saved_execute_messages.len(), 1);
4044 }
4045
4046 #[rstest]
4048 fn test_submit_bracket_with_default_settings_sends_to_client(
4049 mut msgbus: MessageBus,
4050 strategy_id_ema_cross: StrategyId,
4051 client_id_binance: ClientId,
4052 trader_id: TraderId,
4053 client_order_id: ClientOrderId,
4054 instrument_audusd: InstrumentAny,
4055 venue_order_id: VenueOrderId,
4056 process_order_event_handler: ShareableMessageHandler,
4057 cash_account_state_million_usd: AccountState,
4058 mut simple_cache: Cache,
4059 ) {
4060 msgbus.register(
4061 msgbus.switchboard.exec_engine_process,
4062 process_order_event_handler,
4063 );
4064
4065 simple_cache
4066 .add_instrument(instrument_audusd.clone())
4067 .unwrap();
4068
4069 simple_cache
4070 .add_account(AccountAny::Cash(cash_account(
4071 cash_account_state_million_usd,
4072 )))
4073 .unwrap();
4074
4075 let risk_engine = get_risk_engine(
4076 Rc::new(RefCell::new(msgbus)),
4077 Some(Rc::new(RefCell::new(simple_cache))),
4078 None,
4079 None,
4080 false,
4081 );
4082 let entry = OrderTestBuilder::new(OrderType::Market)
4083 .instrument_id(instrument_audusd.id())
4084 .side(OrderSide::Buy)
4085 .quantity(Quantity::from_str("100").unwrap())
4086 .build();
4087
4088 let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
4089 .instrument_id(instrument_audusd.id())
4090 .side(OrderSide::Buy)
4091 .quantity(Quantity::from_str("100").unwrap())
4092 .trigger_price(Price::from_raw(1, 1))
4093 .build();
4094
4095 let take_profit = OrderTestBuilder::new(OrderType::Limit)
4096 .instrument_id(instrument_audusd.id())
4097 .side(OrderSide::Buy)
4098 .quantity(Quantity::from_str("100").unwrap())
4099 .price(Price::from_raw(1001, 4))
4100 .build();
4101
4102 let bracket = OrderList::new(
4103 OrderListId::new("1"),
4104 instrument_audusd.id(),
4105 StrategyId::new("S-001"),
4106 vec![entry, stop_loss, take_profit],
4107 risk_engine.clock.borrow().timestamp_ns(),
4108 );
4109
4110 let _submit_bracket = SubmitOrderList::new(
4111 trader_id,
4112 client_id_binance,
4113 strategy_id_ema_cross,
4114 bracket.instrument_id,
4115 client_order_id,
4116 venue_order_id,
4117 bracket,
4118 None,
4119 None,
4120 UUID4::new(),
4121 risk_engine.clock.borrow().timestamp_ns(),
4122 )
4123 .unwrap();
4124
4125 }
4133
4134 #[rstest]
4135 fn test_submit_bracket_with_emulated_orders_sends_to_emulator() {}
4136
4137 #[rstest]
4138 fn test_submit_bracket_order_when_instrument_not_in_cache_then_denies(
4139 mut msgbus: MessageBus,
4140 strategy_id_ema_cross: StrategyId,
4141 client_id_binance: ClientId,
4142 trader_id: TraderId,
4143 client_order_id: ClientOrderId,
4144 instrument_audusd: InstrumentAny,
4145 venue_order_id: VenueOrderId,
4146 process_order_event_handler: ShareableMessageHandler,
4147 cash_account_state_million_usd: AccountState,
4148 mut simple_cache: Cache,
4149 ) {
4150 msgbus.register(
4151 msgbus.switchboard.exec_engine_process,
4152 process_order_event_handler.clone(),
4153 );
4154
4155 simple_cache
4156 .add_account(AccountAny::Cash(cash_account(
4157 cash_account_state_million_usd,
4158 )))
4159 .unwrap();
4160
4161 let mut risk_engine = get_risk_engine(
4162 Rc::new(RefCell::new(msgbus)),
4163 Some(Rc::new(RefCell::new(simple_cache))),
4164 None,
4165 None,
4166 false,
4167 );
4168 let entry = OrderTestBuilder::new(OrderType::Market)
4169 .instrument_id(instrument_audusd.id())
4170 .side(OrderSide::Buy)
4171 .quantity(Quantity::from_str("100").unwrap())
4172 .build();
4173
4174 let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
4175 .instrument_id(instrument_audusd.id())
4176 .side(OrderSide::Buy)
4177 .quantity(Quantity::from_str("100").unwrap())
4178 .trigger_price(Price::from_raw(1, 1))
4179 .build();
4180
4181 let take_profit = OrderTestBuilder::new(OrderType::Limit)
4182 .instrument_id(instrument_audusd.id())
4183 .side(OrderSide::Buy)
4184 .quantity(Quantity::from_str("100").unwrap())
4185 .price(Price::from_raw(1001, 4))
4186 .build();
4187
4188 let bracket = OrderList::new(
4189 OrderListId::new("1"),
4190 instrument_audusd.id(),
4191 StrategyId::new("S-001"),
4192 vec![entry, stop_loss, take_profit],
4193 risk_engine.clock.borrow().timestamp_ns(),
4194 );
4195
4196 let submit_bracket = SubmitOrderList::new(
4197 trader_id,
4198 client_id_binance,
4199 strategy_id_ema_cross,
4200 bracket.instrument_id,
4201 client_order_id,
4202 venue_order_id,
4203 bracket,
4204 None,
4205 None,
4206 UUID4::new(),
4207 risk_engine.clock.borrow().timestamp_ns(),
4208 )
4209 .unwrap();
4210
4211 risk_engine.execute(TradingCommand::SubmitOrderList(submit_bracket));
4212
4213 let saved_process_messages =
4215 get_process_order_event_handler_messages(process_order_event_handler);
4216 assert_eq!(saved_process_messages.len(), 3);
4217
4218 for event in &saved_process_messages {
4219 assert_eq!(event.event_type(), OrderEventType::Denied);
4220 assert_eq!(
4221 event.message().unwrap(),
4222 Ustr::from("no instrument found for AUD/USD.SIM")
4223 );
4224 }
4225 }
4226
4227 #[rstest]
4228 fn test_submit_order_for_emulation_sends_command_to_emulator() {}
4229
4230 #[rstest]
4232 fn test_modify_order_when_no_order_found_logs_error(
4233 mut msgbus: MessageBus,
4234 strategy_id_ema_cross: StrategyId,
4235 client_id_binance: ClientId,
4236 trader_id: TraderId,
4237 client_order_id: ClientOrderId,
4238 instrument_audusd: InstrumentAny,
4239 venue_order_id: VenueOrderId,
4240 process_order_event_handler: ShareableMessageHandler,
4241 cash_account_state_million_usd: AccountState,
4242 mut simple_cache: Cache,
4243 ) {
4244 msgbus.register(
4245 msgbus.switchboard.exec_engine_process,
4246 process_order_event_handler.clone(),
4247 );
4248
4249 simple_cache
4250 .add_instrument(instrument_audusd.clone())
4251 .unwrap();
4252
4253 simple_cache
4254 .add_account(AccountAny::Cash(cash_account(
4255 cash_account_state_million_usd,
4256 )))
4257 .unwrap();
4258
4259 let mut risk_engine = get_risk_engine(
4260 Rc::new(RefCell::new(msgbus)),
4261 Some(Rc::new(RefCell::new(simple_cache))),
4262 None,
4263 None,
4264 false,
4265 );
4266 let modify_order = ModifyOrder::new(
4267 trader_id,
4268 client_id_binance,
4269 strategy_id_ema_cross,
4270 instrument_audusd.id(),
4271 client_order_id,
4272 venue_order_id,
4273 None,
4274 None,
4275 None,
4276 UUID4::new(),
4277 risk_engine.clock.borrow().timestamp_ns(),
4278 )
4279 .unwrap();
4280
4281 risk_engine.execute(TradingCommand::ModifyOrder(modify_order));
4282
4283 let saved_process_messages =
4284 get_process_order_event_handler_messages(process_order_event_handler);
4285 assert_eq!(saved_process_messages.len(), 0);
4286 }
4287
4288 #[rstest]
4289 fn test_modify_order_beyond_rate_limit_then_rejects(
4290 mut msgbus: MessageBus,
4291 strategy_id_ema_cross: StrategyId,
4292 client_id_binance: ClientId,
4293 trader_id: TraderId,
4294 client_order_id: ClientOrderId,
4295 instrument_audusd: InstrumentAny,
4296 venue_order_id: VenueOrderId,
4297 process_order_event_handler: ShareableMessageHandler,
4298 cash_account_state_million_usd: AccountState,
4299 mut simple_cache: Cache,
4300 ) {
4301 msgbus.register(
4302 msgbus.switchboard.exec_engine_process,
4303 process_order_event_handler.clone(),
4304 );
4305
4306 simple_cache
4307 .add_instrument(instrument_audusd.clone())
4308 .unwrap();
4309
4310 simple_cache
4311 .add_account(AccountAny::Cash(cash_account(
4312 cash_account_state_million_usd,
4313 )))
4314 .unwrap();
4315
4316 let order = OrderTestBuilder::new(OrderType::StopMarket)
4317 .instrument_id(instrument_audusd.id())
4318 .side(OrderSide::Buy)
4319 .quantity(Quantity::from_str("100").unwrap())
4320 .trigger_price(Price::from_raw(10001, 4))
4321 .build();
4322
4323 simple_cache
4324 .add_order(order, None, Some(client_id_binance), true)
4325 .unwrap();
4326
4327 let mut risk_engine = get_risk_engine(
4328 Rc::new(RefCell::new(msgbus)),
4329 Some(Rc::new(RefCell::new(simple_cache))),
4330 None,
4331 None,
4332 false,
4333 );
4334 for i in 0..11 {
4335 let modify_order = ModifyOrder::new(
4336 trader_id,
4337 client_id_binance,
4338 strategy_id_ema_cross,
4339 instrument_audusd.id(),
4340 client_order_id,
4341 venue_order_id,
4342 Some(Quantity::from_str("100").unwrap()),
4343 Some(Price::from_raw(100011 + i, 5)),
4344 None,
4345 UUID4::new(),
4346 risk_engine.clock.borrow().timestamp_ns(),
4347 )
4348 .unwrap();
4349
4350 risk_engine.execute(TradingCommand::ModifyOrder(modify_order));
4351 }
4352
4353 assert_eq!(risk_engine.throttled_modify_order.used(), 1.0);
4354
4355 let saved_process_messages =
4357 get_process_order_event_handler_messages(process_order_event_handler);
4358 assert_eq!(saved_process_messages.len(), 6);
4359 let first_message = saved_process_messages.first().unwrap();
4360 assert_eq!(first_message.event_type(), OrderEventType::ModifyRejected);
4361 assert_eq!(
4362 first_message.message().unwrap(),
4363 Ustr::from("Exceeded MAX_ORDER_MODIFY_RATE")
4364 );
4365 }
4366
4367 #[rstest]
4368 fn test_modify_order_with_default_settings_then_sends_to_client(
4369 mut msgbus: MessageBus,
4370 strategy_id_ema_cross: StrategyId,
4371 client_id_binance: ClientId,
4372 trader_id: TraderId,
4373 client_order_id: ClientOrderId,
4374 instrument_audusd: InstrumentAny,
4375 venue_order_id: VenueOrderId,
4376 process_order_event_handler: ShareableMessageHandler,
4377 execute_order_event_handler: ShareableMessageHandler,
4378 cash_account_state_million_usd: AccountState,
4379 mut simple_cache: Cache,
4380 ) {
4381 msgbus.register(
4382 msgbus.switchboard.exec_engine_process,
4383 process_order_event_handler,
4384 );
4385
4386 msgbus.register(
4387 msgbus.switchboard.exec_engine_execute,
4388 execute_order_event_handler.clone(),
4389 );
4390
4391 simple_cache
4392 .add_instrument(instrument_audusd.clone())
4393 .unwrap();
4394
4395 simple_cache
4396 .add_account(AccountAny::Cash(cash_account(
4397 cash_account_state_million_usd,
4398 )))
4399 .unwrap();
4400
4401 let order = OrderTestBuilder::new(OrderType::StopMarket)
4402 .instrument_id(instrument_audusd.id())
4403 .side(OrderSide::Buy)
4404 .quantity(Quantity::from_str("100").unwrap())
4405 .trigger_price(Price::from_raw(10001, 4))
4406 .build();
4407
4408 simple_cache
4409 .add_order(order.clone(), None, Some(client_id_binance), true)
4410 .unwrap();
4411
4412 let mut risk_engine = get_risk_engine(
4413 Rc::new(RefCell::new(msgbus)),
4414 Some(Rc::new(RefCell::new(simple_cache))),
4415 None,
4416 None,
4417 false,
4418 );
4419 let submit_order = SubmitOrder::new(
4420 trader_id,
4421 client_id_binance,
4422 strategy_id_ema_cross,
4423 instrument_audusd.id(),
4424 client_order_id,
4425 venue_order_id,
4426 order,
4427 None,
4428 None,
4429 UUID4::new(),
4430 risk_engine.clock.borrow().timestamp_ns(),
4431 )
4432 .unwrap();
4433
4434 let modify_order = ModifyOrder::new(
4435 trader_id,
4436 client_id_binance,
4437 strategy_id_ema_cross,
4438 instrument_audusd.id(),
4439 client_order_id,
4440 venue_order_id,
4441 Some(Quantity::from_str("100").unwrap()),
4442 Some(Price::from_raw(100011, 5)),
4443 None,
4444 UUID4::new(),
4445 risk_engine.clock.borrow().timestamp_ns(),
4446 )
4447 .unwrap();
4448
4449 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
4450 risk_engine.execute(TradingCommand::ModifyOrder(modify_order));
4451
4452 let saved_execute_messages =
4453 get_execute_order_event_handler_messages(execute_order_event_handler);
4454 assert_eq!(saved_execute_messages.len(), 2);
4455 assert_eq!(
4456 saved_execute_messages.first().unwrap().instrument_id(),
4457 instrument_audusd.id()
4458 );
4459 }
4460
4461 #[rstest]
4462 fn test_modify_order_for_emulated_order_then_sends_to_emulator() {}
4463
4464 #[rstest]
4465 fn test_submit_order_when_market_order_and_over_free_balance_then_denies_with_betting_account(
4466 mut msgbus: MessageBus,
4467 strategy_id_ema_cross: StrategyId,
4468 client_id_binance: ClientId,
4469 trader_id: TraderId,
4470 client_order_id: ClientOrderId,
4471 instrument_audusd: InstrumentAny,
4472 venue_order_id: VenueOrderId,
4473 process_order_event_handler: ShareableMessageHandler,
4474 cash_account_state_million_usd: AccountState,
4475 quote_audusd: QuoteTick,
4476 mut simple_cache: Cache,
4477 ) {
4478 msgbus.register(
4479 msgbus.switchboard.exec_engine_process,
4480 process_order_event_handler.clone(),
4481 );
4482
4483 simple_cache
4484 .add_instrument(instrument_audusd.clone())
4485 .unwrap();
4486
4487 simple_cache
4488 .add_account(AccountAny::Margin(margin_account(
4489 cash_account_state_million_usd,
4490 )))
4491 .unwrap();
4492
4493 simple_cache.add_quote(quote_audusd).unwrap();
4494
4495 let mut risk_engine = get_risk_engine(
4496 Rc::new(RefCell::new(msgbus)),
4497 Some(Rc::new(RefCell::new(simple_cache))),
4498 None,
4499 None,
4500 false,
4501 );
4502 let order = OrderTestBuilder::new(OrderType::Market)
4503 .instrument_id(instrument_audusd.id())
4504 .side(OrderSide::Buy)
4505 .quantity(Quantity::from_str("100000").unwrap())
4506 .build();
4507
4508 let submit_order = SubmitOrder::new(
4509 trader_id,
4510 client_id_binance,
4511 strategy_id_ema_cross,
4512 instrument_audusd.id(),
4513 client_order_id,
4514 venue_order_id,
4515 order,
4516 None,
4517 None,
4518 UUID4::new(),
4519 risk_engine.clock.borrow().timestamp_ns(),
4520 )
4521 .unwrap();
4522
4523 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
4524 let saved_process_messages =
4525 get_process_order_event_handler_messages(process_order_event_handler);
4526 assert_eq!(saved_process_messages.len(), 0); }
4528
4529 #[rstest]
4530 fn test_submit_order_for_less_than_max_cum_transaction_value_adausdt_with_crypto_cash_account(
4531 mut msgbus: MessageBus,
4532 strategy_id_ema_cross: StrategyId,
4533 client_id_binance: ClientId,
4534 trader_id: TraderId,
4535 client_order_id: ClientOrderId,
4536 instrument_xbtusd_bitmex: InstrumentAny,
4537 venue_order_id: VenueOrderId,
4538 process_order_event_handler: ShareableMessageHandler,
4539 execute_order_event_handler: ShareableMessageHandler,
4540 bitmex_cash_account_state_multi: AccountState,
4541 mut simple_cache: Cache,
4542 ) {
4543 let quote = QuoteTick::new(
4544 instrument_xbtusd_bitmex.id(),
4545 Price::from("0.6109"),
4546 Price::from("0.6110"),
4547 Quantity::from("1000"),
4548 Quantity::from("1000"),
4549 UnixNanos::default(),
4550 UnixNanos::default(),
4551 );
4552
4553 msgbus.register(
4554 msgbus.switchboard.exec_engine_process,
4555 process_order_event_handler.clone(),
4556 );
4557
4558 msgbus.register(
4559 msgbus.switchboard.exec_engine_execute,
4560 execute_order_event_handler.clone(),
4561 );
4562
4563 simple_cache
4564 .add_instrument(instrument_xbtusd_bitmex.clone())
4565 .unwrap();
4566
4567 simple_cache
4568 .add_account(AccountAny::Cash(cash_account(
4569 bitmex_cash_account_state_multi,
4570 )))
4571 .unwrap();
4572
4573 simple_cache.add_quote(quote).unwrap();
4574
4575 let mut risk_engine = get_risk_engine(
4576 Rc::new(RefCell::new(msgbus)),
4577 Some(Rc::new(RefCell::new(simple_cache))),
4578 None,
4579 None,
4580 false,
4581 );
4582 let order = OrderTestBuilder::new(OrderType::Market)
4583 .instrument_id(instrument_xbtusd_bitmex.id())
4584 .side(OrderSide::Buy)
4585 .quantity(Quantity::from_str("440").unwrap())
4586 .build();
4587
4588 let submit_order = SubmitOrder::new(
4589 trader_id,
4590 client_id_binance,
4591 strategy_id_ema_cross,
4592 instrument_xbtusd_bitmex.id(),
4593 client_order_id,
4594 venue_order_id,
4595 order,
4596 None,
4597 None,
4598 UUID4::new(),
4599 risk_engine.clock.borrow().timestamp_ns(),
4600 )
4601 .unwrap();
4602
4603 risk_engine.execute(TradingCommand::SubmitOrder(submit_order));
4604 let saved_process_messages =
4605 get_process_order_event_handler_messages(process_order_event_handler);
4606 assert_eq!(saved_process_messages.len(), 0);
4607
4608 let saved_execute_messages =
4609 get_execute_order_event_handler_messages(execute_order_event_handler);
4610 assert_eq!(saved_execute_messages.len(), 1);
4611 assert_eq!(
4612 saved_execute_messages.first().unwrap().instrument_id(),
4613 instrument_xbtusd_bitmex.id()
4614 );
4615 }
4616
4617 #[rstest]
4618 fn test_partial_fill_and_full_fill_account_balance_correct() {}
4619}