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.adapters.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.adapters.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). The dYdX Indexer reports this as `TAKE_PROFIT`.
208    #[serde(rename = "TAKE_PROFIT", alias = "TAKE_PROFIT_LIMIT")]
209    #[strum(serialize = "TAKE_PROFIT", serialize = "TAKE_PROFIT_LIMIT")]
210    TakeProfitLimit,
211    /// Take-profit order (market).
212    TakeProfitMarket,
213    /// Trailing stop order (parsing only, not supported for submission).
214    TrailingStop,
215}
216
217impl TryFrom<OrderType> for DydxOrderType {
218    type Error = DydxError;
219
220    fn try_from(value: OrderType) -> Result<Self, Self::Error> {
221        match value {
222            OrderType::Market => Ok(Self::Market),
223            OrderType::Limit => Ok(Self::Limit),
224            OrderType::StopMarket => Ok(Self::StopMarket),
225            OrderType::StopLimit => Ok(Self::StopLimit),
226            OrderType::MarketIfTouched => Ok(Self::TakeProfitMarket),
227            OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
228            OrderType::TrailingStopMarket | OrderType::TrailingStopLimit => Ok(Self::TrailingStop),
229            OrderType::MarketToLimit => Err(DydxError::UnsupportedOrderType(format!("{value:?}"))),
230        }
231    }
232}
233
234impl DydxOrderType {
235    /// Tries to convert from Nautilus `OrderType`.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if the order type is not supported by dYdX.
240    pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
241        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
242    }
243
244    /// Returns true if this is a conditional order type.
245    #[must_use]
246    pub const fn is_conditional(&self) -> bool {
247        matches!(
248            self,
249            Self::StopLimit
250                | Self::StopMarket
251                | Self::TakeProfitLimit
252                | Self::TakeProfitMarket
253                | Self::TrailingStop
254        )
255    }
256
257    /// Returns the condition type for this order type.
258    #[must_use]
259    pub const fn condition_type(&self) -> DydxConditionType {
260        match self {
261            Self::StopLimit | Self::StopMarket => DydxConditionType::StopLoss,
262            Self::TakeProfitLimit | Self::TakeProfitMarket => DydxConditionType::TakeProfit,
263            _ => DydxConditionType::Unspecified,
264        }
265    }
266
267    /// Returns true if this order type should execute as market.
268    #[must_use]
269    pub const fn is_market_execution(&self) -> bool {
270        matches!(
271            self,
272            Self::Market | Self::StopMarket | Self::TakeProfitMarket
273        )
274    }
275}
276
277impl From<DydxOrderType> for OrderType {
278    fn from(value: DydxOrderType) -> Self {
279        match value {
280            DydxOrderType::Market => Self::Market,
281            DydxOrderType::Limit => Self::Limit,
282            DydxOrderType::StopMarket => Self::StopMarket,
283            DydxOrderType::StopLimit => Self::StopLimit,
284            DydxOrderType::TakeProfitMarket => Self::MarketIfTouched,
285            DydxOrderType::TakeProfitLimit => Self::LimitIfTouched,
286            DydxOrderType::TrailingStop => Self::TrailingStopMarket,
287        }
288    }
289}
290
291/// dYdX order execution type.
292#[derive(
293    Copy,
294    Clone,
295    Debug,
296    Display,
297    PartialEq,
298    Eq,
299    Hash,
300    AsRefStr,
301    EnumIter,
302    EnumString,
303    Serialize,
304    Deserialize,
305)]
306#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
307pub enum DydxOrderExecution {
308    /// Default execution behavior.
309    Default,
310    /// Immediate-Or-Cancel execution.
311    Ioc,
312    /// Fill-Or-Kill execution.
313    Fok,
314    /// Post-only execution (maker-only).
315    PostOnly,
316}
317
318/// dYdX order flags (bitfield).
319#[derive(
320    Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumIter, Serialize, Deserialize,
321)]
322pub enum DydxOrderFlags {
323    /// Short-term order (0).
324    ShortTerm = 0,
325    /// Conditional order (32).
326    Conditional = 32,
327    /// Long-term order (64).
328    LongTerm = 64,
329}
330
331/// dYdX condition type for conditional orders.
332///
333/// Determines whether the order is a stop-loss (triggers when price
334/// falls below/rises above trigger for sell/buy) or take-profit
335/// (triggers in opposite direction).
336#[derive(
337    Copy,
338    Clone,
339    Debug,
340    Display,
341    PartialEq,
342    Eq,
343    Hash,
344    AsRefStr,
345    EnumIter,
346    EnumString,
347    Serialize,
348    Deserialize,
349)]
350#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
351pub enum DydxConditionType {
352    /// No condition (standard order).
353    Unspecified,
354    /// Stop-loss conditional order.
355    StopLoss,
356    /// Take-profit conditional order.
357    TakeProfit,
358}
359
360/// dYdX asset position side (spot/margin balance).
361#[derive(
362    Copy,
363    Clone,
364    Debug,
365    Display,
366    PartialEq,
367    Eq,
368    Hash,
369    AsRefStr,
370    EnumIter,
371    EnumString,
372    Serialize,
373    Deserialize,
374)]
375#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
376pub enum DydxPositionSide {
377    /// Long (positive balance).
378    Long,
379    /// Short (negative balance / borrowed).
380    Short,
381}
382
383impl From<DydxPositionSide> for PositionSide {
384    fn from(value: DydxPositionSide) -> Self {
385        match value {
386            DydxPositionSide::Long => Self::Long,
387            DydxPositionSide::Short => Self::Short,
388        }
389    }
390}
391
392/// dYdX position status.
393#[derive(
394    Copy,
395    Clone,
396    Debug,
397    Display,
398    PartialEq,
399    Eq,
400    Hash,
401    AsRefStr,
402    EnumIter,
403    EnumString,
404    Serialize,
405    Deserialize,
406)]
407#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
408pub enum DydxPositionStatus {
409    /// Position is open.
410    Open,
411    /// Position is closed.
412    Closed,
413    /// Position was liquidated.
414    Liquidated,
415}
416
417impl DydxPositionStatus {
418    /// Returns whether this status represents a closed position.
419    #[must_use]
420    pub const fn is_closed(&self) -> bool {
421        matches!(self, Self::Closed | Self::Liquidated)
422    }
423}
424
425/// dYdX perpetual market status.
426#[derive(
427    Copy,
428    Clone,
429    Debug,
430    Display,
431    PartialEq,
432    Eq,
433    Hash,
434    AsRefStr,
435    EnumIter,
436    EnumString,
437    Serialize,
438    Deserialize,
439)]
440#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
441pub enum DydxMarketStatus {
442    /// Market is active and trading.
443    Active,
444    /// Market is paused (no trading).
445    Paused,
446    /// Cancel-only mode (no new orders).
447    CancelOnly,
448    /// Post-only mode (only maker orders).
449    PostOnly,
450    /// Market is initializing.
451    Initializing,
452    /// Market is in final settlement.
453    FinalSettlement,
454    /// A status value not modeled by this enum.
455    #[serde(other)]
456    Unknown,
457}
458
459impl From<DydxMarketStatus> for MarketStatusAction {
460    fn from(value: DydxMarketStatus) -> Self {
461        match value {
462            DydxMarketStatus::Active => Self::Trading,
463            DydxMarketStatus::Paused => Self::Pause,
464            DydxMarketStatus::CancelOnly => Self::Halt,
465            DydxMarketStatus::PostOnly => Self::Quoting,
466            DydxMarketStatus::Initializing => Self::PreOpen,
467            DydxMarketStatus::FinalSettlement => Self::Close,
468            // No safe market action for an unmodeled status; emit no change.
469            DydxMarketStatus::Unknown => Self::None,
470        }
471    }
472}
473
474/// dYdX fill type.
475#[derive(
476    Copy,
477    Clone,
478    Debug,
479    Display,
480    PartialEq,
481    Eq,
482    Hash,
483    AsRefStr,
484    EnumIter,
485    EnumString,
486    Serialize,
487    Deserialize,
488)]
489#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
490pub enum DydxFillType {
491    /// Normal limit order fill.
492    Limit,
493    /// Liquidation (taker side).
494    Liquidated,
495    /// Liquidation (maker side).
496    Liquidation,
497    /// Deleveraging (deleveraged account).
498    Deleveraged,
499    /// Deleveraging (offsetting account).
500    Offsetting,
501    /// A fill type not modeled by this enum.
502    #[serde(other)]
503    Unknown,
504}
505
506/// dYdX liquidity side (maker/taker).
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 DydxLiquidity {
523    /// Maker (provides liquidity).
524    Maker,
525    /// Taker (removes liquidity).
526    Taker,
527}
528
529impl From<DydxLiquidity> for LiquiditySide {
530    fn from(value: DydxLiquidity) -> Self {
531        match value {
532            DydxLiquidity::Maker => Self::Maker,
533            DydxLiquidity::Taker => Self::Taker,
534        }
535    }
536}
537
538impl From<LiquiditySide> for DydxLiquidity {
539    fn from(value: LiquiditySide) -> Self {
540        match value {
541            LiquiditySide::Maker => Self::Maker,
542            LiquiditySide::Taker => Self::Taker,
543            LiquiditySide::NoLiquiditySide => Self::Taker, // Default fallback
544        }
545    }
546}
547
548/// dYdX ticker type for market data.
549#[derive(
550    Copy,
551    Clone,
552    Debug,
553    Display,
554    PartialEq,
555    Eq,
556    Hash,
557    AsRefStr,
558    EnumIter,
559    EnumString,
560    Serialize,
561    Deserialize,
562)]
563#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
564pub enum DydxTickerType {
565    /// Perpetual market ticker.
566    Perpetual,
567    /// A market type not modeled by this enum.
568    #[serde(other)]
569    Unknown,
570}
571
572/// dYdX trade type.
573///
574/// Represents the type of trade execution on dYdX.
575#[derive(
576    Copy,
577    Clone,
578    Debug,
579    Display,
580    PartialEq,
581    Eq,
582    Hash,
583    AsRefStr,
584    EnumIter,
585    EnumString,
586    Serialize,
587    Deserialize,
588)]
589#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
590pub enum DydxTradeType {
591    /// Standard limit order.
592    Limit,
593    /// Market order.
594    Market,
595    /// Liquidation trade.
596    Liquidated,
597    /// Sub-order from a TWAP execution.
598    TwapSuborder,
599    /// Stop limit order.
600    StopLimit,
601    /// Take-profit order (limit).
602    TakeProfitLimit,
603    /// A trade type not modeled by this enum.
604    #[serde(other)]
605    Unknown,
606}
607
608/// dYdX transfer types.
609#[derive(
610    Copy,
611    Clone,
612    Debug,
613    Display,
614    PartialEq,
615    Eq,
616    Hash,
617    AsRefStr,
618    EnumIter,
619    EnumString,
620    Serialize,
621    Deserialize,
622)]
623#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
624#[cfg_attr(
625    feature = "python",
626    pyo3::pyclass(
627        module = "nautilus_trader.core.nautilus_pyo3.dydx",
628        eq,
629        eq_int,
630        from_py_object
631    )
632)]
633#[cfg_attr(
634    feature = "python",
635    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.dydx")
636)]
637pub enum DydxTransferType {
638    /// Transfer into the account.
639    TransferIn,
640    /// Transfer out of the account.
641    TransferOut,
642    /// Deposit from external wallet.
643    Deposit,
644    /// Withdrawal to external wallet.
645    Withdrawal,
646}
647
648/// dYdX candlestick resolution.
649#[derive(
650    Copy,
651    Clone,
652    Debug,
653    Display,
654    PartialEq,
655    Eq,
656    Hash,
657    AsRefStr,
658    IntoStaticStr,
659    EnumIter,
660    EnumString,
661    Serialize,
662    Deserialize,
663)]
664#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
665#[derive(Default)]
666#[cfg_attr(
667    feature = "python",
668    pyo3::pyclass(
669        module = "nautilus_trader.core.nautilus_pyo3.dydx",
670        eq,
671        eq_int,
672        from_py_object
673    )
674)]
675#[cfg_attr(
676    feature = "python",
677    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.dydx")
678)]
679pub enum DydxCandleResolution {
680    /// 1 minute candles.
681    #[serde(rename = "1MIN")]
682    #[strum(serialize = "1MIN")]
683    #[default]
684    OneMinute,
685    /// 5 minute candles.
686    #[serde(rename = "5MINS")]
687    #[strum(serialize = "5MINS")]
688    FiveMinutes,
689    /// 15 minute candles.
690    #[serde(rename = "15MINS")]
691    #[strum(serialize = "15MINS")]
692    FifteenMinutes,
693    /// 30 minute candles.
694    #[serde(rename = "30MINS")]
695    #[strum(serialize = "30MINS")]
696    ThirtyMinutes,
697    /// 1 hour candles.
698    #[serde(rename = "1HOUR")]
699    #[strum(serialize = "1HOUR")]
700    OneHour,
701    /// 4 hour candles.
702    #[serde(rename = "4HOURS")]
703    #[strum(serialize = "4HOURS")]
704    FourHours,
705    /// 1 day candles.
706    #[serde(rename = "1DAY")]
707    #[strum(serialize = "1DAY")]
708    OneDay,
709}
710
711impl DydxCandleResolution {
712    /// Maps a Nautilus [`BarSpecification`] to a dYdX candle resolution.
713    ///
714    /// # Errors
715    ///
716    /// Returns an error if the step/aggregation combination is not supported.
717    pub fn from_bar_spec(spec: &BarSpecification) -> anyhow::Result<Self> {
718        match spec.step.get() {
719            1 => match spec.aggregation {
720                BarAggregation::Minute => Ok(Self::OneMinute),
721                BarAggregation::Hour => Ok(Self::OneHour),
722                BarAggregation::Day => Ok(Self::OneDay),
723                _ => anyhow::bail!("Unsupported bar aggregation: {:?}", spec.aggregation),
724            },
725            5 if spec.aggregation == BarAggregation::Minute => Ok(Self::FiveMinutes),
726            15 if spec.aggregation == BarAggregation::Minute => Ok(Self::FifteenMinutes),
727            30 if spec.aggregation == BarAggregation::Minute => Ok(Self::ThirtyMinutes),
728            4 if spec.aggregation == BarAggregation::Hour => Ok(Self::FourHours),
729            step => anyhow::bail!(
730                "Unsupported bar step: {step} with aggregation {:?}",
731                spec.aggregation
732            ),
733        }
734    }
735}
736
737/// dYdX network environment (mainnet vs testnet).
738///
739/// This selects the underlying Cosmos chain for transaction submission.
740#[derive(
741    Copy,
742    Clone,
743    Debug,
744    Default,
745    Display,
746    PartialEq,
747    Eq,
748    Hash,
749    AsRefStr,
750    EnumIter,
751    EnumString,
752    Serialize,
753    Deserialize,
754)]
755#[strum(serialize_all = "lowercase")]
756#[serde(rename_all = "lowercase")]
757#[cfg_attr(
758    feature = "python",
759    pyo3::pyclass(
760        eq,
761        eq_int,
762        module = "nautilus_trader.core.nautilus_pyo3.dydx",
763        from_py_object,
764        rename_all = "SCREAMING_SNAKE_CASE",
765    )
766)]
767#[cfg_attr(
768    feature = "python",
769    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.dydx")
770)]
771pub enum DydxNetwork {
772    /// dYdX mainnet (dydx-mainnet-1).
773    #[default]
774    Mainnet,
775    /// dYdX testnet (dydx-testnet-4).
776    Testnet,
777}
778
779impl DydxNetwork {
780    /// Maps the logical network to the underlying gRPC chain identifier.
781    #[must_use]
782    pub const fn chain_id(self) -> ChainId {
783        match self {
784            Self::Mainnet => ChainId::Mainnet1,
785            Self::Testnet => ChainId::Testnet4,
786        }
787    }
788
789    /// Returns the canonical lowercase string used in config/env.
790    #[must_use]
791    pub const fn as_str(self) -> &'static str {
792        match self {
793            Self::Mainnet => "mainnet",
794            Self::Testnet => "testnet",
795        }
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use rstest::rstest;
802
803    use super::*;
804
805    #[rstest]
806    fn test_reference_enums_tolerate_unmodeled_values() {
807        // Reference/descriptive enums must degrade gracefully on a new venue value
808        // rather than hard-fail deserialization of the whole message.
809        let status: DydxMarketStatus = serde_json::from_str("\"SOME_NEW_STATUS\"").unwrap();
810        let ticker: DydxTickerType = serde_json::from_str("\"SPOT\"").unwrap();
811        let trade: DydxTradeType = serde_json::from_str("\"SOME_NEW_TRADE\"").unwrap();
812        let fill: DydxFillType = serde_json::from_str("\"SOME_NEW_FILL\"").unwrap();
813        assert_eq!(status, DydxMarketStatus::Unknown);
814        assert_eq!(ticker, DydxTickerType::Unknown);
815        assert_eq!(trade, DydxTradeType::Unknown);
816        assert_eq!(fill, DydxFillType::Unknown);
817        assert_eq!(
818            MarketStatusAction::from(DydxMarketStatus::Unknown),
819            MarketStatusAction::None
820        );
821    }
822
823    #[rstest]
824    fn test_order_status_conversion() {
825        assert_eq!(
826            OrderStatus::from(DydxOrderStatus::Open),
827            OrderStatus::Accepted
828        );
829        assert_eq!(
830            OrderStatus::from(DydxOrderStatus::Filled),
831            OrderStatus::Filled
832        );
833        assert_eq!(
834            OrderStatus::from(DydxOrderStatus::Canceled),
835            OrderStatus::Canceled
836        );
837    }
838
839    #[rstest]
840    fn test_liquidity_conversion() {
841        assert_eq!(
842            LiquiditySide::from(DydxLiquidity::Maker),
843            LiquiditySide::Maker
844        );
845        assert_eq!(
846            LiquiditySide::from(DydxLiquidity::Taker),
847            LiquiditySide::Taker
848        );
849    }
850
851    #[rstest]
852    fn test_order_type_is_conditional() {
853        assert!(DydxOrderType::StopLimit.is_conditional());
854        assert!(DydxOrderType::StopMarket.is_conditional());
855        assert!(DydxOrderType::TakeProfitLimit.is_conditional());
856        assert!(DydxOrderType::TakeProfitMarket.is_conditional());
857        assert!(DydxOrderType::TrailingStop.is_conditional());
858        assert!(!DydxOrderType::Limit.is_conditional());
859        assert!(!DydxOrderType::Market.is_conditional());
860    }
861
862    #[rstest]
863    fn test_condition_type_mapping() {
864        assert_eq!(
865            DydxOrderType::StopLimit.condition_type(),
866            DydxConditionType::StopLoss
867        );
868        assert_eq!(
869            DydxOrderType::StopMarket.condition_type(),
870            DydxConditionType::StopLoss
871        );
872        assert_eq!(
873            DydxOrderType::TakeProfitLimit.condition_type(),
874            DydxConditionType::TakeProfit
875        );
876        assert_eq!(
877            DydxOrderType::TakeProfitMarket.condition_type(),
878            DydxConditionType::TakeProfit
879        );
880        assert_eq!(
881            DydxOrderType::Limit.condition_type(),
882            DydxConditionType::Unspecified
883        );
884    }
885
886    #[rstest]
887    fn test_is_market_execution() {
888        assert!(DydxOrderType::Market.is_market_execution());
889        assert!(DydxOrderType::StopMarket.is_market_execution());
890        assert!(DydxOrderType::TakeProfitMarket.is_market_execution());
891        assert!(!DydxOrderType::Limit.is_market_execution());
892        assert!(!DydxOrderType::StopLimit.is_market_execution());
893        assert!(!DydxOrderType::TakeProfitLimit.is_market_execution());
894    }
895
896    #[rstest]
897    fn test_order_type_to_nautilus() {
898        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
899        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
900        assert_eq!(
901            OrderType::from(DydxOrderType::StopMarket),
902            OrderType::StopMarket
903        );
904        assert_eq!(
905            OrderType::from(DydxOrderType::StopLimit),
906            OrderType::StopLimit
907        );
908    }
909
910    #[rstest]
911    fn test_order_side_conversion_from_nautilus() {
912        assert_eq!(
913            DydxOrderSide::try_from(OrderSide::Buy).unwrap(),
914            DydxOrderSide::Buy
915        );
916        assert_eq!(
917            DydxOrderSide::try_from(OrderSide::Sell).unwrap(),
918            DydxOrderSide::Sell
919        );
920        assert!(DydxOrderSide::try_from(OrderSide::NoOrderSide).is_err());
921    }
922
923    #[rstest]
924    fn test_order_side_conversion_to_nautilus() {
925        assert_eq!(OrderSide::from(DydxOrderSide::Buy), OrderSide::Buy);
926        assert_eq!(OrderSide::from(DydxOrderSide::Sell), OrderSide::Sell);
927    }
928
929    #[rstest]
930    fn test_order_type_conversion_from_nautilus() {
931        assert_eq!(
932            DydxOrderType::try_from(OrderType::Market).unwrap(),
933            DydxOrderType::Market
934        );
935        assert_eq!(
936            DydxOrderType::try_from(OrderType::Limit).unwrap(),
937            DydxOrderType::Limit
938        );
939        assert_eq!(
940            DydxOrderType::try_from(OrderType::StopMarket).unwrap(),
941            DydxOrderType::StopMarket
942        );
943        assert_eq!(
944            DydxOrderType::try_from(OrderType::StopLimit).unwrap(),
945            DydxOrderType::StopLimit
946        );
947        assert!(DydxOrderType::try_from(OrderType::MarketToLimit).is_err());
948    }
949
950    #[rstest]
951    fn test_order_type_conversion_to_nautilus() {
952        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
953        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
954        assert_eq!(
955            OrderType::from(DydxOrderType::StopMarket),
956            OrderType::StopMarket
957        );
958        assert_eq!(
959            OrderType::from(DydxOrderType::StopLimit),
960            OrderType::StopLimit
961        );
962    }
963
964    // The dYdX Indexer reports the take-profit-limit variant as `"TAKE_PROFIT"`
965    // (no `_LIMIT` suffix). The serde alias keeps `TAKE_PROFIT_LIMIT` working
966    // for callers that already use the explicit form.
967    #[rstest]
968    #[case("\"TAKE_PROFIT\"", DydxOrderType::TakeProfitLimit)]
969    #[case("\"TAKE_PROFIT_LIMIT\"", DydxOrderType::TakeProfitLimit)]
970    #[case("\"TAKE_PROFIT_MARKET\"", DydxOrderType::TakeProfitMarket)]
971    fn test_dydx_order_type_take_profit_serde(
972        #[case] input: &str,
973        #[case] expected: DydxOrderType,
974    ) {
975        let parsed: DydxOrderType = serde_json::from_str(input).unwrap();
976        assert_eq!(parsed, expected);
977    }
978
979    #[rstest]
980    fn test_dydx_network_chain_id_mapping() {
981        // Test canonical chain ID mapping
982        assert_eq!(DydxNetwork::Mainnet.chain_id(), ChainId::Mainnet1);
983        assert_eq!(DydxNetwork::Testnet.chain_id(), ChainId::Testnet4);
984    }
985
986    #[rstest]
987    fn test_dydx_network_as_str() {
988        // Test string representation for config/env
989        assert_eq!(DydxNetwork::Mainnet.as_str(), "mainnet");
990        assert_eq!(DydxNetwork::Testnet.as_str(), "testnet");
991    }
992
993    #[rstest]
994    fn test_dydx_network_default() {
995        // Test default is mainnet
996        assert_eq!(DydxNetwork::default(), DydxNetwork::Mainnet);
997    }
998
999    #[rstest]
1000    fn test_dydx_network_serde_lowercase() {
1001        // Test lowercase serialization/deserialization
1002        let mainnet = DydxNetwork::Mainnet;
1003        let json = serde_json::to_string(&mainnet).unwrap();
1004        assert_eq!(json, "\"mainnet\"");
1005
1006        let deserialized: DydxNetwork = serde_json::from_str("\"mainnet\"").unwrap();
1007        assert_eq!(deserialized, DydxNetwork::Mainnet);
1008
1009        let testnet = DydxNetwork::Testnet;
1010        let json = serde_json::to_string(&testnet).unwrap();
1011        assert_eq!(json, "\"testnet\"");
1012
1013        let deserialized: DydxNetwork = serde_json::from_str("\"testnet\"").unwrap();
1014        assert_eq!(deserialized, DydxNetwork::Testnet);
1015    }
1016}