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