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