nautilus_risk/engine/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Provides a generic `ExecutionEngine` for all environments.
17
18use 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    // -- COMMANDS --------------------------------------------------------------------------------
241
242    pub fn execute(&mut self, command: TradingCommand) {
243        // This will extend to other commands such as `RiskCommand`
244        self.handle_command(command);
245    }
246
247    pub fn process(&mut self, event: OrderEventAny) {
248        // This will extend to other events such as `RiskEvent`
249        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        // TODO: Create a new Event "TradingStateChanged" in OrderEventAny enum.
263        // let event = OrderEventAny::TradingStateChanged(TradingStateChanged::new(..,self.trading_state,..));
264
265        msgbus::publish(&Ustr::from("events.risk"), &"message"); // TODO: Send the new Event here
266
267        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    // -- COMMAND HANDLERS ------------------------------------------------------------------------
278
279    // Renamed from `execute_command`
280    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; // Denied
320                    }
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; // Denied
344        };
345
346        ////////////////////////////////////////////////////////////////////////////////
347        // PRE-TRADE ORDER(S) CHECKS
348        ////////////////////////////////////////////////////////////////////////////////
349        if !self.check_order(instrument.clone(), order.clone()) {
350            return; // Denied
351        }
352
353        if !self.check_orders_risk(instrument.clone(), Vec::from([order.clone()])) {
354            return; // Denied
355        }
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; // Denied
379        };
380
381        ////////////////////////////////////////////////////////////////////////////////
382        // PRE-TRADE ORDER(S) CHECKS
383        ////////////////////////////////////////////////////////////////////////////////
384        for order in command.order_list.orders.clone() {
385            if !self.check_order(instrument.clone(), order) {
386                return; // Denied
387            }
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; // Denied
396        }
397
398        self.execution_gateway(instrument, TradingCommand::SubmitOrderList(command));
399    }
400
401    fn handle_modify_order(&self, command: ModifyOrder) {
402        ////////////////////////////////////////////////////////////////////////////////
403        // VALIDATE COMMAND
404        ////////////////////////////////////////////////////////////////////////////////
405        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        // Get instrument for orders
441        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; // Denied
454        };
455
456        // Check Price
457        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; // Denied
461        }
462
463        // Check Trigger
464        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; // Denied
468        }
469
470        // Check Quantity
471        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; // Denied
475        }
476
477        // Check TradingState
478        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        // TODO: Fix message bus usage
502        // self.throttled_modify_order.send(command);
503    }
504
505    // -- PRE-TRADE CHECKS ------------------------------------------------------------------------
506
507    fn check_order(&self, instrument: InstrumentAny, order: OrderAny) -> bool {
508        ////////////////////////////////////////////////////////////////////////////////
509        // VALIDATION CHECKS
510        ////////////////////////////////////////////////////////////////////////////////
511        if !self.check_order_price(instrument.clone(), order.clone())
512            || !self.check_order_quantity(instrument, order)
513        {
514            return false; // Denied
515        }
516
517        true
518    }
519
520    fn check_order_price(&self, instrument: InstrumentAny, order: OrderAny) -> bool {
521        ////////////////////////////////////////////////////////////////////////////////
522        // CHECK PRICE
523        ////////////////////////////////////////////////////////////////////////////////
524        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; // Denied
529            }
530        }
531
532        ////////////////////////////////////////////////////////////////////////////////
533        // CHECK TRIGGER
534        ////////////////////////////////////////////////////////////////////////////////
535        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; // Denied
540            }
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; // Denied
551        }
552
553        true
554    }
555
556    fn check_orders_risk(&self, instrument: InstrumentAny, orders: Vec<OrderAny>) -> bool {
557        ////////////////////////////////////////////////////////////////////////////////
558        // CHECK TRIGGER
559        ////////////////////////////////////////////////////////////////////////////////
560        let mut last_px: Option<Price> = None;
561        let mut max_notional: Option<Money> = None;
562
563        // Determine max notional
564        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        // Get account for risk checks
575        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; // TODO: Temporary early return until handling routing/multiple venues
585        };
586        let cash_account = match account {
587            AccountAny::Cash(cash_account) => cash_account,
588            AccountAny::Margin(_) => return true, // TODO: Determine risk controls for margin
589        };
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            // Determine last price based on order type
600            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", // TODO: Use last_trade += offset
635                            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            // Check MAX notional per order limit
658            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; // Denied
667                }
668            }
669
670            // Check MIN notional instrument limit
671            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; // Denied
680                }
681            }
682
683            // // Check MAX notional instrument limit
684            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; // Denied
693                }
694            }
695
696            // Calculate OrderBalanceImpact (valid for CashAccount only)
697            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; // Denied
746                    }
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; // Denied
769                        }
770                    }
771                }
772                // Account is already of type Cash, so no check
773                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; // Denied
811                        }
812                    }
813                }
814            }
815        }
816
817        // Finally
818        true // Passed
819    }
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        // Check precision
848        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        // Check maximum quantity
858        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        // // Check minimum quantity
867        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    // -- DENIALS ---------------------------------------------------------------------------------
879
880    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    // -- EGRESS ----------------------------------------------------------------------------------
957
958    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                    // TODO: Fix message bus usage
1020                    // self.throttled_submit_order.send(submit_order);
1021                }
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        // We intend to extend the risk engine to be able to handle additional events.
1036        // For now we just log.
1037        if self.config.debug {
1038            log::debug!("{}{} {event:?}", RECV, EVT);
1039        }
1040    }
1041}
1042
1043////////////////////////////////////////////////////////////////////////////////
1044// Tests
1045////////////////////////////////////////////////////////////////////////////////
1046#[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    // Market buy order with corresponding fill
1135    #[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    // Market sell order
1145    #[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, // multi cash account
1216        )
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    // Helpers
1277    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    // Tests
1394    #[rstest]
1395    fn test_bypass_config_risk_engine() {
1396        let risk_engine = get_risk_engine(
1397            None, None, None, true, // <-- Bypassing pre-trade risk checks for backtest
1398        );
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    // SUBMIT ORDER TESTS
1527    #[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        // TODO: Limit -> Market
1619        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        // Act
1783        risk_engine.execute(TradingCommand::SubmitOrder(submit_order3));
1784
1785        // Assert: TODO
1786        // assert_eq!(order1.status(), OrderStatus::Filled);
1787        // assert_eq!(order2.status(), OrderStatus::Filled);
1788        // assert_eq!(order3.status(), OrderStatus::Denied);
1789
1790        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        // Act
1892        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        // Assert: TODO
1910        // assert_eq!(order1.status(), OrderStatus::Filled);
1911        // assert_eq!(order2.status(), OrderStatus::Denied);
1912
1913        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")), // <-- Custom position ID
1973            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)) // <- Invalid price
2094            .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)) // <- Invalid price
2167            .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)") // TODO: fix
2197        );
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)) // <- Invalid price
2238            .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        // assert!(saved_process_messages
2265        //     .first()
2266        //     .unwrap()
2267        //     .message()
2268        //     .unwrap()
2269        //     .contains(&format!("invalid (precision {PRECISION})")));
2270    }
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()) // <--- over free balance
2992            .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        // The actual reason is in the first denial; the rest will show `OrderListID` as Denied.
3028        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()) // <--- over free balance
3078            .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        // Correct reason is in First deny, rest will show OrderList`ID` Denied.
3114        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    // TODO: After ExecutionClient
3123    #[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        // TODO: currently, portfolio.is_net_long() is false, because portfolio.net_position() is not updated
3231        // assert!(risk_engine.portfolio.is_net_long(&instrument_xbtusd_bitmex.id()));
3232        // let saved_process_messages =
3233        //     get_process_order_event_handler_messages(process_order_event_handler);
3234        // assert_eq!(saved_process_messages.len(), 1);
3235
3236        // assert_eq!(
3237        //     saved_process_messages.first().unwrap().event_type(),
3238        //     OrderEventType::Denied
3239        // );
3240        // assert_eq!(
3241        //     saved_process_messages.first().unwrap().message().unwrap(),
3242        //     "BUY when TradingState.REDUCING and LONG"
3243        // );
3244    }
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        // TODO: currently, portfolio.is_net_short() is false, because portfolio.net_position() is not updated
3350        // assert!(risk_engine.portfolio.is_net_short(&instrument_xbtusd_bitmex.id()));
3351        // let saved_process_messages =
3352        //     get_process_order_event_handler_messages(process_order_event_handler);
3353        // assert_eq!(saved_process_messages.len(), 1);
3354
3355        // assert_eq!(
3356        //     saved_process_messages.first().unwrap().event_type(),
3357        //     OrderEventType::Denied
3358        // );
3359        // assert_eq!(
3360        //     saved_process_messages.first().unwrap().message().unwrap(),
3361        //     "SELL when TradingState.REDUCING and SHORT"
3362        // );
3363    }
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        // Get messages and test
3413        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        // Get messages and test
3481        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        // Get messages and test
3568        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] // TODO: Revisit after high-precision merged
3579    #[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        // TODO: attempt to add with overflow
3669        // let take_profit = OrderTestBuilder::new(OrderType::Limit)
3670        //     .instrument_id(instrument_xbtusd_bitmex.id())
3671        //     .side(OrderSide::Buy)
3672        //     .quantity(Quantity::from_str("100").unwrap())
3673        //     .price(Price::from_raw(12, 1))
3674        //     .build();
3675
3676        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] // TODO: Revisit after high-precision merged
3707    #[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    // SUBMIT BRACKET ORDER TESTS
3834    #[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        // risk_engine.execute(TradingCommand::SubmitOrderList(submit_bracket));
3908
3909        // Get messages and test
3910        // TODO: complete fn execution_gateway
3911        // let saved_process_messages =
3912        //     get_process_order_event_handler_messages(process_order_event_handler);
3913        // assert_eq!(saved_process_messages.len(), 0);
3914    }
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        // Get messages and test
3990        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    // MODIFY ORDER TESTS
4007    #[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        // Get messages and test
4121        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); // Currently, it executes because check_orders_risk returns true for margin_account
4280    }
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}