1use std::{future::Future, sync::Mutex};
19
20use anyhow::Context;
21use async_trait::async_trait;
22use futures_util::{StreamExt, pin_mut};
23use nautilus_common::{
24 clients::ExecutionClient,
25 live::{get_runtime, runner::get_exec_event_sender},
26 messages::execution::{
27 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
28 GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
29 GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
30 GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder, SubmitOrder,
31 SubmitOrderList,
32 },
33};
34use nautilus_core::{
35 MUTEX_POISONED, UnixNanos,
36 datetime::NANOSECONDS_IN_SECOND,
37 time::{AtomicTime, get_atomic_clock_realtime},
38};
39use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter};
40use nautilus_model::{
41 accounts::AccountAny,
42 enums::{AccountType, OmsType, OrderSide, OrderType, TimeInForce},
43 events::OrderEventAny,
44 identifiers::{AccountId, ClientId, Venue},
45 orders::{Order, OrderAny},
46 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
47 types::{AccountBalance, MarginBalance},
48};
49use tokio::task::JoinHandle;
50
51use crate::{
52 common::{
53 consts::{DERIBIT_VENUE, DERIBIT_WS_HEARTBEAT_SECS},
54 enums::resolve_trigger_type,
55 },
56 config::DeribitExecClientConfig,
57 http::{client::DeribitHttpClient, models::DeribitCurrency, query::GetOrderStateParams},
58 websocket::{
59 auth::DERIBIT_EXECUTION_SESSION_NAME,
60 client::DeribitWebSocketClient,
61 messages::{DeribitOrderParams, NautilusWsMessage},
62 parse::parse_user_order_msg,
63 },
64};
65
66#[derive(Debug)]
68pub struct DeribitExecutionClient {
69 core: ExecutionClientCore,
70 clock: &'static AtomicTime,
71 config: DeribitExecClientConfig,
72 emitter: ExecutionEventEmitter,
73 http_client: DeribitHttpClient,
74 ws_client: DeribitWebSocketClient,
75 ws_stream_handle: Option<JoinHandle<()>>,
76 pending_tasks: Mutex<Vec<JoinHandle<()>>>,
77}
78
79impl DeribitExecutionClient {
80 pub fn new(core: ExecutionClientCore, config: DeribitExecClientConfig) -> anyhow::Result<Self> {
86 let http_client = if config.has_api_credentials() {
87 DeribitHttpClient::new_with_env(
88 config.api_key.clone(),
89 config.api_secret.clone(),
90 config.base_url_http.clone(),
91 config.environment,
92 config.http_timeout_secs,
93 config.max_retries,
94 config.retry_delay_initial_ms,
95 config.retry_delay_max_ms,
96 config.proxy_url.clone(),
97 )?
98 } else {
99 DeribitHttpClient::new(
100 config.base_url_http.clone(),
101 config.environment,
102 config.http_timeout_secs,
103 config.max_retries,
104 config.retry_delay_initial_ms,
105 config.retry_delay_max_ms,
106 config.proxy_url.clone(),
107 )?
108 };
109
110 let mut ws_client = DeribitWebSocketClient::new(
111 config.base_url_ws.clone(),
112 config.api_key.clone(),
113 config.api_secret.clone(),
114 DERIBIT_WS_HEARTBEAT_SECS,
115 config.environment,
116 config.transport_backend,
117 config.proxy_url.clone(),
118 )
119 .context("failed to create WebSocket client for execution")?;
120 ws_client.set_account_id(core.account_id);
122
123 let clock = get_atomic_clock_realtime();
124 let emitter = ExecutionEventEmitter::new(
125 clock,
126 core.trader_id,
127 core.account_id,
128 AccountType::Margin,
129 None,
130 );
131
132 Ok(Self {
133 core,
134 clock,
135 config,
136 emitter,
137 http_client,
138 ws_client,
139 ws_stream_handle: None,
140 pending_tasks: Mutex::new(Vec::new()),
141 })
142 }
143
144 fn spawn_task<F>(&self, description: &'static str, fut: F)
146 where
147 F: Future<Output = anyhow::Result<()>> + Send + 'static,
148 {
149 let runtime = get_runtime();
150 let handle = runtime.spawn(async move {
151 if let Err(e) = fut.await {
152 log::warn!("{description} failed: {e:?}");
153 }
154 });
155
156 let mut tasks = self.pending_tasks.lock().expect(MUTEX_POISONED);
157 tasks.retain(|handle| !handle.is_finished());
158 tasks.push(handle);
159 }
160
161 fn abort_pending_tasks(&self) {
163 let mut tasks = self.pending_tasks.lock().expect(MUTEX_POISONED);
164 for handle in tasks.drain(..) {
165 handle.abort();
166 }
167 }
168
169 fn build_order_params(order: &dyn Order) -> anyhow::Result<DeribitOrderParams> {
171 let order_type = match order.order_type() {
172 OrderType::Limit => "limit",
173 OrderType::Market => "market",
174 OrderType::StopLimit => "stop_limit",
175 OrderType::StopMarket => "stop_market",
176 OrderType::LimitIfTouched => "take_limit",
177 OrderType::MarketIfTouched => "take_market",
178 other => {
179 anyhow::bail!("Unsupported order type {other:?} for Deribit");
180 }
181 }
182 .to_string();
183
184 let time_in_force = if matches!(
185 order.order_type(),
186 OrderType::Market | OrderType::StopMarket | OrderType::MarketIfTouched
187 ) {
188 None
190 } else {
191 Some(
192 match order.time_in_force() {
193 TimeInForce::Gtc => "good_til_cancelled",
194 TimeInForce::Ioc => "immediate_or_cancel",
195 TimeInForce::Fok => "fill_or_kill",
196 TimeInForce::Gtd => {
197 if order.expire_time().is_some() {
198 log::warn!(
199 "Deribit GTD orders expire at 8:00 UTC only - custom expire_time is ignored. \
200 For custom expiry times, use managed GTD with emulation_trigger"
201 );
202 }
203 "good_til_day"
204 }
205 other => {
206 anyhow::bail!("Unsupported time_in_force {other:?} for Deribit");
207 }
208 }
209 .to_string(),
210 )
211 };
212
213 let valid_until = None;
216
217 let trigger = resolve_trigger_type(order.trigger_type());
218
219 Ok(DeribitOrderParams {
220 instrument_name: order.instrument_id().symbol.to_string(),
221 amount: order.quantity().as_decimal(),
222 order_type,
223 label: Some(order.client_order_id().to_string()),
224 price: order.price().map(|p| p.as_decimal()),
225 time_in_force,
226 post_only: if order.is_post_only() {
227 Some(true)
228 } else {
229 None
230 },
231 reject_post_only: if order.is_post_only() {
232 Some(true)
233 } else {
234 None
235 },
236 reduce_only: if order.is_reduce_only() {
237 Some(true)
238 } else {
239 None
240 },
241 trigger_price: order.trigger_price().map(|p| p.as_decimal()),
242 trigger,
243 max_show: None,
244 valid_until,
245 })
246 }
247
248 fn submit_single_order(&self, order: &OrderAny, task_name: &'static str) {
252 if order.is_closed() {
253 log::warn!("Cannot submit closed order {}", order.client_order_id());
254 return;
255 }
256
257 let params = match Self::build_order_params(order) {
258 Ok(params) => params,
259 Err(e) => {
260 let ts_event = self.clock.get_time_ns();
261 self.emitter.emit_order_rejected_event(
262 order.strategy_id(),
263 order.instrument_id(),
264 order.client_order_id(),
265 &format!("{e}"),
266 ts_event,
267 false,
268 );
269 return;
270 }
271 };
272 let client_order_id = order.client_order_id();
273 let trader_id = order.trader_id();
274 let strategy_id = order.strategy_id();
275 let instrument_id = order.instrument_id();
276 let order_side = order.order_side();
277
278 log::debug!("OrderSubmitted client_order_id={client_order_id}");
279 self.emitter.emit_order_submitted(order);
280
281 let ws_client = self.ws_client.clone();
282
283 self.spawn_task(task_name, async move {
284 let result = ws_client
285 .submit_order(
286 order_side,
287 params,
288 client_order_id,
289 trader_id,
290 strategy_id,
291 instrument_id,
292 )
293 .await;
294
295 if let Err(e) = result {
296 log::error!(
297 "Submit order request failed: task={task_name}, client_order_id={client_order_id}, error={e}"
298 );
299 return Err(e.into());
300 }
301
302 Ok(())
303 });
304 }
305
306 fn spawn_stream_handler(
308 &mut self,
309 stream: impl futures_util::Stream<Item = NautilusWsMessage> + Send + 'static,
310 ) {
311 if self.ws_stream_handle.is_some() {
312 return;
313 }
314
315 let emitter = self.emitter.clone();
316
317 let handle = get_runtime().spawn(async move {
318 pin_mut!(stream);
319 while let Some(message) = stream.next().await {
320 dispatch_ws_message(message, &emitter);
321 }
322 });
323
324 self.ws_stream_handle = Some(handle);
325 log::debug!("WebSocket stream handler started");
326 }
327}
328
329#[async_trait(?Send)]
330impl ExecutionClient for DeribitExecutionClient {
331 fn is_connected(&self) -> bool {
332 self.core.is_connected()
333 }
334
335 fn client_id(&self) -> ClientId {
336 self.core.client_id
337 }
338
339 fn account_id(&self) -> AccountId {
340 self.core.account_id
341 }
342
343 fn venue(&self) -> Venue {
344 *DERIBIT_VENUE
345 }
346
347 fn oms_type(&self) -> OmsType {
348 self.core.oms_type
349 }
350
351 fn get_account(&self) -> Option<AccountAny> {
352 self.core.cache().account_owned(&self.core.account_id)
353 }
354
355 fn generate_account_state(
356 &self,
357 balances: Vec<AccountBalance>,
358 margins: Vec<MarginBalance>,
359 reported: bool,
360 ts_event: UnixNanos,
361 ) -> anyhow::Result<()> {
362 self.emitter
363 .emit_account_state(balances, margins, reported, ts_event);
364 Ok(())
365 }
366
367 fn start(&mut self) -> anyhow::Result<()> {
368 if self.core.is_started() {
369 return Ok(());
370 }
371
372 let sender = get_exec_event_sender();
373 self.emitter.set_sender(sender);
374 self.core.set_started();
375
376 log::info!(
377 "Started: client_id={}, account_id={}, account_type={:?}, product_types={:?}, environment={}",
378 self.core.client_id,
379 self.core.account_id,
380 self.core.account_type,
381 self.config.product_types,
382 self.config.environment
383 );
384 Ok(())
385 }
386
387 fn stop(&mut self) -> anyhow::Result<()> {
388 if self.core.is_stopped() {
389 return Ok(());
390 }
391
392 self.core.set_stopped();
393 self.core.set_disconnected();
394 self.abort_pending_tasks();
395 log::info!("Stopped: client_id={}", self.core.client_id);
396 Ok(())
397 }
398
399 async fn connect(&mut self) -> anyhow::Result<()> {
400 if self.core.is_connected() {
401 return Ok(());
402 }
403
404 if !self.config.has_api_credentials() {
406 anyhow::bail!("Missing API credentials; set Deribit environment variables");
407 }
408
409 self.ws_client.set_account_id(self.core.account_id);
411
412 if !self.core.instruments_initialized() {
414 for product_type in &self.config.product_types {
415 let instruments = self
416 .http_client
417 .request_instruments(DeribitCurrency::ANY, Some(*product_type))
418 .await
419 .with_context(|| {
420 format!("failed to request instruments for {product_type:?}")
421 })?;
422
423 if instruments.is_empty() {
424 log::warn!("No instruments returned for {product_type:?}");
425 continue;
426 }
427
428 log::debug!("Fetched {} {product_type:?} instruments", instruments.len());
429 self.ws_client.cache_instruments(&instruments);
430 self.http_client.cache_instruments(&instruments);
431 }
432 self.core.set_instruments_initialized();
433 }
434
435 let account_state = self
437 .http_client
438 .request_account_state(self.core.account_id)
439 .await
440 .context("failed to request account state")?;
441
442 self.emitter.send_account_state(account_state);
443
444 self.ws_client
445 .connect()
446 .await
447 .context("failed to connect WebSocket client for execution")?;
448
449 self.ws_client
450 .authenticate_session(DERIBIT_EXECUTION_SESSION_NAME)
451 .await
452 .map_err(|e| anyhow::anyhow!("failed to authenticate WebSocket session: {e}"))?;
453
454 log::debug!("WebSocket client authenticated for execution");
455
456 self.ws_client
458 .subscribe_user_orders()
459 .await
460 .map_err(|e| anyhow::anyhow!("failed to subscribe to user orders: {e}"))?;
461 self.ws_client
462 .subscribe_user_trades()
463 .await
464 .map_err(|e| anyhow::anyhow!("failed to subscribe to user trades: {e}"))?;
465 self.ws_client
466 .subscribe_user_portfolio()
467 .await
468 .map_err(|e| anyhow::anyhow!("failed to subscribe to user portfolio: {e}"))?;
469
470 if let Err(e) = self.ws_client.wait_for_subscriptions_confirmed(30.0).await {
471 let _ = self.ws_client.unsubscribe_user_orders().await;
473 let _ = self.ws_client.unsubscribe_user_trades().await;
474 let _ = self.ws_client.unsubscribe_user_portfolio().await;
475 anyhow::bail!("subscription confirmation failed: {e}");
476 }
477
478 log::debug!("Subscribed to user order, trade, and portfolio updates");
479
480 let stream = self.ws_client.stream()?;
482 self.spawn_stream_handler(stream);
483
484 self.core.set_connected();
485 log::info!("Connected: client_id={}", self.core.client_id);
486 Ok(())
487 }
488
489 async fn disconnect(&mut self) -> anyhow::Result<()> {
490 if self.core.is_disconnected() {
491 return Ok(());
492 }
493
494 self.abort_pending_tasks();
495
496 if let Some(handle) = self.ws_stream_handle.take() {
498 handle.abort();
499 }
500
501 if let Err(e) = self.ws_client.close().await {
503 log::warn!("Error closing WebSocket client: {e}");
504 }
505
506 self.core.set_disconnected();
507 log::info!("Disconnected: client_id={}", self.core.client_id);
508 Ok(())
509 }
510
511 async fn generate_order_status_report(
512 &self,
513 cmd: &GenerateOrderStatusReport,
514 ) -> anyhow::Result<Option<OrderStatusReport>> {
515 if let Some(venue_order_id) = &cmd.venue_order_id {
517 let params = GetOrderStateParams {
518 order_id: venue_order_id.to_string(),
519 };
520 let ts_init = self.clock.get_time_ns();
521
522 match self.http_client.inner.get_order_state(params).await {
523 Ok(response) => {
524 if let Some(order) = response.result {
525 let symbol = ustr::Ustr::from(&order.instrument_name);
526 if let Some(instrument) = self.http_client.get_instrument(&symbol) {
527 let report = parse_user_order_msg(
528 &order,
529 &instrument,
530 self.core.account_id,
531 ts_init,
532 )?;
533 return Ok(Some(report));
534 } else {
535 log::warn!(
536 "Instrument {} not in cache for order {}",
537 order.instrument_name,
538 order.order_id
539 );
540 }
541 }
542 }
543 Err(e) => {
544 log::warn!("Failed to get order state: {e}");
545 }
546 }
547 return Ok(None);
548 }
549
550 if let Some(client_order_id) = &cmd.client_order_id {
552 let reports = self
553 .http_client
554 .request_order_status_reports(
555 self.core.account_id,
556 cmd.instrument_id,
557 None,
558 None,
559 false, )
561 .await?;
562
563 for report in reports {
565 if report.client_order_id == Some(*client_order_id) {
566 return Ok(Some(report));
567 }
568 }
569 }
570
571 Ok(None)
572 }
573
574 async fn generate_order_status_reports(
575 &self,
576 cmd: &GenerateOrderStatusReports,
577 ) -> anyhow::Result<Vec<OrderStatusReport>> {
578 self.http_client
579 .request_order_status_reports(
580 self.core.account_id,
581 cmd.instrument_id,
582 cmd.start,
583 cmd.end,
584 cmd.open_only,
585 )
586 .await
587 }
588
589 async fn generate_fill_reports(
590 &self,
591 cmd: GenerateFillReports,
592 ) -> anyhow::Result<Vec<FillReport>> {
593 let mut reports = self
594 .http_client
595 .request_fill_reports(self.core.account_id, cmd.instrument_id, cmd.start, cmd.end)
596 .await?;
597
598 if let Some(venue_order_id) = &cmd.venue_order_id {
600 reports.retain(|r| r.venue_order_id == *venue_order_id);
601 }
602
603 Ok(reports)
604 }
605
606 async fn generate_position_status_reports(
607 &self,
608 cmd: &GeneratePositionStatusReports,
609 ) -> anyhow::Result<Vec<PositionStatusReport>> {
610 self.http_client
611 .request_position_status_reports(self.core.account_id, cmd.instrument_id)
612 .await
613 }
614
615 async fn generate_mass_status(
616 &self,
617 lookback_mins: Option<u64>,
618 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
619 log::info!("Generating ExecutionMassStatus (lookback_mins={lookback_mins:?})");
620 let ts_now = self.clock.get_time_ns();
621 let start = lookback_mins.map(|mins| {
622 let lookback_ns = mins
623 .saturating_mul(60)
624 .saturating_mul(NANOSECONDS_IN_SECOND);
625 UnixNanos::from(ts_now.as_u64().saturating_sub(lookback_ns))
626 });
627
628 let order_cmd = GenerateOrderStatusReportsBuilder::default()
629 .ts_init(ts_now)
630 .open_only(false) .start(start)
632 .build()
633 .context("Failed to build GenerateOrderStatusReports")?;
634
635 let fill_cmd = GenerateFillReportsBuilder::default()
636 .ts_init(ts_now)
637 .start(start)
638 .build()
639 .context("Failed to build GenerateFillReports")?;
640
641 let position_cmd = GeneratePositionStatusReportsBuilder::default()
642 .ts_init(ts_now)
643 .start(start)
644 .build()
645 .context("Failed to build GeneratePositionStatusReports")?;
646
647 let (order_reports, fill_reports, position_reports) = tokio::try_join!(
648 self.generate_order_status_reports(&order_cmd),
649 self.generate_fill_reports(fill_cmd),
650 self.generate_position_status_reports(&position_cmd),
651 )?;
652
653 log::info!("Received {} OrderStatusReports", order_reports.len());
654 log::info!("Received {} FillReports", fill_reports.len());
655 log::info!("Received {} PositionReports", position_reports.len());
656
657 let mut mass_status = ExecutionMassStatus::new(
658 self.core.client_id,
659 self.core.account_id,
660 *DERIBIT_VENUE,
661 ts_now,
662 None,
663 );
664
665 mass_status.add_order_reports(order_reports);
666 mass_status.add_fill_reports(fill_reports);
667 mass_status.add_position_reports(position_reports);
668
669 Ok(Some(mass_status))
670 }
671
672 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
673 let http_client = self.http_client.clone();
674 let account_id = self.core.account_id;
675 let emitter = self.emitter.clone();
676
677 self.spawn_task("query_account", async move {
678 let account_state = http_client
679 .request_account_state(account_id)
680 .await
681 .context("failed to query account state (check API credentials are valid)")?;
682
683 emitter.send_account_state(account_state);
684 Ok(())
685 });
686
687 Ok(())
688 }
689
690 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
691 let ws_client = self.ws_client.clone();
692
693 let order_id = cmd
695 .venue_order_id
696 .as_ref()
697 .ok_or_else(|| anyhow::anyhow!("venue_order_id required for query_order"))?
698 .to_string();
699
700 let client_order_id = cmd.client_order_id;
701 let trader_id = cmd.trader_id;
702 let strategy_id = cmd.strategy_id;
703 let instrument_id = cmd.instrument_id;
704
705 log::debug!("Querying order state: order_id={order_id}, client_order_id={client_order_id}");
706
707 self.spawn_task("query_order", async move {
710 ws_client
711 .query_order(
712 &order_id,
713 client_order_id,
714 trader_id,
715 strategy_id,
716 instrument_id,
717 )
718 .await
719 .map_err(|e| anyhow::anyhow!("Query order state failed: {e}"))?;
720 Ok(())
721 });
722
723 Ok(())
724 }
725
726 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
727 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
728 self.submit_single_order(&order, "submit_order");
729 Ok(())
730 }
731
732 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
733 if cmd.order_list.client_order_ids.is_empty() {
734 log::debug!("submit_order_list called with empty order list");
735 return Ok(());
736 }
737
738 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
739
740 log::debug!(
741 "Submitting order list {} with {} orders for instrument={}",
742 cmd.order_list.id,
743 orders.len(),
744 cmd.instrument_id
745 );
746
747 for order in &orders {
750 self.submit_single_order(order, "submit_order_list_item");
751 }
752
753 Ok(())
754 }
755
756 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
757 let ws_client = self.ws_client.clone();
758
759 let order_id = match cmd.venue_order_id.as_ref() {
761 Some(venue_order_id) => venue_order_id.to_string(),
762 None => {
763 return reject_modify_command(
764 &self.emitter,
765 self.clock,
766 &cmd,
767 "venue_order_id required for modify_order",
768 );
769 }
770 };
771
772 let quantity = if let Some(qty) = cmd.quantity {
774 qty
775 } else {
776 let cache = self.core.cache();
778 match cache.order(&cmd.client_order_id) {
779 Some(order) => order.quantity(),
780 None => {
781 return reject_modify_command(
782 &self.emitter,
783 self.clock,
784 &cmd,
785 &format!("Order not found: {}", cmd.client_order_id),
786 );
787 }
788 }
789 };
790
791 let price = match cmd.price {
792 Some(price) => price,
793 None => {
794 return reject_modify_command(
795 &self.emitter,
796 self.clock,
797 &cmd,
798 "price required for modify_order",
799 );
800 }
801 };
802
803 let client_order_id = cmd.client_order_id;
804 let trader_id = cmd.trader_id;
805 let strategy_id = cmd.strategy_id;
806 let instrument_id = cmd.instrument_id;
807
808 log::debug!(
809 "Modifying order: order_id={order_id}, quantity={quantity}, price={price}, client_order_id={client_order_id}"
810 );
811
812 self.spawn_task("modify_order", async move {
814 if let Err(e) = ws_client
815 .modify_order(
816 &order_id,
817 quantity,
818 price,
819 client_order_id,
820 trader_id,
821 strategy_id,
822 instrument_id,
823 )
824 .await
825 {
826 log::error!(
827 "Modify order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
828 );
829 anyhow::bail!("Modify order failed: {e}");
830 }
831 Ok(())
832 });
833
834 Ok(())
835 }
836
837 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
838 let ws_client = self.ws_client.clone();
839
840 let order_id = match cmd.venue_order_id.as_ref() {
842 Some(venue_order_id) => venue_order_id.to_string(),
843 None => {
844 log::warn!(
845 "Cannot cancel order {} - no venue_order_id",
846 cmd.client_order_id
847 );
848 return Ok(());
849 }
850 };
851
852 let client_order_id = cmd.client_order_id;
853 let trader_id = cmd.trader_id;
854 let strategy_id = cmd.strategy_id;
855 let instrument_id = cmd.instrument_id;
856
857 log::debug!("Canceling order: order_id={order_id}, client_order_id={client_order_id}");
858
859 self.spawn_task("cancel_order", async move {
861 if let Err(e) = ws_client
862 .cancel_order(
863 &order_id,
864 client_order_id,
865 trader_id,
866 strategy_id,
867 instrument_id,
868 )
869 .await
870 {
871 log::error!(
872 "Cancel order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
873 );
874 anyhow::bail!("Cancel order failed: {e}");
875 }
876 Ok(())
877 });
878
879 Ok(())
880 }
881
882 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
883 let instrument_id = cmd.instrument_id;
884
885 if cmd.order_side == OrderSide::NoOrderSide {
887 log::debug!(
888 "Cancelling all orders: instrument={instrument_id}, order_side=NoOrderSide (bulk)"
889 );
890
891 let ws_client = self.ws_client.clone();
892 self.spawn_task("cancel_all_orders", async move {
893 if let Err(e) = ws_client.cancel_all_orders(instrument_id, None).await {
894 log::error!("Cancel all orders failed for instrument {instrument_id}: {e}");
895 anyhow::bail!("Cancel all orders failed: {e}");
896 }
897 Ok(())
898 });
899
900 return Ok(());
901 }
902
903 log::debug!(
906 "Cancelling orders by side: instrument={}, order_side={}",
907 instrument_id,
908 cmd.order_side
909 );
910
911 let orders_to_cancel: Vec<_> = {
912 let cache = self.core.cache();
913 let open_orders = cache.orders_open(None, Some(&instrument_id), None, None, None);
914
915 open_orders
916 .into_iter()
917 .filter(|order| order.order_side() == cmd.order_side)
918 .filter_map(|order| {
919 let venue_order_id = order.venue_order_id()?;
920 Some((
921 venue_order_id.to_string(),
922 order.client_order_id(),
923 order.instrument_id(),
924 Some(venue_order_id),
925 ))
926 })
927 .collect()
928 };
929
930 if orders_to_cancel.is_empty() {
931 log::debug!(
932 "No open {} orders to cancel for {}",
933 cmd.order_side,
934 instrument_id
935 );
936 return Ok(());
937 }
938
939 log::debug!(
940 "Cancelling {} {} orders for {}",
941 orders_to_cancel.len(),
942 cmd.order_side,
943 instrument_id
944 );
945
946 for (venue_order_id_str, client_order_id, order_instrument_id, _venue_order_id) in
948 orders_to_cancel
949 {
950 let ws_client = self.ws_client.clone();
951 let trader_id = cmd.trader_id;
952 let strategy_id = cmd.strategy_id;
953
954 self.spawn_task("cancel_order_by_side", async move {
955 if let Err(e) = ws_client
956 .cancel_order(
957 &venue_order_id_str,
958 client_order_id,
959 trader_id,
960 strategy_id,
961 order_instrument_id,
962 )
963 .await
964 {
965 log::error!(
966 "Cancel order failed: order_id={venue_order_id_str}, client_order_id={client_order_id}, error={e}"
967 );
968 }
969 Ok(())
970 });
971 }
972
973 Ok(())
974 }
975
976 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
977 if cmd.cancels.is_empty() {
978 log::debug!("batch_cancel_orders called with empty cancels list");
979 return Ok(());
980 }
981
982 log::debug!(
983 "Batch cancelling {} orders for instrument={}",
984 cmd.cancels.len(),
985 cmd.instrument_id
986 );
987
988 for cancel in &cmd.cancels {
991 let order_id = match &cancel.venue_order_id {
992 Some(id) => id.to_string(),
993 None => {
994 log::warn!(
995 "Cannot cancel order {} - no venue_order_id",
996 cancel.client_order_id
997 );
998 continue;
999 }
1000 };
1001
1002 let ws_client = self.ws_client.clone();
1003 let client_order_id = cancel.client_order_id;
1004 let trader_id = cancel.trader_id;
1005 let strategy_id = cancel.strategy_id;
1006 let instrument_id = cancel.instrument_id;
1007
1008 self.spawn_task("batch_cancel_order", async move {
1009 if let Err(e) = ws_client
1010 .cancel_order(
1011 &order_id,
1012 client_order_id,
1013 trader_id,
1014 strategy_id,
1015 instrument_id,
1016 )
1017 .await
1018 {
1019 log::error!(
1020 "Batch cancel order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
1021 );
1022 anyhow::bail!("Batch cancel order failed: {e}");
1023 }
1024 Ok(())
1025 });
1026 }
1027
1028 Ok(())
1029 }
1030}
1031
1032fn dispatch_ws_message(message: NautilusWsMessage, emitter: &ExecutionEventEmitter) {
1034 match message {
1035 NautilusWsMessage::AccountState(state) => {
1036 emitter.send_account_state(state);
1037 }
1038 NautilusWsMessage::OrderStatusReports(reports) => {
1039 log::debug!("Processing {} order status report(s)", reports.len());
1040 for report in reports {
1041 emitter.send_order_status_report(report);
1042 }
1043 }
1044 NautilusWsMessage::FillReports(reports) => {
1045 log::debug!("Processing {} fill report(s)", reports.len());
1046 for report in reports {
1047 emitter.send_fill_report(report);
1048 }
1049 }
1050 NautilusWsMessage::OrderRejected(event) => {
1051 emitter.send_order_event(OrderEventAny::Rejected(event));
1052 }
1053 NautilusWsMessage::OrderAccepted(event) => {
1054 emitter.send_order_event(OrderEventAny::Accepted(event));
1055 }
1056 NautilusWsMessage::OrderCanceled(event) => {
1057 emitter.send_order_event(OrderEventAny::Canceled(event));
1058 }
1059 NautilusWsMessage::OrderExpired(event) => {
1060 emitter.send_order_event(OrderEventAny::Expired(event));
1061 }
1062 NautilusWsMessage::OrderUpdated(event) => {
1063 emitter.send_order_event(OrderEventAny::Updated(event));
1064 }
1065 NautilusWsMessage::OrderCancelRejected(event) => {
1066 emitter.send_order_event(OrderEventAny::CancelRejected(event));
1067 }
1068 NautilusWsMessage::OrderModifyRejected(event) => {
1069 emitter.send_order_event(OrderEventAny::ModifyRejected(event));
1070 }
1071 NautilusWsMessage::Error(e) => {
1072 log::warn!("WebSocket error: {e}");
1073 }
1074 NautilusWsMessage::Reconnected => {
1075 log::info!("WebSocket reconnected");
1076 }
1077 NautilusWsMessage::Authenticated(auth) => {
1078 log::debug!("WebSocket authenticated: scope={}", auth.scope);
1079 }
1080 NautilusWsMessage::AuthenticationFailed(reason) => {
1081 log::error!("Authentication failed in execution client: {reason}");
1082 }
1083 NautilusWsMessage::Data(_)
1084 | NautilusWsMessage::Deltas(_)
1085 | NautilusWsMessage::Instrument(_)
1086 | NautilusWsMessage::InstrumentStatus(_)
1087 | NautilusWsMessage::FundingRates(_)
1088 | NautilusWsMessage::OptionGreeks(_)
1089 | NautilusWsMessage::Raw(_) => {
1090 log::trace!("Ignoring data message in execution client");
1092 }
1093 }
1094}
1095
1096fn reject_modify_command(
1097 emitter: &ExecutionEventEmitter,
1098 clock: &AtomicTime,
1099 cmd: &ModifyOrder,
1100 reason: &str,
1101) -> anyhow::Result<()> {
1102 let ts_event = clock.get_time_ns();
1103 emitter.emit_order_modify_rejected_event(
1104 cmd.strategy_id,
1105 cmd.instrument_id,
1106 cmd.client_order_id,
1107 cmd.venue_order_id,
1108 reason,
1109 ts_event,
1110 );
1111 anyhow::bail!("{reason}");
1112}