nautilus_bitmex/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//! BitMEX-specific enumerations shared by HTTP and WebSocket components.
17
18use nautilus_model::enums::{
19    ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
20};
21use serde::{Deserialize, Deserializer, Serialize};
22use strum::{AsRefStr, Display, EnumIter, EnumString};
23
24use crate::error::{BitmexError, BitmexNonRetryableError};
25
26/// Represents the status of a BitMEX symbol.
27#[derive(
28    Copy,
29    Clone,
30    Debug,
31    Display,
32    PartialEq,
33    Eq,
34    AsRefStr,
35    EnumIter,
36    EnumString,
37    Serialize,
38    Deserialize,
39)]
40#[serde(rename_all = "PascalCase")]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", eq, eq_int)
44)]
45pub enum BitmexSymbolStatus {
46    /// Symbol is open for trading.
47    Open,
48    /// Symbol is closed for trading.
49    Closed,
50    /// Symbol is unlisted.
51    Unlisted,
52}
53
54/// Represents the side of an order or trade (Buy/Sell).
55#[derive(
56    Copy,
57    Clone,
58    Debug,
59    Display,
60    PartialEq,
61    Eq,
62    AsRefStr,
63    EnumIter,
64    EnumString,
65    Serialize,
66    Deserialize,
67)]
68pub enum BitmexSide {
69    /// Buy side of a trade or order.
70    #[serde(rename = "Buy", alias = "BUY", alias = "buy")]
71    Buy,
72    /// Sell side of a trade or order.
73    #[serde(rename = "Sell", alias = "SELL", alias = "sell")]
74    Sell,
75}
76
77impl TryFrom<OrderSide> for BitmexSide {
78    type Error = BitmexError;
79
80    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
81        match value {
82            OrderSide::Buy => Ok(Self::Buy),
83            OrderSide::Sell => Ok(Self::Sell),
84            _ => Err(BitmexError::NonRetryable {
85                source: BitmexNonRetryableError::Validation {
86                    field: "order_side".to_string(),
87                    message: format!("Invalid order side: {value:?}"),
88                },
89            }),
90        }
91    }
92}
93
94impl BitmexSide {
95    /// Try to convert from Nautilus OrderSide.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the order side is not Buy or Sell.
100    pub fn try_from_order_side(value: OrderSide) -> anyhow::Result<Self> {
101        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
102    }
103}
104
105impl From<BitmexSide> for OrderSide {
106    fn from(side: BitmexSide) -> Self {
107        match side {
108            BitmexSide::Buy => Self::Buy,
109            BitmexSide::Sell => Self::Sell,
110        }
111    }
112}
113
114/// Represents the position side for BitMEX positions.
115#[derive(
116    Copy,
117    Clone,
118    Debug,
119    Display,
120    PartialEq,
121    Eq,
122    AsRefStr,
123    EnumIter,
124    EnumString,
125    Serialize,
126    Deserialize,
127)]
128#[cfg_attr(
129    feature = "python",
130    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", eq, eq_int)
131)]
132pub enum BitmexPositionSide {
133    /// Long position.
134    #[serde(rename = "LONG", alias = "Long", alias = "long")]
135    Long,
136    /// Short position.
137    #[serde(rename = "SHORT", alias = "Short", alias = "short")]
138    Short,
139    /// No position.
140    #[serde(rename = "FLAT", alias = "Flat", alias = "flat")]
141    Flat,
142}
143
144impl From<BitmexPositionSide> for PositionSide {
145    fn from(side: BitmexPositionSide) -> Self {
146        match side {
147            BitmexPositionSide::Long => Self::Long,
148            BitmexPositionSide::Short => Self::Short,
149            BitmexPositionSide::Flat => Self::Flat,
150        }
151    }
152}
153
154impl From<PositionSide> for BitmexPositionSide {
155    fn from(side: PositionSide) -> Self {
156        match side {
157            PositionSide::Long => Self::Long,
158            PositionSide::Short => Self::Short,
159            PositionSide::Flat | PositionSide::NoPositionSide => Self::Flat,
160        }
161    }
162}
163
164/// Represents the available order types on BitMEX.
165#[derive(
166    Copy,
167    Clone,
168    Debug,
169    Display,
170    PartialEq,
171    Eq,
172    AsRefStr,
173    EnumIter,
174    EnumString,
175    Serialize,
176    Deserialize,
177)]
178pub enum BitmexOrderType {
179    /// Market order, executed immediately at current market price.
180    Market,
181    /// Limit order, executed only at specified price or better.
182    Limit,
183    /// Stop Market order, triggers a market order when price reaches stop price.
184    Stop,
185    /// Stop Limit order, triggers a limit order when price reaches stop price.
186    StopLimit,
187    /// Market if touched order, triggers a market order when price reaches touch price.
188    MarketIfTouched,
189    /// Limit if touched order, triggers a limit order when price reaches touch price.
190    LimitIfTouched,
191    /// Pegged order, price automatically tracks market.
192    Pegged,
193}
194
195impl TryFrom<OrderType> for BitmexOrderType {
196    type Error = BitmexError;
197
198    fn try_from(value: OrderType) -> Result<Self, Self::Error> {
199        match value {
200            OrderType::Market => Ok(Self::Market),
201            OrderType::Limit => Ok(Self::Limit),
202            OrderType::StopMarket => Ok(Self::Stop),
203            OrderType::StopLimit => Ok(Self::StopLimit),
204            OrderType::MarketIfTouched => Ok(Self::MarketIfTouched),
205            OrderType::LimitIfTouched => Ok(Self::LimitIfTouched),
206            OrderType::TrailingStopMarket => Ok(Self::Pegged),
207            OrderType::TrailingStopLimit => Ok(Self::Pegged),
208            OrderType::MarketToLimit => Err(BitmexError::NonRetryable {
209                source: BitmexNonRetryableError::Validation {
210                    field: "order_type".to_string(),
211                    message: "MarketToLimit order type is not supported by BitMEX".to_string(),
212                },
213            }),
214        }
215    }
216}
217
218impl BitmexOrderType {
219    /// Try to convert from Nautilus OrderType with anyhow::Result.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the order type is MarketToLimit (not supported by BitMEX).
224    pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
225        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
226    }
227}
228
229impl From<BitmexOrderType> for OrderType {
230    fn from(value: BitmexOrderType) -> Self {
231        match value {
232            BitmexOrderType::Market => Self::Market,
233            BitmexOrderType::Limit => Self::Limit,
234            BitmexOrderType::Stop => Self::StopMarket,
235            BitmexOrderType::StopLimit => Self::StopLimit,
236            BitmexOrderType::MarketIfTouched => Self::MarketIfTouched,
237            BitmexOrderType::LimitIfTouched => Self::LimitIfTouched,
238            BitmexOrderType::Pegged => Self::Limit,
239        }
240    }
241}
242
243/// Represents the possible states of an order throughout its lifecycle.
244#[derive(
245    Copy,
246    Clone,
247    Debug,
248    Display,
249    PartialEq,
250    Eq,
251    AsRefStr,
252    EnumIter,
253    EnumString,
254    Serialize,
255    Deserialize,
256)]
257pub enum BitmexOrderStatus {
258    /// Order has been placed but not yet processed.
259    New,
260    /// Order has been partially filled.
261    PartiallyFilled,
262    /// Order has been completely filled.
263    Filled,
264    /// Order cancellation is pending.
265    PendingCancel,
266    /// Order has been canceled by user or system.
267    Canceled,
268    /// Order was rejected by the system.
269    Rejected,
270    /// Order has expired according to its time in force.
271    Expired,
272}
273
274impl From<BitmexOrderStatus> for OrderStatus {
275    fn from(value: BitmexOrderStatus) -> Self {
276        match value {
277            BitmexOrderStatus::New => Self::Accepted,
278            BitmexOrderStatus::PartiallyFilled => Self::PartiallyFilled,
279            BitmexOrderStatus::Filled => Self::Filled,
280            BitmexOrderStatus::PendingCancel => Self::PendingCancel,
281            BitmexOrderStatus::Canceled => Self::Canceled,
282            BitmexOrderStatus::Rejected => Self::Rejected,
283            BitmexOrderStatus::Expired => Self::Expired,
284        }
285    }
286}
287
288/// Specifies how long an order should remain active.
289#[derive(
290    Copy,
291    Clone,
292    Debug,
293    Display,
294    PartialEq,
295    Eq,
296    AsRefStr,
297    EnumIter,
298    EnumString,
299    Serialize,
300    Deserialize,
301)]
302pub enum BitmexTimeInForce {
303    Day,
304    GoodTillCancel,
305    AtTheOpening,
306    ImmediateOrCancel,
307    FillOrKill,
308    GoodTillCrossing,
309    GoodTillDate,
310    AtTheClose,
311    GoodThroughCrossing,
312    AtCrossing,
313}
314
315impl TryFrom<BitmexTimeInForce> for TimeInForce {
316    type Error = BitmexError;
317
318    fn try_from(value: BitmexTimeInForce) -> Result<Self, Self::Error> {
319        match value {
320            BitmexTimeInForce::Day => Ok(Self::Day),
321            BitmexTimeInForce::GoodTillCancel => Ok(Self::Gtc),
322            BitmexTimeInForce::GoodTillDate => Ok(Self::Gtd),
323            BitmexTimeInForce::ImmediateOrCancel => Ok(Self::Ioc),
324            BitmexTimeInForce::FillOrKill => Ok(Self::Fok),
325            BitmexTimeInForce::AtTheOpening => Ok(Self::AtTheOpen),
326            BitmexTimeInForce::AtTheClose => Ok(Self::AtTheClose),
327            _ => Err(BitmexError::NonRetryable {
328                source: BitmexNonRetryableError::Validation {
329                    field: "time_in_force".to_string(),
330                    message: format!("Unsupported BitmexTimeInForce: {value}"),
331                },
332            }),
333        }
334    }
335}
336
337impl TryFrom<TimeInForce> for BitmexTimeInForce {
338    type Error = crate::error::BitmexError;
339
340    fn try_from(value: TimeInForce) -> Result<Self, Self::Error> {
341        match value {
342            TimeInForce::Day => Ok(Self::Day),
343            TimeInForce::Gtc => Ok(Self::GoodTillCancel),
344            TimeInForce::Gtd => Ok(Self::GoodTillDate),
345            TimeInForce::Ioc => Ok(Self::ImmediateOrCancel),
346            TimeInForce::Fok => Ok(Self::FillOrKill),
347            TimeInForce::AtTheOpen => Ok(Self::AtTheOpening),
348            TimeInForce::AtTheClose => Ok(Self::AtTheClose),
349        }
350    }
351}
352
353impl BitmexTimeInForce {
354    /// Try to convert from Nautilus TimeInForce with anyhow::Result.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if the time in force is not supported by BitMEX.
359    pub fn try_from_time_in_force(value: TimeInForce) -> anyhow::Result<Self> {
360        Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
361    }
362}
363
364/// Represents the available contingency types on BitMEX.
365#[derive(
366    Copy,
367    Clone,
368    Debug,
369    Display,
370    PartialEq,
371    Eq,
372    AsRefStr,
373    EnumIter,
374    EnumString,
375    Serialize,
376    Deserialize,
377)]
378pub enum BitmexContingencyType {
379    OneCancelsTheOther,
380    OneTriggersTheOther,
381    OneUpdatesTheOtherAbsolute,
382    OneUpdatesTheOtherProportional,
383    #[serde(rename = "")]
384    Unknown, // Can be empty
385}
386
387impl From<BitmexContingencyType> for ContingencyType {
388    fn from(value: BitmexContingencyType) -> Self {
389        match value {
390            BitmexContingencyType::OneCancelsTheOther => Self::Oco,
391            BitmexContingencyType::OneTriggersTheOther => Self::Oto,
392            BitmexContingencyType::OneUpdatesTheOtherProportional => Self::Ouo,
393            BitmexContingencyType::OneUpdatesTheOtherAbsolute => Self::Ouo,
394            BitmexContingencyType::Unknown => Self::NoContingency,
395        }
396    }
397}
398
399impl TryFrom<ContingencyType> for BitmexContingencyType {
400    type Error = BitmexError;
401
402    fn try_from(value: ContingencyType) -> Result<Self, Self::Error> {
403        match value {
404            ContingencyType::NoContingency => Ok(Self::Unknown),
405            ContingencyType::Oco => Ok(Self::OneCancelsTheOther),
406            ContingencyType::Oto => Ok(Self::OneTriggersTheOther),
407            ContingencyType::Ouo => Err(BitmexError::NonRetryable {
408                source: BitmexNonRetryableError::Validation {
409                    field: "contingency_type".to_string(),
410                    message: "OUO contingency type not supported by BitMEX".to_string(),
411                },
412            }),
413        }
414    }
415}
416
417/// Represents the available peg price types on BitMEX.
418#[derive(
419    Copy,
420    Clone,
421    Debug,
422    Display,
423    PartialEq,
424    Eq,
425    AsRefStr,
426    EnumIter,
427    EnumString,
428    Serialize,
429    Deserialize,
430)]
431pub enum BitmexPegPriceType {
432    LastPeg,
433    OpeningPeg,
434    MidPricePeg,
435    MarketPeg,
436    PrimaryPeg,
437    PegToVWAP,
438    TrailingStopPeg,
439    PegToLimitPrice,
440    ShortSaleMinPricePeg,
441    #[serde(rename = "")]
442    Unknown, // Can be empty
443}
444
445/// Represents the available execution instruments on BitMEX.
446#[derive(
447    Copy,
448    Clone,
449    Debug,
450    Display,
451    PartialEq,
452    Eq,
453    AsRefStr,
454    EnumIter,
455    EnumString,
456    Serialize,
457    Deserialize,
458)]
459pub enum BitmexExecInstruction {
460    ParticipateDoNotInitiate,
461    AllOrNone,
462    MarkPrice,
463    IndexPrice,
464    LastPrice,
465    Close,
466    ReduceOnly,
467    Fixed,
468    #[serde(rename = "")]
469    Unknown, // Can be empty
470}
471
472impl BitmexExecInstruction {
473    /// Joins execution instructions into the comma-separated string expected by BitMEX.
474    pub fn join(instructions: &[Self]) -> String {
475        instructions
476            .iter()
477            .map(std::string::ToString::to_string)
478            .collect::<Vec<_>>()
479            .join(",")
480    }
481}
482
483/// Represents the type of execution that generated a trade.
484#[derive(
485    Copy,
486    Clone,
487    Debug,
488    Display,
489    PartialEq,
490    Eq,
491    AsRefStr,
492    EnumIter,
493    EnumString,
494    Serialize,
495    Deserialize,
496)]
497pub enum BitmexExecType {
498    /// New order placed.
499    New,
500    /// Normal trade execution.
501    Trade,
502    /// Order canceled.
503    Canceled,
504    /// Cancel request rejected.
505    CancelReject,
506    /// Order replaced.
507    Replaced,
508    /// Order rejected.
509    Rejected,
510    /// Order amendment rejected.
511    AmendReject,
512    /// Funding rate execution.
513    Funding,
514    /// Settlement execution.
515    Settlement,
516    /// Order suspended.
517    Suspended,
518    /// Order released.
519    Released,
520    /// Insurance payment.
521    Insurance,
522    /// Rebalance.
523    Rebalance,
524    /// Liquidation execution.
525    Liquidation,
526    /// Bankruptcy execution.
527    Bankruptcy,
528    /// Trial fill (testnet only).
529    TrialFill,
530    /// Stop/trigger order activated by system.
531    TriggeredOrActivatedBySystem,
532}
533
534/// Indicates whether the execution was maker or taker.
535#[derive(
536    Copy,
537    Clone,
538    Debug,
539    Display,
540    PartialEq,
541    Eq,
542    AsRefStr,
543    EnumIter,
544    EnumString,
545    Serialize,
546    Deserialize,
547)]
548pub enum BitmexLiquidityIndicator {
549    /// Provided liquidity to the order book (maker).
550    /// BitMEX returns "Added" in REST API responses and "AddedLiquidity" in WebSocket messages.
551    #[serde(rename = "Added")]
552    #[serde(alias = "AddedLiquidity")]
553    Maker,
554    /// Took liquidity from the order book (taker).
555    /// BitMEX returns "Removed" in REST API responses and "RemovedLiquidity" in WebSocket messages.
556    #[serde(rename = "Removed")]
557    #[serde(alias = "RemovedLiquidity")]
558    Taker,
559}
560
561impl From<BitmexLiquidityIndicator> for LiquiditySide {
562    fn from(value: BitmexLiquidityIndicator) -> Self {
563        match value {
564            BitmexLiquidityIndicator::Maker => Self::Maker,
565            BitmexLiquidityIndicator::Taker => Self::Taker,
566        }
567    }
568}
569
570/// Represents BitMEX instrument types.
571#[derive(
572    Copy,
573    Clone,
574    Debug,
575    Display,
576    PartialEq,
577    Eq,
578    AsRefStr,
579    EnumIter,
580    EnumString,
581    Serialize,
582    Deserialize,
583)]
584#[serde(rename_all = "UPPERCASE")]
585pub enum BitmexInstrumentType {
586    #[serde(rename = "FXXXS")]
587    Unknown1, // TODO: Determine name (option)
588
589    #[serde(rename = "FMXXS")]
590    Unknown2, // TODO: Determine name (option)
591
592    /// Prediction Markets (non-standardized financial future on index, cash settled).
593    /// CFI code FFICSX - traders predict outcomes of events.
594    #[serde(rename = "FFICSX")]
595    PredictionMarket,
596
597    /// Perpetual Contracts.
598    #[serde(rename = "FFWCSX")]
599    PerpetualContract,
600
601    /// Perpetual Contracts (FX underliers).
602    #[serde(rename = "FFWCSF")]
603    PerpetualContractFx,
604
605    /// Spot.
606    #[serde(rename = "IFXXXP")]
607    Spot,
608
609    /// Futures.
610    #[serde(rename = "FFCCSX")]
611    Futures,
612
613    /// BitMEX Basket Index.
614    #[serde(rename = "MRBXXX")]
615    BasketIndex,
616
617    /// BitMEX Crypto Index.
618    #[serde(rename = "MRCXXX")]
619    CryptoIndex,
620
621    /// BitMEX FX Index.
622    #[serde(rename = "MRFXXX")]
623    FxIndex,
624
625    /// BitMEX Lending/Premium Index.
626    #[serde(rename = "MRRXXX")]
627    LendingIndex,
628
629    /// BitMEX Volatility Index.
630    #[serde(rename = "MRIXXX")]
631    VolatilityIndex,
632}
633
634/// Represents the different types of instrument subscriptions available on BitMEX.
635#[derive(Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize)]
636pub enum BitmexProductType {
637    /// All instruments AND indices.
638    #[serde(rename = "instrument")]
639    All,
640
641    /// All instruments, but no indices.
642    #[serde(rename = "CONTRACTS")]
643    Contracts,
644
645    /// All indices, but no tradeable instruments.
646    #[serde(rename = "INDICES")]
647    Indices,
648
649    /// Only derivative instruments, and no indices.
650    #[serde(rename = "DERIVATIVES")]
651    Derivatives,
652
653    /// Only spot instruments, and no indices.
654    #[serde(rename = "SPOT")]
655    Spot,
656
657    /// Specific instrument subscription (e.g., "instrument:XBTUSD").
658    #[serde(rename = "instrument")]
659    #[serde(untagged)]
660    Specific(String),
661}
662
663impl BitmexProductType {
664    /// Converts the product type to its websocket subscription string
665    #[must_use]
666    pub fn to_subscription(&self) -> String {
667        match self {
668            Self::All => "instrument".to_string(),
669            Self::Specific(symbol) => format!("instrument:{symbol}"),
670            Self::Contracts => "CONTRACTS".to_string(),
671            Self::Indices => "INDICES".to_string(),
672            Self::Derivatives => "DERIVATIVES".to_string(),
673            Self::Spot => "SPOT".to_string(),
674        }
675    }
676}
677
678impl<'de> Deserialize<'de> for BitmexProductType {
679    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
680    where
681        D: Deserializer<'de>,
682    {
683        let s = String::deserialize(deserializer)?;
684
685        match s.as_str() {
686            "instrument" => Ok(Self::All),
687            "CONTRACTS" => Ok(Self::Contracts),
688            "INDICES" => Ok(Self::Indices),
689            "DERIVATIVES" => Ok(Self::Derivatives),
690            "SPOT" => Ok(Self::Spot),
691            s if s.starts_with("instrument:") => {
692                let symbol = s.strip_prefix("instrument:").unwrap();
693                Ok(Self::Specific(symbol.to_string()))
694            }
695            _ => Err(serde::de::Error::custom(format!(
696                "Invalid product type: {s}"
697            ))),
698        }
699    }
700}
701
702/// Represents the tick direction of the last trade.
703#[derive(
704    Copy,
705    Clone,
706    Debug,
707    Display,
708    PartialEq,
709    Eq,
710    AsRefStr,
711    EnumIter,
712    EnumString,
713    Serialize,
714    Deserialize,
715)]
716pub enum BitmexTickDirection {
717    /// Price increased on last trade.
718    PlusTick,
719    /// Price decreased on last trade.
720    MinusTick,
721    /// Price unchanged, but previous tick was plus.
722    ZeroPlusTick,
723    /// Price unchanged, but previous tick was minus.
724    ZeroMinusTick,
725}
726
727/// Represents the state of an instrument.
728#[derive(
729    Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
730)]
731pub enum BitmexInstrumentState {
732    /// Instrument is open for trading.
733    Open,
734    /// Instrument is closed for trading.
735    Closed,
736    /// Instrument is unlisted.
737    Unlisted,
738    /// Instrument is settled.
739    Settled,
740}
741
742/// Represents the fair price calculation method.
743#[derive(
744    Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
745)]
746pub enum BitmexFairMethod {
747    /// Funding rate based.
748    FundingRate,
749    /// Impact mid price.
750    ImpactMidPrice,
751    /// Last price.
752    LastPrice,
753}
754
755/// Represents the mark price calculation method.
756#[derive(
757    Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
758)]
759pub enum BitmexMarkMethod {
760    /// Fair price.
761    FairPrice,
762    /// Last price.
763    LastPrice,
764    /// Composite index.
765    CompositeIndex,
766}
767
768////////////////////////////////////////////////////////////////////////////////
769// Tests
770////////////////////////////////////////////////////////////////////////////////
771
772#[cfg(test)]
773mod tests {
774    use rstest::rstest;
775
776    use super::*;
777
778    #[rstest]
779    fn test_bitmex_side_deserialization() {
780        // Test all case variations
781        assert_eq!(
782            serde_json::from_str::<BitmexSide>(r#""Buy""#).unwrap(),
783            BitmexSide::Buy
784        );
785        assert_eq!(
786            serde_json::from_str::<BitmexSide>(r#""BUY""#).unwrap(),
787            BitmexSide::Buy
788        );
789        assert_eq!(
790            serde_json::from_str::<BitmexSide>(r#""buy""#).unwrap(),
791            BitmexSide::Buy
792        );
793        assert_eq!(
794            serde_json::from_str::<BitmexSide>(r#""Sell""#).unwrap(),
795            BitmexSide::Sell
796        );
797        assert_eq!(
798            serde_json::from_str::<BitmexSide>(r#""SELL""#).unwrap(),
799            BitmexSide::Sell
800        );
801        assert_eq!(
802            serde_json::from_str::<BitmexSide>(r#""sell""#).unwrap(),
803            BitmexSide::Sell
804        );
805    }
806
807    #[rstest]
808    fn test_bitmex_order_type_deserialization() {
809        assert_eq!(
810            serde_json::from_str::<BitmexOrderType>(r#""Market""#).unwrap(),
811            BitmexOrderType::Market
812        );
813        assert_eq!(
814            serde_json::from_str::<BitmexOrderType>(r#""Limit""#).unwrap(),
815            BitmexOrderType::Limit
816        );
817        assert_eq!(
818            serde_json::from_str::<BitmexOrderType>(r#""Stop""#).unwrap(),
819            BitmexOrderType::Stop
820        );
821        assert_eq!(
822            serde_json::from_str::<BitmexOrderType>(r#""StopLimit""#).unwrap(),
823            BitmexOrderType::StopLimit
824        );
825        assert_eq!(
826            serde_json::from_str::<BitmexOrderType>(r#""MarketIfTouched""#).unwrap(),
827            BitmexOrderType::MarketIfTouched
828        );
829        assert_eq!(
830            serde_json::from_str::<BitmexOrderType>(r#""LimitIfTouched""#).unwrap(),
831            BitmexOrderType::LimitIfTouched
832        );
833        assert_eq!(
834            serde_json::from_str::<BitmexOrderType>(r#""Pegged""#).unwrap(),
835            BitmexOrderType::Pegged
836        );
837    }
838
839    #[rstest]
840    fn test_instrument_type_serialization() {
841        assert_eq!(
842            serde_json::to_string(&BitmexInstrumentType::PerpetualContract).unwrap(),
843            r#""FFWCSX""#
844        );
845        assert_eq!(
846            serde_json::to_string(&BitmexInstrumentType::PerpetualContractFx).unwrap(),
847            r#""FFWCSF""#
848        );
849        assert_eq!(
850            serde_json::to_string(&BitmexInstrumentType::Spot).unwrap(),
851            r#""IFXXXP""#
852        );
853        assert_eq!(
854            serde_json::to_string(&BitmexInstrumentType::Futures).unwrap(),
855            r#""FFCCSX""#
856        );
857        assert_eq!(
858            serde_json::to_string(&BitmexInstrumentType::BasketIndex).unwrap(),
859            r#""MRBXXX""#
860        );
861        assert_eq!(
862            serde_json::to_string(&BitmexInstrumentType::CryptoIndex).unwrap(),
863            r#""MRCXXX""#
864        );
865        assert_eq!(
866            serde_json::to_string(&BitmexInstrumentType::FxIndex).unwrap(),
867            r#""MRFXXX""#
868        );
869        assert_eq!(
870            serde_json::to_string(&BitmexInstrumentType::LendingIndex).unwrap(),
871            r#""MRRXXX""#
872        );
873        assert_eq!(
874            serde_json::to_string(&BitmexInstrumentType::VolatilityIndex).unwrap(),
875            r#""MRIXXX""#
876        );
877        assert_eq!(
878            serde_json::to_string(&BitmexInstrumentType::PredictionMarket).unwrap(),
879            r#""FFICSX""#
880        );
881    }
882
883    #[rstest]
884    fn test_instrument_type_deserialization() {
885        assert_eq!(
886            serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSX""#).unwrap(),
887            BitmexInstrumentType::PerpetualContract
888        );
889        assert_eq!(
890            serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSF""#).unwrap(),
891            BitmexInstrumentType::PerpetualContractFx
892        );
893        assert_eq!(
894            serde_json::from_str::<BitmexInstrumentType>(r#""IFXXXP""#).unwrap(),
895            BitmexInstrumentType::Spot
896        );
897        assert_eq!(
898            serde_json::from_str::<BitmexInstrumentType>(r#""FFCCSX""#).unwrap(),
899            BitmexInstrumentType::Futures
900        );
901        assert_eq!(
902            serde_json::from_str::<BitmexInstrumentType>(r#""MRBXXX""#).unwrap(),
903            BitmexInstrumentType::BasketIndex
904        );
905        assert_eq!(
906            serde_json::from_str::<BitmexInstrumentType>(r#""MRCXXX""#).unwrap(),
907            BitmexInstrumentType::CryptoIndex
908        );
909        assert_eq!(
910            serde_json::from_str::<BitmexInstrumentType>(r#""MRFXXX""#).unwrap(),
911            BitmexInstrumentType::FxIndex
912        );
913        assert_eq!(
914            serde_json::from_str::<BitmexInstrumentType>(r#""MRRXXX""#).unwrap(),
915            BitmexInstrumentType::LendingIndex
916        );
917        assert_eq!(
918            serde_json::from_str::<BitmexInstrumentType>(r#""MRIXXX""#).unwrap(),
919            BitmexInstrumentType::VolatilityIndex
920        );
921        assert_eq!(
922            serde_json::from_str::<BitmexInstrumentType>(r#""FFICSX""#).unwrap(),
923            BitmexInstrumentType::PredictionMarket
924        );
925
926        // Error case
927        assert!(serde_json::from_str::<BitmexInstrumentType>(r#""INVALID""#).is_err());
928    }
929
930    #[rstest]
931    fn test_subscription_strings() {
932        assert_eq!(BitmexProductType::All.to_subscription(), "instrument");
933        assert_eq!(
934            BitmexProductType::Specific("XBTUSD".to_string()).to_subscription(),
935            "instrument:XBTUSD"
936        );
937        assert_eq!(BitmexProductType::Contracts.to_subscription(), "CONTRACTS");
938        assert_eq!(BitmexProductType::Indices.to_subscription(), "INDICES");
939        assert_eq!(
940            BitmexProductType::Derivatives.to_subscription(),
941            "DERIVATIVES"
942        );
943        assert_eq!(BitmexProductType::Spot.to_subscription(), "SPOT");
944    }
945
946    #[rstest]
947    fn test_serialization() {
948        // Test serialization
949        assert_eq!(
950            serde_json::to_string(&BitmexProductType::All).unwrap(),
951            r#""instrument""#
952        );
953        assert_eq!(
954            serde_json::to_string(&BitmexProductType::Specific("XBTUSD".to_string())).unwrap(),
955            r#""XBTUSD""#
956        );
957        assert_eq!(
958            serde_json::to_string(&BitmexProductType::Contracts).unwrap(),
959            r#""CONTRACTS""#
960        );
961    }
962
963    #[rstest]
964    fn test_deserialization() {
965        assert_eq!(
966            serde_json::from_str::<BitmexProductType>(r#""instrument""#).unwrap(),
967            BitmexProductType::All
968        );
969        assert_eq!(
970            serde_json::from_str::<BitmexProductType>(r#""instrument:XBTUSD""#).unwrap(),
971            BitmexProductType::Specific("XBTUSD".to_string())
972        );
973        assert_eq!(
974            serde_json::from_str::<BitmexProductType>(r#""CONTRACTS""#).unwrap(),
975            BitmexProductType::Contracts
976        );
977    }
978
979    #[rstest]
980    fn test_error_cases() {
981        assert!(serde_json::from_str::<BitmexProductType>(r#""invalid_type""#).is_err());
982        assert!(serde_json::from_str::<BitmexProductType>(r"123").is_err());
983        assert!(serde_json::from_str::<BitmexProductType>(r"{}").is_err());
984    }
985
986    #[rstest]
987    fn test_order_side_try_from() {
988        // Valid conversions
989        assert_eq!(
990            BitmexSide::try_from(OrderSide::Buy).unwrap(),
991            BitmexSide::Buy
992        );
993        assert_eq!(
994            BitmexSide::try_from(OrderSide::Sell).unwrap(),
995            BitmexSide::Sell
996        );
997
998        // Invalid conversions
999        let result = BitmexSide::try_from(OrderSide::NoOrderSide);
1000        assert!(result.is_err());
1001        match result {
1002            Err(BitmexError::NonRetryable {
1003                source: BitmexNonRetryableError::Validation { field, .. },
1004                ..
1005            }) => {
1006                assert_eq!(field, "order_side");
1007            }
1008            _ => panic!("Expected validation error"),
1009        }
1010    }
1011
1012    #[rstest]
1013    fn test_order_type_try_from() {
1014        // Valid conversions
1015        assert_eq!(
1016            BitmexOrderType::try_from(OrderType::Market).unwrap(),
1017            BitmexOrderType::Market
1018        );
1019        assert_eq!(
1020            BitmexOrderType::try_from(OrderType::Limit).unwrap(),
1021            BitmexOrderType::Limit
1022        );
1023
1024        // MarketToLimit should fail
1025        let result = BitmexOrderType::try_from(OrderType::MarketToLimit);
1026        assert!(result.is_err());
1027        match result {
1028            Err(BitmexError::NonRetryable {
1029                source: BitmexNonRetryableError::Validation { message, .. },
1030                ..
1031            }) => {
1032                assert!(message.contains("not supported"));
1033            }
1034            _ => panic!("Expected validation error"),
1035        }
1036    }
1037
1038    #[rstest]
1039    fn test_time_in_force_conversions() {
1040        // BitMEX to Nautilus (all supported variants)
1041        assert_eq!(
1042            TimeInForce::try_from(BitmexTimeInForce::Day).unwrap(),
1043            TimeInForce::Day
1044        );
1045        assert_eq!(
1046            TimeInForce::try_from(BitmexTimeInForce::GoodTillCancel).unwrap(),
1047            TimeInForce::Gtc
1048        );
1049        assert_eq!(
1050            TimeInForce::try_from(BitmexTimeInForce::ImmediateOrCancel).unwrap(),
1051            TimeInForce::Ioc
1052        );
1053
1054        // Unsupported BitMEX variants should fail
1055        let result = TimeInForce::try_from(BitmexTimeInForce::GoodTillCrossing);
1056        assert!(result.is_err());
1057        match result {
1058            Err(BitmexError::NonRetryable {
1059                source: BitmexNonRetryableError::Validation { field, message },
1060                ..
1061            }) => {
1062                assert_eq!(field, "time_in_force");
1063                assert!(message.contains("Unsupported"));
1064            }
1065            _ => panic!("Expected validation error"),
1066        }
1067
1068        // Nautilus to BitMEX (all supported variants)
1069        assert_eq!(
1070            BitmexTimeInForce::try_from(TimeInForce::Day).unwrap(),
1071            BitmexTimeInForce::Day
1072        );
1073        assert_eq!(
1074            BitmexTimeInForce::try_from(TimeInForce::Gtc).unwrap(),
1075            BitmexTimeInForce::GoodTillCancel
1076        );
1077        assert_eq!(
1078            BitmexTimeInForce::try_from(TimeInForce::Fok).unwrap(),
1079            BitmexTimeInForce::FillOrKill
1080        );
1081    }
1082
1083    #[rstest]
1084    fn test_helper_methods() {
1085        // Test try_from_order_side helper
1086        let result = BitmexSide::try_from_order_side(OrderSide::Buy);
1087        assert!(result.is_ok());
1088        assert_eq!(result.unwrap(), BitmexSide::Buy);
1089
1090        let result = BitmexSide::try_from_order_side(OrderSide::NoOrderSide);
1091        assert!(result.is_err());
1092
1093        // Test try_from_order_type helper
1094        let result = BitmexOrderType::try_from_order_type(OrderType::Limit);
1095        assert!(result.is_ok());
1096        assert_eq!(result.unwrap(), BitmexOrderType::Limit);
1097
1098        let result = BitmexOrderType::try_from_order_type(OrderType::MarketToLimit);
1099        assert!(result.is_err());
1100
1101        // Test try_from_time_in_force helper
1102        let result = BitmexTimeInForce::try_from_time_in_force(TimeInForce::Ioc);
1103        assert!(result.is_ok());
1104        assert_eq!(result.unwrap(), BitmexTimeInForce::ImmediateOrCancel);
1105    }
1106}