1pub mod config;
19
20use std::{cell::RefCell, fmt::Debug, rc::Rc};
21
22use ahash::AHashMap;
23use config::RiskEngineConfig;
24use indexmap::IndexMap;
25use nautilus_common::{
26 cache::Cache,
27 clock::Clock,
28 logging::{CMD, EVT, RECV},
29 messages::{
30 execution::{BatchModifyOrders, ModifyOrder, SubmitOrder, SubmitOrderList, TradingCommand},
31 system::trading::TradingStateChanged,
32 },
33 msgbus,
34 msgbus::{MessagingSwitchboard, TypedHandler, TypedIntoHandler, get_message_bus},
35 runner::try_get_trading_cmd_sender,
36 throttler::{RateLimit, Throttler},
37};
38use nautilus_core::{UUID4, WeakCell};
39use nautilus_execution::trailing::{
40 trailing_stop_calculate_with_bid_ask, trailing_stop_calculate_with_last,
41};
42use nautilus_model::{
43 accounts::{Account, AccountAny},
44 enums::{
45 OrderSide, OrderStatus, PositionSide, TimeInForce, TradingState, TrailingOffsetType,
46 TriggerType,
47 },
48 events::{OrderDenied, OrderDeniedReason, OrderEventAny, OrderModifyRejected, PositionEvent},
49 identifiers::{AccountId, InstrumentId},
50 instruments::{Instrument, InstrumentAny},
51 orders::{Order, OrderAny},
52 types::{Currency, Money, Price, Quantity, money::MoneyRaw, quantity::QuantityRaw},
53};
54use nautilus_portfolio::Portfolio;
55use rust_decimal::Decimal;
56use ustr::Ustr;
57
58fn format_rate_limit(rate_limit: &RateLimit) -> String {
59 let interval_ns = rate_limit.interval_ns();
60 let limit = rate_limit.limit();
61 let total_secs = interval_ns / 1_000_000_000;
62 let remainder_ns = interval_ns % 1_000_000_000;
63 let hours = total_secs / 3600;
64 let minutes = (total_secs % 3600) / 60;
65 let seconds = total_secs % 60;
66
67 if remainder_ns == 0 {
68 format!("{limit}/{hours:02}:{minutes:02}:{seconds:02}")
69 } else {
70 let micros = remainder_ns / 1_000;
71 format!("{limit}/{hours:02}:{minutes:02}:{seconds:02}.{micros:06}")
72 }
73}
74
75type SubmitCommandFn = Box<dyn Fn(TradingCommand)>;
76type ModifyOrderFn = Box<dyn Fn(ModifyOrder)>;
77
78#[allow(dead_code)]
85pub struct RiskEngine {
86 clock: Rc<RefCell<dyn Clock>>,
87 cache: Rc<RefCell<Cache>>,
88 portfolio: Portfolio,
89 pub throttled_submit: Throttler<TradingCommand, SubmitCommandFn>,
90 pub throttled_modify_order: Throttler<ModifyOrder, ModifyOrderFn>,
91 max_notional_per_order: AHashMap<InstrumentId, Decimal>,
92 trading_state: TradingState,
93 config: RiskEngineConfig,
94 command_count: u64,
95 event_count: u64,
96}
97
98impl Debug for RiskEngine {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct(stringify!(RiskEngine)).finish()
101 }
102}
103
104impl RiskEngine {
105 pub fn new(
107 config: RiskEngineConfig,
108 portfolio: Portfolio,
109 clock: Rc<RefCell<dyn Clock>>,
110 cache: Rc<RefCell<Cache>>,
111 ) -> Self {
112 let throttled_submit = Self::create_submit_throttler(&config, clock.clone(), cache.clone());
113
114 let throttled_modify_order =
115 Self::create_modify_order_throttler(&config, clock.clone(), cache.clone());
116
117 Self {
118 clock,
119 cache,
120 portfolio,
121 throttled_submit,
122 throttled_modify_order,
123 max_notional_per_order: config.max_notional_per_order.clone(),
124 trading_state: TradingState::Active,
125 config,
126 command_count: 0,
127 event_count: 0,
128 }
129 }
130
131 pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
133 let weak = WeakCell::from(Rc::downgrade(engine));
134
135 let weak_execute = weak.clone();
136 msgbus::register_trading_command_endpoint(
137 MessagingSwitchboard::risk_engine_execute(),
138 TypedIntoHandler::from(move |cmd: TradingCommand| {
139 if let Some(rc) = weak_execute.upgrade() {
140 rc.borrow_mut().execute(cmd);
141 }
142 }),
143 );
144
145 msgbus::register_trading_command_endpoint(
154 MessagingSwitchboard::risk_engine_queue_execute(),
155 TypedIntoHandler::from(move |cmd: TradingCommand| {
156 if let Some(sender) = try_get_trading_cmd_sender() {
157 sender.execute(cmd);
158 } else {
159 let endpoint = MessagingSwitchboard::risk_engine_execute();
160 msgbus::send_trading_command(endpoint, cmd);
161 }
162 }),
163 );
164
165 let weak_process = weak.clone();
166 msgbus::register_order_event_endpoint(
167 MessagingSwitchboard::risk_engine_process(),
168 TypedIntoHandler::from(move |event: OrderEventAny| {
169 if let Some(rc) = weak_process.upgrade() {
170 rc.borrow_mut().process(event);
171 }
172 }),
173 );
174
175 let weak_order_events = weak.clone();
176 msgbus::subscribe_order_events(
177 "events.order.*".into(),
178 TypedHandler::from(move |event: &OrderEventAny| {
179 if let Some(rc) = weak_order_events.upgrade() {
180 rc.borrow_mut().process(event.clone());
181 }
182 }),
183 Some(10),
184 );
185
186 let weak_position_events = weak;
187 msgbus::subscribe_position_events(
188 "events.position.*".into(),
189 TypedHandler::from(move |event: &PositionEvent| {
190 if let Some(rc) = weak_position_events.upgrade() {
191 rc.borrow_mut().process_position_event(event);
192 }
193 }),
194 Some(10),
195 );
196 }
197
198 fn create_submit_throttler(
199 config: &RiskEngineConfig,
200 clock: Rc<RefCell<dyn Clock>>,
201 cache: Rc<RefCell<Cache>>,
202 ) -> Throttler<TradingCommand, SubmitCommandFn> {
203 let success_handler = {
204 Box::new(move |command: TradingCommand| {
205 let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
206 msgbus::send_trading_command(endpoint, command);
207 }) as Box<dyn Fn(TradingCommand)>
208 };
209
210 let failure_handler = {
211 let cache = cache;
212 let clock = clock.clone();
213 Box::new(move |command: TradingCommand| {
214 let reason = OrderDeniedReason::RateLimitExceeded.to_string();
215
216 match command {
217 TradingCommand::SubmitOrder(submit_order) => {
218 log::warn!(
219 "SubmitOrder for {} DENIED: {reason}",
220 submit_order.client_order_id,
221 );
222
223 Self::handle_submit_order_cache(&cache, &submit_order);
224
225 let denied = Self::create_order_denied(&submit_order, &reason, &clock);
226
227 let endpoint = MessagingSwitchboard::exec_engine_process();
228 msgbus::send_order_event(endpoint, denied);
229 }
230 TradingCommand::SubmitOrderList(submit_order_list) => {
231 log::warn!(
232 "SubmitOrderList for {} DENIED: {reason}",
233 submit_order_list.order_list.id,
234 );
235
236 let orders: Vec<OrderAny> = cache.borrow().orders_for_ids(
237 &submit_order_list.order_list.client_order_ids,
238 &submit_order_list,
239 );
240
241 let timestamp = clock.borrow().timestamp_ns();
242
243 for order in &orders {
244 if order.status() == OrderStatus::Initialized {
245 let denied = OrderEventAny::Denied(OrderDenied::new(
246 order.trader_id(),
247 order.strategy_id(),
248 order.instrument_id(),
249 order.client_order_id(),
250 reason.as_str().into(),
251 UUID4::new(),
252 timestamp,
253 timestamp,
254 ));
255 let endpoint = MessagingSwitchboard::exec_engine_process();
256 msgbus::send_order_event(endpoint, denied);
257 }
258 }
259 }
260 _ => {
261 log::error!("Unexpected command type in submit throttler: {command}");
262 }
263 }
264 }) as Box<dyn Fn(TradingCommand)>
265 };
266
267 Throttler::new(
268 config.max_order_submit,
269 clock,
270 "ORDER_SUBMIT_THROTTLER",
271 success_handler,
272 Some(failure_handler),
273 Ustr::from(UUID4::new().as_str()),
274 )
275 }
276
277 fn create_modify_order_throttler(
278 config: &RiskEngineConfig,
279 clock: Rc<RefCell<dyn Clock>>,
280 cache: Rc<RefCell<Cache>>,
281 ) -> Throttler<ModifyOrder, ModifyOrderFn> {
282 let success_handler = {
283 Box::new(move |order: ModifyOrder| {
284 let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
285 msgbus::send_trading_command(endpoint, TradingCommand::ModifyOrder(order));
286 }) as Box<dyn Fn(ModifyOrder)>
287 };
288
289 let failure_handler = {
290 let cache = cache;
291 let clock = clock.clone();
292 Box::new(move |order: ModifyOrder| {
293 let reason = "Exceeded MAX_ORDER_MODIFY_RATE";
294 log::warn!(
295 "SubmitOrder for {} DENIED: {}",
296 order.client_order_id,
297 reason
298 );
299
300 let Some(order) = Self::get_existing_order(&cache, &order) else {
301 return;
302 };
303
304 let rejected = Self::create_modify_rejected(&order, reason, &clock);
305
306 let endpoint = MessagingSwitchboard::exec_engine_process();
307 msgbus::send_order_event(endpoint, rejected);
308 }) as Box<dyn Fn(ModifyOrder)>
309 };
310
311 Throttler::new(
312 config.max_order_modify,
313 clock,
314 "ORDER_MODIFY_THROTTLER",
315 success_handler,
316 Some(failure_handler),
317 Ustr::from(UUID4::new().as_str()),
318 )
319 }
320
321 fn handle_submit_order_cache(cache: &Rc<RefCell<Cache>>, submit_order: &SubmitOrder) {
322 let cache = cache.borrow();
323 if !cache.order_exists(&submit_order.client_order_id) {
324 log::error!(
325 "Order not found in cache for client_order_id: {}",
326 submit_order.client_order_id
327 );
328 }
329 }
330
331 fn get_existing_order(cache: &Rc<RefCell<Cache>>, order: &ModifyOrder) -> Option<OrderAny> {
332 let cache = cache.borrow();
333 if let Some(order) = cache.order(&order.client_order_id) {
334 Some(order.clone())
335 } else {
336 log::error!(
337 "Order with command.client_order_id: {} not found",
338 order.client_order_id
339 );
340 None
341 }
342 }
343
344 fn create_order_denied(
345 submit_order: &SubmitOrder,
346 reason: &str,
347 clock: &Rc<RefCell<dyn Clock>>,
348 ) -> OrderEventAny {
349 let timestamp = clock.borrow().timestamp_ns();
350 OrderEventAny::Denied(OrderDenied::new(
351 submit_order.trader_id,
352 submit_order.strategy_id,
353 submit_order.instrument_id,
354 submit_order.client_order_id,
355 reason.into(),
356 UUID4::new(),
357 timestamp,
358 timestamp,
359 ))
360 }
361
362 fn create_modify_rejected(
363 order: &OrderAny,
364 reason: &str,
365 clock: &Rc<RefCell<dyn Clock>>,
366 ) -> OrderEventAny {
367 let timestamp = clock.borrow().timestamp_ns();
368 OrderEventAny::ModifyRejected(OrderModifyRejected::new(
369 order.trader_id(),
370 order.strategy_id(),
371 order.instrument_id(),
372 order.client_order_id(),
373 reason.into(),
374 UUID4::new(),
375 timestamp,
376 timestamp,
377 false,
378 order.venue_order_id(),
379 None,
380 ))
381 }
382
383 pub fn execute(&mut self, command: TradingCommand) {
386 self.command_count += 1;
387
388 self.handle_command(command);
390 }
391
392 #[expect(
394 clippy::needless_pass_by_value,
395 reason = "message bus dispatch passes owned order events"
396 )]
397 pub fn process(&mut self, event: OrderEventAny) {
398 self.event_count += 1;
399
400 self.handle_event(&event);
402 }
403
404 fn process_position_event(&mut self, event: &PositionEvent) {
405 self.event_count += 1;
406
407 self.handle_position_event(event);
408 }
409
410 pub fn set_trading_state(&mut self, state: TradingState) {
412 if state == self.trading_state {
413 log::warn!("No change to trading state: already set to {state:?}");
414 return;
415 }
416
417 self.trading_state = state;
418
419 let ts_now = self.clock.borrow().timestamp_ns();
420 let trader_id = get_message_bus().borrow().trader_id;
421
422 let config = self.config_as_map();
423 let event =
424 TradingStateChanged::new(trader_id, state, config, UUID4::new(), ts_now, ts_now);
425
426 msgbus::publish_any("events.risk".into(), &event);
427
428 log::info!("Trading state set to {state:?}");
429 }
430
431 pub fn set_max_notional_per_order(&mut self, instrument_id: InstrumentId, new_value: Decimal) {
433 self.max_notional_per_order.insert(instrument_id, new_value);
434
435 let new_value_str = new_value.to_string();
436 log::info!("Set MAX_NOTIONAL_PER_ORDER: {instrument_id} {new_value_str}");
437 }
438
439 pub fn start(&mut self) {
441 log::info!("Started");
442 }
443
444 pub fn stop(&mut self) {
446 log::info!("Stopped");
447 }
448
449 pub fn reset(&mut self) {
451 self.throttled_submit.reset();
452 self.throttled_modify_order.reset();
453 self.max_notional_per_order = self.config.max_notional_per_order.clone();
454 self.trading_state = TradingState::Active;
455 self.command_count = 0;
456 self.event_count = 0;
457
458 log::info!("Reset");
459 }
460
461 pub fn dispose(&mut self) {
463 log::info!("Disposed");
464 }
465
466 #[must_use]
468 pub fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
469 &self.clock
470 }
471
472 #[must_use]
474 pub fn cache(&self) -> &Rc<RefCell<Cache>> {
475 &self.cache
476 }
477
478 pub fn portfolio_mut(&mut self) -> &mut Portfolio {
480 &mut self.portfolio
481 }
482
483 #[must_use]
485 pub const fn config(&self) -> &RiskEngineConfig {
486 &self.config
487 }
488
489 #[must_use]
491 pub const fn command_count(&self) -> u64 {
492 self.command_count
493 }
494
495 #[must_use]
497 pub const fn event_count(&self) -> u64 {
498 self.event_count
499 }
500
501 #[must_use]
503 pub const fn trading_state(&self) -> TradingState {
504 self.trading_state
505 }
506
507 #[must_use]
509 pub const fn max_notional_per_order(&self) -> &AHashMap<InstrumentId, Decimal> {
510 &self.max_notional_per_order
511 }
512
513 fn config_as_map(&self) -> IndexMap<String, String> {
514 let mut map = IndexMap::new();
515 map.insert("bypass".to_string(), self.config.bypass.to_string());
516 map.insert(
517 "max_order_submit_rate".to_string(),
518 format_rate_limit(&self.config.max_order_submit),
519 );
520 map.insert(
521 "max_order_modify_rate".to_string(),
522 format_rate_limit(&self.config.max_order_modify),
523 );
524
525 for (instrument_id, value) in &self.max_notional_per_order {
526 map.insert(
527 format!("max_notional_per_order.{instrument_id}"),
528 value.to_string(),
529 );
530 }
531
532 map.insert("debug".to_string(), self.config.debug.to_string());
533 map
534 }
535
536 fn handle_command(&mut self, command: TradingCommand) {
537 if self.config.debug {
538 log::debug!("{CMD}{RECV} {command:?}");
539 }
540
541 match command {
542 TradingCommand::SubmitOrder(submit_order) => self.handle_submit_order(submit_order),
543 TradingCommand::SubmitOrderList(submit_order_list) => {
544 self.handle_submit_order_list(submit_order_list);
545 }
546 TradingCommand::ModifyOrder(modify_order) => self.handle_modify_order(modify_order),
547 TradingCommand::ModifyOrders(modify_orders) => {
548 self.handle_batch_modify_orders(modify_orders);
549 }
550 TradingCommand::QueryAccount(query_account) => {
551 Self::send_to_execution(TradingCommand::QueryAccount(query_account));
552 }
553 _ => {
554 log::error!("Cannot handle command: {command}");
555 }
556 }
557 }
558
559 fn handle_submit_order(&mut self, command: SubmitOrder) {
560 if self.config.bypass {
561 Self::send_to_execution(TradingCommand::SubmitOrder(command));
562 return;
563 }
564
565 let order = {
566 let cache = self.cache.borrow();
567 let Some(order) = cache.order(&command.client_order_id) else {
568 log::error!(
569 "Cannot handle submit order: order not found in cache for {}",
570 command.client_order_id
571 );
572 return;
573 };
574 order.clone()
575 };
576
577 if let Some(position_id) = command.position_id
578 && order.is_reduce_only()
579 {
580 let position_exists = {
581 let cache = self.cache.borrow();
582 cache
583 .position(&position_id)
584 .map(|pos| (pos.side, pos.quantity))
585 };
586
587 if let Some((pos_side, pos_quantity)) = position_exists {
588 if !order.would_reduce_only(pos_side, pos_quantity) {
589 self.deny_command(
590 TradingCommand::SubmitOrder(command),
591 &OrderDeniedReason::ReduceOnlyWouldIncreasePosition { position_id }
592 .to_string(),
593 );
594 return; }
596 } else {
597 self.deny_command(
598 TradingCommand::SubmitOrder(command),
599 &OrderDeniedReason::PositionNotFound { position_id }.to_string(),
600 );
601 return;
602 }
603 }
604
605 let instrument_exists = {
606 let cache = self.cache.borrow();
607 cache.instrument(&command.instrument_id).cloned()
608 };
609
610 let Some(instrument) = instrument_exists else {
611 self.deny_command(
612 TradingCommand::SubmitOrder(command.clone()),
613 &OrderDeniedReason::InstrumentNotFound {
614 instrument_id: command.instrument_id,
615 }
616 .to_string(),
617 );
618 return; };
620
621 if !self.check_order(&instrument, &order) {
622 return; }
624
625 if !self.check_orders_risk(&instrument, &[order]) {
626 return; }
628
629 self.execution_gateway(&instrument, TradingCommand::SubmitOrder(command));
631 }
632
633 fn handle_submit_order_list(&mut self, command: SubmitOrderList) {
634 if self.config.bypass {
635 Self::send_to_execution(TradingCommand::SubmitOrderList(command));
636 return;
637 }
638
639 let orders: Vec<OrderAny> = self
640 .cache
641 .borrow()
642 .orders_for_ids(&command.order_list.client_order_ids, &command);
643
644 if orders.len() != command.order_list.client_order_ids.len() {
645 self.deny_order_list(
646 &orders,
647 &OrderDeniedReason::OrderListIncomplete {
648 order_list_id: command.order_list.id,
649 }
650 .to_string(),
651 );
652 return; }
654
655 let mut instruments: AHashMap<InstrumentId, InstrumentAny> = AHashMap::new();
659
660 for order in &orders {
661 let instrument_id = order.instrument_id();
662 if instruments.contains_key(&instrument_id) {
663 continue;
664 }
665 let resolved = self.cache.borrow().instrument(&instrument_id).cloned();
666 let Some(instrument) = resolved else {
667 self.deny_command(
668 TradingCommand::SubmitOrderList(command),
669 &OrderDeniedReason::InstrumentNotFound { instrument_id }.to_string(),
670 );
671 return; };
673 instruments.insert(instrument_id, instrument);
674 }
675
676 for order in &orders {
677 let Some(instrument) = instruments.get(&order.instrument_id()) else {
678 self.deny_order(
679 order,
680 &OrderDeniedReason::InstrumentNotFound {
681 instrument_id: order.instrument_id(),
682 }
683 .to_string(),
684 );
685 return; };
687
688 if !self.check_order(instrument, order) {
689 return; }
691 }
692
693 let representative = if let Some(instrument) = instruments.get(&command.instrument_id) {
694 instrument.clone()
695 } else {
696 self.deny_order_list(
697 &orders,
698 &OrderDeniedReason::InstrumentNotFound {
699 instrument_id: command.instrument_id,
700 }
701 .to_string(),
702 );
703 return; };
705
706 if !self.check_orders_risk(&representative, &orders) {
707 self.deny_order_list(
708 &orders,
709 &OrderDeniedReason::OrderListDenied {
710 order_list_id: command.order_list.id,
711 }
712 .to_string(),
713 );
714 return; }
716
717 self.execution_gateway(&representative, TradingCommand::SubmitOrderList(command));
718 }
719
720 fn handle_modify_order(&mut self, command: ModifyOrder) {
721 if self.config.bypass {
722 Self::send_to_execution(TradingCommand::ModifyOrder(command));
723 return;
724 }
725
726 if !self.validate_modify_order(&command) {
727 return;
728 }
729
730 self.throttled_modify_order.send(command);
731 }
732
733 fn handle_batch_modify_orders(&mut self, command: BatchModifyOrders) {
734 if self.config.bypass {
735 Self::send_to_execution(TradingCommand::ModifyOrders(command));
736 return;
737 }
738
739 if command.modifies.is_empty() {
740 log::warn!("Cannot handle BatchModifyOrders: no modify commands");
741 return;
742 }
743
744 let mut rejected_client_order_ids = Vec::new();
745 let mut valid = true;
746
747 for modify in &command.modifies {
748 if modify.instrument_id != command.instrument_id {
749 if let Some(order) = self
750 .cache
751 .borrow()
752 .order(&modify.client_order_id)
753 .map(|o| o.clone())
754 {
755 self.reject_modify_order(
756 &order,
757 &format!(
758 "BatchModifyOrders instrument {} does not match child instrument {}",
759 command.instrument_id, modify.instrument_id
760 ),
761 );
762 }
763 rejected_client_order_ids.push(modify.client_order_id);
764 valid = false;
765 continue;
766 }
767
768 if !self.validate_modify_order(modify) {
769 rejected_client_order_ids.push(modify.client_order_id);
770 valid = false;
771 }
772 }
773
774 if !valid {
775 let reason = "BatchModifyOrders rejected because one or more child modifications failed validation";
776
777 for modify in &command.modifies {
778 if rejected_client_order_ids.contains(&modify.client_order_id) {
779 continue;
780 }
781
782 let Some(order) = Self::get_existing_order(&self.cache, modify) else {
783 continue;
784 };
785
786 self.reject_modify_order(&order, reason);
787 }
788 return;
789 }
790
791 if !self
792 .throttled_modify_order
793 .try_reserve(command.modifies.len())
794 {
795 let reason = "Exceeded MAX_ORDER_MODIFY_RATE";
796
797 for modify in &command.modifies {
798 let Some(order) = Self::get_existing_order(&self.cache, modify) else {
799 continue;
800 };
801 self.reject_modify_order(&order, reason);
802 }
803 return;
804 }
805
806 Self::send_to_execution(TradingCommand::ModifyOrders(command));
807 }
808
809 fn validate_modify_order(&self, command: &ModifyOrder) -> bool {
810 let order_exists = {
811 let cache = self.cache.borrow();
812 cache.order(&command.client_order_id).map(|o| o.clone())
813 };
814
815 let Some(order) = order_exists else {
816 log::error!(
817 "ModifyOrder DENIED: Order with command.client_order_id: {} not found",
818 command.client_order_id
819 );
820 return false;
821 };
822
823 if order.is_closed() {
824 self.reject_modify_order(
825 &order,
826 &format!(
827 "Order with command.client_order_id: {} already closed",
828 command.client_order_id
829 ),
830 );
831 return false;
832 } else if order.status() == OrderStatus::PendingCancel {
833 self.reject_modify_order(
834 &order,
835 &format!(
836 "Order with command.client_order_id: {} is already pending cancel",
837 command.client_order_id
838 ),
839 );
840 return false;
841 }
842
843 let maybe_instrument = {
844 let cache = self.cache.borrow();
845 cache.instrument(&command.instrument_id).cloned()
846 };
847
848 let Some(instrument) = maybe_instrument else {
849 self.reject_modify_order(
850 &order,
851 &format!("no instrument found for {:?}", command.instrument_id),
852 );
853 return false;
854 };
855
856 let mut risk_msg = Self::check_price(&instrument, command.price);
858 if let Some(risk_msg) = risk_msg {
859 self.reject_modify_order(&order, &risk_msg);
860 return false;
861 }
862
863 risk_msg = Self::check_price(&instrument, command.trigger_price);
865 if let Some(risk_msg) = risk_msg {
866 self.reject_modify_order(&order, &risk_msg);
867 return false;
868 }
869
870 risk_msg = Self::check_quantity(&instrument, command.quantity, order.is_quote_quantity());
872 if let Some(risk_msg) = risk_msg {
873 self.reject_modify_order(&order, &risk_msg);
874 return false;
875 }
876
877 match self.trading_state {
879 TradingState::Halted => {
880 self.reject_modify_order(&order, "TradingState is HALTED: Cannot modify order");
881 return false;
882 }
883 TradingState::Reducing => {
884 if let Some(quantity) = command.quantity
885 && quantity > order.quantity()
886 && ((order.is_buy() && self.portfolio.is_net_long(&instrument.id()))
887 || (order.is_sell() && self.portfolio.is_net_short(&instrument.id())))
888 {
889 self.reject_modify_order(
890 &order,
891 &format!(
892 "TradingState is REDUCING and update will increase exposure {}",
893 instrument.id()
894 ),
895 );
896 return false;
897 }
898 }
899 TradingState::Active => {}
900 }
901
902 true
903 }
904
905 fn check_order(&self, instrument: &InstrumentAny, order: &OrderAny) -> bool {
906 if !self.check_order_price(instrument, order)
907 || !self.check_order_quantity(instrument, order)
908 {
909 return false; }
911
912 if order.time_in_force() == TimeInForce::Gtd {
913 let Some(expire_time) = order.expire_time() else {
914 self.deny_order(order, &OrderDeniedReason::MissingExpireTime.to_string());
915 return false; };
917
918 if expire_time <= self.clock.borrow().timestamp_ns() {
919 self.deny_order(
920 order,
921 &OrderDeniedReason::ExpireTimeInPast {
922 expire_time: expire_time.to_rfc3339(),
923 }
924 .to_string(),
925 );
926 return false; }
928 }
929
930 true
931 }
932
933 fn check_order_price(&self, instrument: &InstrumentAny, order: &OrderAny) -> bool {
934 if order.price().is_some() {
935 let risk_msg = Self::check_price(instrument, order.price());
936 if let Some(risk_msg) = risk_msg {
937 self.deny_order(order, &risk_msg);
938 return false; }
940 }
941
942 if order.trigger_price().is_some() {
943 let risk_msg = Self::check_price(instrument, order.trigger_price());
944 if let Some(risk_msg) = risk_msg {
945 self.deny_order(order, &format!("trigger {risk_msg}"));
946 return false; }
948 }
949
950 true
951 }
952
953 fn check_order_quantity(&self, instrument: &InstrumentAny, order: &OrderAny) -> bool {
954 let risk_msg = Self::check_quantity(
955 instrument,
956 Some(order.quantity()),
957 order.is_quote_quantity(),
958 );
959
960 if let Some(risk_msg) = risk_msg {
961 self.deny_order(order, &risk_msg);
962 return false; }
964
965 true
966 }
967
968 fn check_orders_risk(&self, instrument: &InstrumentAny, orders: &[OrderAny]) -> bool {
969 let mut orders_by_account: AHashMap<Option<AccountId>, Vec<&OrderAny>> = AHashMap::new();
970 for order in orders {
971 orders_by_account
972 .entry(order.account_id())
973 .or_default()
974 .push(order);
975 }
976
977 for (account_id, account_orders) in &orders_by_account {
978 if !self.check_orders_risk_for_account(instrument, account_orders, *account_id) {
979 return false;
980 }
981 }
982
983 true
984 }
985
986 #[allow(
987 clippy::too_many_lines,
988 reason = "risk checks keep related denial branches together for auditability"
989 )]
990 fn check_orders_risk_for_account(
991 &self,
992 instrument: &InstrumentAny,
993 orders: &[&OrderAny],
994 account_id: Option<AccountId>,
995 ) -> bool {
996 let mut last_px: Option<Price> = None;
997 let mut max_notional: Option<Money> = None;
998
999 let max_notional_setting = self.max_notional_per_order.get(&instrument.id());
1001 if let Some(max_notional_setting_val) = max_notional_setting.copied() {
1002 let Ok(max_notional_value) =
1003 Money::from_decimal(max_notional_setting_val, instrument.quote_currency())
1004 else {
1005 for order in orders {
1006 self.deny_order(
1007 order,
1008 &OrderDeniedReason::InvalidMaxNotionalPerOrder {
1009 instrument_id: instrument.id(),
1010 value: max_notional_setting_val,
1011 }
1012 .to_string(),
1013 );
1014 }
1015 return false; };
1017 max_notional = Some(max_notional_value);
1018 }
1019
1020 let resolved_account = {
1022 let cache = self.cache.borrow();
1023
1024 if let Some(account_id) = account_id {
1025 cache.account_owned(&account_id)
1026 } else {
1027 cache.account_for_venue_owned(&instrument.id().venue)
1028 }
1029 };
1030
1031 let Some(mut account) = resolved_account else {
1032 log::debug!(
1033 "Cannot find account for venue {} (account_id={account_id:?})",
1034 instrument.id().venue
1035 );
1036 return true;
1037 };
1038
1039 let is_margin = matches!(account, AccountAny::Margin(_));
1040 let is_betting = matches!(account, AccountAny::Betting(_));
1041 let free = match &account {
1042 AccountAny::Margin(margin) => margin.balance_free(Some(instrument.quote_currency())),
1043 AccountAny::Cash(cash) => cash.balance_free(Some(instrument.quote_currency())),
1044 AccountAny::Betting(betting) => betting.balance_free(Some(instrument.quote_currency())),
1045 };
1046 let allow_borrowing = match &account {
1047 AccountAny::Cash(cash) => cash.allow_borrowing,
1048 AccountAny::Margin(_) | AccountAny::Betting(_) => false,
1049 };
1050
1051 if self.config.debug {
1052 log::debug!("Free balance: {free:?}");
1053 }
1054
1055 let (net_long_qty_raw, pending_sell_qty_raw) = {
1058 let cache = self.cache.borrow();
1059 let long_qty: QuantityRaw = cache
1060 .positions_open(
1061 None,
1062 Some(&instrument.id()),
1063 None,
1064 None,
1065 Some(PositionSide::Long),
1066 )
1067 .iter()
1068 .map(|pos| pos.quantity.raw)
1069 .sum();
1070 let pending_sells: QuantityRaw = cache
1071 .orders_open(
1072 None,
1073 Some(&instrument.id()),
1074 None,
1075 None,
1076 Some(OrderSide::Sell),
1077 )
1078 .iter()
1079 .map(|ord| ord.leaves_qty().raw)
1080 .sum();
1081 (long_qty, pending_sells)
1082 };
1083
1084 let available_long_qty_raw = net_long_qty_raw.saturating_sub(pending_sell_qty_raw);
1086
1087 if self.config.debug && net_long_qty_raw > 0 {
1088 log::debug!(
1089 "Net LONG qty (raw): {net_long_qty_raw}, pending sells: {pending_sell_qty_raw}, available: {available_long_qty_raw}"
1090 );
1091 }
1092
1093 let available_short_qty_raw = if is_margin || is_betting {
1095 let cache = self.cache.borrow();
1096 let short_qty: QuantityRaw = cache
1097 .positions_open(
1098 None,
1099 Some(&instrument.id()),
1100 None,
1101 None,
1102 Some(PositionSide::Short),
1103 )
1104 .iter()
1105 .map(|pos| pos.quantity.raw)
1106 .sum();
1107 let pending_buys: QuantityRaw = cache
1108 .orders_open(
1109 None,
1110 Some(&instrument.id()),
1111 None,
1112 None,
1113 Some(OrderSide::Buy),
1114 )
1115 .iter()
1116 .map(|ord| ord.leaves_qty().raw)
1117 .sum();
1118
1119 if self.config.debug && short_qty > 0 {
1120 log::debug!(
1121 "Net SHORT qty (raw): {short_qty}, pending buys: {pending_buys}, available: {}",
1122 short_qty.saturating_sub(pending_buys)
1123 );
1124 }
1125
1126 short_qty.saturating_sub(pending_buys)
1127 } else {
1128 0
1129 };
1130
1131 let mut cum_sell_qty_raw: QuantityRaw = 0;
1133 let mut cum_buy_qty_raw: QuantityRaw = 0;
1134
1135 let mut cum_notional_buy: Option<Money> = None;
1136 let mut cum_notional_sell: Option<Money> = None;
1137 let mut cum_margin_required: Option<Money> = None;
1138 let mut base_currency: Option<Currency> = None;
1139
1140 for order in orders {
1141 last_px = match order {
1143 OrderAny::Market(_) | OrderAny::MarketToLimit(_) => {
1144 if last_px.is_none() {
1145 let quote_price = {
1146 let cache = self.cache.borrow();
1147 cache.quote(&instrument.id()).map(|last_quote| {
1148 match order.order_side() {
1149 OrderSide::Buy => Ok(last_quote.ask_price),
1150 OrderSide::Sell => Ok(last_quote.bid_price),
1151 OrderSide::NoOrderSide => {
1152 Err(OrderDeniedReason::InvalidOrderSide {
1153 order_side: order.order_side(),
1154 }
1155 .to_string())
1156 }
1157 }
1158 })
1159 };
1160
1161 if let Some(quote_price) = quote_price {
1162 match quote_price {
1163 Ok(price) => Some(price),
1164 Err(reason) => {
1165 self.deny_order(order, &reason);
1166 return false; }
1168 }
1169 } else {
1170 let cache = self.cache.borrow();
1171 let last_trade = cache.trade(&instrument.id());
1172
1173 if let Some(last_trade) = last_trade {
1174 Some(last_trade.price)
1175 } else {
1176 log::warn!(
1177 "Cannot check MARKET order risk: no prices for {}",
1178 instrument.id()
1179 );
1180 continue;
1181 }
1182 }
1183 } else {
1184 last_px
1185 }
1186 }
1187 OrderAny::StopMarket(_) | OrderAny::MarketIfTouched(_) => order.trigger_price(),
1188 OrderAny::TrailingStopMarket(_) | OrderAny::TrailingStopLimit(_) => {
1189 if let Some(trigger_price) = order.trigger_price() {
1190 Some(trigger_price)
1191 } else {
1192 let Some(offset_type) = order.trailing_offset_type() else {
1194 self.deny_order(
1195 order,
1196 &OrderDeniedReason::MissingTrailingOffsetType.to_string(),
1197 );
1198 return false; };
1200
1201 if !matches!(
1202 offset_type,
1203 TrailingOffsetType::Price
1204 | TrailingOffsetType::BasisPoints
1205 | TrailingOffsetType::Ticks
1206 ) {
1207 self.deny_order(
1208 order,
1209 &OrderDeniedReason::UnsupportedTrailingOffsetType { offset_type }
1210 .to_string(),
1211 );
1212 return false;
1213 }
1214
1215 let Some(trigger_type) = order.trigger_type() else {
1216 self.deny_order(
1217 order,
1218 &OrderDeniedReason::MissingTriggerType.to_string(),
1219 );
1220 return false; };
1222 let Some(trailing_offset) = order.trailing_offset() else {
1223 self.deny_order(
1224 order,
1225 &OrderDeniedReason::MissingTrailingOffset.to_string(),
1226 );
1227 return false; };
1229
1230 let calc_result: Result<Option<Price>, String> = {
1233 let cache = self.cache.borrow();
1234
1235 if trigger_type == TriggerType::BidAsk {
1236 if let Some(quote) = cache.quote(&instrument.id()) {
1237 trailing_stop_calculate_with_bid_ask(
1238 instrument.price_increment(),
1239 offset_type,
1240 order.order_side_specified(),
1241 trailing_offset,
1242 quote.bid_price,
1243 quote.ask_price,
1244 )
1245 .map(Some)
1246 .map_err(|e| e.to_string())
1247 } else {
1248 log::warn!(
1249 "Cannot check {} order risk: no trigger price set and no bid/ask quotes available for {}",
1250 order.order_type(),
1251 instrument.id()
1252 );
1253 Ok(None)
1254 }
1255 } else if let Some(last_trade) = cache.trade(&instrument.id()) {
1256 trailing_stop_calculate_with_last(
1257 instrument.price_increment(),
1258 offset_type,
1259 order.order_side_specified(),
1260 trailing_offset,
1261 last_trade.price,
1262 )
1263 .map(Some)
1264 .map_err(|e| e.to_string())
1265 } else if trigger_type == TriggerType::LastOrBidAsk {
1266 if let Some(quote) = cache.quote(&instrument.id()) {
1267 trailing_stop_calculate_with_bid_ask(
1268 instrument.price_increment(),
1269 offset_type,
1270 order.order_side_specified(),
1271 trailing_offset,
1272 quote.bid_price,
1273 quote.ask_price,
1274 )
1275 .map(Some)
1276 .map_err(|e| e.to_string())
1277 } else {
1278 log::warn!(
1279 "Cannot check {} order risk: no trigger price set and no market data available for {}",
1280 order.order_type(),
1281 instrument.id()
1282 );
1283 Ok(None)
1284 }
1285 } else {
1286 log::warn!(
1287 "Cannot check {} order risk: no trigger price set and no market data available for {}",
1288 order.order_type(),
1289 instrument.id()
1290 );
1291 Ok(None)
1292 }
1293 };
1294 match calc_result {
1297 Ok(Some(trigger)) => Some(trigger),
1298 Ok(None) => {
1299 continue;
1300 }
1301 Err(e) => {
1302 self.deny_order(
1303 order,
1304 &OrderDeniedReason::TrailingStopCalcFailed { detail: e }
1305 .to_string(),
1306 );
1307 return false;
1308 }
1309 }
1310 }
1311 }
1312 _ => order.price(),
1313 };
1314
1315 let Some(last_px) = last_px else {
1316 log::error!("Cannot check order risk: no price available");
1317 continue;
1318 };
1319
1320 let effective_price = if order.is_quote_quantity()
1322 && !instrument.is_inverse()
1323 && matches!(order, OrderAny::Limit(_) | OrderAny::StopLimit(_))
1324 {
1325 let cache = self.cache.borrow();
1327 if let Some(quote_tick) = cache.quote(&instrument.id()) {
1328 match order.order_side() {
1329 OrderSide::Buy => last_px.min(quote_tick.ask_price),
1331 OrderSide::Sell => last_px.max(quote_tick.bid_price),
1333 OrderSide::NoOrderSide => last_px,
1334 }
1335 } else {
1336 last_px }
1338 } else {
1339 last_px
1340 };
1341
1342 let effective_quantity = if order.is_quote_quantity() && !instrument.is_inverse() {
1343 instrument.calculate_base_quantity(order.quantity(), effective_price)
1344 } else {
1345 order.quantity()
1346 };
1347
1348 if !order.is_quote_quantity() {
1354 if let Some(max_quantity) = instrument.max_quantity()
1355 && effective_quantity > max_quantity
1356 {
1357 self.deny_order(
1358 order,
1359 &OrderDeniedReason::QuantityExceedsMaximum {
1360 effective_quantity,
1361 max_quantity,
1362 }
1363 .to_string(),
1364 );
1365 return false; }
1367
1368 if let Some(min_quantity) = instrument.min_quantity()
1369 && effective_quantity < min_quantity
1370 {
1371 self.deny_order(
1372 order,
1373 &OrderDeniedReason::QuantityBelowMinimum {
1374 effective_quantity,
1375 min_quantity,
1376 }
1377 .to_string(),
1378 );
1379 return false; }
1381 }
1382
1383 let notional =
1384 instrument.calculate_notional_value(effective_quantity, last_px, Some(true));
1385
1386 if self.config.debug {
1387 log::debug!("Notional: {notional:?}");
1388 }
1389
1390 if let Some(max_notional_value) = max_notional
1392 && notional > max_notional_value
1393 {
1394 self.deny_order(
1395 order,
1396 &OrderDeniedReason::NotionalExceedsMaxPerOrder {
1397 max_notional: max_notional_value,
1398 notional,
1399 }
1400 .to_string(),
1401 );
1402 return false; }
1404
1405 if let Some(min_notional) = instrument.min_notional()
1407 && notional.currency == min_notional.currency
1408 && notional < min_notional
1409 {
1410 self.deny_order(
1411 order,
1412 &OrderDeniedReason::NotionalBelowMinimum {
1413 min_notional,
1414 notional,
1415 }
1416 .to_string(),
1417 );
1418 return false; }
1420
1421 if let Some(max_notional) = instrument.max_notional()
1423 && notional.currency == max_notional.currency
1424 && notional > max_notional
1425 {
1426 self.deny_order(
1427 order,
1428 &OrderDeniedReason::NotionalExceedsMaximum {
1429 max_notional,
1430 notional,
1431 }
1432 .to_string(),
1433 );
1434 return false; }
1436
1437 if is_margin {
1438 let margin_req = match &mut account {
1440 AccountAny::Margin(margin) => margin
1441 .calculate_initial_margin(instrument, effective_quantity, last_px, None)
1442 .unwrap_or_else(|e| {
1443 log::error!("Failed to calculate initial margin: {e}");
1444 Money::zero(instrument.quote_currency())
1445 }),
1446 _ => unreachable!(),
1447 };
1448
1449 if self.config.debug {
1450 log::debug!("Initial margin required: {margin_req}");
1451 }
1452
1453 let is_reducing = order.is_reduce_only()
1455 || (order.is_sell()
1456 && (cum_sell_qty_raw + effective_quantity.raw) <= available_long_qty_raw)
1457 || (order.is_buy()
1458 && (cum_buy_qty_raw + effective_quantity.raw) <= available_short_qty_raw);
1459
1460 if order.is_sell() {
1461 cum_sell_qty_raw += effective_quantity.raw;
1462 } else if order.is_buy() {
1463 cum_buy_qty_raw += effective_quantity.raw;
1464 }
1465
1466 if is_reducing {
1467 if self.config.debug {
1468 log::debug!("Position-reducing order skips margin check");
1469 }
1470 continue;
1471 }
1472
1473 let margin_free = match &account {
1476 AccountAny::Margin(margin) => margin.balance_free(Some(margin_req.currency)),
1477 _ => unreachable!(),
1478 };
1479
1480 let Some(margin_free_val) = margin_free else {
1481 if self.config.debug {
1482 log::debug!(
1483 "No balance for margin currency {}, skipping margin check",
1484 margin_req.currency
1485 );
1486 }
1487 continue;
1488 };
1489
1490 if margin_req > margin_free_val {
1492 self.deny_order(
1493 order,
1494 &OrderDeniedReason::MarginExceedsFreeBalance {
1495 free: margin_free_val,
1496 margin_required: margin_req,
1497 }
1498 .to_string(),
1499 );
1500 return false;
1501 }
1502
1503 match cum_margin_required.as_mut() {
1505 Some(cum) => cum.raw += margin_req.raw,
1506 None => cum_margin_required = Some(margin_req),
1507 }
1508
1509 if self.config.debug {
1510 log::debug!("Cumulative margin required: {cum_margin_required:?}");
1511 }
1512
1513 if let Some(cum_margin) = cum_margin_required
1514 && cum_margin > margin_free_val
1515 {
1516 self.deny_order(
1517 order,
1518 &OrderDeniedReason::CumMarginExceedsFreeBalance {
1519 free: margin_free_val,
1520 cum_margin,
1521 }
1522 .to_string(),
1523 );
1524 return false;
1525 }
1526 } else {
1527 let notional =
1529 instrument.calculate_notional_value(effective_quantity, last_px, None);
1530 let order_balance_impact = if is_betting {
1531 match &mut account {
1532 AccountAny::Betting(betting) => Money::from_raw(
1533 -betting
1534 .calculate_balance_locked(
1535 instrument,
1536 order.order_side(),
1537 effective_quantity,
1538 last_px,
1539 None,
1540 )
1541 .unwrap_or_else(|e| {
1542 log::error!("Failed to calculate betting balance locked: {e}");
1543 Money::zero(instrument.quote_currency())
1544 })
1545 .raw,
1546 instrument.quote_currency(),
1547 ),
1548 _ => unreachable!(),
1549 }
1550 } else {
1551 match order.order_side() {
1552 OrderSide::Buy => Money::from_raw(-notional.raw, notional.currency),
1553 OrderSide::Sell => Money::from_raw(notional.raw, notional.currency),
1554 OrderSide::NoOrderSide => {
1555 self.deny_order(
1556 order,
1557 &OrderDeniedReason::InvalidOrderSide {
1558 order_side: order.order_side(),
1559 }
1560 .to_string(),
1561 );
1562 return false; }
1564 }
1565 };
1566
1567 if self.config.debug {
1568 log::debug!("Balance impact: {order_balance_impact}");
1569 }
1570
1571 let is_position_reducing = if order.is_buy() {
1573 let reducing = order.is_reduce_only()
1574 || (cum_buy_qty_raw + effective_quantity.raw) <= available_short_qty_raw;
1575 cum_buy_qty_raw += effective_quantity.raw;
1576 reducing
1577 } else if order.is_sell() {
1578 let reducing = order.is_reduce_only()
1579 || (cum_sell_qty_raw + effective_quantity.raw) <= available_long_qty_raw;
1580 cum_sell_qty_raw += effective_quantity.raw;
1581 reducing
1582 } else {
1583 false
1584 };
1585
1586 if is_position_reducing {
1587 if self.config.debug {
1588 log::debug!("Position-reducing order skips balance check");
1589 }
1590 continue;
1591 }
1592
1593 if !allow_borrowing
1595 && let Some(free_val) = free
1596 && (free_val.as_decimal() + order_balance_impact.as_decimal()) < Decimal::ZERO
1597 {
1598 self.deny_order(
1599 order,
1600 &OrderDeniedReason::NotionalExceedsFreeBalance {
1601 free: free_val,
1602 notional,
1603 }
1604 .to_string(),
1605 );
1606 return false;
1607 }
1608
1609 if base_currency.is_none() {
1610 base_currency = instrument.base_currency();
1611 }
1612
1613 if order.is_buy() {
1614 match cum_notional_buy.as_mut() {
1615 Some(cum_notional_buy_val) => {
1616 cum_notional_buy_val.raw += -order_balance_impact.raw;
1617 }
1618 None => {
1619 cum_notional_buy = Some(Money::from_raw(
1620 -order_balance_impact.raw,
1621 order_balance_impact.currency,
1622 ));
1623 }
1624 }
1625
1626 if self.config.debug {
1627 log::debug!("Cumulative notional BUY: {cum_notional_buy:?}");
1628 }
1629
1630 if !allow_borrowing
1631 && let (Some(free), Some(cum_notional_buy)) = (free, cum_notional_buy)
1632 && cum_notional_buy > free
1633 {
1634 self.deny_order(
1635 order,
1636 &OrderDeniedReason::CumNotionalExceedsFreeBalance {
1637 free,
1638 cum_notional: cum_notional_buy,
1639 }
1640 .to_string(),
1641 );
1642 return false; }
1644 } else if order.is_sell() {
1645 if is_betting {
1646 match cum_notional_sell.as_mut() {
1647 Some(cum_notional_sell_val) => {
1648 cum_notional_sell_val.raw += -order_balance_impact.raw;
1649 }
1650 None => {
1651 cum_notional_sell = Some(Money::from_raw(
1652 -order_balance_impact.raw,
1653 order_balance_impact.currency,
1654 ));
1655 }
1656 }
1657
1658 if self.config.debug {
1659 log::debug!("Cumulative betting SELL liability: {cum_notional_sell:?}");
1660 }
1661
1662 if !allow_borrowing
1663 && let (Some(free), Some(cum_notional_sell)) = (free, cum_notional_sell)
1664 && cum_notional_sell > free
1665 {
1666 self.deny_order(
1667 order,
1668 &OrderDeniedReason::CumNotionalExceedsFreeBalance {
1669 free,
1670 cum_notional: cum_notional_sell,
1671 }
1672 .to_string(),
1673 );
1674 return false;
1675 }
1676
1677 continue;
1678 }
1679
1680 let has_base_currency = match &account {
1681 AccountAny::Margin(_) => false,
1682 AccountAny::Cash(cash) => cash.base_currency.is_some(),
1683 AccountAny::Betting(betting) => betting.base_currency.is_some(),
1684 };
1685
1686 if has_base_currency {
1687 match cum_notional_sell.as_mut() {
1688 Some(cum_notional_sell_val) => {
1689 cum_notional_sell_val.raw += order_balance_impact.raw;
1690 }
1691 None => {
1692 cum_notional_sell = Some(Money::from_raw(
1693 order_balance_impact.raw,
1694 order_balance_impact.currency,
1695 ));
1696 }
1697 }
1698
1699 if self.config.debug {
1700 log::debug!("Cumulative notional SELL: {cum_notional_sell:?}");
1701 }
1702
1703 if !allow_borrowing
1704 && let (Some(free), Some(cum_notional_sell)) = (free, cum_notional_sell)
1705 && cum_notional_sell > free
1706 {
1707 self.deny_order(
1708 order,
1709 &OrderDeniedReason::CumNotionalExceedsFreeBalance {
1710 free,
1711 cum_notional: cum_notional_sell,
1712 }
1713 .to_string(),
1714 );
1715 return false; }
1717 } else if let Some(base_currency) = base_currency {
1718 let cash_value_raw: MoneyRaw = match effective_quantity.raw.try_into() {
1719 Ok(value) => value,
1720 Err(e) => {
1721 self.deny_order(
1722 order,
1723 &OrderDeniedReason::QuantityConversionFailed {
1724 detail: e.to_string(),
1725 }
1726 .to_string(),
1727 );
1728 return false; }
1730 };
1731 let cash_value = Money::from_raw(cash_value_raw, base_currency);
1732
1733 let base_free = match &account {
1735 AccountAny::Margin(_) => None,
1736 AccountAny::Cash(cash) => cash.balance_free(Some(base_currency)),
1737 AccountAny::Betting(betting) => {
1738 betting.balance_free(Some(base_currency))
1739 }
1740 };
1741
1742 if self.config.debug
1743 && let AccountAny::Cash(cash) = &account
1744 {
1745 log::debug!("Cash value: {cash_value:?}");
1746 log::debug!("Total: {:?}", cash.balance_total(Some(base_currency)));
1747 log::debug!("Locked: {:?}", cash.balance_locked(Some(base_currency)));
1748 log::debug!("Free: {base_free:?}");
1749 }
1750
1751 if self.config.debug
1752 && let AccountAny::Betting(betting) = &account
1753 {
1754 log::debug!("Cash value: {cash_value:?}");
1755 log::debug!("Total: {:?}", betting.balance_total(Some(base_currency)));
1756 log::debug!(
1757 "Locked: {:?}",
1758 betting.balance_locked(Some(base_currency))
1759 );
1760 log::debug!("Free: {base_free:?}");
1761 }
1762
1763 match cum_notional_sell {
1764 Some(mut value) => {
1765 value.raw += cash_value.raw;
1766 cum_notional_sell = Some(value);
1767 }
1768 None => cum_notional_sell = Some(cash_value),
1769 }
1770
1771 if self.config.debug {
1772 log::debug!("Cumulative notional SELL: {cum_notional_sell:?}");
1773 }
1774
1775 if !allow_borrowing
1776 && let (Some(base_free), Some(cum_notional_sell)) =
1777 (base_free, cum_notional_sell)
1778 && cum_notional_sell.raw > base_free.raw
1779 {
1780 self.deny_order(
1781 order,
1782 &OrderDeniedReason::CumNotionalExceedsFreeBalance {
1783 free: base_free,
1784 cum_notional: cum_notional_sell,
1785 }
1786 .to_string(),
1787 );
1788 return false; }
1790 }
1791 }
1792 }
1793 }
1794
1795 true }
1798
1799 fn check_price(instrument: &InstrumentAny, price: Option<Price>) -> Option<String> {
1800 let price_val = price?;
1801
1802 if price_val.precision > instrument.price_precision() {
1803 return Some(format!(
1804 "price {} invalid (precision {} > {})",
1805 price_val,
1806 price_val.precision,
1807 instrument.price_precision()
1808 ));
1809 }
1810
1811 if !instrument.allows_negative_price() && price_val.raw <= 0 {
1812 return Some(format!("price {price_val} invalid (<= 0)"));
1813 }
1814
1815 None
1816 }
1817
1818 fn check_quantity(
1819 instrument: &InstrumentAny,
1820 quantity: Option<Quantity>,
1821 is_quote_quantity: bool,
1822 ) -> Option<String> {
1823 let quantity_val = quantity?;
1824
1825 if quantity_val.precision > instrument.size_precision() {
1827 return Some(format!(
1828 "quantity {} invalid (precision {} > {})",
1829 quantity_val,
1830 quantity_val.precision,
1831 instrument.size_precision()
1832 ));
1833 }
1834
1835 if is_quote_quantity {
1837 return None;
1838 }
1839
1840 if let Some(max_quantity) = instrument.max_quantity()
1842 && quantity_val > max_quantity
1843 {
1844 return Some(format!(
1845 "quantity {quantity_val} invalid (> maximum trade size of {max_quantity})"
1846 ));
1847 }
1848
1849 if let Some(min_quantity) = instrument.min_quantity()
1851 && quantity_val < min_quantity
1852 {
1853 return Some(format!(
1854 "quantity {quantity_val} invalid (< minimum trade size of {min_quantity})"
1855 ));
1856 }
1857
1858 None
1859 }
1860
1861 fn deny_command(&self, command: TradingCommand, reason: &str) {
1862 match command {
1863 TradingCommand::SubmitOrder(command) => {
1864 let order = {
1865 let cache = self.cache.borrow();
1866 cache.order(&command.client_order_id).map(|o| o.clone())
1867 };
1868
1869 if let Some(ref order) = order {
1870 self.deny_order(order, reason);
1871 } else {
1872 log::error!(
1873 "Cannot deny order: not found in cache for {}",
1874 command.client_order_id
1875 );
1876 }
1877 }
1878 TradingCommand::SubmitOrderList(command) => {
1879 let orders: Vec<OrderAny> = self
1880 .cache
1881 .borrow()
1882 .orders_for_ids(&command.order_list.client_order_ids, &command);
1883 self.deny_order_list(&orders, reason);
1884 }
1885 _ => {
1886 log::error!("Cannot deny command {command}");
1887 }
1888 }
1889 }
1890
1891 fn deny_order(&self, order: &OrderAny, reason: &str) {
1892 log::warn!(
1893 "SubmitOrder for {} DENIED: {}",
1894 order.client_order_id(),
1895 reason
1896 );
1897
1898 if order.status() != OrderStatus::Initialized {
1899 return;
1900 }
1901
1902 {
1904 let mut cache = self.cache.borrow_mut();
1905 if !cache.order_exists(&order.client_order_id())
1906 && let Err(e) = cache.add_order(order.clone(), None, None, false)
1907 {
1908 log::error!("Cannot add order to cache: {e}");
1909 return;
1910 }
1911 }
1912
1913 let denied = OrderEventAny::Denied(OrderDenied::new(
1914 order.trader_id(),
1915 order.strategy_id(),
1916 order.instrument_id(),
1917 order.client_order_id(),
1918 reason.into(),
1919 UUID4::new(),
1920 self.clock.borrow().timestamp_ns(),
1921 self.clock.borrow().timestamp_ns(),
1922 ));
1923
1924 let endpoint = MessagingSwitchboard::exec_engine_process();
1925 msgbus::send_order_event(endpoint, denied);
1926 }
1927
1928 fn deny_order_list(&self, orders: &[OrderAny], reason: &str) {
1929 for order in orders {
1930 if !order.is_closed() {
1931 self.deny_order(order, reason);
1932 }
1933 }
1934 }
1935
1936 fn reject_modify_order(&self, order: &OrderAny, reason: &str) {
1937 let ts_event = self.clock.borrow().timestamp_ns();
1938 let denied = OrderEventAny::ModifyRejected(OrderModifyRejected::new(
1939 order.trader_id(),
1940 order.strategy_id(),
1941 order.instrument_id(),
1942 order.client_order_id(),
1943 reason.into(),
1944 UUID4::new(),
1945 ts_event,
1946 ts_event,
1947 false,
1948 order.venue_order_id(),
1949 order.account_id(),
1950 ));
1951
1952 let endpoint = MessagingSwitchboard::exec_engine_process();
1953 msgbus::send_order_event(endpoint, denied);
1954 }
1955
1956 fn execution_gateway(&mut self, instrument: &InstrumentAny, command: TradingCommand) {
1957 match self.trading_state {
1958 TradingState::Halted => match command {
1959 TradingCommand::SubmitOrder(submit_order) => {
1960 let order = {
1961 let cache = self.cache.borrow();
1962 cache
1963 .order(&submit_order.client_order_id)
1964 .map(|o| o.clone())
1965 };
1966
1967 if let Some(ref order) = order {
1968 self.deny_order(order, &OrderDeniedReason::TradingHalted.to_string());
1969 }
1970 }
1971 TradingCommand::SubmitOrderList(submit_order_list) => {
1972 let orders: Vec<OrderAny> = self.cache.borrow().orders_for_ids(
1973 &submit_order_list.order_list.client_order_ids,
1974 &submit_order_list,
1975 );
1976 self.deny_order_list(&orders, &OrderDeniedReason::TradingHalted.to_string());
1977 }
1978 _ => {}
1979 },
1980 TradingState::Reducing => {
1981 match &command {
1982 TradingCommand::SubmitOrder(submit_order) => {
1983 let order = {
1984 let cache = self.cache.borrow();
1985 cache
1986 .order(&submit_order.client_order_id)
1987 .map(|o| o.clone())
1988 };
1989
1990 if let Some(ref order) = order
1991 && ((order.is_buy() && self.portfolio.is_net_long(&instrument.id()))
1992 || (order.is_sell()
1993 && self.portfolio.is_net_short(&instrument.id())))
1994 {
1995 self.deny_order(
1996 order,
1997 &OrderDeniedReason::TradingStateReducing {
1998 order_side: order.order_side(),
1999 instrument_id: instrument.id(),
2000 }
2001 .to_string(),
2002 );
2003 return;
2004 }
2005 }
2006 TradingCommand::SubmitOrderList(submit_order_list) => {
2007 let orders: Vec<OrderAny> = self.cache.borrow().orders_for_ids(
2008 &submit_order_list.order_list.client_order_ids,
2009 &submit_order_list,
2010 );
2011
2012 for order in &orders {
2013 let order_instrument_id = order.instrument_id();
2014 if (order.is_buy() && self.portfolio.is_net_long(&order_instrument_id))
2015 || (order.is_sell()
2016 && self.portfolio.is_net_short(&order_instrument_id))
2017 {
2018 self.deny_order_list(
2019 &orders,
2020 &OrderDeniedReason::TradingStateReducing {
2021 order_side: order.order_side(),
2022 instrument_id: order_instrument_id,
2023 }
2024 .to_string(),
2025 );
2026 return;
2027 }
2028 }
2029 }
2030 _ => {}
2031 }
2032 self.throttled_submit.send(command);
2034 }
2035 TradingState::Active => match command {
2036 TradingCommand::SubmitOrder(_) | TradingCommand::SubmitOrderList(_) => {
2037 self.throttled_submit.send(command);
2038 }
2039 _ => {}
2040 },
2041 }
2042 }
2043
2044 fn send_to_execution(command: TradingCommand) {
2045 let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
2046 msgbus::send_trading_command(endpoint, command);
2047 }
2048
2049 fn handle_event(&self, event: &OrderEventAny) {
2050 if self.config.debug {
2053 log::debug!("{RECV}{EVT} {event:?}");
2054 }
2055 }
2056
2057 fn handle_position_event(&self, event: &PositionEvent) {
2058 if self.config.debug {
2059 log::debug!("{RECV}{EVT} {event:?}");
2060 }
2061 }
2062}