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;
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#[cfg_attr(
170    feature = "python",
171    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", eq, eq_int)
172)]
173pub enum DydxOrderType {
174    /// Limit order with specified price.
175    Limit,
176    /// Market order (executed at best available price).
177    Market,
178    /// Stop-limit order (triggered at stop price, executed as limit).
179    StopLimit,
180    /// Stop-market order (triggered at stop price, executed as market).
181    StopMarket,
182    /// Take-profit order (limit).
183    TakeProfitLimit,
184    /// Take-profit order (market).
185    TakeProfitMarket,
186    /// Trailing stop order.
187    TrailingStop,
188}
189
190impl TryFrom<OrderType> for DydxOrderType {
191    type Error = DydxError;
192
193    fn try_from(value: OrderType) -> Result<Self, Self::Error> {
194        match value {
195            OrderType::Market => Ok(Self::Market),
196            OrderType::Limit => Ok(Self::Limit),
197            OrderType::StopMarket => Ok(Self::StopMarket),
198            OrderType::StopLimit => Ok(Self::StopLimit),
199            OrderType::MarketIfTouched => Ok(Self::TakeProfitMarket),
200            OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
201            OrderType::TrailingStopMarket | OrderType::TrailingStopLimit => Ok(Self::TrailingStop),
202            OrderType::MarketToLimit => Err(DydxError::UnsupportedOrderType(format!("{value:?}"))),
203        }
204    }
205}
206
207impl DydxOrderType {
208    /// Try to convert from Nautilus `OrderType`.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the order type is not supported by dYdX.
213    pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
214        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
215    }
216
217    /// Returns true if this is a conditional order type.
218    #[must_use]
219    pub const fn is_conditional(&self) -> bool {
220        matches!(
221            self,
222            Self::StopLimit
223                | Self::StopMarket
224                | Self::TakeProfitLimit
225                | Self::TakeProfitMarket
226                | Self::TrailingStop
227        )
228    }
229
230    /// Returns the condition type for this order type.
231    #[must_use]
232    pub const fn condition_type(&self) -> DydxConditionType {
233        match self {
234            Self::StopLimit | Self::StopMarket => DydxConditionType::StopLoss,
235            Self::TakeProfitLimit | Self::TakeProfitMarket => DydxConditionType::TakeProfit,
236            _ => DydxConditionType::Unspecified,
237        }
238    }
239
240    /// Returns true if this order type should execute as market.
241    #[must_use]
242    pub const fn is_market_execution(&self) -> bool {
243        matches!(
244            self,
245            Self::Market | Self::StopMarket | Self::TakeProfitMarket
246        )
247    }
248}
249
250impl From<DydxOrderType> for OrderType {
251    fn from(value: DydxOrderType) -> Self {
252        match value {
253            DydxOrderType::Market => Self::Market,
254            DydxOrderType::Limit => Self::Limit,
255            DydxOrderType::StopMarket => Self::StopMarket,
256            DydxOrderType::StopLimit => Self::StopLimit,
257            DydxOrderType::TakeProfitMarket => Self::MarketIfTouched,
258            DydxOrderType::TakeProfitLimit => Self::LimitIfTouched,
259            DydxOrderType::TrailingStop => Self::TrailingStopMarket,
260        }
261    }
262}
263
264/// dYdX order execution type.
265#[derive(
266    Copy,
267    Clone,
268    Debug,
269    Display,
270    PartialEq,
271    Eq,
272    Hash,
273    AsRefStr,
274    EnumIter,
275    EnumString,
276    Serialize,
277    Deserialize,
278)]
279#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
280pub enum DydxOrderExecution {
281    /// Default execution behavior.
282    Default,
283    /// Immediate-Or-Cancel execution.
284    Ioc,
285    /// Fill-Or-Kill execution.
286    Fok,
287    /// Post-only execution (maker-only).
288    PostOnly,
289}
290
291/// dYdX order flags (bitfield).
292#[derive(
293    Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, EnumIter, Serialize, Deserialize,
294)]
295pub enum DydxOrderFlags {
296    /// Short-term order (0).
297    ShortTerm = 0,
298    /// Conditional order (32).
299    Conditional = 32,
300    /// Long-term order (64).
301    LongTerm = 64,
302}
303
304/// dYdX condition type for conditional orders.
305///
306/// Determines whether the order is a stop-loss (triggers when price
307/// falls below/rises above trigger for sell/buy) or take-profit
308/// (triggers in opposite direction).
309#[derive(
310    Copy,
311    Clone,
312    Debug,
313    Display,
314    PartialEq,
315    Eq,
316    Hash,
317    AsRefStr,
318    EnumIter,
319    EnumString,
320    Serialize,
321    Deserialize,
322)]
323#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
324pub enum DydxConditionType {
325    /// No condition (standard order).
326    Unspecified,
327    /// Stop-loss conditional order.
328    StopLoss,
329    /// Take-profit conditional order.
330    TakeProfit,
331}
332
333/// dYdX position status.
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 DydxPositionStatus {
350    /// Position is open.
351    Open,
352    /// Position is closed.
353    Closed,
354    /// Position was liquidated.
355    Liquidated,
356}
357
358impl From<DydxPositionStatus> for PositionSide {
359    fn from(value: DydxPositionStatus) -> Self {
360        match value {
361            DydxPositionStatus::Open => Self::Long, // Default, actual side from position size
362            DydxPositionStatus::Closed => Self::Flat,
363            DydxPositionStatus::Liquidated => Self::Flat,
364        }
365    }
366}
367
368/// dYdX perpetual market 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 DydxMarketStatus {
385    /// Market is active and trading.
386    Active,
387    /// Market is paused (no trading).
388    Paused,
389    /// Cancel-only mode (no new orders).
390    CancelOnly,
391    /// Post-only mode (only maker orders).
392    PostOnly,
393    /// Market is initializing.
394    Initializing,
395    /// Market is in final settlement.
396    FinalSettlement,
397}
398
399/// dYdX fill type.
400#[derive(
401    Copy,
402    Clone,
403    Debug,
404    Display,
405    PartialEq,
406    Eq,
407    Hash,
408    AsRefStr,
409    EnumIter,
410    EnumString,
411    Serialize,
412    Deserialize,
413)]
414#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
415pub enum DydxFillType {
416    /// Normal limit order fill.
417    Limit,
418    /// Liquidation (taker side).
419    Liquidated,
420    /// Liquidation (maker side).
421    Liquidation,
422    /// Deleveraging (deleveraged account).
423    Deleveraged,
424    /// Deleveraging (offsetting account).
425    Offsetting,
426}
427
428/// dYdX liquidity side (maker/taker).
429#[derive(
430    Copy,
431    Clone,
432    Debug,
433    Display,
434    PartialEq,
435    Eq,
436    Hash,
437    AsRefStr,
438    EnumIter,
439    EnumString,
440    Serialize,
441    Deserialize,
442)]
443#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
444pub enum DydxLiquidity {
445    /// Maker (provides liquidity).
446    Maker,
447    /// Taker (removes liquidity).
448    Taker,
449}
450
451impl From<DydxLiquidity> for LiquiditySide {
452    fn from(value: DydxLiquidity) -> Self {
453        match value {
454            DydxLiquidity::Maker => Self::Maker,
455            DydxLiquidity::Taker => Self::Taker,
456        }
457    }
458}
459
460impl From<LiquiditySide> for DydxLiquidity {
461    fn from(value: LiquiditySide) -> Self {
462        match value {
463            LiquiditySide::Maker => Self::Maker,
464            LiquiditySide::Taker => Self::Taker,
465            LiquiditySide::NoLiquiditySide => Self::Taker, // Default fallback
466        }
467    }
468}
469
470/// dYdX ticker type for market data.
471#[derive(
472    Copy,
473    Clone,
474    Debug,
475    Display,
476    PartialEq,
477    Eq,
478    Hash,
479    AsRefStr,
480    EnumIter,
481    EnumString,
482    Serialize,
483    Deserialize,
484)]
485#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
486pub enum DydxTickerType {
487    /// Perpetual market ticker.
488    Perpetual,
489}
490
491/// dYdX trade type.
492///
493/// Represents the type of trade execution on dYdX.
494#[derive(
495    Copy,
496    Clone,
497    Debug,
498    Display,
499    PartialEq,
500    Eq,
501    Hash,
502    AsRefStr,
503    EnumIter,
504    EnumString,
505    Serialize,
506    Deserialize,
507)]
508#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
509pub enum DydxTradeType {
510    /// Standard limit order.
511    Limit,
512    /// Market order.
513    Market,
514    /// Liquidation trade.
515    Liquidated,
516    /// Sub-order from a TWAP execution.
517    TwapSuborder,
518    /// Stop limit order.
519    StopLimit,
520    /// Take profit limit order.
521    TakeProfitLimit,
522}
523
524/// dYdX candlestick resolution.
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")]
540#[derive(Default)]
541pub enum DydxCandleResolution {
542    /// 1 minute candles.
543    #[serde(rename = "1MIN")]
544    #[strum(serialize = "1MIN")]
545    #[default]
546    OneMinute,
547    /// 5 minute candles.
548    #[serde(rename = "5MINS")]
549    #[strum(serialize = "5MINS")]
550    FiveMinutes,
551    /// 15 minute candles.
552    #[serde(rename = "15MINS")]
553    #[strum(serialize = "15MINS")]
554    FifteenMinutes,
555    /// 30 minute candles.
556    #[serde(rename = "30MINS")]
557    #[strum(serialize = "30MINS")]
558    ThirtyMinutes,
559    /// 1 hour candles.
560    #[serde(rename = "1HOUR")]
561    #[strum(serialize = "1HOUR")]
562    OneHour,
563    /// 4 hour candles.
564    #[serde(rename = "4HOURS")]
565    #[strum(serialize = "4HOURS")]
566    FourHours,
567    /// 1 day candles.
568    #[serde(rename = "1DAY")]
569    #[strum(serialize = "1DAY")]
570    OneDay,
571}
572
573////////////////////////////////////////////////////////////////////////////////
574// Tests
575////////////////////////////////////////////////////////////////////////////////
576
577#[cfg(test)]
578mod tests {
579    use rstest::rstest;
580
581    use super::*;
582
583    #[rstest]
584    fn test_order_status_conversion() {
585        assert_eq!(
586            OrderStatus::from(DydxOrderStatus::Open),
587            OrderStatus::Accepted
588        );
589        assert_eq!(
590            OrderStatus::from(DydxOrderStatus::Filled),
591            OrderStatus::Filled
592        );
593        assert_eq!(
594            OrderStatus::from(DydxOrderStatus::Canceled),
595            OrderStatus::Canceled
596        );
597    }
598
599    #[rstest]
600    fn test_liquidity_conversion() {
601        assert_eq!(
602            LiquiditySide::from(DydxLiquidity::Maker),
603            LiquiditySide::Maker
604        );
605        assert_eq!(
606            LiquiditySide::from(DydxLiquidity::Taker),
607            LiquiditySide::Taker
608        );
609    }
610
611    #[rstest]
612    fn test_order_type_is_conditional() {
613        assert!(DydxOrderType::StopLimit.is_conditional());
614        assert!(DydxOrderType::StopMarket.is_conditional());
615        assert!(DydxOrderType::TakeProfitLimit.is_conditional());
616        assert!(DydxOrderType::TakeProfitMarket.is_conditional());
617        assert!(DydxOrderType::TrailingStop.is_conditional());
618        assert!(!DydxOrderType::Limit.is_conditional());
619        assert!(!DydxOrderType::Market.is_conditional());
620    }
621
622    #[rstest]
623    fn test_condition_type_mapping() {
624        assert_eq!(
625            DydxOrderType::StopLimit.condition_type(),
626            DydxConditionType::StopLoss
627        );
628        assert_eq!(
629            DydxOrderType::StopMarket.condition_type(),
630            DydxConditionType::StopLoss
631        );
632        assert_eq!(
633            DydxOrderType::TakeProfitLimit.condition_type(),
634            DydxConditionType::TakeProfit
635        );
636        assert_eq!(
637            DydxOrderType::TakeProfitMarket.condition_type(),
638            DydxConditionType::TakeProfit
639        );
640        assert_eq!(
641            DydxOrderType::Limit.condition_type(),
642            DydxConditionType::Unspecified
643        );
644    }
645
646    #[rstest]
647    fn test_is_market_execution() {
648        assert!(DydxOrderType::Market.is_market_execution());
649        assert!(DydxOrderType::StopMarket.is_market_execution());
650        assert!(DydxOrderType::TakeProfitMarket.is_market_execution());
651        assert!(!DydxOrderType::Limit.is_market_execution());
652        assert!(!DydxOrderType::StopLimit.is_market_execution());
653        assert!(!DydxOrderType::TakeProfitLimit.is_market_execution());
654    }
655
656    #[rstest]
657    fn test_order_type_to_nautilus() {
658        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
659        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
660        assert_eq!(
661            OrderType::from(DydxOrderType::StopMarket),
662            OrderType::StopMarket
663        );
664        assert_eq!(
665            OrderType::from(DydxOrderType::StopLimit),
666            OrderType::StopLimit
667        );
668    }
669
670    #[rstest]
671    fn test_order_side_conversion_from_nautilus() {
672        assert_eq!(
673            DydxOrderSide::try_from(OrderSide::Buy).unwrap(),
674            DydxOrderSide::Buy
675        );
676        assert_eq!(
677            DydxOrderSide::try_from(OrderSide::Sell).unwrap(),
678            DydxOrderSide::Sell
679        );
680        assert!(DydxOrderSide::try_from(OrderSide::NoOrderSide).is_err());
681    }
682
683    #[rstest]
684    fn test_order_side_conversion_to_nautilus() {
685        assert_eq!(OrderSide::from(DydxOrderSide::Buy), OrderSide::Buy);
686        assert_eq!(OrderSide::from(DydxOrderSide::Sell), OrderSide::Sell);
687    }
688
689    #[rstest]
690    fn test_order_type_conversion_from_nautilus() {
691        assert_eq!(
692            DydxOrderType::try_from(OrderType::Market).unwrap(),
693            DydxOrderType::Market
694        );
695        assert_eq!(
696            DydxOrderType::try_from(OrderType::Limit).unwrap(),
697            DydxOrderType::Limit
698        );
699        assert_eq!(
700            DydxOrderType::try_from(OrderType::StopMarket).unwrap(),
701            DydxOrderType::StopMarket
702        );
703        assert_eq!(
704            DydxOrderType::try_from(OrderType::StopLimit).unwrap(),
705            DydxOrderType::StopLimit
706        );
707        assert!(DydxOrderType::try_from(OrderType::MarketToLimit).is_err());
708    }
709
710    #[rstest]
711    fn test_order_type_conversion_to_nautilus() {
712        assert_eq!(OrderType::from(DydxOrderType::Market), OrderType::Market);
713        assert_eq!(OrderType::from(DydxOrderType::Limit), OrderType::Limit);
714        assert_eq!(
715            OrderType::from(DydxOrderType::StopMarket),
716            OrderType::StopMarket
717        );
718        assert_eq!(
719            OrderType::from(DydxOrderType::StopLimit),
720            OrderType::StopLimit
721        );
722    }
723}