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