1use std::{future::Future, io, time::Duration};
19
20use anyhow::Context;
21use async_trait::async_trait;
22use futures_util::{StreamExt, pin_mut};
23use nautilus_common::{
24 clients::ExecutionClient,
25 live::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 DurationNanos, Params, UnixNanos,
36 time::{AtomicTime, get_atomic_clock_realtime},
37};
38use nautilus_live::{
39 ExecutionClientCore, ExecutionEventEmitter, SocketControl,
40 task::{TaskGroup, TaskGroupGuard},
41};
42use nautilus_model::{
43 accounts::AccountAny,
44 enums::{AccountType, OmsType, OrderType, TimeInForce},
45 events::OrderEventAny,
46 identifiers::{AccountId, ClientId, Venue},
47 orders::{Order, OrderAny},
48 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
49 types::{AccountBalance, MarginBalance},
50};
51use serde::Serialize;
52
53use crate::{
54 common::{
55 consts::{DERIBIT_VENUE, DERIBIT_WS_HEARTBEAT_SECS},
56 enums::resolve_trigger_type,
57 },
58 config::DeribitExecutionClientConfig,
59 http::{client::DeribitHttpClient, models::DeribitCurrency, query::GetOrderStateParams},
60 websocket::{
61 auth::DERIBIT_EXECUTION_SESSION_NAME,
62 client::DeribitWebSocketClient,
63 messages::{DeribitEditParams, DeribitOrderParams, NautilusWsMessage},
64 parse::parse_user_order_msg,
65 },
66};
67
68#[derive(Debug)]
70pub struct DeribitExecutionClient {
71 core: ExecutionClientCore,
72 clock: &'static AtomicTime,
73 config: DeribitExecutionClientConfig,
74 emitter: ExecutionEventEmitter,
75 http_client: DeribitHttpClient,
76 ws_client: DeribitWebSocketClient,
77 session_tasks: TaskGroup,
78 pending_tasks: TaskGroup,
79}
80
81impl DeribitExecutionClient {
82 pub fn new(
88 core: ExecutionClientCore,
89 config: DeribitExecutionClientConfig,
90 ) -> anyhow::Result<Self> {
91 let api_key = config
92 .api_key
93 .as_ref()
94 .map(|value| value.expose_secret().to_owned());
95 let api_secret = config
96 .api_secret
97 .as_ref()
98 .map(|value| value.expose_secret().to_owned());
99 let proxy_url = config
100 .proxy_url
101 .as_ref()
102 .map(|value| value.expose_secret().to_owned());
103 let http_client = if config.has_api_credentials() {
104 DeribitHttpClient::new_with_env(
105 api_key.clone(),
106 api_secret.clone(),
107 config.base_url_http.clone(),
108 config.environment,
109 config.http_timeout_secs,
110 config.max_retries,
111 config.retry_delay_initial_ms,
112 config.retry_delay_max_ms,
113 proxy_url.clone(),
114 )?
115 } else {
116 DeribitHttpClient::new(
117 config.base_url_http.clone(),
118 config.environment,
119 config.http_timeout_secs,
120 config.max_retries,
121 config.retry_delay_initial_ms,
122 config.retry_delay_max_ms,
123 proxy_url.clone(),
124 )?
125 };
126
127 let mut ws_client = DeribitWebSocketClient::new(
128 config.base_url_ws.clone(),
129 api_key,
130 api_secret,
131 DERIBIT_WS_HEARTBEAT_SECS,
132 config.auth_timeout_secs,
133 config.environment,
134 config.transport_backend,
135 proxy_url,
136 )
137 .context("failed to create WebSocket client for execution")?
138 .with_socket_control(SocketControl::new(
139 core.client_id,
140 Some(*DERIBIT_VENUE),
141 "deribit-user-streams",
142 ));
143 ws_client.set_account_id(core.account_id);
145
146 let clock = get_atomic_clock_realtime();
147 let emitter = ExecutionEventEmitter::new(
148 clock,
149 core.trader_id,
150 core.account_id,
151 AccountType::Margin,
152 None,
153 );
154
155 let session_tasks = TaskGroup::new();
156 let pending_tasks = TaskGroup::new();
157
158 Ok(Self {
159 core,
160 clock,
161 config,
162 emitter,
163 http_client,
164 ws_client,
165 session_tasks,
166 pending_tasks,
167 })
168 }
169
170 fn spawn_task<F>(&self, description: &'static str, fut: F)
172 where
173 F: Future<Output = anyhow::Result<()>> + Send + 'static,
174 {
175 let future = async move {
176 if let Err(e) = fut.await {
177 log::warn!("{description} failed: {e:?}");
178 }
179 };
180
181 if let Err(e) = self.pending_tasks.spawn(future) {
182 log::warn!("Skipping Deribit {description} after shutdown began: {e}");
183 }
184 }
185
186 fn abort_pending_tasks(&self) {
188 self.pending_tasks.begin_shutdown();
189 }
190
191 fn abort_session_tasks(&self) {
192 self.session_tasks.begin_shutdown();
193 self.ws_client.begin_shutdown();
194 }
195
196 async fn await_pending_tasks(&self) -> anyhow::Result<()> {
197 self.pending_tasks.begin_shutdown();
198 self.pending_tasks
199 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
200 .await
201 .map_err(|e| anyhow::anyhow!("Failed to terminate Deribit execution tasks: {e}"))?;
202 Ok(())
203 }
204
205 async fn await_session_tasks(&self) -> anyhow::Result<()> {
206 self.session_tasks.begin_shutdown();
207 self.session_tasks
208 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
209 .await
210 .map_err(|e| anyhow::anyhow!("Failed to terminate Deribit session tasks: {e}"))?;
211 Ok(())
212 }
213
214 async fn teardown_partial_connect(&self) -> anyhow::Result<()> {
215 self.abort_session_tasks();
216 self.abort_pending_tasks();
217
218 let mut errors = Vec::new();
219 if let Err(e) = self.ws_client.close().await {
220 errors.push(format!("WebSocket shutdown failed: {e}"));
221 }
222 let (session_result, pending_result) =
223 tokio::join!(self.await_session_tasks(), self.await_pending_tasks());
224
225 if let Err(e) = session_result {
226 errors.push(e.to_string());
227 }
228
229 if let Err(e) = pending_result {
230 errors.push(e.to_string());
231 }
232 self.core.set_disconnected();
233
234 if errors.is_empty() {
235 Ok(())
236 } else {
237 anyhow::bail!(errors.join("; "))
238 }
239 }
240
241 fn build_order_params(order: &dyn Order) -> anyhow::Result<DeribitOrderParams> {
243 let order_type = match order.order_type() {
244 OrderType::Limit => "limit",
245 OrderType::Market => "market",
246 OrderType::StopLimit => "stop_limit",
247 OrderType::StopMarket => "stop_market",
248 OrderType::LimitIfTouched => "take_limit",
249 OrderType::MarketIfTouched => "take_market",
250 other => {
251 anyhow::bail!("Unsupported order type {other:?} for Deribit");
252 }
253 }
254 .to_string();
255
256 let time_in_force = if matches!(
257 order.order_type(),
258 OrderType::Market | OrderType::StopMarket | OrderType::MarketIfTouched
259 ) {
260 None
262 } else {
263 Some(
264 match order.time_in_force() {
265 TimeInForce::Gtc => "good_til_cancelled",
266 TimeInForce::Ioc => "immediate_or_cancel",
267 TimeInForce::Fok => "fill_or_kill",
268 TimeInForce::Gtd => {
269 if order.expire_time().is_some() {
270 log::warn!(
271 "Deribit GTD orders expire at 8:00 UTC only - custom expire_time is ignored. \
272 For custom expiry times, use managed GTD with emulation_trigger"
273 );
274 }
275 "good_til_day"
276 }
277 other => {
278 anyhow::bail!("Unsupported time_in_force {other:?} for Deribit");
279 }
280 }
281 .to_string(),
282 )
283 };
284
285 let valid_until = None;
288
289 let trigger = resolve_trigger_type(order.trigger_type());
290
291 let params = DeribitOrderParams {
292 instrument_name: order.instrument_id().symbol.to_string(),
293 amount: order.quantity().as_decimal(),
294 order_type,
295 label: Some(order.client_order_id().to_string()),
296 price: order.price().map(|p| p.as_decimal()),
297 time_in_force,
298 post_only: if order.is_post_only() {
299 Some(true)
300 } else {
301 None
302 },
303 reject_post_only: if order.is_post_only() {
304 Some(true)
305 } else {
306 None
307 },
308 reduce_only: if order.is_reduce_only() {
309 Some(true)
310 } else {
311 None
312 },
313 trigger_price: order.trigger_price().map(|p| p.as_decimal()),
314 trigger,
315 max_show: None,
316 valid_until,
317 };
318
319 check_params_serialize(¶ms)?;
320
321 Ok(params)
322 }
323
324 fn submit_single_order(&self, order: &OrderAny, task_name: &'static str) {
328 if order.is_closed() {
329 log::warn!("Cannot submit closed order {}", order.client_order_id());
330 return;
331 }
332
333 let params = match Self::build_order_params(order) {
334 Ok(params) => params,
335 Err(e) => {
336 let ts_event = self.clock.get_time_ns();
337 self.emitter.emit_order_rejected_event(
338 order.strategy_id(),
339 order.instrument_id(),
340 order.client_order_id(),
341 &format!("{e}"),
342 ts_event,
343 false,
344 );
345 return;
346 }
347 };
348 let client_order_id = order.client_order_id();
349 let trader_id = order.trader_id();
350 let strategy_id = order.strategy_id();
351 let instrument_id = order.instrument_id();
352 let order_side = order.order_side();
353
354 log::debug!("OrderSubmitted client_order_id={client_order_id}");
355 self.emitter.emit_order_submitted(order);
356
357 let ws_client = self.ws_client.clone();
358
359 self.spawn_task(task_name, async move {
360 let result = ws_client
361 .submit_order(
362 order_side,
363 params,
364 client_order_id,
365 trader_id,
366 strategy_id,
367 instrument_id,
368 )
369 .await;
370
371 if let Err(e) = result {
372 log::error!(
373 "Submit order request failed: task={task_name}, client_order_id={client_order_id}, error={e}"
374 );
375 return Err(e.into());
376 }
377
378 Ok(())
379 });
380 }
381
382 fn spawn_stream_handler(
384 &self,
385 stream: impl futures_util::Stream<Item = NautilusWsMessage> + Send + 'static,
386 ) -> anyhow::Result<()> {
387 let emitter = self.emitter.clone();
388
389 self.session_tasks.spawn(async move {
390 pin_mut!(stream);
391 while let Some(message) = stream.next().await {
392 dispatch_ws_message(message, &emitter);
393 }
394 })?;
395
396 log::debug!("WebSocket stream handler started");
397 Ok(())
398 }
399}
400
401#[async_trait(?Send)]
402impl ExecutionClient for DeribitExecutionClient {
403 fn is_connected(&self) -> bool {
404 self.core.is_connected()
405 }
406
407 fn client_id(&self) -> ClientId {
408 self.core.client_id
409 }
410
411 fn account_id(&self) -> AccountId {
412 self.core.account_id
413 }
414
415 fn venue(&self) -> Venue {
416 *DERIBIT_VENUE
417 }
418
419 fn oms_type(&self) -> OmsType {
420 self.core.oms_type
421 }
422
423 fn get_account(&self) -> Option<AccountAny> {
424 self.core.cache().account_owned(&self.core.account_id)
425 }
426
427 fn generate_account_state(
428 &self,
429 balances: Vec<AccountBalance>,
430 margins: Vec<MarginBalance>,
431 reported: bool,
432 ts_event: UnixNanos,
433 info: Option<Params>,
434 ) -> anyhow::Result<()> {
435 self.emitter
436 .emit_account_state(balances, margins, reported, ts_event, info);
437 Ok(())
438 }
439
440 fn start(&mut self) -> anyhow::Result<()> {
441 if self.core.is_started() {
442 return Ok(());
443 }
444
445 let sender = get_exec_event_sender();
446 self.emitter.set_sender(sender);
447 self.core.set_started();
448
449 log::info!(
450 "Started: client_id={}, account_id={}, account_type={:?}, product_types={:?}, environment={}",
451 self.core.client_id,
452 self.core.account_id,
453 self.core.account_type,
454 self.config.product_types,
455 self.config.environment
456 );
457 Ok(())
458 }
459
460 fn stop(&mut self) -> anyhow::Result<()> {
461 if self.core.is_stopped() {
462 return Ok(());
463 }
464
465 self.core.set_stopped();
466 self.core.set_disconnected();
467 self.abort_session_tasks();
468 self.abort_pending_tasks();
469 log::info!("Stopped: client_id={}", self.core.client_id);
470 Ok(())
471 }
472
473 async fn connect(&mut self) -> anyhow::Result<()> {
474 if self.core.is_connected() && self.pending_tasks.is_open() && self.session_tasks.is_open()
475 {
476 return Ok(());
477 }
478
479 if !self.pending_tasks.is_open() {
480 self.await_pending_tasks().await?;
481 self.pending_tasks
482 .start_generation()
483 .map_err(|e| anyhow::anyhow!("Failed to start Deribit task generation: {e}"))?;
484 }
485
486 if !self.session_tasks.is_open() || !self.session_tasks.is_empty() {
487 self.abort_session_tasks();
488
489 if self.ws_client.is_active() {
490 self.ws_client
491 .close()
492 .await
493 .context("failed to close stale Deribit WebSocket")?;
494 }
495 self.await_session_tasks().await?;
496 self.session_tasks
497 .start_generation()
498 .map_err(|e| anyhow::anyhow!("Failed to start Deribit session generation: {e}"))?;
499 } else if self.ws_client.is_active() {
500 self.ws_client
501 .close()
502 .await
503 .context("failed to close stale Deribit WebSocket")?;
504 }
505 let ws_client = self.ws_client.clone();
506 let setup_guard =
507 TaskGroupGuard::new(&[&self.session_tasks, &self.pending_tasks], move || {
508 ws_client.begin_shutdown();
509 });
510
511 if !self.config.has_api_credentials() {
513 anyhow::bail!("Missing API credentials; set Deribit environment variables");
514 }
515
516 self.ws_client.set_account_id(self.core.account_id);
518
519 if !self.core.instruments_initialized() {
521 for product_type in &self.config.product_types {
522 let instruments = self
523 .http_client
524 .request_instruments(DeribitCurrency::ANY, Some(*product_type))
525 .await
526 .with_context(|| {
527 format!("failed to request instruments for {product_type:?}")
528 })?;
529
530 if instruments.is_empty() {
531 log::warn!("No instruments returned for {product_type:?}");
532 continue;
533 }
534
535 log::debug!("Fetched {} {product_type:?} instruments", instruments.len());
536 self.ws_client.cache_instruments(&instruments);
537 self.http_client.cache_instruments(&instruments);
538 }
539 self.core.set_instruments_initialized();
540 }
541
542 let account_state = self
544 .http_client
545 .request_account_state(self.core.account_id)
546 .await
547 .context("failed to request account state")?;
548
549 self.emitter.send_account_state(account_state);
550
551 let session_result = async {
552 self.ws_client
553 .connect()
554 .await
555 .context("failed to connect WebSocket client for execution")?;
556
557 self.ws_client
558 .authenticate_session(DERIBIT_EXECUTION_SESSION_NAME)
559 .await
560 .map_err(|e| anyhow::anyhow!("failed to authenticate WebSocket session: {e}"))?;
561
562 log::debug!("WebSocket client authenticated for execution");
563
564 self.ws_client
566 .subscribe_user_orders()
567 .await
568 .map_err(|e| anyhow::anyhow!("failed to subscribe to user orders: {e}"))?;
569 self.ws_client
570 .subscribe_user_trades()
571 .await
572 .map_err(|e| anyhow::anyhow!("failed to subscribe to user trades: {e}"))?;
573 self.ws_client
574 .subscribe_user_portfolio()
575 .await
576 .map_err(|e| anyhow::anyhow!("failed to subscribe to user portfolio: {e}"))?;
577
578 if let Err(e) = self.ws_client.wait_for_subscriptions_confirmed(30.0).await {
579 let _ = self.ws_client.unsubscribe_user_orders().await;
581 let _ = self.ws_client.unsubscribe_user_trades().await;
582 let _ = self.ws_client.unsubscribe_user_portfolio().await;
583 anyhow::bail!("subscription confirmation failed: {e}");
584 }
585
586 log::debug!("Subscribed to user order, trade, and portfolio updates");
587
588 let stream = self.ws_client.stream()?;
590 self.spawn_stream_handler(stream)?;
591
592 Ok::<(), anyhow::Error>(())
593 }
594 .await;
595
596 if let Err(e) = session_result {
597 if let Err(teardown_error) = self.teardown_partial_connect().await {
598 return Err(e.context(format!(
599 "Deribit execution startup teardown failed: {teardown_error}"
600 )));
601 }
602 return Err(e);
603 }
604
605 self.core.set_connected();
606 setup_guard.disarm();
607 log::info!("Connected: client_id={}", self.core.client_id);
608 Ok(())
609 }
610
611 async fn disconnect(&mut self) -> anyhow::Result<()> {
612 self.teardown_partial_connect().await?;
613 log::info!("Disconnected: client_id={}", self.core.client_id);
614 Ok(())
615 }
616
617 async fn generate_order_status_report(
618 &self,
619 cmd: &GenerateOrderStatusReport,
620 ) -> anyhow::Result<Option<OrderStatusReport>> {
621 if let Some(venue_order_id) = &cmd.venue_order_id {
623 let params = GetOrderStateParams {
624 order_id: venue_order_id.to_string(),
625 };
626 let ts_init = self.clock.get_time_ns();
627
628 match self.http_client.inner.get_order_state(params).await {
629 Ok(response) => {
630 if let Some(order) = response.result {
631 let symbol = order.instrument_name;
632 if let Some(instrument) = self.http_client.get_instrument(&symbol) {
633 let report = parse_user_order_msg(
634 &order,
635 &instrument,
636 self.core.account_id,
637 ts_init,
638 )?;
639 return Ok(Some(report));
640 } else {
641 log::warn!(
642 "Instrument {} not in cache for order {}",
643 order.instrument_name,
644 order.order_id
645 );
646 }
647 }
648 }
649 Err(e) => {
650 log::warn!("Failed to get order state: {e}");
651 }
652 }
653 return Ok(None);
654 }
655
656 if let Some(client_order_id) = &cmd.client_order_id {
658 let reports = self
659 .http_client
660 .request_order_status_reports(
661 self.core.account_id,
662 cmd.instrument_id,
663 None,
664 None,
665 false, )
667 .await?;
668
669 for report in reports {
671 if report.client_order_id == Some(*client_order_id) {
672 return Ok(Some(report));
673 }
674 }
675 }
676
677 Ok(None)
678 }
679
680 async fn generate_order_status_reports(
681 &self,
682 cmd: &GenerateOrderStatusReports,
683 ) -> anyhow::Result<Vec<OrderStatusReport>> {
684 self.http_client
685 .request_order_status_reports(
686 self.core.account_id,
687 cmd.instrument_id,
688 cmd.start,
689 cmd.end,
690 cmd.open_only,
691 )
692 .await
693 }
694
695 async fn generate_fill_reports(
696 &self,
697 cmd: GenerateFillReports,
698 ) -> anyhow::Result<Vec<FillReport>> {
699 let mut reports = self
700 .http_client
701 .request_fill_reports(self.core.account_id, cmd.instrument_id, cmd.start, cmd.end)
702 .await?;
703
704 if let Some(venue_order_id) = &cmd.venue_order_id {
706 reports.retain(|r| r.venue_order_id == *venue_order_id);
707 }
708
709 Ok(reports)
710 }
711
712 async fn generate_position_status_reports(
713 &self,
714 cmd: &GeneratePositionStatusReports,
715 ) -> anyhow::Result<Vec<PositionStatusReport>> {
716 self.http_client
717 .request_position_status_reports(self.core.account_id, cmd.instrument_id)
718 .await
719 }
720
721 async fn generate_mass_status(
722 &self,
723 lookback_mins: Option<u64>,
724 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
725 log::info!("Generating ExecutionMassStatus (lookback_mins={lookback_mins:?})");
726 let ts_now = self.clock.get_time_ns();
727 let start = lookback_mins
728 .map(DurationNanos::try_from_mins)
729 .transpose()?
730 .map(|lookback| ts_now.saturating_sub(lookback));
731
732 let order_cmd = GenerateOrderStatusReportsBuilder::default()
733 .ts_init(ts_now)
734 .open_only(false) .start(start)
736 .build()
737 .context("Failed to build GenerateOrderStatusReports")?;
738
739 let fill_cmd = GenerateFillReportsBuilder::default()
740 .ts_init(ts_now)
741 .start(start)
742 .build()
743 .context("Failed to build GenerateFillReports")?;
744
745 let position_cmd = GeneratePositionStatusReportsBuilder::default()
746 .ts_init(ts_now)
747 .start(start)
748 .build()
749 .context("Failed to build GeneratePositionStatusReports")?;
750
751 let (order_reports, fill_reports, position_reports) = tokio::try_join!(
752 self.generate_order_status_reports(&order_cmd),
753 self.generate_fill_reports(fill_cmd),
754 self.generate_position_status_reports(&position_cmd),
755 )?;
756
757 log::info!("Received {} OrderStatusReports", order_reports.len());
758 log::info!("Received {} FillReports", fill_reports.len());
759 log::info!("Received {} PositionReports", position_reports.len());
760
761 let mut mass_status = ExecutionMassStatus::new(
762 self.core.client_id,
763 self.core.account_id,
764 *DERIBIT_VENUE,
765 ts_now,
766 None,
767 );
768
769 mass_status.add_order_reports(order_reports);
770 mass_status.add_fill_reports(fill_reports);
771 mass_status.add_position_reports(position_reports);
772
773 Ok(Some(mass_status))
774 }
775
776 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
777 let http_client = self.http_client.clone();
778 let account_id = self.core.account_id;
779 let emitter = self.emitter.clone();
780
781 self.spawn_task("query_account", async move {
782 let account_state = http_client
783 .request_account_state(account_id)
784 .await
785 .context("failed to query account state (check API credentials are valid)")?;
786
787 emitter.send_account_state(account_state);
788 Ok(())
789 });
790
791 Ok(())
792 }
793
794 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
795 let ws_client = self.ws_client.clone();
796
797 let order_id = cmd
799 .venue_order_id
800 .as_ref()
801 .ok_or_else(|| anyhow::anyhow!("venue_order_id required for query_order"))?
802 .to_string();
803
804 let client_order_id = cmd.client_order_id;
805 let trader_id = cmd.trader_id;
806 let strategy_id = cmd.strategy_id;
807 let instrument_id = cmd.instrument_id;
808
809 log::debug!("Querying order state: order_id={order_id}, client_order_id={client_order_id}");
810
811 self.spawn_task("query_order", async move {
814 ws_client
815 .query_order(
816 &order_id,
817 client_order_id,
818 trader_id,
819 strategy_id,
820 instrument_id,
821 )
822 .await
823 .map_err(|e| anyhow::anyhow!("Query order state failed: {e}"))?;
824 Ok(())
825 });
826
827 Ok(())
828 }
829
830 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
831 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
832 self.submit_single_order(&order, "submit_order");
833 Ok(())
834 }
835
836 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
837 if cmd.order_list.client_order_ids.is_empty() {
838 log::debug!("submit_order_list called with empty order list");
839 return Ok(());
840 }
841
842 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
843
844 log::debug!(
845 "Submitting order list {} with {} orders for instrument={}",
846 cmd.order_list.id,
847 orders.len(),
848 cmd.instrument_id
849 );
850
851 for order in &orders {
854 self.submit_single_order(order, "submit_order_list_item");
855 }
856
857 Ok(())
858 }
859
860 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
861 let ws_client = self.ws_client.clone();
862
863 let order_id = match cmd.venue_order_id.as_ref() {
865 Some(venue_order_id) => venue_order_id.to_string(),
866 None => {
867 return reject_modify_command(
868 &self.emitter,
869 self.clock,
870 &cmd,
871 "venue_order_id required for modify_order",
872 );
873 }
874 };
875
876 let quantity = if let Some(qty) = cmd.quantity {
878 qty
879 } else {
880 let cache = self.core.cache();
882 match cache.order(&cmd.client_order_id) {
883 Some(order) => order.quantity(),
884 None => {
885 return reject_modify_command(
886 &self.emitter,
887 self.clock,
888 &cmd,
889 &format!("Order not found: {}", cmd.client_order_id),
890 );
891 }
892 }
893 };
894
895 let price = match cmd.price {
896 Some(price) => price,
897 None => {
898 return reject_modify_command(
899 &self.emitter,
900 self.clock,
901 &cmd,
902 "price required for modify_order",
903 );
904 }
905 };
906
907 let client_order_id = cmd.client_order_id;
908 let trader_id = cmd.trader_id;
909 let strategy_id = cmd.strategy_id;
910 let instrument_id = cmd.instrument_id;
911
912 log::debug!(
913 "Modifying order: order_id={order_id}, quantity={quantity}, price={price}, client_order_id={client_order_id}"
914 );
915
916 let params = DeribitEditParams {
917 order_id: order_id.clone(),
918 amount: quantity.as_decimal(),
919 price: Some(price.as_decimal()),
920 post_only: None,
921 reject_post_only: None,
922 reduce_only: None,
923 trigger_price: None,
924 };
925
926 if let Err(e) = check_params_serialize(¶ms) {
927 return reject_modify_command(&self.emitter, self.clock, &cmd, &e.to_string());
928 }
929
930 self.spawn_task("modify_order", async move {
932 if let Err(e) = ws_client
933 .modify_order(
934 params,
935 client_order_id,
936 trader_id,
937 strategy_id,
938 instrument_id,
939 )
940 .await
941 {
942 log::error!(
943 "Modify order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
944 );
945 anyhow::bail!("Modify order failed: {e}");
946 }
947 Ok(())
948 });
949
950 Ok(())
951 }
952
953 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
954 let ws_client = self.ws_client.clone();
955
956 let order_id = match cmd.venue_order_id.as_ref() {
958 Some(venue_order_id) => venue_order_id.to_string(),
959 None => {
960 log::warn!(
961 "Cannot cancel order {} - no venue_order_id",
962 cmd.client_order_id
963 );
964 return Ok(());
965 }
966 };
967
968 let client_order_id = cmd.client_order_id;
969 let trader_id = cmd.trader_id;
970 let strategy_id = cmd.strategy_id;
971 let instrument_id = cmd.instrument_id;
972
973 log::debug!("Canceling order: order_id={order_id}, client_order_id={client_order_id}");
974
975 self.spawn_task("cancel_order", async move {
977 if let Err(e) = ws_client
978 .cancel_order(
979 &order_id,
980 client_order_id,
981 trader_id,
982 strategy_id,
983 instrument_id,
984 )
985 .await
986 {
987 log::error!(
988 "Cancel order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
989 );
990 anyhow::bail!("Cancel order failed: {e}");
991 }
992 Ok(())
993 });
994
995 Ok(())
996 }
997
998 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
999 let instrument_id = cmd.instrument_id;
1000
1001 let Some(order_side) = cmd.order_side else {
1003 log::debug!(
1004 "Cancelling all orders: instrument={instrument_id}, order_side=None (bulk)"
1005 );
1006
1007 let ws_client = self.ws_client.clone();
1008 self.spawn_task("cancel_all_orders", async move {
1009 if let Err(e) = ws_client.cancel_all_orders(instrument_id, None).await {
1010 log::error!("Cancel all orders failed for instrument {instrument_id}: {e}");
1011 anyhow::bail!("Cancel all orders failed: {e}");
1012 }
1013 Ok(())
1014 });
1015
1016 return Ok(());
1017 };
1018
1019 log::debug!(
1022 "Cancelling orders by side: instrument={instrument_id}, order_side={order_side}"
1023 );
1024
1025 let orders_to_cancel: Vec<_> = {
1026 let cache = self.core.cache();
1027 let open_orders = cache.orders_open(None, Some(&instrument_id), None, None, None);
1028
1029 open_orders
1030 .into_iter()
1031 .filter(|order| order.order_side() == order_side)
1032 .filter_map(|order| {
1033 let venue_order_id = order.venue_order_id()?;
1034 Some((
1035 venue_order_id.to_string(),
1036 order.client_order_id(),
1037 order.instrument_id(),
1038 Some(venue_order_id),
1039 ))
1040 })
1041 .collect()
1042 };
1043
1044 if orders_to_cancel.is_empty() {
1045 log::debug!("No open {order_side} orders to cancel for {instrument_id}");
1046 return Ok(());
1047 }
1048
1049 log::debug!(
1050 "Cancelling {} {order_side} orders for {instrument_id}",
1051 orders_to_cancel.len(),
1052 );
1053
1054 for (venue_order_id_str, client_order_id, order_instrument_id, _venue_order_id) in
1056 orders_to_cancel
1057 {
1058 let ws_client = self.ws_client.clone();
1059 let trader_id = cmd.trader_id;
1060 let strategy_id = cmd.strategy_id;
1061
1062 self.spawn_task("cancel_order_by_side", async move {
1063 if let Err(e) = ws_client
1064 .cancel_order(
1065 &venue_order_id_str,
1066 client_order_id,
1067 trader_id,
1068 strategy_id,
1069 order_instrument_id,
1070 )
1071 .await
1072 {
1073 log::error!(
1074 "Cancel order failed: order_id={venue_order_id_str}, client_order_id={client_order_id}, error={e}"
1075 );
1076 }
1077 Ok(())
1078 });
1079 }
1080
1081 Ok(())
1082 }
1083
1084 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1085 if cmd.cancels.is_empty() {
1086 log::debug!("batch_cancel_orders called with empty cancels list");
1087 return Ok(());
1088 }
1089
1090 log::debug!(
1091 "Batch cancelling {} orders for instrument={}",
1092 cmd.cancels.len(),
1093 cmd.instrument_id
1094 );
1095
1096 for cancel in &cmd.cancels {
1099 let order_id = match &cancel.venue_order_id {
1100 Some(id) => id.to_string(),
1101 None => {
1102 log::warn!(
1103 "Cannot cancel order {} - no venue_order_id",
1104 cancel.client_order_id
1105 );
1106 continue;
1107 }
1108 };
1109
1110 let ws_client = self.ws_client.clone();
1111 let client_order_id = cancel.client_order_id;
1112 let trader_id = cancel.trader_id;
1113 let strategy_id = cancel.strategy_id;
1114 let instrument_id = cancel.instrument_id;
1115
1116 self.spawn_task("batch_cancel_order", async move {
1117 if let Err(e) = ws_client
1118 .cancel_order(
1119 &order_id,
1120 client_order_id,
1121 trader_id,
1122 strategy_id,
1123 instrument_id,
1124 )
1125 .await
1126 {
1127 log::error!(
1128 "Batch cancel order failed: order_id={order_id}, client_order_id={client_order_id}, error={e}"
1129 );
1130 anyhow::bail!("Batch cancel order failed: {e}");
1131 }
1132 Ok(())
1133 });
1134 }
1135
1136 Ok(())
1137 }
1138}
1139
1140fn dispatch_ws_message(message: NautilusWsMessage, emitter: &ExecutionEventEmitter) {
1142 match message {
1143 NautilusWsMessage::AccountState(state) => {
1144 emitter.send_account_state(state);
1145 }
1146 NautilusWsMessage::OrderStatusReports(reports) => {
1147 log::debug!("Processing {} order status report(s)", reports.len());
1148 for report in reports {
1149 emitter.send_order_status_report(report);
1150 }
1151 }
1152 NautilusWsMessage::FillReports(reports) => {
1153 log::debug!("Processing {} fill report(s)", reports.len());
1154 for report in reports {
1155 emitter.send_fill_report(report);
1156 }
1157 }
1158 NautilusWsMessage::OrderFilled(event) => {
1159 emitter.send_order_event(OrderEventAny::Filled(event));
1160 }
1161 NautilusWsMessage::OrderRejected(event) => {
1162 emitter.send_order_event(OrderEventAny::Rejected(event));
1163 }
1164 NautilusWsMessage::OrderAccepted(event) => {
1165 emitter.send_order_event(OrderEventAny::Accepted(event));
1166 }
1167 NautilusWsMessage::OrderCanceled(event) => {
1168 emitter.send_order_event(OrderEventAny::Canceled(event));
1169 }
1170 NautilusWsMessage::OrderExpired(event) => {
1171 emitter.send_order_event(OrderEventAny::Expired(event));
1172 }
1173 NautilusWsMessage::OrderUpdated(event) => {
1174 emitter.send_order_event(OrderEventAny::Updated(event));
1175 }
1176 NautilusWsMessage::OrderCancelRejected(event) => {
1177 emitter.send_order_event(OrderEventAny::CancelRejected(event));
1178 }
1179 NautilusWsMessage::OrderModifyRejected(event) => {
1180 emitter.send_order_event(OrderEventAny::ModifyRejected(event));
1181 }
1182 NautilusWsMessage::Error(e) => {
1183 log::warn!("WebSocket error: {e}");
1184 }
1185 NautilusWsMessage::Reconnected => {
1186 log::info!("WebSocket reconnected");
1187 }
1188 NautilusWsMessage::Authenticated(auth) => {
1189 log::debug!("WebSocket authenticated: scope={}", auth.scope);
1190 }
1191 NautilusWsMessage::AuthenticationFailed(reason) => {
1192 log::error!("Authentication failed in execution client: {reason}");
1193 }
1194 NautilusWsMessage::Data(_)
1195 | NautilusWsMessage::Deltas(_)
1196 | NautilusWsMessage::Instrument(_)
1197 | NautilusWsMessage::InstrumentStatus(_)
1198 | NautilusWsMessage::FundingRates(_)
1199 | NautilusWsMessage::OptionGreeks(_)
1200 | NautilusWsMessage::Raw(_) => {
1201 log::trace!("Ignoring data message in execution client");
1203 }
1204 }
1205}
1206
1207fn reject_modify_command(
1208 emitter: &ExecutionEventEmitter,
1209 clock: &AtomicTime,
1210 cmd: &ModifyOrder,
1211 reason: &str,
1212) -> anyhow::Result<()> {
1213 let ts_event = clock.get_time_ns();
1214 emitter.emit_order_modify_rejected_event(
1215 cmd.strategy_id,
1216 cmd.instrument_id,
1217 cmd.client_order_id,
1218 cmd.venue_order_id,
1219 reason,
1220 ts_event,
1221 );
1222 anyhow::bail!("{reason}");
1223}
1224
1225fn check_params_serialize<T: Serialize>(params: &T) -> anyhow::Result<()> {
1228 serde_json::to_writer(io::sink(), params)
1229 .map_err(|e| anyhow::anyhow!("Cannot serialize order params: {e}"))
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use nautilus_common::messages::{ExecutionEvent, execution::ExecutionReport};
1235 use nautilus_core::UUID4;
1236 use nautilus_model::{
1237 enums::{LiquiditySide, OrderSide},
1238 events::OrderFilled,
1239 identifiers::{ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId},
1240 types::{Currency, Money, Price, Quantity},
1241 };
1242 use rstest::rstest;
1243
1244 use super::*;
1245
1246 fn dispatch_test_rig() -> (
1247 ExecutionEventEmitter,
1248 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
1249 ) {
1250 let trader_id = TraderId::from("TRADER-001");
1251 let account_id = AccountId::from("DERIBIT-001");
1252 let mut emitter = ExecutionEventEmitter::new(
1253 get_atomic_clock_realtime(),
1254 trader_id,
1255 account_id,
1256 AccountType::Margin,
1257 None,
1258 );
1259 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1260 emitter.set_sender(tx);
1261 (emitter, rx)
1262 }
1263
1264 fn fill_report() -> FillReport {
1265 FillReport::new(
1266 AccountId::from("DERIBIT-001"),
1267 InstrumentId::from("BTC-PERPETUAL.DERIBIT"),
1268 VenueOrderId::from("ETH-584830574"),
1269 TradeId::from("ETH-2696068"),
1270 OrderSide::Buy,
1271 Quantity::from("1.000000"),
1272 Price::from("203.80"),
1273 Money::from("0.00036801 USDT"),
1274 LiquiditySide::Taker,
1275 Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
1276 None,
1277 UnixNanos::from(2),
1278 UnixNanos::from(3),
1279 None,
1280 )
1281 }
1282
1283 #[rstest]
1284 fn dispatch_tracked_fill_uses_order_event_path() {
1285 let (emitter, mut rx) = dispatch_test_rig();
1286 let report = fill_report();
1287 let filled = OrderFilled::new(
1288 TraderId::from("TRADER-001"),
1289 StrategyId::from("S-001"),
1290 report.instrument_id,
1291 report.client_order_id.unwrap(),
1292 report.venue_order_id,
1293 report.account_id,
1294 report.trade_id,
1295 report.order_side,
1296 OrderType::Market,
1297 report.last_qty,
1298 report.last_px,
1299 Currency::USDT(),
1300 report.liquidity_side,
1301 UUID4::new(),
1302 report.ts_event,
1303 report.ts_init,
1304 false,
1305 None,
1306 Some(report.commission),
1307 None,
1308 );
1309
1310 dispatch_ws_message(NautilusWsMessage::OrderFilled(filled), &emitter);
1311
1312 assert!(matches!(
1313 rx.try_recv().unwrap(),
1314 ExecutionEvent::Order(OrderEventAny::Filled(event))
1315 if event.client_order_id == ClientOrderId::from("O-19700101-000000-001-001-1")
1316 && event.trade_id == TradeId::from("ETH-2696068")
1317 ));
1318 assert!(rx.try_recv().is_err());
1319 }
1320
1321 #[rstest]
1322 fn dispatch_untracked_fill_keeps_report_path() {
1323 let (emitter, mut rx) = dispatch_test_rig();
1324 let report = fill_report();
1325
1326 dispatch_ws_message(NautilusWsMessage::FillReports(vec![report]), &emitter);
1327
1328 assert!(matches!(
1329 rx.try_recv().unwrap(),
1330 ExecutionEvent::Report(ExecutionReport::Fill(_))
1331 ));
1332 assert!(rx.try_recv().is_err());
1333 }
1334}