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