1use std::sync::Arc;
28
29use futures_util::{StreamExt, stream::FuturesUnordered};
30use nautilus_model::{
31 enums::{OrderSide, TimeInForce},
32 identifiers::InstrumentId,
33 types::{Price, Quantity},
34};
35use nautilus_network::ratelimiter::quota::Quota;
36
37use crate::{
38 error::DydxError,
39 execution::{
40 block_time::BlockTimeMonitor,
41 broadcaster::TxBroadcaster,
42 order_builder::OrderMessageBuilder,
43 tx_manager::TransactionManager,
44 types::{ConditionalOrderType, LimitOrderParams, OrderLifetime},
45 },
46 grpc::{DydxGrpcClient, types::ChainId},
47 http::client::DydxHttpClient,
48};
49
50#[derive(Debug)]
71pub struct OrderSubmitter {
72 tx_manager: Arc<TransactionManager>,
74 broadcaster: Arc<TxBroadcaster>,
76 order_builder: Arc<OrderMessageBuilder>,
78 block_time_monitor: Arc<BlockTimeMonitor>,
80}
81
82impl OrderSubmitter {
83 #[expect(clippy::too_many_arguments)]
100 pub fn new(
101 grpc_client: DydxGrpcClient,
102 http_client: DydxHttpClient,
103 private_key: &str,
104 wallet_address: String,
105 subaccount_number: u32,
106 chain_id: ChainId,
107 block_time_monitor: Arc<BlockTimeMonitor>,
108 grpc_quota: Option<Quota>,
109 ) -> Result<Self, DydxError> {
110 let tx_manager = Arc::new(TransactionManager::new(
112 grpc_client.clone(),
113 private_key,
114 wallet_address.clone(),
115 chain_id,
116 )?);
117
118 let broadcaster = Arc::new(TxBroadcaster::new(grpc_client, grpc_quota));
119
120 let order_builder = Arc::new(OrderMessageBuilder::new(
121 http_client,
122 wallet_address,
123 subaccount_number,
124 block_time_monitor.clone(),
125 ));
126
127 Ok(Self {
128 tx_manager,
129 broadcaster,
130 order_builder,
131 block_time_monitor,
132 })
133 }
134
135 pub fn from_components(
139 tx_manager: Arc<TransactionManager>,
140 broadcaster: Arc<TxBroadcaster>,
141 order_builder: Arc<OrderMessageBuilder>,
142 block_time_monitor: Arc<BlockTimeMonitor>,
143 ) -> Self {
144 Self {
145 tx_manager,
146 broadcaster,
147 order_builder,
148 block_time_monitor,
149 }
150 }
151
152 #[must_use]
154 pub fn current_block_height(&self) -> u32 {
155 self.block_time_monitor.current_block_height() as u32
156 }
157
158 #[must_use]
160 pub fn block_time_monitor(&self) -> &BlockTimeMonitor {
161 &self.block_time_monitor
162 }
163
164 #[must_use]
166 pub fn wallet_address(&self) -> &str {
167 self.tx_manager.wallet_address()
168 }
169
170 #[must_use]
172 pub fn order_builder(&self) -> &OrderMessageBuilder {
173 &self.order_builder
174 }
175
176 #[must_use]
178 pub fn tx_manager(&self) -> &TransactionManager {
179 &self.tx_manager
180 }
181
182 pub async fn submit_market_order(
195 &self,
196 instrument_id: InstrumentId,
197 client_order_id: u32,
198 client_metadata: u32,
199 side: OrderSide,
200 quantity: Quantity,
201 ) -> Result<String, DydxError> {
202 log::debug!(
203 "Submitting market order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, quantity={quantity}"
204 );
205
206 let block_height = self.current_block_height();
207
208 let msg = self.order_builder.build_market_order(
210 instrument_id,
211 client_order_id,
212 client_metadata,
213 side,
214 quantity,
215 block_height,
216 )?;
217
218 let operation = format!("Submit market order {client_order_id}");
220 let tx_hash = self
221 .broadcaster
222 .broadcast_short_term(&self.tx_manager, vec![msg], &operation)
223 .await?;
224
225 Ok(tx_hash)
226 }
227
228 #[expect(clippy::too_many_arguments)]
241 pub async fn submit_limit_order(
242 &self,
243 instrument_id: InstrumentId,
244 client_order_id: u32,
245 client_metadata: u32,
246 side: OrderSide,
247 price: Price,
248 quantity: Quantity,
249 time_in_force: TimeInForce,
250 post_only: bool,
251 reduce_only: bool,
252 expire_time: Option<i64>,
253 ) -> Result<String, DydxError> {
254 log::debug!(
255 "Submitting limit order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, price={price}, \
256 quantity={quantity}, tif={time_in_force:?}, post_only={post_only}, reduce_only={reduce_only}"
257 );
258
259 let block_height = self.current_block_height();
260
261 let msg = self.order_builder.build_limit_order(
263 instrument_id,
264 client_order_id,
265 client_metadata,
266 side,
267 price,
268 quantity,
269 time_in_force,
270 post_only,
271 reduce_only,
272 block_height,
273 expire_time,
274 )?;
275
276 let is_short_term = OrderLifetime::from_time_in_force(
278 time_in_force,
279 expire_time,
280 false,
281 self.order_builder.max_short_term_secs(),
282 )
283 .is_short_term();
284
285 let operation = format!("Submit limit order {client_order_id}");
287 let tx_hash = if is_short_term {
288 self.broadcaster
289 .broadcast_short_term(&self.tx_manager, vec![msg], &operation)
290 .await?
291 } else {
292 self.broadcaster
293 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
294 .await?
295 };
296
297 Ok(tx_hash)
298 }
299
300 pub async fn submit_limit_orders_batch(
316 &self,
317 orders: Vec<LimitOrderParams>,
318 ) -> Result<Vec<String>, DydxError> {
319 if orders.is_empty() {
320 return Ok(Vec::new());
321 }
322
323 let block_height = self.current_block_height();
324
325 let has_short_term = orders
327 .iter()
328 .any(|params| self.order_builder.is_short_term_order(params));
329
330 if has_short_term {
331 log::debug!(
335 "Submitting {} short-term limit orders concurrently (sequence not consumed)",
336 orders.len()
337 );
338
339 let mut tx_hashes = Vec::with_capacity(orders.len());
340 let mut submissions = FuturesUnordered::new();
341
342 for params in orders {
343 let tx_manager = Arc::clone(&self.tx_manager);
344 let broadcaster = Arc::clone(&self.broadcaster);
345 let order_builder = Arc::clone(&self.order_builder);
346
347 submissions.push(async move {
348 let msg = order_builder.build_limit_order_from_params(¶ms, block_height)?;
349 let operation = format!("Submit short-term order {}", params.client_order_id);
350 broadcaster
351 .broadcast_short_term(&tx_manager, vec![msg], &operation)
352 .await
353 });
354 }
355
356 while let Some(result) = submissions.next().await {
358 match result {
359 Ok(tx_hash) => tx_hashes.push(tx_hash),
360 Err(e) => return Err(e),
361 }
362 }
363
364 Ok(tx_hashes)
365 } else {
366 log::debug!(
368 "Batch submitting {} long-term limit orders in single transaction",
369 orders.len()
370 );
371
372 let msgs = self
373 .order_builder
374 .build_limit_orders_batch(&orders, block_height)?;
375
376 let operation = format!("Submit batch of {} limit orders", msgs.len());
377 let tx_hash = self
378 .broadcaster
379 .broadcast_with_retry(&self.tx_manager, msgs, &operation)
380 .await?;
381
382 Ok(vec![tx_hash])
383 }
384 }
385
386 pub async fn cancel_order(
398 &self,
399 instrument_id: InstrumentId,
400 client_order_id: u32,
401 time_in_force: TimeInForce,
402 expire_time_ns: Option<nautilus_core::UnixNanos>,
403 ) -> Result<String, DydxError> {
404 log::debug!("Cancelling order: client_id={client_order_id}, instrument={instrument_id}");
405
406 let block_height = self.current_block_height();
407
408 let msg = self.order_builder.build_cancel_order(
410 instrument_id,
411 client_order_id,
412 time_in_force,
413 expire_time_ns,
414 block_height,
415 )?;
416
417 let is_short_term = self
419 .order_builder
420 .is_short_term_cancel(time_in_force, expire_time_ns);
421
422 let operation = format!("Cancel order {client_order_id}");
424 let tx_hash = if is_short_term {
425 self.broadcaster
426 .broadcast_short_term(&self.tx_manager, vec![msg], &operation)
427 .await?
428 } else {
429 self.broadcaster
430 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
431 .await?
432 };
433
434 Ok(tx_hash)
435 }
436
437 pub async fn cancel_orders_batch(
455 &self,
456 orders: &[(
457 InstrumentId,
458 u32,
459 TimeInForce,
460 Option<nautilus_core::UnixNanos>,
461 )],
462 ) -> Result<String, DydxError> {
463 if orders.is_empty() {
464 return Err(DydxError::Order("No orders to cancel".to_string()));
465 }
466
467 let block_height = self.current_block_height();
468
469 let (short_term, long_term): (Vec<_>, Vec<_>) =
471 orders.iter().partition(|(_, _, tif, expire_ns)| {
472 self.order_builder.is_short_term_cancel(*tif, *expire_ns)
473 });
474
475 log::debug!(
476 "Batch cancelling {} orders (short_term={}, long_term={})",
477 orders.len(),
478 short_term.len(),
479 long_term.len(),
480 );
481
482 let mut tx_hashes = Vec::new();
483
484 if !short_term.is_empty() {
486 let st_pairs: Vec<_> = short_term
487 .iter()
488 .map(|(inst_id, client_id, _, _)| (*inst_id, *client_id))
489 .collect();
490
491 let msg = self
492 .order_builder
493 .build_batch_cancel_short_term(&st_pairs, block_height)?;
494
495 let operation = format!("BatchCancel {} short-term orders", st_pairs.len());
496 let tx_hash = self
497 .broadcaster
498 .broadcast_short_term(&self.tx_manager, vec![msg], &operation)
499 .await?;
500 tx_hashes.push(tx_hash);
501 }
502
503 if !long_term.is_empty() {
505 let lt_orders: Vec<_> = long_term
506 .iter()
507 .map(|(inst_id, client_id, tif, expire_ns)| {
508 (*inst_id, *client_id, *tif, *expire_ns)
509 })
510 .collect();
511
512 let msgs = self
513 .order_builder
514 .build_cancel_orders_batch(<_orders, block_height)?;
515
516 let operation = format!("BatchCancel {} long-term orders", lt_orders.len());
517 let tx_hash = self
518 .broadcaster
519 .broadcast_with_retry(&self.tx_manager, msgs, &operation)
520 .await?;
521 tx_hashes.push(tx_hash);
522 }
523
524 Ok(tx_hashes.join(","))
525 }
526
527 #[expect(clippy::too_many_arguments)]
539 pub async fn submit_stop_market_order(
540 &self,
541 instrument_id: InstrumentId,
542 client_order_id: u32,
543 client_metadata: u32,
544 side: OrderSide,
545 trigger_price: Price,
546 quantity: Quantity,
547 reduce_only: bool,
548 expire_time: Option<i64>,
549 ) -> Result<String, DydxError> {
550 log::debug!(
551 "Submitting stop market order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, \
552 trigger={trigger_price}, qty={quantity}"
553 );
554
555 let msg = self.order_builder.build_stop_market_order(
557 instrument_id,
558 client_order_id,
559 client_metadata,
560 side,
561 trigger_price,
562 quantity,
563 reduce_only,
564 expire_time,
565 )?;
566
567 let operation = format!("Submit stop market order {client_order_id}");
569 let tx_hash = self
570 .broadcaster
571 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
572 .await?;
573
574 Ok(tx_hash)
575 }
576
577 #[expect(clippy::too_many_arguments)]
590 pub async fn submit_stop_limit_order(
591 &self,
592 instrument_id: InstrumentId,
593 client_order_id: u32,
594 client_metadata: u32,
595 side: OrderSide,
596 trigger_price: Price,
597 limit_price: Price,
598 quantity: Quantity,
599 time_in_force: TimeInForce,
600 post_only: bool,
601 reduce_only: bool,
602 expire_time: Option<i64>,
603 ) -> Result<String, DydxError> {
604 log::debug!(
605 "Submitting stop limit order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, \
606 trigger={trigger_price}, limit={limit_price}, qty={quantity}"
607 );
608
609 let msg = self.order_builder.build_stop_limit_order(
611 instrument_id,
612 client_order_id,
613 client_metadata,
614 side,
615 trigger_price,
616 limit_price,
617 quantity,
618 time_in_force,
619 post_only,
620 reduce_only,
621 expire_time,
622 )?;
623
624 let operation = format!("Submit stop limit order {client_order_id}");
626 let tx_hash = self
627 .broadcaster
628 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
629 .await?;
630
631 Ok(tx_hash)
632 }
633
634 #[expect(clippy::too_many_arguments)]
647 pub async fn submit_take_profit_market_order(
648 &self,
649 instrument_id: InstrumentId,
650 client_order_id: u32,
651 client_metadata: u32,
652 side: OrderSide,
653 trigger_price: Price,
654 quantity: Quantity,
655 reduce_only: bool,
656 expire_time: Option<i64>,
657 ) -> Result<String, DydxError> {
658 log::debug!(
659 "Submitting take profit market order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, \
660 trigger={trigger_price}, qty={quantity}"
661 );
662
663 let msg = self.order_builder.build_take_profit_market_order(
665 instrument_id,
666 client_order_id,
667 client_metadata,
668 side,
669 trigger_price,
670 quantity,
671 reduce_only,
672 expire_time,
673 )?;
674
675 let operation = format!("Submit take profit market order {client_order_id}");
677 let tx_hash = self
678 .broadcaster
679 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
680 .await?;
681
682 Ok(tx_hash)
683 }
684
685 #[expect(clippy::too_many_arguments)]
698 pub async fn submit_take_profit_limit_order(
699 &self,
700 instrument_id: InstrumentId,
701 client_order_id: u32,
702 client_metadata: u32,
703 side: OrderSide,
704 trigger_price: Price,
705 limit_price: Price,
706 quantity: Quantity,
707 time_in_force: TimeInForce,
708 post_only: bool,
709 reduce_only: bool,
710 expire_time: Option<i64>,
711 ) -> Result<String, DydxError> {
712 log::debug!(
713 "Submitting take profit limit order: client_id={client_order_id}, meta={client_metadata:#x}, side={side:?}, \
714 trigger={trigger_price}, limit={limit_price}, qty={quantity}"
715 );
716
717 let msg = self.order_builder.build_take_profit_limit_order(
719 instrument_id,
720 client_order_id,
721 client_metadata,
722 side,
723 trigger_price,
724 limit_price,
725 quantity,
726 time_in_force,
727 post_only,
728 reduce_only,
729 expire_time,
730 )?;
731
732 let operation = format!("Submit take profit limit order {client_order_id}");
734 let tx_hash = self
735 .broadcaster
736 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
737 .await?;
738
739 Ok(tx_hash)
740 }
741
742 #[expect(clippy::too_many_arguments)]
755 pub async fn submit_conditional_order(
756 &self,
757 instrument_id: InstrumentId,
758 client_order_id: u32,
759 client_metadata: u32,
760 order_type: ConditionalOrderType,
761 side: OrderSide,
762 trigger_price: Price,
763 limit_price: Option<Price>,
764 quantity: Quantity,
765 time_in_force: Option<TimeInForce>,
766 post_only: bool,
767 reduce_only: bool,
768 expire_time: Option<i64>,
769 ) -> Result<String, DydxError> {
770 let msg = self.order_builder.build_conditional_order(
772 instrument_id,
773 client_order_id,
774 client_metadata,
775 order_type,
776 side,
777 trigger_price,
778 limit_price,
779 quantity,
780 time_in_force,
781 post_only,
782 reduce_only,
783 expire_time,
784 )?;
785
786 let operation = format!("Submit {order_type:?} order {client_order_id}");
788 let tx_hash = self
789 .broadcaster
790 .broadcast_with_retry(&self.tx_manager, vec![msg], &operation)
791 .await?;
792
793 Ok(tx_hash)
794 }
795}