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