Skip to main content

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