1use chrono::{DateTime, Duration, Utc};
22use nautilus_model::{
23 enums::{OrderSide, TimeInForce},
24 identifiers::InstrumentId,
25 types::{Price, Quantity},
26};
27
28use crate::{
29 common::parse::{order_side_to_proto, time_in_force_to_proto_with_post_only},
30 error::DydxError,
31 grpc::{
32 DydxGrpcClient, OrderBuilder, OrderGoodUntil, OrderMarketParams,
33 SHORT_TERM_ORDER_MAXIMUM_LIFETIME, TxBuilder, Wallet, types::ChainId,
34 },
35 http::client::DydxHttpClient,
36 proto::{
37 ToAny,
38 dydxprotocol::clob::{MsgCancelOrder, MsgPlaceOrder},
39 },
40};
41
42const GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS: i64 = 90;
44
45#[derive(Debug, Clone, Copy)]
47pub enum ConditionalOrderType {
48 StopMarket,
50 StopLimit,
52 TakeProfitMarket,
54 TakeProfitLimit,
56}
57
58fn calculate_conditional_order_expiration(
68 time_in_force: TimeInForce,
69 expire_time: Option<i64>,
70) -> Result<DateTime<Utc>, DydxError> {
71 if let Some(expire_ts) = expire_time {
72 DateTime::from_timestamp(expire_ts, 0)
73 .ok_or_else(|| DydxError::Parse(format!("Invalid expire timestamp: {expire_ts}")))
74 } else {
75 let expiration = match time_in_force {
76 TimeInForce::Gtc => Utc::now() + Duration::days(GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS),
77 TimeInForce::Ioc | TimeInForce::Fok => {
78 Utc::now() + Duration::hours(1)
80 }
81 _ => Utc::now() + Duration::days(GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS),
83 };
84 Ok(expiration)
85 }
86}
87
88#[derive(Debug)]
89pub struct OrderSubmitter {
90 grpc_client: DydxGrpcClient,
91 http_client: DydxHttpClient,
92 wallet_address: String,
93 subaccount_number: u32,
94 chain_id: ChainId,
95 authenticator_ids: Vec<u64>,
96}
97
98impl OrderSubmitter {
99 pub fn new(
100 grpc_client: DydxGrpcClient,
101 http_client: DydxHttpClient,
102 wallet_address: String,
103 subaccount_number: u32,
104 chain_id: ChainId,
105 authenticator_ids: Vec<u64>,
106 ) -> Self {
107 Self {
108 grpc_client,
109 http_client,
110 wallet_address,
111 subaccount_number,
112 chain_id,
113 authenticator_ids,
114 }
115 }
116
117 pub async fn submit_market_order(
125 &self,
126 wallet: &Wallet,
127 instrument_id: InstrumentId,
128 client_order_id: u32,
129 side: OrderSide,
130 quantity: Quantity,
131 block_height: u32,
132 ) -> Result<(), DydxError> {
133 tracing::info!(
134 "Submitting market order: client_id={}, side={:?}, quantity={}",
135 client_order_id,
136 side,
137 quantity
138 );
139
140 let market_params = self.get_market_params(instrument_id)?;
142
143 let mut builder = OrderBuilder::new(
145 market_params,
146 self.wallet_address.clone(),
147 self.subaccount_number,
148 client_order_id,
149 );
150
151 let proto_side = order_side_to_proto(side);
152 let size_decimal = quantity.as_decimal();
153
154 builder = builder.market(proto_side, size_decimal);
155 builder = builder.short_term(); builder = builder.until(OrderGoodUntil::Block(
157 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
158 ));
159
160 let order = builder
161 .build()
162 .map_err(|e| DydxError::Order(format!("Failed to build market order: {e}")))?;
163
164 let msg_place_order = MsgPlaceOrder { order: Some(order) };
166
167 self.broadcast_order_message(wallet, msg_place_order).await
169 }
170
171 #[allow(clippy::too_many_arguments)]
179 pub async fn submit_limit_order(
180 &self,
181 wallet: &Wallet,
182 instrument_id: InstrumentId,
183 client_order_id: u32,
184 side: OrderSide,
185 price: Price,
186 quantity: Quantity,
187 time_in_force: TimeInForce,
188 post_only: bool,
189 reduce_only: bool,
190 block_height: u32,
191 expire_time: Option<i64>,
192 ) -> Result<(), DydxError> {
193 tracing::info!(
194 "Submitting limit order: client_id={}, side={:?}, price={}, quantity={}, tif={:?}, post_only={}, reduce_only={}",
195 client_order_id,
196 side,
197 price,
198 quantity,
199 time_in_force,
200 post_only,
201 reduce_only
202 );
203
204 let market_params = self.get_market_params(instrument_id)?;
206
207 let mut builder = OrderBuilder::new(
209 market_params,
210 self.wallet_address.clone(),
211 self.subaccount_number,
212 client_order_id,
213 );
214
215 let proto_side = order_side_to_proto(side);
216 let price_decimal = price.as_decimal();
217 let size_decimal = quantity.as_decimal();
218
219 builder = builder.limit(proto_side, price_decimal, size_decimal);
220
221 let proto_tif = time_in_force_to_proto_with_post_only(time_in_force, post_only);
223 builder = builder.time_in_force(proto_tif);
224
225 if reduce_only {
227 builder = builder.reduce_only(true);
228 }
229
230 if let Some(expire_ts) = expire_time {
232 builder = builder.long_term();
233 builder = builder.until(OrderGoodUntil::Time(
234 DateTime::from_timestamp(expire_ts, 0)
235 .ok_or_else(|| DydxError::Parse("Invalid expire timestamp".to_string()))?,
236 ));
237 } else {
238 builder = builder.short_term();
239 builder = builder.until(OrderGoodUntil::Block(
240 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
241 ));
242 }
243
244 let order = builder
245 .build()
246 .map_err(|e| DydxError::Order(format!("Failed to build limit order: {e}")))?;
247
248 let msg_place_order = MsgPlaceOrder { order: Some(order) };
250
251 self.broadcast_order_message(wallet, msg_place_order).await
253 }
254
255 pub async fn cancel_order(
265 &self,
266 wallet: &Wallet,
267 instrument_id: InstrumentId,
268 client_order_id: u32,
269 block_height: u32,
270 ) -> Result<(), DydxError> {
271 tracing::info!(
272 "Cancelling order: client_id={}, instrument={}",
273 client_order_id,
274 instrument_id
275 );
276
277 let market_params = self.get_market_params(instrument_id)?;
279
280 let msg_cancel = MsgCancelOrder {
282 order_id: Some(crate::proto::dydxprotocol::clob::OrderId {
283 subaccount_id: Some(crate::proto::dydxprotocol::subaccounts::SubaccountId {
284 owner: self.wallet_address.clone(),
285 number: self.subaccount_number,
286 }),
287 client_id: client_order_id,
288 order_flags: 0, clob_pair_id: market_params.clob_pair_id,
290 }),
291 good_til_oneof: Some(
292 crate::proto::dydxprotocol::clob::msg_cancel_order::GoodTilOneof::GoodTilBlock(
293 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
294 ),
295 ),
296 };
297
298 self.broadcast_cancel_message(wallet, msg_cancel).await
300 }
301
302 pub async fn cancel_orders_batch(
318 &self,
319 wallet: &Wallet,
320 orders: &[(InstrumentId, u32)],
321 block_height: u32,
322 ) -> Result<(), DydxError> {
323 if orders.is_empty() {
324 return Ok(());
325 }
326
327 tracing::info!(
328 "Batch cancelling {} orders in single transaction",
329 orders.len()
330 );
331
332 let mut cancel_msgs = Vec::with_capacity(orders.len());
334 for (instrument_id, client_order_id) in orders {
335 let market_params = self.get_market_params(*instrument_id)?;
336
337 let msg_cancel = MsgCancelOrder {
338 order_id: Some(crate::proto::dydxprotocol::clob::OrderId {
339 subaccount_id: Some(crate::proto::dydxprotocol::subaccounts::SubaccountId {
340 owner: self.wallet_address.clone(),
341 number: self.subaccount_number,
342 }),
343 client_id: *client_order_id,
344 order_flags: 0,
345 clob_pair_id: market_params.clob_pair_id,
346 }),
347 good_til_oneof: Some(
348 crate::proto::dydxprotocol::clob::msg_cancel_order::GoodTilOneof::GoodTilBlock(
349 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
350 ),
351 ),
352 };
353 cancel_msgs.push(msg_cancel);
354 }
355
356 self.broadcast_cancel_messages_batch(wallet, cancel_msgs)
358 .await
359 }
360
361 #[allow(clippy::too_many_arguments)]
371 pub async fn submit_conditional_order(
372 &self,
373 wallet: &Wallet,
374 instrument_id: InstrumentId,
375 client_order_id: u32,
376 order_type: ConditionalOrderType,
377 side: OrderSide,
378 trigger_price: Price,
379 limit_price: Option<Price>,
380 quantity: Quantity,
381 time_in_force: Option<TimeInForce>,
382 post_only: bool,
383 reduce_only: bool,
384 expire_time: Option<i64>,
385 ) -> Result<(), DydxError> {
386 let market_params = self.get_market_params(instrument_id)?;
387
388 let mut builder = OrderBuilder::new(
389 market_params,
390 self.wallet_address.clone(),
391 self.subaccount_number,
392 client_order_id,
393 );
394
395 let proto_side = order_side_to_proto(side);
396 let trigger_decimal = trigger_price.as_decimal();
397 let size_decimal = quantity.as_decimal();
398
399 builder = match order_type {
401 ConditionalOrderType::StopMarket => {
402 tracing::info!(
403 "Submitting stop market order: client_id={}, side={:?}, trigger={}, qty={}",
404 client_order_id,
405 side,
406 trigger_price,
407 quantity
408 );
409 builder.stop_market(proto_side, trigger_decimal, size_decimal)
410 }
411 ConditionalOrderType::StopLimit => {
412 let limit = limit_price.ok_or_else(|| {
413 DydxError::Order("StopLimit requires limit_price".to_string())
414 })?;
415 tracing::info!(
416 "Submitting stop limit order: client_id={}, side={:?}, trigger={}, limit={}, qty={}",
417 client_order_id,
418 side,
419 trigger_price,
420 limit,
421 quantity
422 );
423 builder.stop_limit(
424 proto_side,
425 limit.as_decimal(),
426 trigger_decimal,
427 size_decimal,
428 )
429 }
430 ConditionalOrderType::TakeProfitMarket => {
431 tracing::info!(
432 "Submitting take profit market order: client_id={}, side={:?}, trigger={}, qty={}",
433 client_order_id,
434 side,
435 trigger_price,
436 quantity
437 );
438 builder.take_profit_market(proto_side, trigger_decimal, size_decimal)
439 }
440 ConditionalOrderType::TakeProfitLimit => {
441 let limit = limit_price.ok_or_else(|| {
442 DydxError::Order("TakeProfitLimit requires limit_price".to_string())
443 })?;
444 tracing::info!(
445 "Submitting take profit limit order: client_id={}, side={:?}, trigger={}, limit={}, qty={}",
446 client_order_id,
447 side,
448 trigger_price,
449 limit,
450 quantity
451 );
452 builder.take_profit_limit(
453 proto_side,
454 limit.as_decimal(),
455 trigger_decimal,
456 size_decimal,
457 )
458 }
459 };
460
461 let effective_tif = time_in_force.unwrap_or(TimeInForce::Gtc);
463 if matches!(
464 order_type,
465 ConditionalOrderType::StopLimit | ConditionalOrderType::TakeProfitLimit
466 ) {
467 let proto_tif = time_in_force_to_proto_with_post_only(effective_tif, post_only);
468 builder = builder.time_in_force(proto_tif);
469 }
470
471 if reduce_only {
472 builder = builder.reduce_only(true);
473 }
474
475 let expire = calculate_conditional_order_expiration(effective_tif, expire_time)?;
476 builder = builder.until(OrderGoodUntil::Time(expire));
477
478 let order = builder
479 .build()
480 .map_err(|e| DydxError::Order(format!("Failed to build {order_type:?} order: {e}")))?;
481
482 let msg_place_order = MsgPlaceOrder { order: Some(order) };
483 self.broadcast_order_message(wallet, msg_place_order).await
484 }
485
486 #[allow(clippy::too_many_arguments)]
494 pub async fn submit_stop_market_order(
495 &self,
496 wallet: &Wallet,
497 instrument_id: InstrumentId,
498 client_order_id: u32,
499 side: OrderSide,
500 trigger_price: Price,
501 quantity: Quantity,
502 reduce_only: bool,
503 expire_time: Option<i64>,
504 ) -> Result<(), DydxError> {
505 self.submit_conditional_order(
506 wallet,
507 instrument_id,
508 client_order_id,
509 ConditionalOrderType::StopMarket,
510 side,
511 trigger_price,
512 None,
513 quantity,
514 None,
515 false,
516 reduce_only,
517 expire_time,
518 )
519 .await
520 }
521
522 #[allow(clippy::too_many_arguments)]
531 pub async fn submit_stop_limit_order(
532 &self,
533 wallet: &Wallet,
534 instrument_id: InstrumentId,
535 client_order_id: u32,
536 side: OrderSide,
537 trigger_price: Price,
538 limit_price: Price,
539 quantity: Quantity,
540 time_in_force: TimeInForce,
541 post_only: bool,
542 reduce_only: bool,
543 expire_time: Option<i64>,
544 ) -> Result<(), DydxError> {
545 self.submit_conditional_order(
546 wallet,
547 instrument_id,
548 client_order_id,
549 ConditionalOrderType::StopLimit,
550 side,
551 trigger_price,
552 Some(limit_price),
553 quantity,
554 Some(time_in_force),
555 post_only,
556 reduce_only,
557 expire_time,
558 )
559 .await
560 }
561
562 #[allow(clippy::too_many_arguments)]
571 pub async fn submit_take_profit_market_order(
572 &self,
573 wallet: &Wallet,
574 instrument_id: InstrumentId,
575 client_order_id: u32,
576 side: OrderSide,
577 trigger_price: Price,
578 quantity: Quantity,
579 reduce_only: bool,
580 expire_time: Option<i64>,
581 ) -> Result<(), DydxError> {
582 self.submit_conditional_order(
583 wallet,
584 instrument_id,
585 client_order_id,
586 ConditionalOrderType::TakeProfitMarket,
587 side,
588 trigger_price,
589 None,
590 quantity,
591 None,
592 false,
593 reduce_only,
594 expire_time,
595 )
596 .await
597 }
598
599 #[allow(clippy::too_many_arguments)]
608 pub async fn submit_take_profit_limit_order(
609 &self,
610 wallet: &Wallet,
611 instrument_id: InstrumentId,
612 client_order_id: u32,
613 side: OrderSide,
614 trigger_price: Price,
615 limit_price: Price,
616 quantity: Quantity,
617 time_in_force: TimeInForce,
618 post_only: bool,
619 reduce_only: bool,
620 expire_time: Option<i64>,
621 ) -> Result<(), DydxError> {
622 self.submit_conditional_order(
623 wallet,
624 instrument_id,
625 client_order_id,
626 ConditionalOrderType::TakeProfitLimit,
627 side,
628 trigger_price,
629 Some(limit_price),
630 quantity,
631 Some(time_in_force),
632 post_only,
633 reduce_only,
634 expire_time,
635 )
636 .await
637 }
638
639 fn get_market_params(
645 &self,
646 instrument_id: InstrumentId,
647 ) -> Result<OrderMarketParams, DydxError> {
648 let market = self
650 .http_client
651 .get_market_params(&instrument_id)
652 .ok_or_else(|| {
653 DydxError::Order(format!(
654 "Market params for instrument '{instrument_id}' not found in cache"
655 ))
656 })?;
657
658 Ok(OrderMarketParams {
659 atomic_resolution: market.atomic_resolution,
660 clob_pair_id: market.clob_pair_id,
661 oracle_price: None, quantum_conversion_exponent: market.quantum_conversion_exponent,
663 step_base_quantums: market.step_base_quantums,
664 subticks_per_tick: market.subticks_per_tick,
665 })
666 }
667
668 async fn broadcast_tx_message<T: ToAny>(
673 &self,
674 wallet: &Wallet,
675 msg: T,
676 operation: &str,
677 ) -> Result<(), DydxError> {
678 let mut account = wallet
680 .account_offline(0)
681 .map_err(|e| DydxError::Wallet(format!("Failed to derive account: {e}")))?;
682
683 let mut grpc_client = self.grpc_client.clone();
685 let base_account = grpc_client
686 .get_account(&self.wallet_address)
687 .await
688 .map_err(|e| {
689 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
690 "Failed to fetch account info: {e}"
691 ))))
692 })?;
693
694 account.set_account_info(base_account.account_number, base_account.sequence);
696
697 let tx_builder =
699 TxBuilder::new(self.chain_id.clone(), "adydx".to_string()).map_err(|e| {
700 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
701 "TxBuilder init failed: {e}"
702 ))))
703 })?;
704
705 let any_msg = msg.to_any();
707
708 let auth_ids = if self.authenticator_ids.is_empty() {
710 None
711 } else {
712 Some(self.authenticator_ids.as_slice())
713 };
714 let tx_raw = tx_builder
715 .build_transaction(&account, vec![any_msg], None, auth_ids)
716 .map_err(|e| {
717 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
718 "Failed to build tx: {e}"
719 ))))
720 })?;
721
722 let tx_bytes = tx_raw.to_bytes().map_err(|e| {
724 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
725 "Failed to serialize tx: {e}"
726 ))))
727 })?;
728
729 let mut grpc_client = self.grpc_client.clone();
730 let tx_hash = grpc_client.broadcast_tx(tx_bytes).await.map_err(|e| {
731 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
732 "Broadcast failed: {e}"
733 ))))
734 })?;
735
736 tracing::info!("{} successfully: tx_hash={}", operation, tx_hash);
737 Ok(())
738 }
739
740 async fn broadcast_order_message(
742 &self,
743 wallet: &Wallet,
744 msg: MsgPlaceOrder,
745 ) -> Result<(), DydxError> {
746 self.broadcast_tx_message(wallet, msg, "Order placed").await
747 }
748
749 async fn broadcast_cancel_message(
751 &self,
752 wallet: &Wallet,
753 msg: MsgCancelOrder,
754 ) -> Result<(), DydxError> {
755 self.broadcast_tx_message(wallet, msg, "Order cancelled")
756 .await
757 }
758
759 async fn broadcast_cancel_messages_batch(
761 &self,
762 wallet: &Wallet,
763 msgs: Vec<MsgCancelOrder>,
764 ) -> Result<(), DydxError> {
765 let count = msgs.len();
766
767 let mut account = wallet
769 .account_offline(0)
770 .map_err(|e| DydxError::Wallet(format!("Failed to derive account: {e}")))?;
771
772 let mut grpc_client = self.grpc_client.clone();
774 let base_account = grpc_client
775 .get_account(&self.wallet_address)
776 .await
777 .map_err(|e| {
778 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
779 "Failed to fetch account info: {e}"
780 ))))
781 })?;
782
783 account.set_account_info(base_account.account_number, base_account.sequence);
784
785 let tx_builder =
787 TxBuilder::new(self.chain_id.clone(), "adydx".to_string()).map_err(|e| {
788 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
789 "TxBuilder init failed: {e}"
790 ))))
791 })?;
792
793 let any_msgs: Vec<_> = msgs.into_iter().map(|m| m.to_any()).collect();
795
796 let auth_ids = if self.authenticator_ids.is_empty() {
797 None
798 } else {
799 Some(self.authenticator_ids.as_slice())
800 };
801 let tx_raw = tx_builder
802 .build_transaction(&account, any_msgs, None, auth_ids)
803 .map_err(|e| {
804 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
805 "Failed to build tx: {e}"
806 ))))
807 })?;
808
809 let tx_bytes = tx_raw.to_bytes().map_err(|e| {
810 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
811 "Failed to serialize tx: {e}"
812 ))))
813 })?;
814
815 let mut grpc_client = self.grpc_client.clone();
816 let tx_hash = grpc_client.broadcast_tx(tx_bytes).await.map_err(|e| {
817 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
818 "Broadcast failed: {e}"
819 ))))
820 })?;
821
822 tracing::info!("Batch cancelled {} orders: tx_hash={}", count, tx_hash);
823 Ok(())
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use rstest::rstest;
830
831 use super::*;
832
833 #[rstest]
834 fn test_cancel_orders_batch_builds_multiple_messages() {
835 let btc_id = InstrumentId::from("BTC-USD-PERP.DYDX");
836 let eth_id = InstrumentId::from("ETH-USD-PERP.DYDX");
837 let orders = [(btc_id, 100u32), (btc_id, 101u32), (eth_id, 200u32)];
838
839 assert_eq!(orders.len(), 3);
840 assert_eq!(orders[0], (btc_id, 100));
841 assert_eq!(orders[1], (btc_id, 101));
842 assert_eq!(orders[2], (eth_id, 200));
843 }
844
845 #[rstest]
846 fn test_cancel_orders_batch_empty_returns_ok() {
847 let orders: [(InstrumentId, u32); 0] = [];
848 assert!(orders.is_empty());
849 }
850
851 #[rstest]
852 fn test_conditional_order_expiration_with_explicit_timestamp() {
853 let expire_ts = 1735689600i64; let result =
855 calculate_conditional_order_expiration(TimeInForce::Gtd, Some(expire_ts)).unwrap();
856 assert_eq!(result.timestamp(), expire_ts);
857 }
858
859 #[rstest]
860 fn test_conditional_order_expiration_gtc_uses_90_days() {
861 let now = Utc::now();
862 let result = calculate_conditional_order_expiration(TimeInForce::Gtc, None).unwrap();
863
864 let expected_min = now + Duration::days(89);
865 let expected_max = now + Duration::days(91);
866
867 assert!(result > expected_min);
868 assert!(result < expected_max);
869 }
870
871 #[rstest]
872 fn test_conditional_order_expiration_gtd_without_timestamp_uses_90_days() {
873 let now = Utc::now();
874 let result = calculate_conditional_order_expiration(TimeInForce::Gtd, None).unwrap();
875
876 let expected_min = now + Duration::days(89);
877 let expected_max = now + Duration::days(91);
878
879 assert!(result > expected_min);
880 assert!(result < expected_max);
881 }
882
883 #[rstest]
884 fn test_conditional_order_expiration_ioc_uses_1_hour() {
885 let now = Utc::now();
886 let result = calculate_conditional_order_expiration(TimeInForce::Ioc, None).unwrap();
887
888 let expected_min = now + Duration::minutes(59);
889 let expected_max = now + Duration::minutes(61);
890
891 assert!(result > expected_min);
892 assert!(result < expected_max);
893 }
894
895 #[rstest]
896 fn test_conditional_order_expiration_fok_uses_1_hour() {
897 let now = Utc::now();
898 let result = calculate_conditional_order_expiration(TimeInForce::Fok, None).unwrap();
899
900 let expected_min = now + Duration::minutes(59);
901 let expected_max = now + Duration::minutes(61);
902
903 assert!(result > expected_min);
904 assert!(result < expected_max);
905 }
906
907 #[rstest]
908 fn test_conditional_order_expiration_day_uses_90_days() {
909 let now = Utc::now();
910 let result = calculate_conditional_order_expiration(TimeInForce::Day, None).unwrap();
911
912 let expected_min = now + Duration::days(89);
913 let expected_max = now + Duration::days(91);
914
915 assert!(result > expected_min);
916 assert!(result < expected_max);
917 }
918
919 #[rstest]
920 fn test_conditional_order_expiration_invalid_timestamp_returns_error() {
921 let result = calculate_conditional_order_expiration(TimeInForce::Gtd, Some(i64::MAX));
923 assert!(result.is_err());
924 }
925
926 #[rstest]
927 fn test_conditional_order_type_debug_format() {
928 assert_eq!(
929 format!("{:?}", ConditionalOrderType::StopMarket),
930 "StopMarket"
931 );
932 assert_eq!(
933 format!("{:?}", ConditionalOrderType::StopLimit),
934 "StopLimit"
935 );
936 assert_eq!(
937 format!("{:?}", ConditionalOrderType::TakeProfitMarket),
938 "TakeProfitMarket"
939 );
940 assert_eq!(
941 format!("{:?}", ConditionalOrderType::TakeProfitLimit),
942 "TakeProfitLimit"
943 );
944 }
945}