Skip to main content

nautilus_dydx/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Enumerations mapping dYdX v4 concepts onto idiomatic Nautilus variants.
17
18use nautilus_model::{
19    data::BarSpecification,
20    enums::{BarAggregation, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide},
21};
22use serde::{Deserialize, Serialize};
23use strum::{AsRefStr, Display, EnumIter, EnumString, IntoStaticStr};
24
25use crate::{error::DydxError, grpc::types::ChainId};
26
27/// dYdX order status throughout its lifecycle.
28#[derive(
29    Copy,
30    Clone,
31    Debug,
32    Display,
33    PartialEq,
34    Eq,
35    Hash,
36    AsRefStr,
37    EnumIter,
38    EnumString,
39    Serialize,
40    Deserialize,
41)]
42#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
43pub enum DydxOrderStatus {
44    /// Order is open and active.
45    Open,
46    /// Order is filled completely.
47    Filled,
48    /// Order is canceled.
49    Canceled,
50    /// Order is best effort canceled (short-term orders).
51    BestEffortCanceled,
52    /// Order is partially filled.
53    PartiallyFilled,
54    /// Order is best effort opened (pending confirmation).
55    BestEffortOpened,
56    /// Order is untriggered (conditional orders).
57    Untriggered,
58}
59
60impl From<DydxOrderStatus> for OrderStatus {
61    fn from(value: DydxOrderStatus) -> Self {
62        match value {
63            DydxOrderStatus::Open | DydxOrderStatus::BestEffortOpened => Self::Accepted,
64            DydxOrderStatus::PartiallyFilled => Self::PartiallyFilled,
65            DydxOrderStatus::Filled => Self::Filled,
66            DydxOrderStatus::Canceled | DydxOrderStatus::BestEffortCanceled => Self::Canceled,
67            DydxOrderStatus::Untriggered => Self::PendingUpdate,
68        }
69    }
70}
71
72/// dYdX time-in-force specifications.
73#[derive(
74    Copy,
75    Clone,
76    Debug,
77    Display,
78    PartialEq,
79    Eq,
80    Hash,
81    AsRefStr,
82    EnumIter,
83    EnumString,
84    Serialize,
85    Deserialize,
86)]
87#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
88pub enum DydxTimeInForce {
89    /// Good-Til-Time (GTT) - order expires at specified time.
90    Gtt,
91    /// Fill-Or-Kill (FOK) - must fill completely immediately or cancel.
92    Fok,
93    /// Immediate-Or-Cancel (IOC) - fill immediately, cancel remainder.
94    Ioc,
95}
96
97/// dYdX order side.
98#[derive(
99    Copy,
100    Clone,
101    Debug,
102    Display,
103    PartialEq,
104    Eq,
105    Hash,
106    AsRefStr,
107    EnumIter,
108    EnumString,
109    Serialize,
110    Deserialize,
111)]
112#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
113#[cfg_attr(
114    feature = "python",
115    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", eq, eq_int)
116)]
117pub enum DydxOrderSide {
118    /// Buy order.
119    Buy,
120    /// Sell order.
121    Sell,
122}
123
124impl TryFrom<OrderSide> for DydxOrderSide {
125    type Error = DydxError;
126
127    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
128        match value {
129            OrderSide::Buy => Ok(Self::Buy),
130            OrderSide::Sell => Ok(Self::Sell),
131            _ => Err(DydxError::InvalidOrderSide(format!("{value:?}"))),
132        }
133    }
134}
135
136impl DydxOrderSide {
137    /// Try to convert from Nautilus `OrderSide`.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the order side is not `Buy` or `Sell`.
142    pub fn try_from_order_side(value: OrderSide) -> anyhow::Result<Self> {
143        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
144    }
145}
146
147impl From<DydxOrderSide> for OrderSide {
148    fn from(side: DydxOrderSide) -> Self {
149        match side {
150            DydxOrderSide::Buy => Self::Buy,
151            DydxOrderSide::Sell => Self::Sell,
152        }
153    }
154}
155
156/// dYdX order type.
157#[derive(
158    Copy,
159    Clone,
160    Debug,
161    Display,
162    PartialEq,
163    Eq,
164    Hash,
165    AsRefStr,
166    EnumIter,
167    EnumString,
168    Serialize,
169    Deserialize,
170)]
171#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
172#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
173#[cfg_attr(
174    feature = "python",
175    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", eq, eq_int)
176)]
177pub enum DydxOrderType {
178    /// Limit order with specified price.
179    Limit,
180    /// Market order (executed at best available price).
181    Market,
182    /// Stop-limit order (triggered at stop price, executed as limit).
183    StopLimit,
184    /// Stop-market order (triggered at stop price, executed as market).
185    StopMarket,
186    /// Take-profit order (limit).
187    TakeProfitLimit,
188    /// Take-profit order (market).
189    TakeProfitMarket,
190    /// Trailing stop order.
191    TrailingStop,
192}
193
194impl TryFrom<OrderType> for DydxOrderType {
195    type Error = DydxError;
196
197    fn try_from(value: OrderType) -> Result<Self, Self::Error> {
198        match value {
199            OrderType::Market => Ok(Self::Market),
200            OrderType::Limit => Ok(Self::Limit),
201            OrderType::StopMarket => Ok(Self::StopMarket),
202            OrderType::StopLimit => Ok(Self::StopLimit),
203            OrderType::MarketIfTouched => Ok(Self::TakeProfitMarket),
204            OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
205            OrderType::TrailingStopMarket | OrderType::TrailingStopLimit => Ok(Self::TrailingStop),
206            OrderType::MarketToLimit => Err(DydxError::UnsupportedOrderType(format!("{value:?}"))),
207        }
208    }
209}
210
211impl DydxOrderType {
212    /// Try to convert from Nautilus `OrderType`.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the order type is not supported by dYdX.
217    pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
218        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
219    }
220
221    /// Returns true if this is a conditional order type.
222    #[must_use]
223    pub const fn is_conditional(&self) -> bool {
224        matches!(
225            self,
226            Self::StopLimit
227                | Self::StopMarket
228                | Self::TakeProfitLimit
229                | Self::TakeProfitMarket
230                | Self::TrailingStop
231        )
232    }
233
234    /// Returns the condition type for this order type.
235    #[must_use]
236    pub const fn condition_type(&self) -> DydxConditionType {
237        match self {
238            Self::StopLimit | Self::StopMarket => DydxConditionType::StopLoss,
239            Self::TakeProfitLimit | Self::TakeProfitMarket => DydxConditionType::TakeProfit,
240            _ => DydxConditionType::Unspecified,
241        }
242    }
243
244    /// Returns true if this order type should execute as market.
245    #[must_use]
246    pub const fn is_market_execution(&self) -> bool {
247        matches!(
248            self,
249            Self::Market | Self::StopMarket | Self::TakeProfitMarket
250        )
251    }
252}
253
254impl From<DydxOrderType> for OrderType {
255    fn from(value: DydxOrderType) -> Self {
256        match value {
257            DydxOrderType::Market => Self::Market,
258            DydxOrderType::Limit => Self::Limit,
259            DydxOrderType::StopMarket => Self::StopMarket,
260            DydxOrderType::StopLimit => Self::StopLimit,
261            DydxOrderType::TakeProfitMarket => Self::MarketIfTouched,
262            DydxOrderType::TakeProfitLimit => Self::LimitIfTouched,
263            DydxOrderType::TrailingStop => Self::TrailingStopMarket,
264        }
265    }
266}
267
268/// dYdX order execution type.
269#[derive(
270    Copy,
271    Clone,
272    Debug,
273    Display,
274    PartialEq,
275    Eq,
276    Hash,
277    AsRefStr,
278    EnumIter,
279    EnumString,
280    Serialize,
281    Deserialize,
282)]
283#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
284pub enum DydxOrderExecution {
285    /// Default execution behavior.
286    Default,
287    /// Immediate-Or-Cancel execution.
288    Ioc,
289    /// Fill-Or-Kill execution.
290    Fok,
291    /// Post-only execution (maker-only).
292    PostOnly,
293}
294
295/// dYdX order flags (bitfield).
296#[derive(
297    Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumIter, Serialize, Deserialize,
298)]
299pub enum DydxOrderFlags {
300    /// Short-term order (0).
301    ShortTerm = 0,
302    /// Conditional order (32).
303    Conditional = 32,
304    /// Long-term order (64).
305    LongTerm = 64,
306}
307
308/// dYdX condition type for conditional orders.
309///
310/// Determines whether the order is a stop-loss (triggers when price
311/// falls below/rises above trigger for sell/buy) or take-profit
312/// (triggers in opposite direction).
313#[derive(
314    Copy,
315    Clone,
316    Debug,
317    Display,
318    PartialEq,
319    Eq,
320    Hash,
321    AsRefStr,
322    EnumIter,
323    EnumString,
324    Serialize,
325    Deserialize,
326)]
327#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
328pub enum DydxConditionType {
329    /// No condition (standard order).
330    Unspecified,
331    /// Stop-loss conditional order.
332    StopLoss,
333    /// Take-profit conditional order.
334    TakeProfit,
335}
336
337/// dYdX asset position side (spot/margin balance).
338#[derive(
339    Copy,
340    Clone,
341    Debug,
342    Display,
343    PartialEq,
344    Eq,
345    Hash,
346    AsRefStr,
347    EnumIter,
348    EnumString,
349    Serialize,
350    Deserialize,
351)]
352#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
353pub enum DydxPositionSide {
354    /// Long (positive balance).
355    Long,
356    /// Short (negative balance / borrowed).
357    Short,
358}
359
360impl From<DydxPositionSide> for PositionSide {
361    fn from(value: DydxPositionSide) -> Self {
362        match value {
363            DydxPositionSide::Long => Self::Long,
364            DydxPositionSide::Short => Self::Short,
365        }
366    }
367}
368
369/// dYdX position status.
370#[derive(
371    Copy,
372    Clone,
373    Debug,
374    Display,
375    PartialEq,
376    Eq,
377    Hash,
378    AsRefStr,
379    EnumIter,
380    EnumString,
381    Serialize,
382    Deserialize,
383)]
384#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
385pub enum DydxPositionStatus {
386    /// Position is open.
387    Open,
388    /// Position is closed.
389    Closed,
390    /// Position was liquidated.
391    Liquidated,
392}
393
394impl From<DydxPositionStatus> for PositionSide {
395    fn from(value: DydxPositionStatus) -> Self {
396        match value {
397            DydxPositionStatus::Open => Self::Long, // Default, actual side from position size
398            DydxPositionStatus::Closed => Self::Flat,
399            DydxPositionStatus::Liquidated => Self::Flat,
400        }
401    }
402}
403
404/// dYdX perpetual market status.
405#[derive(
406    Copy,
407    Clone,
408    Debug,
409    Display,
410    PartialEq,
411    Eq,
412    Hash,
413    AsRefStr,
414    EnumIter,
415    EnumString,
416    Serialize,
417    Deserialize,
418)]
419#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
420pub enum DydxMarketStatus {
421    /// Market is active and trading.
422    Active,
423    /// Market is paused (no trading).
424    Paused,
425    /// Cancel-only mode (no new orders).
426    CancelOnly,
427    /// Post-only mode (only maker orders).
428    PostOnly,
429    /// Market is initializing.
430    Initializing,
431    /// Market is in final settlement.
432    FinalSettlement,
433}
434
435/// dYdX fill type.
436#[derive(
437    Copy,
438    Clone,
439    Debug,
440    Display,
441    PartialEq,
442    Eq,
443    Hash,
444    AsRefStr,
445    EnumIter,
446    EnumString,
447    Serialize,
448    Deserialize,
449)]
450#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
451pub enum DydxFillType {
452    /// Normal limit order fill.
453    Limit,
454    /// Liquidation (taker side).
455    Liquidated,
456    /// Liquidation (maker side).
457    Liquidation,
458    /// Deleveraging (deleveraged account).
459    Deleveraged,
460    /// Deleveraging (offsetting account).
461    Offsetting,
462}
463
464/// dYdX liquidity side (maker/taker).
465#[derive(
466    Copy,
467    Clone,
468    Debug,
469    Display,
470    PartialEq,
471    Eq,
472    Hash,
473    AsRefStr,
474    EnumIter,
475    EnumString,
476    Serialize,
477    Deserialize,
478)]
479#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
480pub enum DydxLiquidity {
481    /// Maker (provides liquidity).
482    Maker,
483    /// Taker (removes liquidity).
484    Taker,
485}
486
487impl From<DydxLiquidity> for LiquiditySide {
488    fn from(value: DydxLiquidity) -> Self {
489        match value {
490            DydxLiquidity::Maker => Self::Maker,
491            DydxLiquidity::Taker => Self::Taker,
492        }
493    }
494}
495
496impl From<LiquiditySide> for DydxLiquidity {
497    fn from(value: LiquiditySide) -> Self {
498        match value {
499            LiquiditySide::Maker => Self::Maker,
500            LiquiditySide::Taker => Self::Taker,
501            LiquiditySide::NoLiquiditySide => Self::Taker, // Default fallback
502        }
503    }
504}
505
506/// dYdX ticker type for market data.
507#[derive(
508    Copy,
509    Clone,
510    Debug,
511    Display,
512    PartialEq,
513    Eq,
514    Hash,
515    AsRefStr,
516    EnumIter,
517    EnumString,
518    Serialize,
519    Deserialize,
520)]
521#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
522pub enum DydxTickerType {
523    /// Perpetual market ticker.
524    Perpetual,
525}
526
527/// dYdX trade type.
528///
529/// Represents the type of trade execution on dYdX.
530#[derive(
531    Copy,
532    Clone,
533    Debug,
534    Display,
535    PartialEq,
536    Eq,
537    Hash,
538    AsRefStr,
539    EnumIter,
540    EnumString,
541    Serialize,
542    Deserialize,
543)]
544#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
545pub enum DydxTradeType {
546    /// Standard limit order.
547    Limit,
548    /// Market order.
549    Market,
550    /// Liquidation trade.
551    Liquidated,
552    /// Sub-order from a TWAP execution.
553    TwapSuborder,
554    /// Stop limit order.
555    StopLimit,
556    /// Take profit limit order.
557    TakeProfitLimit,
558}
559
560/// dYdX transfer types.
561#[derive(
562    Copy,
563    Clone,
564    Debug,
565    Display,
566    PartialEq,
567    Eq,
568    Hash,
569    AsRefStr,
570    EnumIter,
571    EnumString,
572    Serialize,
573    Deserialize,
574)]
575#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
576#[cfg_attr(
577    feature = "python",
578    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", eq, eq_int)
579)]
580pub enum DydxTransferType {
581    /// Transfer into the account.
582    TransferIn,
583    /// Transfer out of the account.
584    TransferOut,
585    /// Deposit from external wallet.
586    Deposit,
587    /// Withdrawal to external wallet.
588    Withdrawal,
589}
590
591/// dYdX candlestick resolution.
592#[derive(
593    Copy,
594    Clone,
595    Debug,
596    Display,
597    PartialEq,
598    Eq,
599    Hash,
600    AsRefStr,
601    IntoStaticStr,
602    EnumIter,
603    EnumString,
604    Serialize,
605    Deserialize,
606)]
607#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
608#[derive(Default)]
609#[cfg_attr(
610    feature = "python",
611    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", eq, eq_int)
612)]
613pub enum DydxCandleResolution {
614    /// 1 minute candles.
615    #[serde(rename = "1MIN")]
616    #[strum(serialize = "1MIN")]
617    #[default]
618    OneMinute,
619    /// 5 minute candles.
620    #[serde(rename = "5MINS")]
621    #[strum(serialize = "5MINS")]
622    FiveMinutes,
623    /// 15 minute candles.
624    #[serde(rename = "15MINS")]
625    #[strum(serialize = "15MINS")]
626    FifteenMinutes,
627    /// 30 minute candles.
628    #[serde(rename = "30MINS")]
629    #[strum(serialize = "30MINS")]
630    ThirtyMinutes,
631    /// 1 hour candles.
632    #[serde(rename = "1HOUR")]
633    #[strum(serialize = "1HOUR")]
634    OneHour,
635    /// 4 hour candles.
636    #[serde(rename = "4HOURS")]
637    #[strum(serialize = "4HOURS")]
638    FourHours,
639    /// 1 day candles.
640    #[serde(rename = "1DAY")]
641    #[strum(serialize = "1DAY")]
642    OneDay,
643}
644
645impl DydxCandleResolution {
646    /// Maps a Nautilus [`BarSpecification`] to a dYdX candle resolution.
647    ///
648    /// # Errors
649    ///
650    /// Returns an error if the step/aggregation combination is not supported.
651    pub fn from_bar_spec(spec: &BarSpecification) -> anyhow::Result<Self> {
652        match spec.step.get() {
653            1 => match spec.aggregation {
654                BarAggregation::Minute => Ok(Self::OneMinute),
655                BarAggregation::Hour => Ok(Self::OneHour),
656                BarAggregation::Day => Ok(Self::OneDay),
657                _ => anyhow::bail!("Unsupported bar aggregation: {:?}", spec.aggregation),
658            },
659            5 if spec.aggregation == BarAggregation::Minute => Ok(Self::FiveMinutes),
660            15 if spec.aggregation == BarAggregation::Minute => Ok(Self::FifteenMinutes),
661            30 if spec.aggregation == BarAggregation::Minute => Ok(Self::ThirtyMinutes),
662            4 if spec.aggregation == BarAggregation::Hour => Ok(Self::FourHours),
663            step => anyhow::bail!(
664                "Unsupported bar step: {step} with aggregation {:?}",
665                spec.aggregation
666            ),
667        }
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use rstest::rstest;
674
675    use super::*;
676
677    #[rstest]
678    fn test_order_status_conversion() {
679        assert_eq!(
680            OrderStatus::from(DydxOrderStatus::Open),
681            OrderStatus::Accepted
682        );
683        assert_eq!(
684            OrderStatus::from(DydxOrderStatus::Filled),
685            OrderStatus::Filled
686        );
687        assert_eq!(
688            OrderStatus::from(DydxOrderStatus::Canceled),
689            OrderStatus::Canceled
690        );
691    }
692
693    #[rstest]
694    fn test_liquidity_conversion() {
695        assert_eq!(
696            LiquiditySide::from(DydxLiquidity::Maker),
697            LiquiditySide::Maker
698        );
699        assert_eq!(
700            LiquiditySide::from(DydxLiquidity::Taker),
701            LiquiditySide::Taker
702        );
703    }
704
705    #[rstest]
706    fn test_order_type_is_conditional() {
707        assert!(DydxOrderType::StopLimit.is_conditional());
708        assert!(DydxOrderType::StopMarket.is_conditional());
709        assert!(DydxOrderType::TakeProfitLimit.is_conditional());
710        assert!(DydxOrderType::TakeProfitMarket.is_conditional());
711        assert!(DydxOrderType::TrailingStop.is_conditional());
712        assert!(!DydxOrderType::Limit.is_conditional());
713        assert!(!DydxOrderType::Market.is_conditional());
714    }
715
716    #[rstest]
717    fn test_condition_type_mapping() {
718        assert_eq!(
719            DydxOrderType::StopLimit.condition_type(),
720            DydxConditionType::StopLoss
721        );
722        assert_eq!(
723            DydxOrderType::StopMarket.condition_type(),
724            DydxConditionType::StopLoss
725        );
726        assert_eq!(
727            DydxOrderType::TakeProfitLimit.condition_type(),
728            DydxConditionType::TakeProfit
729        );
730        assert_eq!(
731            DydxOrderType::TakeProfitMarket.condition_type(),
732            DydxConditionType::TakeProfit
733        );
734        assert_eq!(
735            DydxOrderType::Limit.condition_type(),
736            DydxConditionType::Unspecified
737        );
738    }
739
740    #[rstest]
741    fn test_is_market_execution() {
742        assert!(DydxOrderType::Market.is_market_execution());
743        assert!(DydxOrderType::StopMarket.is_market_execution());
744        assert!(DydxOrderType::TakeProfitMarket.is_market_execution());
745        assert!(!DydxOrderType::Limit.is_market_execution());
746        assert!(!DydxOrderType::StopLimit.is_market_execution());
747        assert!(!DydxOrderType::TakeProfitLimit.is_market_execution());
748    }
749
750    #[rstest]
751    fn test_order_type_to_nautilus() {
752        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
753        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
754        assert_eq!(
755            OrderType::from(DydxOrderType::StopMarket),
756            OrderType::StopMarket
757        );
758        assert_eq!(
759            OrderType::from(DydxOrderType::StopLimit),
760            OrderType::StopLimit
761        );
762    }
763
764    #[rstest]
765    fn test_order_side_conversion_from_nautilus() {
766        assert_eq!(
767            DydxOrderSide::try_from(OrderSide::Buy).unwrap(),
768            DydxOrderSide::Buy
769        );
770        assert_eq!(
771            DydxOrderSide::try_from(OrderSide::Sell).unwrap(),
772            DydxOrderSide::Sell
773        );
774        assert!(DydxOrderSide::try_from(OrderSide::NoOrderSide).is_err());
775    }
776
777    #[rstest]
778    fn test_order_side_conversion_to_nautilus() {
779        assert_eq!(OrderSide::from(DydxOrderSide::Buy), OrderSide::Buy);
780        assert_eq!(OrderSide::from(DydxOrderSide::Sell), OrderSide::Sell);
781    }
782
783    #[rstest]
784    fn test_order_type_conversion_from_nautilus() {
785        assert_eq!(
786            DydxOrderType::try_from(OrderType::Market).unwrap(),
787            DydxOrderType::Market
788        );
789        assert_eq!(
790            DydxOrderType::try_from(OrderType::Limit).unwrap(),
791            DydxOrderType::Limit
792        );
793        assert_eq!(
794            DydxOrderType::try_from(OrderType::StopMarket).unwrap(),
795            DydxOrderType::StopMarket
796        );
797        assert_eq!(
798            DydxOrderType::try_from(OrderType::StopLimit).unwrap(),
799            DydxOrderType::StopLimit
800        );
801        assert!(DydxOrderType::try_from(OrderType::MarketToLimit).is_err());
802    }
803
804    #[rstest]
805    fn test_order_type_conversion_to_nautilus() {
806        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
807        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
808        assert_eq!(
809            OrderType::from(DydxOrderType::StopMarket),
810            OrderType::StopMarket
811        );
812        assert_eq!(
813            OrderType::from(DydxOrderType::StopLimit),
814            OrderType::StopLimit
815        );
816    }
817
818    #[rstest]
819    fn test_dydx_network_chain_id_mapping() {
820        // Test canonical chain ID mapping
821        assert_eq!(DydxNetwork::Mainnet.chain_id(), ChainId::Mainnet1);
822        assert_eq!(DydxNetwork::Testnet.chain_id(), ChainId::Testnet4);
823    }
824
825    #[rstest]
826    fn test_dydx_network_as_str() {
827        // Test string representation for config/env
828        assert_eq!(DydxNetwork::Mainnet.as_str(), "mainnet");
829        assert_eq!(DydxNetwork::Testnet.as_str(), "testnet");
830    }
831
832    #[rstest]
833    fn test_dydx_network_default() {
834        // Test default is mainnet
835        assert_eq!(DydxNetwork::default(), DydxNetwork::Mainnet);
836    }
837
838    #[rstest]
839    fn test_dydx_network_serde_lowercase() {
840        // Test lowercase serialization/deserialization
841        let mainnet = DydxNetwork::Mainnet;
842        let json = serde_json::to_string(&mainnet).unwrap();
843        assert_eq!(json, "\"mainnet\"");
844
845        let deserialized: DydxNetwork = serde_json::from_str("\"mainnet\"").unwrap();
846        assert_eq!(deserialized, DydxNetwork::Mainnet);
847
848        let testnet = DydxNetwork::Testnet;
849        let json = serde_json::to_string(&testnet).unwrap();
850        assert_eq!(json, "\"testnet\"");
851
852        let deserialized: DydxNetwork = serde_json::from_str("\"testnet\"").unwrap();
853        assert_eq!(deserialized, DydxNetwork::Testnet);
854    }
855}
856
857/// dYdX network environment (mainnet vs testnet).
858///
859/// This selects the underlying Cosmos chain for transaction submission.
860#[derive(
861    Copy,
862    Clone,
863    Debug,
864    Default,
865    Display,
866    PartialEq,
867    Eq,
868    Hash,
869    AsRefStr,
870    EnumString,
871    Serialize,
872    Deserialize,
873)]
874#[strum(serialize_all = "lowercase")]
875#[serde(rename_all = "lowercase")]
876#[cfg_attr(
877    feature = "python",
878    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx")
879)]
880pub enum DydxNetwork {
881    /// dYdX mainnet (dydx-mainnet-1)
882    #[default]
883    Mainnet,
884    /// dYdX testnet (dydx-testnet-4)
885    Testnet,
886}
887
888impl DydxNetwork {
889    /// Map the logical network to the underlying gRPC chain identifier.
890    #[must_use]
891    pub const fn chain_id(self) -> ChainId {
892        match self {
893            Self::Mainnet => ChainId::Mainnet1,
894            Self::Testnet => ChainId::Testnet4,
895        }
896    }
897
898    /// Return the canonical lowercase string used in config/env.
899    #[must_use]
900    pub const fn as_str(self) -> &'static str {
901        match self {
902            Self::Mainnet => "mainnet",
903            Self::Testnet => "testnet",
904        }
905    }
906}