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