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 log::info!(
134 "Submitting market order: client_id={client_order_id}, side={side:?}, quantity={quantity}"
135 );
136
137 let market_params = self.get_market_params(instrument_id)?;
139
140 let mut builder = OrderBuilder::new(
142 market_params,
143 self.wallet_address.clone(),
144 self.subaccount_number,
145 client_order_id,
146 );
147
148 let proto_side = order_side_to_proto(side);
149 let size_decimal = quantity.as_decimal();
150
151 builder = builder.market(proto_side, size_decimal);
152 builder = builder.short_term(); builder = builder.until(OrderGoodUntil::Block(
154 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
155 ));
156
157 let order = builder
158 .build()
159 .map_err(|e| DydxError::Order(format!("Failed to build market order: {e}")))?;
160
161 let msg_place_order = MsgPlaceOrder { order: Some(order) };
163
164 self.broadcast_order_message(wallet, msg_place_order).await
166 }
167
168 #[allow(clippy::too_many_arguments)]
176 pub async fn submit_limit_order(
177 &self,
178 wallet: &Wallet,
179 instrument_id: InstrumentId,
180 client_order_id: u32,
181 side: OrderSide,
182 price: Price,
183 quantity: Quantity,
184 time_in_force: TimeInForce,
185 post_only: bool,
186 reduce_only: bool,
187 block_height: u32,
188 expire_time: Option<i64>,
189 ) -> Result<(), DydxError> {
190 log::info!(
191 "Submitting limit order: client_id={client_order_id}, side={side:?}, price={price}, quantity={quantity}, tif={time_in_force:?}, post_only={post_only}, reduce_only={reduce_only}"
192 );
193
194 let market_params = self.get_market_params(instrument_id)?;
196
197 let mut builder = OrderBuilder::new(
199 market_params,
200 self.wallet_address.clone(),
201 self.subaccount_number,
202 client_order_id,
203 );
204
205 let proto_side = order_side_to_proto(side);
206 let price_decimal = price.as_decimal();
207 let size_decimal = quantity.as_decimal();
208
209 builder = builder.limit(proto_side, price_decimal, size_decimal);
210
211 let proto_tif = time_in_force_to_proto_with_post_only(time_in_force, post_only);
213 builder = builder.time_in_force(proto_tif);
214
215 if reduce_only {
217 builder = builder.reduce_only(true);
218 }
219
220 if let Some(expire_ts) = expire_time {
222 builder = builder.long_term();
223 builder = builder.until(OrderGoodUntil::Time(
224 DateTime::from_timestamp(expire_ts, 0)
225 .ok_or_else(|| DydxError::Parse("Invalid expire timestamp".to_string()))?,
226 ));
227 } else {
228 builder = builder.short_term();
229 builder = builder.until(OrderGoodUntil::Block(
230 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
231 ));
232 }
233
234 let order = builder
235 .build()
236 .map_err(|e| DydxError::Order(format!("Failed to build limit order: {e}")))?;
237
238 let msg_place_order = MsgPlaceOrder { order: Some(order) };
240
241 self.broadcast_order_message(wallet, msg_place_order).await
243 }
244
245 pub async fn cancel_order(
255 &self,
256 wallet: &Wallet,
257 instrument_id: InstrumentId,
258 client_order_id: u32,
259 block_height: u32,
260 ) -> Result<(), DydxError> {
261 log::info!("Cancelling order: client_id={client_order_id}, instrument={instrument_id}");
262
263 let market_params = self.get_market_params(instrument_id)?;
265
266 let msg_cancel = MsgCancelOrder {
268 order_id: Some(crate::proto::dydxprotocol::clob::OrderId {
269 subaccount_id: Some(crate::proto::dydxprotocol::subaccounts::SubaccountId {
270 owner: self.wallet_address.clone(),
271 number: self.subaccount_number,
272 }),
273 client_id: client_order_id,
274 order_flags: 0, clob_pair_id: market_params.clob_pair_id,
276 }),
277 good_til_oneof: Some(
278 crate::proto::dydxprotocol::clob::msg_cancel_order::GoodTilOneof::GoodTilBlock(
279 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
280 ),
281 ),
282 };
283
284 self.broadcast_cancel_message(wallet, msg_cancel).await
286 }
287
288 pub async fn cancel_orders_batch(
304 &self,
305 wallet: &Wallet,
306 orders: &[(InstrumentId, u32)],
307 block_height: u32,
308 ) -> Result<(), DydxError> {
309 if orders.is_empty() {
310 return Ok(());
311 }
312
313 log::info!(
314 "Batch cancelling {} orders in single transaction",
315 orders.len()
316 );
317
318 let mut cancel_msgs = Vec::with_capacity(orders.len());
320 for (instrument_id, client_order_id) in orders {
321 let market_params = self.get_market_params(*instrument_id)?;
322
323 let msg_cancel = MsgCancelOrder {
324 order_id: Some(crate::proto::dydxprotocol::clob::OrderId {
325 subaccount_id: Some(crate::proto::dydxprotocol::subaccounts::SubaccountId {
326 owner: self.wallet_address.clone(),
327 number: self.subaccount_number,
328 }),
329 client_id: *client_order_id,
330 order_flags: 0,
331 clob_pair_id: market_params.clob_pair_id,
332 }),
333 good_til_oneof: Some(
334 crate::proto::dydxprotocol::clob::msg_cancel_order::GoodTilOneof::GoodTilBlock(
335 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
336 ),
337 ),
338 };
339 cancel_msgs.push(msg_cancel);
340 }
341
342 self.broadcast_cancel_messages_batch(wallet, cancel_msgs)
344 .await
345 }
346
347 #[allow(clippy::too_many_arguments)]
357 pub async fn submit_conditional_order(
358 &self,
359 wallet: &Wallet,
360 instrument_id: InstrumentId,
361 client_order_id: u32,
362 order_type: ConditionalOrderType,
363 side: OrderSide,
364 trigger_price: Price,
365 limit_price: Option<Price>,
366 quantity: Quantity,
367 time_in_force: Option<TimeInForce>,
368 post_only: bool,
369 reduce_only: bool,
370 expire_time: Option<i64>,
371 ) -> Result<(), DydxError> {
372 let market_params = self.get_market_params(instrument_id)?;
373
374 let mut builder = OrderBuilder::new(
375 market_params,
376 self.wallet_address.clone(),
377 self.subaccount_number,
378 client_order_id,
379 );
380
381 let proto_side = order_side_to_proto(side);
382 let trigger_decimal = trigger_price.as_decimal();
383 let size_decimal = quantity.as_decimal();
384
385 builder = match order_type {
387 ConditionalOrderType::StopMarket => {
388 log::info!(
389 "Submitting stop market order: client_id={client_order_id}, side={side:?}, trigger={trigger_price}, qty={quantity}"
390 );
391 builder.stop_market(proto_side, trigger_decimal, size_decimal)
392 }
393 ConditionalOrderType::StopLimit => {
394 let limit = limit_price.ok_or_else(|| {
395 DydxError::Order("StopLimit requires limit_price".to_string())
396 })?;
397 log::info!(
398 "Submitting stop limit order: client_id={client_order_id}, side={side:?}, trigger={trigger_price}, limit={limit}, qty={quantity}"
399 );
400 builder.stop_limit(
401 proto_side,
402 limit.as_decimal(),
403 trigger_decimal,
404 size_decimal,
405 )
406 }
407 ConditionalOrderType::TakeProfitMarket => {
408 log::info!(
409 "Submitting take profit market order: client_id={client_order_id}, side={side:?}, trigger={trigger_price}, qty={quantity}"
410 );
411 builder.take_profit_market(proto_side, trigger_decimal, size_decimal)
412 }
413 ConditionalOrderType::TakeProfitLimit => {
414 let limit = limit_price.ok_or_else(|| {
415 DydxError::Order("TakeProfitLimit requires limit_price".to_string())
416 })?;
417 log::info!(
418 "Submitting take profit limit order: client_id={client_order_id}, side={side:?}, trigger={trigger_price}, limit={limit}, qty={quantity}"
419 );
420 builder.take_profit_limit(
421 proto_side,
422 limit.as_decimal(),
423 trigger_decimal,
424 size_decimal,
425 )
426 }
427 };
428
429 let effective_tif = time_in_force.unwrap_or(TimeInForce::Gtc);
431 if matches!(
432 order_type,
433 ConditionalOrderType::StopLimit | ConditionalOrderType::TakeProfitLimit
434 ) {
435 let proto_tif = time_in_force_to_proto_with_post_only(effective_tif, post_only);
436 builder = builder.time_in_force(proto_tif);
437 }
438
439 if reduce_only {
440 builder = builder.reduce_only(true);
441 }
442
443 let expire = calculate_conditional_order_expiration(effective_tif, expire_time)?;
444 builder = builder.until(OrderGoodUntil::Time(expire));
445
446 let order = builder
447 .build()
448 .map_err(|e| DydxError::Order(format!("Failed to build {order_type:?} order: {e}")))?;
449
450 let msg_place_order = MsgPlaceOrder { order: Some(order) };
451 self.broadcast_order_message(wallet, msg_place_order).await
452 }
453
454 #[allow(clippy::too_many_arguments)]
462 pub async fn submit_stop_market_order(
463 &self,
464 wallet: &Wallet,
465 instrument_id: InstrumentId,
466 client_order_id: u32,
467 side: OrderSide,
468 trigger_price: Price,
469 quantity: Quantity,
470 reduce_only: bool,
471 expire_time: Option<i64>,
472 ) -> Result<(), DydxError> {
473 self.submit_conditional_order(
474 wallet,
475 instrument_id,
476 client_order_id,
477 ConditionalOrderType::StopMarket,
478 side,
479 trigger_price,
480 None,
481 quantity,
482 None,
483 false,
484 reduce_only,
485 expire_time,
486 )
487 .await
488 }
489
490 #[allow(clippy::too_many_arguments)]
499 pub async fn submit_stop_limit_order(
500 &self,
501 wallet: &Wallet,
502 instrument_id: InstrumentId,
503 client_order_id: u32,
504 side: OrderSide,
505 trigger_price: Price,
506 limit_price: Price,
507 quantity: Quantity,
508 time_in_force: TimeInForce,
509 post_only: bool,
510 reduce_only: bool,
511 expire_time: Option<i64>,
512 ) -> Result<(), DydxError> {
513 self.submit_conditional_order(
514 wallet,
515 instrument_id,
516 client_order_id,
517 ConditionalOrderType::StopLimit,
518 side,
519 trigger_price,
520 Some(limit_price),
521 quantity,
522 Some(time_in_force),
523 post_only,
524 reduce_only,
525 expire_time,
526 )
527 .await
528 }
529
530 #[allow(clippy::too_many_arguments)]
539 pub async fn submit_take_profit_market_order(
540 &self,
541 wallet: &Wallet,
542 instrument_id: InstrumentId,
543 client_order_id: u32,
544 side: OrderSide,
545 trigger_price: Price,
546 quantity: Quantity,
547 reduce_only: bool,
548 expire_time: Option<i64>,
549 ) -> Result<(), DydxError> {
550 self.submit_conditional_order(
551 wallet,
552 instrument_id,
553 client_order_id,
554 ConditionalOrderType::TakeProfitMarket,
555 side,
556 trigger_price,
557 None,
558 quantity,
559 None,
560 false,
561 reduce_only,
562 expire_time,
563 )
564 .await
565 }
566
567 #[allow(clippy::too_many_arguments)]
576 pub async fn submit_take_profit_limit_order(
577 &self,
578 wallet: &Wallet,
579 instrument_id: InstrumentId,
580 client_order_id: u32,
581 side: OrderSide,
582 trigger_price: Price,
583 limit_price: Price,
584 quantity: Quantity,
585 time_in_force: TimeInForce,
586 post_only: bool,
587 reduce_only: bool,
588 expire_time: Option<i64>,
589 ) -> Result<(), DydxError> {
590 self.submit_conditional_order(
591 wallet,
592 instrument_id,
593 client_order_id,
594 ConditionalOrderType::TakeProfitLimit,
595 side,
596 trigger_price,
597 Some(limit_price),
598 quantity,
599 Some(time_in_force),
600 post_only,
601 reduce_only,
602 expire_time,
603 )
604 .await
605 }
606
607 fn get_market_params(
613 &self,
614 instrument_id: InstrumentId,
615 ) -> Result<OrderMarketParams, DydxError> {
616 let market = self
618 .http_client
619 .get_market_params(&instrument_id)
620 .ok_or_else(|| {
621 DydxError::Order(format!(
622 "Market params for instrument '{instrument_id}' not found in cache"
623 ))
624 })?;
625
626 Ok(OrderMarketParams {
627 atomic_resolution: market.atomic_resolution,
628 clob_pair_id: market.clob_pair_id,
629 oracle_price: None, quantum_conversion_exponent: market.quantum_conversion_exponent,
631 step_base_quantums: market.step_base_quantums,
632 subticks_per_tick: market.subticks_per_tick,
633 })
634 }
635
636 async fn broadcast_tx_message<T: ToAny>(
641 &self,
642 wallet: &Wallet,
643 msg: T,
644 operation: &str,
645 ) -> Result<(), DydxError> {
646 let mut account = wallet
648 .account_offline(0)
649 .map_err(|e| DydxError::Wallet(format!("Failed to derive account: {e}")))?;
650
651 let mut grpc_client = self.grpc_client.clone();
653 let base_account = grpc_client
654 .get_account(&self.wallet_address)
655 .await
656 .map_err(|e| {
657 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
658 "Failed to fetch account info: {e}"
659 ))))
660 })?;
661
662 account.set_account_info(base_account.account_number, base_account.sequence);
664
665 let tx_builder =
667 TxBuilder::new(self.chain_id.clone(), "adydx".to_string()).map_err(|e| {
668 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
669 "TxBuilder init failed: {e}"
670 ))))
671 })?;
672
673 let any_msg = msg.to_any();
675
676 let auth_ids = if self.authenticator_ids.is_empty() {
678 None
679 } else {
680 Some(self.authenticator_ids.as_slice())
681 };
682 let tx_raw = tx_builder
683 .build_transaction(&account, vec![any_msg], None, auth_ids)
684 .map_err(|e| {
685 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
686 "Failed to build tx: {e}"
687 ))))
688 })?;
689
690 let tx_bytes = tx_raw.to_bytes().map_err(|e| {
692 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
693 "Failed to serialize tx: {e}"
694 ))))
695 })?;
696
697 log::debug!(
698 "Broadcasting {} with {} bytes, account_seq={}",
699 operation,
700 tx_bytes.len(),
701 account.sequence_number
702 );
703
704 let mut grpc_client = self.grpc_client.clone();
705 let tx_hash = grpc_client.broadcast_tx(tx_bytes).await.map_err(|e| {
706 log::error!("gRPC broadcast failed for {operation}: {e}");
707 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
708 "Broadcast failed: {e}"
709 ))))
710 })?;
711
712 log::info!("{operation} successfully: tx_hash={tx_hash}");
713 Ok(())
714 }
715
716 async fn broadcast_order_message(
718 &self,
719 wallet: &Wallet,
720 msg: MsgPlaceOrder,
721 ) -> Result<(), DydxError> {
722 self.broadcast_tx_message(wallet, msg, "Order placed").await
723 }
724
725 async fn broadcast_cancel_message(
727 &self,
728 wallet: &Wallet,
729 msg: MsgCancelOrder,
730 ) -> Result<(), DydxError> {
731 self.broadcast_tx_message(wallet, msg, "Order cancelled")
732 .await
733 }
734
735 async fn broadcast_cancel_messages_batch(
737 &self,
738 wallet: &Wallet,
739 msgs: Vec<MsgCancelOrder>,
740 ) -> Result<(), DydxError> {
741 let count = msgs.len();
742
743 let mut account = wallet
745 .account_offline(0)
746 .map_err(|e| DydxError::Wallet(format!("Failed to derive account: {e}")))?;
747
748 let mut grpc_client = self.grpc_client.clone();
750 let base_account = grpc_client
751 .get_account(&self.wallet_address)
752 .await
753 .map_err(|e| {
754 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
755 "Failed to fetch account info: {e}"
756 ))))
757 })?;
758
759 account.set_account_info(base_account.account_number, base_account.sequence);
760
761 let tx_builder =
763 TxBuilder::new(self.chain_id.clone(), "adydx".to_string()).map_err(|e| {
764 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
765 "TxBuilder init failed: {e}"
766 ))))
767 })?;
768
769 let any_msgs: Vec<_> = msgs.into_iter().map(|m| m.to_any()).collect();
771
772 let auth_ids = if self.authenticator_ids.is_empty() {
773 None
774 } else {
775 Some(self.authenticator_ids.as_slice())
776 };
777 let tx_raw = tx_builder
778 .build_transaction(&account, any_msgs, None, auth_ids)
779 .map_err(|e| {
780 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
781 "Failed to build tx: {e}"
782 ))))
783 })?;
784
785 let tx_bytes = tx_raw.to_bytes().map_err(|e| {
786 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
787 "Failed to serialize tx: {e}"
788 ))))
789 })?;
790
791 let mut grpc_client = self.grpc_client.clone();
792 let tx_hash = grpc_client.broadcast_tx(tx_bytes).await.map_err(|e| {
793 DydxError::Grpc(Box::new(tonic::Status::internal(format!(
794 "Broadcast failed: {e}"
795 ))))
796 })?;
797
798 log::info!("Batch cancelled {count} orders: tx_hash={tx_hash}");
799 Ok(())
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use rstest::rstest;
806
807 use super::*;
808
809 #[rstest]
810 fn test_cancel_orders_batch_builds_multiple_messages() {
811 let btc_id = InstrumentId::from("BTC-USD-PERP.DYDX");
812 let eth_id = InstrumentId::from("ETH-USD-PERP.DYDX");
813 let orders = [(btc_id, 100u32), (btc_id, 101u32), (eth_id, 200u32)];
814
815 assert_eq!(orders.len(), 3);
816 assert_eq!(orders[0], (btc_id, 100));
817 assert_eq!(orders[1], (btc_id, 101));
818 assert_eq!(orders[2], (eth_id, 200));
819 }
820
821 #[rstest]
822 fn test_cancel_orders_batch_empty_returns_ok() {
823 let orders: [(InstrumentId, u32); 0] = [];
824 assert!(orders.is_empty());
825 }
826
827 #[rstest]
828 fn test_conditional_order_expiration_with_explicit_timestamp() {
829 let expire_ts = 1735689600i64; let result =
831 calculate_conditional_order_expiration(TimeInForce::Gtd, Some(expire_ts)).unwrap();
832 assert_eq!(result.timestamp(), expire_ts);
833 }
834
835 #[rstest]
836 fn test_conditional_order_expiration_gtc_uses_90_days() {
837 let now = Utc::now();
838 let result = calculate_conditional_order_expiration(TimeInForce::Gtc, None).unwrap();
839
840 let expected_min = now + Duration::days(89);
841 let expected_max = now + Duration::days(91);
842
843 assert!(result > expected_min);
844 assert!(result < expected_max);
845 }
846
847 #[rstest]
848 fn test_conditional_order_expiration_gtd_without_timestamp_uses_90_days() {
849 let now = Utc::now();
850 let result = calculate_conditional_order_expiration(TimeInForce::Gtd, None).unwrap();
851
852 let expected_min = now + Duration::days(89);
853 let expected_max = now + Duration::days(91);
854
855 assert!(result > expected_min);
856 assert!(result < expected_max);
857 }
858
859 #[rstest]
860 fn test_conditional_order_expiration_ioc_uses_1_hour() {
861 let now = Utc::now();
862 let result = calculate_conditional_order_expiration(TimeInForce::Ioc, None).unwrap();
863
864 let expected_min = now + Duration::minutes(59);
865 let expected_max = now + Duration::minutes(61);
866
867 assert!(result > expected_min);
868 assert!(result < expected_max);
869 }
870
871 #[rstest]
872 fn test_conditional_order_expiration_fok_uses_1_hour() {
873 let now = Utc::now();
874 let result = calculate_conditional_order_expiration(TimeInForce::Fok, None).unwrap();
875
876 let expected_min = now + Duration::minutes(59);
877 let expected_max = now + Duration::minutes(61);
878
879 assert!(result > expected_min);
880 assert!(result < expected_max);
881 }
882
883 #[rstest]
884 fn test_conditional_order_expiration_day_uses_90_days() {
885 let now = Utc::now();
886 let result = calculate_conditional_order_expiration(TimeInForce::Day, None).unwrap();
887
888 let expected_min = now + Duration::days(89);
889 let expected_max = now + Duration::days(91);
890
891 assert!(result > expected_min);
892 assert!(result < expected_max);
893 }
894
895 #[rstest]
896 fn test_conditional_order_expiration_invalid_timestamp_returns_error() {
897 let result = calculate_conditional_order_expiration(TimeInForce::Gtd, Some(i64::MAX));
899 assert!(result.is_err());
900 }
901
902 #[rstest]
903 fn test_conditional_order_type_debug_format() {
904 assert_eq!(
905 format!("{:?}", ConditionalOrderType::StopMarket),
906 "StopMarket"
907 );
908 assert_eq!(
909 format!("{:?}", ConditionalOrderType::StopLimit),
910 "StopLimit"
911 );
912 assert_eq!(
913 format!("{:?}", ConditionalOrderType::TakeProfitMarket),
914 "TakeProfitMarket"
915 );
916 assert_eq!(
917 format!("{:?}", ConditionalOrderType::TakeProfitLimit),
918 "TakeProfitLimit"
919 );
920 }
921}