nautilus_dydx/common/
enums.rs

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