1use std::{collections::HashMap, sync::Arc};
30
31use cosmrs::Any;
32use jiff::{SignedDuration, Timestamp};
33use nautilus_common::cache::InstrumentLookupError;
34use nautilus_model::{
35 enums::{OrderSide, TimeInForce},
36 identifiers::InstrumentId,
37 types::{Price, Quantity},
38};
39
40use super::{
41 block_time::BlockTimeMonitor,
42 types::{
43 ConditionalOrderType, GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS, LimitOrderParams,
44 ORDER_FLAG_SHORT_TERM, OrderLifetime, calculate_conditional_order_expiration,
45 },
46};
47use crate::{
48 common::parse::{
49 nanos_to_secs_i64, order_side_to_proto, time_in_force_to_proto_with_post_only,
50 },
51 error::DydxError,
52 grpc::{OrderBuilder, OrderGoodUntil, OrderMarketParams, SHORT_TERM_ORDER_MAXIMUM_LIFETIME},
53 http::client::DydxHttpClient,
54 proto::{
55 ToAny,
56 dydxprotocol::{
57 clob::{
58 MsgBatchCancel, MsgCancelOrder, MsgPlaceOrder, OrderBatch, OrderId,
59 msg_cancel_order::GoodTilOneof,
60 },
61 subaccounts::SubaccountId,
62 },
63 },
64};
65
66#[derive(Debug)]
81pub struct OrderMessageBuilder {
82 http_client: DydxHttpClient,
83 wallet_address: String,
84 subaccount_number: u32,
85 block_time_monitor: Arc<BlockTimeMonitor>,
87}
88
89impl OrderMessageBuilder {
90 #[must_use]
92 pub fn new(
93 http_client: DydxHttpClient,
94 wallet_address: String,
95 subaccount_number: u32,
96 block_time_monitor: Arc<BlockTimeMonitor>,
97 ) -> Self {
98 Self {
99 http_client,
100 wallet_address,
101 subaccount_number,
102 block_time_monitor,
103 }
104 }
105
106 #[must_use]
113 pub fn max_short_term_secs(&self) -> f64 {
114 SHORT_TERM_ORDER_MAXIMUM_LIFETIME as f64
115 * self.block_time_monitor.seconds_per_block_or_default()
116 }
117
118 #[must_use]
120 fn expire_time_to_secs(
121 &self,
122 order_expire_time_ns: Option<nautilus_core::UnixNanos>,
123 ) -> Option<i64> {
124 order_expire_time_ns.map(nanos_to_secs_i64)
125 }
126
127 #[must_use]
138 pub fn get_order_lifetime(&self, params: &LimitOrderParams) -> OrderLifetime {
139 let expire_time = self.expire_time_to_secs(params.expire_time_ns);
140 OrderLifetime::from_time_in_force(
141 params.time_in_force,
142 expire_time,
143 false,
144 self.max_short_term_secs(),
145 )
146 }
147
148 #[must_use]
155 pub fn is_short_term_order(&self, params: &LimitOrderParams) -> bool {
156 self.get_order_lifetime(params).is_short_term()
157 }
158
159 #[must_use]
166 pub fn is_short_term_cancel(
167 &self,
168 time_in_force: TimeInForce,
169 expire_time_ns: Option<nautilus_core::UnixNanos>,
170 ) -> bool {
171 let expire_time = self.expire_time_to_secs(expire_time_ns);
172 OrderLifetime::from_time_in_force(
173 time_in_force,
174 expire_time,
175 false,
176 self.max_short_term_secs(),
177 )
178 .is_short_term()
179 }
180
181 pub fn build_market_order(
189 &self,
190 instrument_id: InstrumentId,
191 client_order_id: u32,
192 client_metadata: u32,
193 side: OrderSide,
194 quantity: Quantity,
195 block_height: u32,
196 ) -> Result<Any, DydxError> {
197 self.build_market_order_with_reduce_only(
198 instrument_id,
199 client_order_id,
200 client_metadata,
201 side,
202 quantity,
203 false,
204 block_height,
205 )
206 }
207
208 #[expect(
209 clippy::too_many_arguments,
210 reason = "mirrors build_market_order with an explicit reduce-only flag"
211 )]
212 pub(crate) fn build_market_order_with_reduce_only(
213 &self,
214 instrument_id: InstrumentId,
215 client_order_id: u32,
216 client_metadata: u32,
217 side: OrderSide,
218 quantity: Quantity,
219 reduce_only: bool,
220 block_height: u32,
221 ) -> Result<Any, DydxError> {
222 let market_params = self.get_market_params(instrument_id)?;
223
224 let builder = OrderBuilder::new(
225 market_params,
226 self.wallet_address.clone(),
227 self.subaccount_number,
228 client_order_id,
229 client_metadata,
230 )
231 .market(order_side_to_proto(side), quantity.as_decimal())
232 .reduce_only(reduce_only)
233 .short_term()
234 .until(OrderGoodUntil::Block(
235 block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
236 ));
237
238 let order = builder
239 .build()
240 .map_err(|e| DydxError::Order(format!("Failed to build market order: {e}")))?;
241
242 Ok(MsgPlaceOrder { order: Some(order) }.to_any())
243 }
244
245 #[expect(clippy::too_many_arguments)]
253 pub fn build_limit_order(
254 &self,
255 instrument_id: InstrumentId,
256 client_order_id: u32,
257 client_metadata: u32,
258 side: OrderSide,
259 price: Price,
260 quantity: Quantity,
261 time_in_force: TimeInForce,
262 post_only: bool,
263 reduce_only: bool,
264 block_height: u32,
265 expire_time: Option<i64>,
266 ) -> Result<Any, DydxError> {
267 let market_params = self.get_market_params(instrument_id)?;
268 let lifetime = OrderLifetime::from_time_in_force(
269 time_in_force,
270 expire_time,
271 false,
272 self.max_short_term_secs(),
273 );
274
275 let mut builder = OrderBuilder::new(
276 market_params,
277 self.wallet_address.clone(),
278 self.subaccount_number,
279 client_order_id,
280 client_metadata,
281 )
282 .limit(
283 order_side_to_proto(side),
284 price.as_decimal(),
285 quantity.as_decimal(),
286 )
287 .time_in_force(time_in_force_to_proto_with_post_only(
288 time_in_force,
289 post_only,
290 ));
291
292 if reduce_only {
293 builder = builder.reduce_only(true);
294 }
295
296 builder = self.apply_order_lifetime(builder, lifetime, block_height, expire_time)?;
298
299 let order = builder
300 .build()
301 .map_err(|e| DydxError::Order(format!("Failed to build limit order: {e}")))?;
302
303 Ok(MsgPlaceOrder { order: Some(order) }.to_any())
304 }
305
306 pub fn build_limit_order_from_params(
312 &self,
313 params: &LimitOrderParams,
314 block_height: u32,
315 ) -> Result<Any, DydxError> {
316 let expire_time = self.expire_time_to_secs(params.expire_time_ns);
317
318 self.build_limit_order(
319 params.instrument_id,
320 params.client_order_id,
321 params.client_metadata,
322 params.side,
323 params.price,
324 params.quantity,
325 params.time_in_force,
326 params.post_only,
327 params.reduce_only,
328 block_height,
329 expire_time,
330 )
331 }
332
333 pub fn build_limit_orders_batch(
339 &self,
340 orders: &[LimitOrderParams],
341 block_height: u32,
342 ) -> Result<Vec<Any>, DydxError> {
343 orders
344 .iter()
345 .map(|params| self.build_limit_order_from_params(params, block_height))
346 .collect()
347 }
348
349 pub fn build_cancel_order(
358 &self,
359 instrument_id: InstrumentId,
360 client_order_id: u32,
361 time_in_force: TimeInForce,
362 expire_time_ns: Option<nautilus_core::UnixNanos>,
363 block_height: u32,
364 ) -> Result<Any, DydxError> {
365 let expire_time = self.expire_time_to_secs(expire_time_ns);
366 let market_params = self.get_market_params(instrument_id)?;
367 let lifetime = OrderLifetime::from_time_in_force(
368 time_in_force,
369 expire_time,
370 false,
371 self.max_short_term_secs(),
372 );
373
374 let (order_flags, good_til_oneof) = match lifetime {
375 OrderLifetime::ShortTerm => (
376 0,
377 GoodTilOneof::GoodTilBlock(block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME),
378 ),
379 OrderLifetime::LongTerm | OrderLifetime::Conditional => {
380 let cancel_good_til = (Timestamp::now()
381 + SignedDuration::from_hours(24 * GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS))
382 .as_second() as u32;
383 (
384 lifetime.order_flags(),
385 GoodTilOneof::GoodTilBlockTime(cancel_good_til),
386 )
387 }
388 };
389
390 let msg = MsgCancelOrder {
391 order_id: Some(OrderId {
392 subaccount_id: Some(SubaccountId {
393 owner: self.wallet_address.clone(),
394 number: self.subaccount_number,
395 }),
396 client_id: client_order_id,
397 order_flags,
398 clob_pair_id: market_params.clob_pair_id,
399 }),
400 good_til_oneof: Some(good_til_oneof),
401 };
402
403 Ok(msg.to_any())
404 }
405
406 pub fn build_cancel_order_with_flags(
415 &self,
416 instrument_id: InstrumentId,
417 client_order_id: u32,
418 order_flags: u32,
419 block_height: u32,
420 ) -> Result<Any, DydxError> {
421 let market_params = self.get_market_params(instrument_id)?;
422
423 let good_til_oneof = if order_flags == ORDER_FLAG_SHORT_TERM {
424 GoodTilOneof::GoodTilBlock(block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME)
425 } else {
426 let cancel_good_til = (Timestamp::now()
427 + SignedDuration::from_hours(24 * GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS))
428 .as_second() as u32;
429 GoodTilOneof::GoodTilBlockTime(cancel_good_til)
430 };
431
432 let msg = MsgCancelOrder {
433 order_id: Some(OrderId {
434 subaccount_id: Some(SubaccountId {
435 owner: self.wallet_address.clone(),
436 number: self.subaccount_number,
437 }),
438 client_id: client_order_id,
439 order_flags,
440 clob_pair_id: market_params.clob_pair_id,
441 }),
442 good_til_oneof: Some(good_til_oneof),
443 };
444
445 Ok(msg.to_any())
446 }
447
448 pub fn build_cancel_orders_batch(
456 &self,
457 orders: &[(
458 InstrumentId,
459 u32,
460 TimeInForce,
461 Option<nautilus_core::UnixNanos>,
462 )],
463 block_height: u32,
464 ) -> Result<Vec<Any>, DydxError> {
465 orders
466 .iter()
467 .map(|(instrument_id, client_order_id, tif, expire_time_ns)| {
468 self.build_cancel_order(
469 *instrument_id,
470 *client_order_id,
471 *tif,
472 *expire_time_ns,
473 block_height,
474 )
475 })
476 .collect()
477 }
478
479 pub fn build_cancel_orders_batch_with_flags(
488 &self,
489 orders: &[(InstrumentId, u32, u32)],
490 block_height: u32,
491 ) -> Result<Vec<Any>, DydxError> {
492 orders
493 .iter()
494 .map(|(instrument_id, client_order_id, order_flags)| {
495 self.build_cancel_order_with_flags(
496 *instrument_id,
497 *client_order_id,
498 *order_flags,
499 block_height,
500 )
501 })
502 .collect()
503 }
504
505 pub fn build_batch_cancel_short_term(
514 &self,
515 orders: &[(InstrumentId, u32)],
516 block_height: u32,
517 ) -> Result<Any, DydxError> {
518 let mut clob_groups: HashMap<u32, Vec<u32>> = HashMap::new();
520
521 for (instrument_id, client_order_id) in orders {
522 let market_params = self.get_market_params(*instrument_id)?;
523 clob_groups
524 .entry(market_params.clob_pair_id)
525 .or_default()
526 .push(*client_order_id);
527 }
528
529 let short_term_cancels: Vec<OrderBatch> = clob_groups
530 .into_iter()
531 .map(|(clob_pair_id, client_ids)| OrderBatch {
532 clob_pair_id,
533 client_ids,
534 })
535 .collect();
536
537 let msg = MsgBatchCancel {
538 subaccount_id: Some(SubaccountId {
539 owner: self.wallet_address.clone(),
540 number: self.subaccount_number,
541 }),
542 short_term_cancels,
543 good_til_block: block_height + SHORT_TERM_ORDER_MAXIMUM_LIFETIME,
544 };
545
546 Ok(msg.to_any())
547 }
548
549 #[expect(clippy::too_many_arguments)]
572 pub fn build_cancel_and_replace(
573 &self,
574 instrument_id: InstrumentId,
575 old_client_order_id: u32,
576 _new_client_order_id: u32,
577 old_time_in_force: TimeInForce,
578 old_expire_time_ns: Option<nautilus_core::UnixNanos>,
579 new_params: &LimitOrderParams,
580 block_height: u32,
581 ) -> Result<Vec<Any>, DydxError> {
582 let cancel_msg = self.build_cancel_order(
584 instrument_id,
585 old_client_order_id,
586 old_time_in_force,
587 old_expire_time_ns,
588 block_height,
589 )?;
590
591 let place_msg = self.build_limit_order_from_params(new_params, block_height)?;
593
594 Ok(vec![cancel_msg, place_msg])
596 }
597
598 pub fn build_cancel_and_replace_with_flags(
606 &self,
607 instrument_id: InstrumentId,
608 old_client_order_id: u32,
609 old_order_flags: u32,
610 new_params: &LimitOrderParams,
611 block_height: u32,
612 ) -> Result<Vec<Any>, DydxError> {
613 let cancel_msg = self.build_cancel_order_with_flags(
615 instrument_id,
616 old_client_order_id,
617 old_order_flags,
618 block_height,
619 )?;
620
621 let place_msg = self.build_limit_order_from_params(new_params, block_height)?;
623
624 Ok(vec![cancel_msg, place_msg])
626 }
627
628 #[expect(clippy::too_many_arguments)]
636 pub fn build_conditional_order(
637 &self,
638 instrument_id: InstrumentId,
639 client_order_id: u32,
640 client_metadata: u32,
641 order_type: ConditionalOrderType,
642 side: OrderSide,
643 trigger_price: Price,
644 limit_price: Option<Price>,
645 quantity: Quantity,
646 time_in_force: Option<TimeInForce>,
647 post_only: bool,
648 reduce_only: bool,
649 expire_time: Option<i64>,
650 ) -> Result<Any, DydxError> {
651 let market_params = self.get_market_params(instrument_id)?;
652
653 let mut builder = OrderBuilder::new(
654 market_params,
655 self.wallet_address.clone(),
656 self.subaccount_number,
657 client_order_id,
658 client_metadata,
659 );
660
661 let proto_side = order_side_to_proto(side);
662 let trigger_decimal = trigger_price.as_decimal();
663 let size_decimal = quantity.as_decimal();
664
665 builder = match order_type {
667 ConditionalOrderType::StopMarket => {
668 builder.stop_market(proto_side, trigger_decimal, size_decimal)
669 }
670 ConditionalOrderType::StopLimit => {
671 let limit = limit_price.ok_or_else(|| {
672 DydxError::Order("StopLimit requires limit_price".to_string())
673 })?;
674 builder.stop_limit(
675 proto_side,
676 limit.as_decimal(),
677 trigger_decimal,
678 size_decimal,
679 )
680 }
681 ConditionalOrderType::TakeProfitMarket => {
682 builder.take_profit_market(proto_side, trigger_decimal, size_decimal)
683 }
684 ConditionalOrderType::TakeProfitLimit => {
685 let limit = limit_price.ok_or_else(|| {
686 DydxError::Order("TakeProfitLimit requires limit_price".to_string())
687 })?;
688 builder.take_profit_limit(
689 proto_side,
690 limit.as_decimal(),
691 trigger_decimal,
692 size_decimal,
693 )
694 }
695 };
696
697 let effective_tif = time_in_force.unwrap_or(TimeInForce::Gtc);
699
700 if matches!(
701 order_type,
702 ConditionalOrderType::StopLimit | ConditionalOrderType::TakeProfitLimit
703 ) {
704 let proto_tif = time_in_force_to_proto_with_post_only(effective_tif, post_only);
705 builder = builder.time_in_force(proto_tif);
706 }
707
708 if reduce_only {
709 builder = builder.reduce_only(true);
710 }
711
712 let expire = calculate_conditional_order_expiration(effective_tif, expire_time)?;
714 builder = builder.until(OrderGoodUntil::Time(expire));
715
716 let order = builder
717 .build()
718 .map_err(|e| DydxError::Order(format!("Failed to build {order_type:?} order: {e}")))?;
719
720 Ok(MsgPlaceOrder { order: Some(order) }.to_any())
721 }
722
723 #[expect(clippy::too_many_arguments)]
729 pub fn build_stop_market_order(
730 &self,
731 instrument_id: InstrumentId,
732 client_order_id: u32,
733 client_metadata: u32,
734 side: OrderSide,
735 trigger_price: Price,
736 quantity: Quantity,
737 reduce_only: bool,
738 expire_time: Option<i64>,
739 ) -> Result<Any, DydxError> {
740 self.build_conditional_order(
741 instrument_id,
742 client_order_id,
743 client_metadata,
744 ConditionalOrderType::StopMarket,
745 side,
746 trigger_price,
747 None,
748 quantity,
749 None,
750 false,
751 reduce_only,
752 expire_time,
753 )
754 }
755
756 #[expect(clippy::too_many_arguments)]
762 pub fn build_stop_limit_order(
763 &self,
764 instrument_id: InstrumentId,
765 client_order_id: u32,
766 client_metadata: u32,
767 side: OrderSide,
768 trigger_price: Price,
769 limit_price: Price,
770 quantity: Quantity,
771 time_in_force: TimeInForce,
772 post_only: bool,
773 reduce_only: bool,
774 expire_time: Option<i64>,
775 ) -> Result<Any, DydxError> {
776 self.build_conditional_order(
777 instrument_id,
778 client_order_id,
779 client_metadata,
780 ConditionalOrderType::StopLimit,
781 side,
782 trigger_price,
783 Some(limit_price),
784 quantity,
785 Some(time_in_force),
786 post_only,
787 reduce_only,
788 expire_time,
789 )
790 }
791
792 #[expect(clippy::too_many_arguments)]
798 pub fn build_take_profit_market_order(
799 &self,
800 instrument_id: InstrumentId,
801 client_order_id: u32,
802 client_metadata: u32,
803 side: OrderSide,
804 trigger_price: Price,
805 quantity: Quantity,
806 reduce_only: bool,
807 expire_time: Option<i64>,
808 ) -> Result<Any, DydxError> {
809 self.build_conditional_order(
810 instrument_id,
811 client_order_id,
812 client_metadata,
813 ConditionalOrderType::TakeProfitMarket,
814 side,
815 trigger_price,
816 None,
817 quantity,
818 None,
819 false,
820 reduce_only,
821 expire_time,
822 )
823 }
824
825 #[expect(clippy::too_many_arguments)]
831 pub fn build_take_profit_limit_order(
832 &self,
833 instrument_id: InstrumentId,
834 client_order_id: u32,
835 client_metadata: u32,
836 side: OrderSide,
837 trigger_price: Price,
838 limit_price: Price,
839 quantity: Quantity,
840 time_in_force: TimeInForce,
841 post_only: bool,
842 reduce_only: bool,
843 expire_time: Option<i64>,
844 ) -> Result<Any, DydxError> {
845 self.build_conditional_order(
846 instrument_id,
847 client_order_id,
848 client_metadata,
849 ConditionalOrderType::TakeProfitLimit,
850 side,
851 trigger_price,
852 Some(limit_price),
853 quantity,
854 Some(time_in_force),
855 post_only,
856 reduce_only,
857 expire_time,
858 )
859 }
860
861 fn get_market_params(
863 &self,
864 instrument_id: InstrumentId,
865 ) -> Result<OrderMarketParams, DydxError> {
866 let market = self
867 .http_client
868 .get_market_params(&instrument_id)
869 .ok_or_else(|| {
870 DydxError::Order(InstrumentLookupError::not_found(instrument_id).to_string())
871 })?;
872
873 Ok(OrderMarketParams {
874 atomic_resolution: market.atomic_resolution,
875 clob_pair_id: market.clob_pair_id,
876 oracle_price: market.oracle_price,
877 quantum_conversion_exponent: market.quantum_conversion_exponent,
878 step_base_quantums: market.step_base_quantums,
879 subticks_per_tick: market.subticks_per_tick,
880 })
881 }
882
883 fn apply_order_lifetime(
885 &self,
886 builder: OrderBuilder,
887 lifetime: OrderLifetime,
888 block_height: u32,
889 expire_time: Option<i64>,
890 ) -> Result<OrderBuilder, DydxError> {
891 match lifetime {
892 OrderLifetime::ShortTerm => {
893 let blocks_offset = self.calculate_block_offset(expire_time);
894 Ok(builder
895 .short_term()
896 .until(OrderGoodUntil::Block(block_height + blocks_offset)))
897 }
898 OrderLifetime::LongTerm => {
899 let expire_dt = self.calculate_expire_datetime(expire_time)?;
900 Ok(builder.long_term().until(OrderGoodUntil::Time(expire_dt)))
901 }
902 OrderLifetime::Conditional => {
903 Err(DydxError::Order(
905 "Use build_conditional_order for conditional orders".to_string(),
906 ))
907 }
908 }
909 }
910
911 fn calculate_block_offset(&self, expire_time: Option<i64>) -> u32 {
916 if let Some(expire_ts) = expire_time {
917 let now = Timestamp::now().as_second();
918 let seconds = expire_ts - now;
919 self.seconds_to_blocks(seconds)
920 } else {
921 SHORT_TERM_ORDER_MAXIMUM_LIFETIME
922 }
923 }
924
925 fn seconds_to_blocks(&self, seconds: i64) -> u32 {
930 if seconds <= 0 {
931 return 1; }
933
934 let secs_per_block = self.block_time_monitor.seconds_per_block_or_default();
935 let blocks = (seconds as f64 / secs_per_block).ceil() as u32;
936
937 blocks.clamp(1, SHORT_TERM_ORDER_MAXIMUM_LIFETIME)
938 }
939
940 fn calculate_expire_datetime(&self, expire_time: Option<i64>) -> Result<Timestamp, DydxError> {
942 if let Some(expire_ts) = expire_time {
943 Timestamp::from_second(expire_ts)
944 .map_err(|_| DydxError::Parse(format!("Invalid expire timestamp: {expire_ts}")))
945 } else {
946 Ok(Timestamp::now()
947 + SignedDuration::from_hours(24 * GTC_CONDITIONAL_ORDER_EXPIRATION_DAYS))
948 }
949 }
950}
951
952#[cfg(test)]
953mod tests {
954 use cosmrs::proto::traits::Message;
955 use nautilus_core::UnixNanos;
956 use nautilus_model::instruments::Instrument;
957 use rstest::rstest;
958
959 use super::*;
960 use crate::{
961 common::testing::load_json_result_fixture,
962 http::{models::MarketsResponse, parse::parse_instrument_any},
963 proto::OrderTimeInForce,
964 };
965
966 const TEST_MAX_SHORT_TERM_SECS: f64 = 10.0;
968
969 fn test_order_builder_with_market() -> (OrderMessageBuilder, InstrumentId) {
970 let json = load_json_result_fixture("http_get_perpetual_markets.json");
971 let mut response: MarketsResponse =
972 serde_json::from_value(json).expect("failed to parse markets fixture");
973 let market = response
974 .markets
975 .remove("BTC-USD")
976 .expect("BTC-USD market missing from fixture");
977 let instrument = parse_instrument_any(&market, None, None, UnixNanos::default())
978 .expect("failed to parse BTC-USD instrument");
979 let instrument_id = instrument.id();
980 let http_client = DydxHttpClient::default();
981 http_client.instrument_cache.insert(instrument, market);
982
983 (
984 OrderMessageBuilder::new(
985 http_client,
986 "dydx1testwalletaddress".to_string(),
987 0,
988 Arc::new(BlockTimeMonitor::new()),
989 ),
990 instrument_id,
991 )
992 }
993
994 #[rstest]
995 #[case::reduce_only(true)]
996 #[case::not_reduce_only(false)]
997 fn test_build_market_order_encodes_reduce_only(#[case] reduce_only: bool) {
998 let (builder, instrument_id) = test_order_builder_with_market();
999
1000 let message = builder
1001 .build_market_order_with_reduce_only(
1002 instrument_id,
1003 42,
1004 7,
1005 OrderSide::Sell,
1006 Quantity::from("0.001"),
1007 reduce_only,
1008 100,
1009 )
1010 .expect("failed to build market order");
1011 let message = MsgPlaceOrder::decode(message.value.as_slice())
1012 .expect("failed to decode MsgPlaceOrder");
1013 let order = message.order.expect("MsgPlaceOrder missing order");
1014
1015 assert_eq!(order.reduce_only, reduce_only);
1016 assert_eq!(order.time_in_force, OrderTimeInForce::Ioc as i32);
1017 }
1018
1019 #[rstest]
1020 fn test_get_market_params_missing_cache_returns_canonical_error() {
1021 let builder = OrderMessageBuilder::new(
1022 DydxHttpClient::default(),
1023 "dydx1testwalletaddress".to_string(),
1024 0,
1025 Arc::new(BlockTimeMonitor::new()),
1026 );
1027 let instrument_id = InstrumentId::from("BTC-USD.DYDX");
1028
1029 let result = builder.get_market_params(instrument_id);
1030
1031 match result {
1032 Err(DydxError::Order(reason)) => {
1033 assert_eq!(
1034 reason,
1035 InstrumentLookupError::not_found(instrument_id).to_string()
1036 );
1037 }
1038 other => panic!("Expected DydxError::Order, was {other:?}"),
1039 }
1040 }
1041
1042 #[rstest]
1043 fn test_order_lifetime_routing() {
1044 let lifetime = OrderLifetime::from_time_in_force(
1046 TimeInForce::Ioc,
1047 None,
1048 false,
1049 TEST_MAX_SHORT_TERM_SECS,
1050 );
1051 assert!(lifetime.is_short_term());
1052
1053 let lifetime = OrderLifetime::from_time_in_force(
1055 TimeInForce::Gtc,
1056 None,
1057 false,
1058 TEST_MAX_SHORT_TERM_SECS,
1059 );
1060 assert!(!lifetime.is_short_term());
1061
1062 let lifetime = OrderLifetime::from_time_in_force(
1064 TimeInForce::Gtc,
1065 None,
1066 true,
1067 TEST_MAX_SHORT_TERM_SECS,
1068 );
1069 assert!(lifetime.is_conditional());
1070 }
1071
1072 #[rstest]
1073 fn test_order_lifetime_with_short_expiry() {
1074 let expire_time = Some(Timestamp::now().as_second() + 5);
1076 let lifetime = OrderLifetime::from_time_in_force(
1077 TimeInForce::Gtd,
1078 expire_time,
1079 false,
1080 TEST_MAX_SHORT_TERM_SECS,
1081 );
1082 assert!(lifetime.is_short_term());
1083 }
1084
1085 #[rstest]
1086 fn test_order_lifetime_with_long_expiry() {
1087 let expire_time = Some(Timestamp::now().as_second() + 60);
1089 let lifetime = OrderLifetime::from_time_in_force(
1090 TimeInForce::Gtd,
1091 expire_time,
1092 false,
1093 TEST_MAX_SHORT_TERM_SECS,
1094 );
1095 assert!(!lifetime.is_short_term());
1096 }
1097}