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.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.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        Self::try_from(value)
370    }
371}
372
373/// Represents the available contingency types on BitMEX.
374#[derive(
375    Copy,
376    Clone,
377    Debug,
378    Display,
379    PartialEq,
380    Eq,
381    AsRefStr,
382    EnumIter,
383    EnumString,
384    Serialize,
385    Deserialize,
386)]
387pub enum BitmexContingencyType {
388    OneCancelsTheOther,
389    OneTriggersTheOther,
390    OneUpdatesTheOtherAbsolute,
391    OneUpdatesTheOtherProportional,
392    #[serde(rename = "")]
393    Unknown, // Can be empty
394}
395
396impl From<BitmexContingencyType> for ContingencyType {
397    fn from(value: BitmexContingencyType) -> Self {
398        match value {
399            BitmexContingencyType::OneCancelsTheOther => Self::Oco,
400            BitmexContingencyType::OneTriggersTheOther => Self::Oto,
401            BitmexContingencyType::OneUpdatesTheOtherProportional => Self::Ouo,
402            BitmexContingencyType::OneUpdatesTheOtherAbsolute => Self::Ouo,
403            BitmexContingencyType::Unknown => Self::NoContingency,
404        }
405    }
406}
407
408impl TryFrom<ContingencyType> for BitmexContingencyType {
409    type Error = anyhow::Error;
410
411    fn try_from(value: ContingencyType) -> Result<Self, Self::Error> {
412        match value {
413            ContingencyType::NoContingency => Ok(Self::Unknown),
414            ContingencyType::Oco => Ok(Self::OneCancelsTheOther),
415            ContingencyType::Oto => Ok(Self::OneTriggersTheOther),
416            ContingencyType::Ouo => anyhow::bail!("OUO contingency type not supported by BitMEX"),
417        }
418    }
419}
420
421/// Represents the available peg price types on BitMEX.
422#[derive(
423    Copy,
424    Clone,
425    Debug,
426    Display,
427    PartialEq,
428    Eq,
429    AsRefStr,
430    EnumIter,
431    EnumString,
432    Serialize,
433    Deserialize,
434)]
435pub enum BitmexPegPriceType {
436    LastPeg,
437    OpeningPeg,
438    MidPricePeg,
439    MarketPeg,
440    PrimaryPeg,
441    PegToVWAP,
442    TrailingStopPeg,
443    PegToLimitPrice,
444    ShortSaleMinPricePeg,
445    #[serde(rename = "")]
446    Unknown, // Can be empty
447}
448
449/// Represents the available execution instruments on BitMEX.
450#[derive(
451    Copy,
452    Clone,
453    Debug,
454    Display,
455    PartialEq,
456    Eq,
457    AsRefStr,
458    EnumIter,
459    EnumString,
460    Serialize,
461    Deserialize,
462)]
463pub enum BitmexExecInstruction {
464    ParticipateDoNotInitiate,
465    AllOrNone,
466    MarkPrice,
467    IndexPrice,
468    LastPrice,
469    Close,
470    ReduceOnly,
471    Fixed,
472    #[serde(rename = "")]
473    Unknown, // Can be empty
474}
475
476impl BitmexExecInstruction {
477    /// Joins execution instructions into the comma-separated string expected by BitMEX.
478    pub fn join(instructions: &[Self]) -> String {
479        instructions
480            .iter()
481            .map(ToString::to_string)
482            .collect::<Vec<_>>()
483            .join(",")
484    }
485}
486
487/// Represents the type of execution that generated a trade.
488#[derive(Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize)]
489pub enum BitmexExecType {
490    /// New order placed.
491    New,
492    /// Normal trade execution.
493    Trade,
494    /// Order canceled.
495    Canceled,
496    /// Cancel request rejected.
497    CancelReject,
498    /// Order replaced.
499    Replaced,
500    /// Order rejected.
501    Rejected,
502    /// Order amendment rejected.
503    AmendReject,
504    /// Funding rate execution.
505    Funding,
506    /// Settlement execution.
507    Settlement,
508    /// Order suspended.
509    Suspended,
510    /// Order released.
511    Released,
512    /// Insurance payment.
513    Insurance,
514    /// Rebalance.
515    Rebalance,
516    /// Liquidation execution.
517    Liquidation,
518    /// Bankruptcy execution.
519    Bankruptcy,
520    /// Trial fill (testnet only).
521    TrialFill,
522    /// Stop/trigger order activated by system.
523    TriggeredOrActivatedBySystem,
524    /// Unknown execution type (not yet supported).
525    #[strum(disabled)]
526    Unknown(String),
527}
528
529impl<'de> Deserialize<'de> for BitmexExecType {
530    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
531    where
532        D: Deserializer<'de>,
533    {
534        let s = String::deserialize(deserializer)?;
535
536        match s.as_str() {
537            "New" => Ok(Self::New),
538            "Trade" => Ok(Self::Trade),
539            "Canceled" => Ok(Self::Canceled),
540            "CancelReject" => Ok(Self::CancelReject),
541            "Replaced" => Ok(Self::Replaced),
542            "Rejected" => Ok(Self::Rejected),
543            "AmendReject" => Ok(Self::AmendReject),
544            "Funding" => Ok(Self::Funding),
545            "Settlement" => Ok(Self::Settlement),
546            "Suspended" => Ok(Self::Suspended),
547            "Released" => Ok(Self::Released),
548            "Insurance" => Ok(Self::Insurance),
549            "Rebalance" => Ok(Self::Rebalance),
550            "Liquidation" => Ok(Self::Liquidation),
551            "Bankruptcy" => Ok(Self::Bankruptcy),
552            "TrialFill" => Ok(Self::TrialFill),
553            "TriggeredOrActivatedBySystem" => Ok(Self::TriggeredOrActivatedBySystem),
554            other => Ok(Self::Unknown(other.to_string())),
555        }
556    }
557}
558
559/// Indicates whether the execution was maker or taker.
560#[derive(
561    Copy,
562    Clone,
563    Debug,
564    Display,
565    PartialEq,
566    Eq,
567    AsRefStr,
568    EnumIter,
569    EnumString,
570    Serialize,
571    Deserialize,
572)]
573pub enum BitmexLiquidityIndicator {
574    /// Provided liquidity to the order book (maker).
575    /// BitMEX returns "Added" in REST API responses and "AddedLiquidity" in WebSocket messages.
576    #[serde(rename = "Added")]
577    #[serde(alias = "AddedLiquidity")]
578    Maker,
579    /// Took liquidity from the order book (taker).
580    /// BitMEX returns "Removed" in REST API responses and "RemovedLiquidity" in WebSocket messages.
581    #[serde(rename = "Removed")]
582    #[serde(alias = "RemovedLiquidity")]
583    Taker,
584}
585
586impl From<BitmexLiquidityIndicator> for LiquiditySide {
587    fn from(value: BitmexLiquidityIndicator) -> Self {
588        match value {
589            BitmexLiquidityIndicator::Maker => Self::Maker,
590            BitmexLiquidityIndicator::Taker => Self::Taker,
591        }
592    }
593}
594
595/// Represents BitMEX instrument types (CFI codes).
596///
597/// The CFI (Classification of Financial Instruments) code is a 6-character code
598/// following ISO 10962 standard that classifies financial instruments.
599///
600/// See: <https://support.bitmex.com/hc/en-gb/articles/6299296145565-What-are-the-Typ-Values-for-Instrument-endpoint>
601#[derive(
602    Copy,
603    Clone,
604    Debug,
605    Display,
606    PartialEq,
607    Eq,
608    AsRefStr,
609    EnumIter,
610    EnumString,
611    Serialize,
612    Deserialize,
613)]
614#[serde(rename_all = "UPPERCASE")]
615pub enum BitmexInstrumentType {
616    /// Legacy futures (settled).
617    #[serde(rename = "FXXXS")]
618    LegacyFutures,
619
620    /// Legacy futures (settled, variant).
621    #[serde(rename = "FXXXN")]
622    LegacyFuturesN,
623
624    /// Futures spreads (settled).
625    #[serde(rename = "FMXXS")]
626    FuturesSpreads,
627
628    /// Prediction Markets (non-standardized financial future on index, cash settled).
629    /// CFI code FFICSX - traders predict outcomes of events.
630    #[serde(rename = "FFICSX")]
631    PredictionMarket,
632
633    /// Stock-based Perpetual Contracts (e.g., SPY, equity derivatives).
634    /// CFI code FFSCSX - financial future on stocks, cash settled.
635    #[serde(rename = "FFSCSX")]
636    StockPerpetual,
637
638    /// Perpetual Contracts (crypto).
639    #[serde(rename = "FFWCSX")]
640    PerpetualContract,
641
642    /// Perpetual Contracts (FX underliers).
643    #[serde(rename = "FFWCSF")]
644    PerpetualContractFx,
645
646    /// Futures (calendar futures, cash settled).
647    #[serde(rename = "FFCCSX")]
648    Futures,
649
650    /// Spot trading pairs.
651    #[serde(rename = "IFXXXP")]
652    Spot,
653
654    /// Call options (European, cash settled).
655    #[serde(rename = "OCECCS")]
656    CallOption,
657
658    /// Put options (European, cash settled).
659    #[serde(rename = "OPECCS")]
660    PutOption,
661
662    /// Swap rate contracts (yield products).
663    #[serde(rename = "SRMCSX")]
664    SwapRate,
665
666    /// Reference basket contracts.
667    #[serde(rename = "RCSXXX")]
668    ReferenceBasket,
669
670    /// BitMEX Basket Index.
671    #[serde(rename = "MRBXXX")]
672    BasketIndex,
673
674    /// BitMEX Crypto Index.
675    #[serde(rename = "MRCXXX")]
676    CryptoIndex,
677
678    /// BitMEX FX Index.
679    #[serde(rename = "MRFXXX")]
680    FxIndex,
681
682    /// BitMEX Lending/Premium Index.
683    #[serde(rename = "MRRXXX")]
684    LendingIndex,
685
686    /// BitMEX Volatility Index.
687    #[serde(rename = "MRIXXX")]
688    VolatilityIndex,
689
690    /// BitMEX Stock/Securities Index.
691    #[serde(rename = "MRSXXX")]
692    StockIndex,
693
694    /// BitMEX Yield/Dividend Index.
695    #[serde(rename = "MRVDXX")]
696    YieldIndex,
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}
818
819impl From<&BitmexInstrumentState> for MarketStatusAction {
820    fn from(state: &BitmexInstrumentState) -> Self {
821        match state {
822            BitmexInstrumentState::Open => Self::Trading,
823            BitmexInstrumentState::Closed => Self::Close,
824            BitmexInstrumentState::Settled => Self::Close,
825            BitmexInstrumentState::Unlisted => Self::NotAvailableForTrading,
826            BitmexInstrumentState::Delisted => Self::NotAvailableForTrading,
827        }
828    }
829}
830
831/// Represents the fair price calculation method.
832#[derive(
833    Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
834)]
835pub enum BitmexFairMethod {
836    /// Funding rate based.
837    FundingRate,
838    /// Impact mid price.
839    ImpactMidPrice,
840    /// Last price.
841    LastPrice,
842}
843
844/// Represents the mark price calculation method.
845#[derive(
846    Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
847)]
848pub enum BitmexMarkMethod {
849    /// Fair price.
850    FairPrice,
851    /// Fair price for stock-based perpetuals.
852    FairPriceStox,
853    /// Last price.
854    LastPrice,
855    /// Last price for pre-launch instruments.
856    LastPricePreLaunch,
857    /// Composite index.
858    CompositeIndex,
859}
860
861/// BitMEX API environment.
862#[derive(
863    Copy,
864    Clone,
865    Debug,
866    Default,
867    Display,
868    PartialEq,
869    Eq,
870    Hash,
871    AsRefStr,
872    EnumIter,
873    EnumString,
874    Serialize,
875    Deserialize,
876)]
877#[serde(rename_all = "lowercase")]
878#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
879#[cfg_attr(
880    feature = "python",
881    pyo3::pyclass(
882        eq,
883        eq_int,
884        module = "nautilus_trader.core.nautilus_pyo3.bitmex",
885        from_py_object,
886        rename_all = "SCREAMING_SNAKE_CASE",
887    )
888)]
889#[cfg_attr(
890    feature = "python",
891    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bitmex")
892)]
893pub enum BitmexEnvironment {
894    /// Live trading environment.
895    #[default]
896    Mainnet,
897    /// Testnet environment.
898    Testnet,
899}
900
901#[cfg(test)]
902mod tests {
903    use rstest::rstest;
904
905    use super::*;
906
907    #[rstest]
908    fn test_bitmex_side_deserialization() {
909        // Test all case variations
910        assert_eq!(
911            serde_json::from_str::<BitmexSide>(r#""Buy""#).unwrap(),
912            BitmexSide::Buy
913        );
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#""Sell""#).unwrap(),
924            BitmexSide::Sell
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    }
935
936    #[rstest]
937    fn test_bitmex_order_type_deserialization() {
938        assert_eq!(
939            serde_json::from_str::<BitmexOrderType>(r#""Market""#).unwrap(),
940            BitmexOrderType::Market
941        );
942        assert_eq!(
943            serde_json::from_str::<BitmexOrderType>(r#""Limit""#).unwrap(),
944            BitmexOrderType::Limit
945        );
946        assert_eq!(
947            serde_json::from_str::<BitmexOrderType>(r#""Stop""#).unwrap(),
948            BitmexOrderType::Stop
949        );
950        assert_eq!(
951            serde_json::from_str::<BitmexOrderType>(r#""StopLimit""#).unwrap(),
952            BitmexOrderType::StopLimit
953        );
954        assert_eq!(
955            serde_json::from_str::<BitmexOrderType>(r#""MarketIfTouched""#).unwrap(),
956            BitmexOrderType::MarketIfTouched
957        );
958        assert_eq!(
959            serde_json::from_str::<BitmexOrderType>(r#""LimitIfTouched""#).unwrap(),
960            BitmexOrderType::LimitIfTouched
961        );
962        assert_eq!(
963            serde_json::from_str::<BitmexOrderType>(r#""Pegged""#).unwrap(),
964            BitmexOrderType::Pegged
965        );
966    }
967
968    #[rstest]
969    fn test_instrument_type_serialization() {
970        // Tradeable instruments
971        assert_eq!(
972            serde_json::to_string(&BitmexInstrumentType::PerpetualContract).unwrap(),
973            r#""FFWCSX""#
974        );
975        assert_eq!(
976            serde_json::to_string(&BitmexInstrumentType::PerpetualContractFx).unwrap(),
977            r#""FFWCSF""#
978        );
979        assert_eq!(
980            serde_json::to_string(&BitmexInstrumentType::StockPerpetual).unwrap(),
981            r#""FFSCSX""#
982        );
983        assert_eq!(
984            serde_json::to_string(&BitmexInstrumentType::Spot).unwrap(),
985            r#""IFXXXP""#
986        );
987        assert_eq!(
988            serde_json::to_string(&BitmexInstrumentType::Futures).unwrap(),
989            r#""FFCCSX""#
990        );
991        assert_eq!(
992            serde_json::to_string(&BitmexInstrumentType::PredictionMarket).unwrap(),
993            r#""FFICSX""#
994        );
995        assert_eq!(
996            serde_json::to_string(&BitmexInstrumentType::CallOption).unwrap(),
997            r#""OCECCS""#
998        );
999        assert_eq!(
1000            serde_json::to_string(&BitmexInstrumentType::PutOption).unwrap(),
1001            r#""OPECCS""#
1002        );
1003        assert_eq!(
1004            serde_json::to_string(&BitmexInstrumentType::SwapRate).unwrap(),
1005            r#""SRMCSX""#
1006        );
1007
1008        // Legacy instruments
1009        assert_eq!(
1010            serde_json::to_string(&BitmexInstrumentType::LegacyFutures).unwrap(),
1011            r#""FXXXS""#
1012        );
1013        assert_eq!(
1014            serde_json::to_string(&BitmexInstrumentType::LegacyFuturesN).unwrap(),
1015            r#""FXXXN""#
1016        );
1017        assert_eq!(
1018            serde_json::to_string(&BitmexInstrumentType::FuturesSpreads).unwrap(),
1019            r#""FMXXS""#
1020        );
1021        assert_eq!(
1022            serde_json::to_string(&BitmexInstrumentType::ReferenceBasket).unwrap(),
1023            r#""RCSXXX""#
1024        );
1025
1026        // Index types
1027        assert_eq!(
1028            serde_json::to_string(&BitmexInstrumentType::BasketIndex).unwrap(),
1029            r#""MRBXXX""#
1030        );
1031        assert_eq!(
1032            serde_json::to_string(&BitmexInstrumentType::CryptoIndex).unwrap(),
1033            r#""MRCXXX""#
1034        );
1035        assert_eq!(
1036            serde_json::to_string(&BitmexInstrumentType::FxIndex).unwrap(),
1037            r#""MRFXXX""#
1038        );
1039        assert_eq!(
1040            serde_json::to_string(&BitmexInstrumentType::LendingIndex).unwrap(),
1041            r#""MRRXXX""#
1042        );
1043        assert_eq!(
1044            serde_json::to_string(&BitmexInstrumentType::VolatilityIndex).unwrap(),
1045            r#""MRIXXX""#
1046        );
1047        assert_eq!(
1048            serde_json::to_string(&BitmexInstrumentType::StockIndex).unwrap(),
1049            r#""MRSXXX""#
1050        );
1051        assert_eq!(
1052            serde_json::to_string(&BitmexInstrumentType::YieldIndex).unwrap(),
1053            r#""MRVDXX""#
1054        );
1055    }
1056
1057    #[rstest]
1058    fn test_instrument_type_deserialization() {
1059        // Tradeable instruments
1060        assert_eq!(
1061            serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSX""#).unwrap(),
1062            BitmexInstrumentType::PerpetualContract
1063        );
1064        assert_eq!(
1065            serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSF""#).unwrap(),
1066            BitmexInstrumentType::PerpetualContractFx
1067        );
1068        assert_eq!(
1069            serde_json::from_str::<BitmexInstrumentType>(r#""FFSCSX""#).unwrap(),
1070            BitmexInstrumentType::StockPerpetual
1071        );
1072        assert_eq!(
1073            serde_json::from_str::<BitmexInstrumentType>(r#""IFXXXP""#).unwrap(),
1074            BitmexInstrumentType::Spot
1075        );
1076        assert_eq!(
1077            serde_json::from_str::<BitmexInstrumentType>(r#""FFCCSX""#).unwrap(),
1078            BitmexInstrumentType::Futures
1079        );
1080        assert_eq!(
1081            serde_json::from_str::<BitmexInstrumentType>(r#""FFICSX""#).unwrap(),
1082            BitmexInstrumentType::PredictionMarket
1083        );
1084        assert_eq!(
1085            serde_json::from_str::<BitmexInstrumentType>(r#""OCECCS""#).unwrap(),
1086            BitmexInstrumentType::CallOption
1087        );
1088        assert_eq!(
1089            serde_json::from_str::<BitmexInstrumentType>(r#""OPECCS""#).unwrap(),
1090            BitmexInstrumentType::PutOption
1091        );
1092        assert_eq!(
1093            serde_json::from_str::<BitmexInstrumentType>(r#""SRMCSX""#).unwrap(),
1094            BitmexInstrumentType::SwapRate
1095        );
1096
1097        // Legacy instruments
1098        assert_eq!(
1099            serde_json::from_str::<BitmexInstrumentType>(r#""FXXXS""#).unwrap(),
1100            BitmexInstrumentType::LegacyFutures
1101        );
1102        assert_eq!(
1103            serde_json::from_str::<BitmexInstrumentType>(r#""FXXXN""#).unwrap(),
1104            BitmexInstrumentType::LegacyFuturesN
1105        );
1106        assert_eq!(
1107            serde_json::from_str::<BitmexInstrumentType>(r#""FMXXS""#).unwrap(),
1108            BitmexInstrumentType::FuturesSpreads
1109        );
1110        assert_eq!(
1111            serde_json::from_str::<BitmexInstrumentType>(r#""RCSXXX""#).unwrap(),
1112            BitmexInstrumentType::ReferenceBasket
1113        );
1114
1115        // Index types
1116        assert_eq!(
1117            serde_json::from_str::<BitmexInstrumentType>(r#""MRBXXX""#).unwrap(),
1118            BitmexInstrumentType::BasketIndex
1119        );
1120        assert_eq!(
1121            serde_json::from_str::<BitmexInstrumentType>(r#""MRCXXX""#).unwrap(),
1122            BitmexInstrumentType::CryptoIndex
1123        );
1124        assert_eq!(
1125            serde_json::from_str::<BitmexInstrumentType>(r#""MRFXXX""#).unwrap(),
1126            BitmexInstrumentType::FxIndex
1127        );
1128        assert_eq!(
1129            serde_json::from_str::<BitmexInstrumentType>(r#""MRRXXX""#).unwrap(),
1130            BitmexInstrumentType::LendingIndex
1131        );
1132        assert_eq!(
1133            serde_json::from_str::<BitmexInstrumentType>(r#""MRIXXX""#).unwrap(),
1134            BitmexInstrumentType::VolatilityIndex
1135        );
1136        assert_eq!(
1137            serde_json::from_str::<BitmexInstrumentType>(r#""MRSXXX""#).unwrap(),
1138            BitmexInstrumentType::StockIndex
1139        );
1140        assert_eq!(
1141            serde_json::from_str::<BitmexInstrumentType>(r#""MRVDXX""#).unwrap(),
1142            BitmexInstrumentType::YieldIndex
1143        );
1144
1145        // Error case
1146        assert!(serde_json::from_str::<BitmexInstrumentType>(r#""INVALID""#).is_err());
1147    }
1148
1149    #[rstest]
1150    fn test_subscription_strings() {
1151        assert_eq!(BitmexProductType::All.to_subscription(), "instrument");
1152        assert_eq!(
1153            BitmexProductType::Specific("XBTUSD".to_string()).to_subscription(),
1154            "instrument:XBTUSD"
1155        );
1156        assert_eq!(BitmexProductType::Contracts.to_subscription(), "CONTRACTS");
1157        assert_eq!(BitmexProductType::Indices.to_subscription(), "INDICES");
1158        assert_eq!(
1159            BitmexProductType::Derivatives.to_subscription(),
1160            "DERIVATIVES"
1161        );
1162        assert_eq!(BitmexProductType::Spot.to_subscription(), "SPOT");
1163    }
1164
1165    #[rstest]
1166    fn test_serialization() {
1167        // Test serialization
1168        assert_eq!(
1169            serde_json::to_string(&BitmexProductType::All).unwrap(),
1170            r#""instrument""#
1171        );
1172        assert_eq!(
1173            serde_json::to_string(&BitmexProductType::Specific("XBTUSD".to_string())).unwrap(),
1174            r#""XBTUSD""#
1175        );
1176        assert_eq!(
1177            serde_json::to_string(&BitmexProductType::Contracts).unwrap(),
1178            r#""CONTRACTS""#
1179        );
1180    }
1181
1182    #[rstest]
1183    fn test_deserialization() {
1184        assert_eq!(
1185            serde_json::from_str::<BitmexProductType>(r#""instrument""#).unwrap(),
1186            BitmexProductType::All
1187        );
1188        assert_eq!(
1189            serde_json::from_str::<BitmexProductType>(r#""instrument:XBTUSD""#).unwrap(),
1190            BitmexProductType::Specific("XBTUSD".to_string())
1191        );
1192        assert_eq!(
1193            serde_json::from_str::<BitmexProductType>(r#""CONTRACTS""#).unwrap(),
1194            BitmexProductType::Contracts
1195        );
1196    }
1197
1198    #[rstest]
1199    fn test_error_cases() {
1200        assert!(serde_json::from_str::<BitmexProductType>(r#""invalid_type""#).is_err());
1201        assert!(serde_json::from_str::<BitmexProductType>("123").is_err());
1202        assert!(serde_json::from_str::<BitmexProductType>("{}").is_err());
1203    }
1204
1205    #[rstest]
1206    fn test_order_side_from_specified() {
1207        assert_eq!(BitmexSide::from(OrderSideSpecified::Buy), BitmexSide::Buy);
1208        assert_eq!(BitmexSide::from(OrderSideSpecified::Sell), BitmexSide::Sell);
1209    }
1210
1211    #[rstest]
1212    fn test_order_type_try_from() {
1213        // Valid conversions
1214        assert_eq!(
1215            BitmexOrderType::try_from(OrderType::Market).unwrap(),
1216            BitmexOrderType::Market
1217        );
1218        assert_eq!(
1219            BitmexOrderType::try_from(OrderType::Limit).unwrap(),
1220            BitmexOrderType::Limit
1221        );
1222
1223        // MarketToLimit should fail
1224        let result = BitmexOrderType::try_from(OrderType::MarketToLimit);
1225        assert!(result.is_err());
1226        assert!(result.unwrap_err().to_string().contains("not supported"));
1227    }
1228
1229    #[rstest]
1230    fn test_time_in_force_conversions() {
1231        // BitMEX to Nautilus (all supported variants)
1232        assert_eq!(
1233            TimeInForce::try_from(BitmexTimeInForce::Day).unwrap(),
1234            TimeInForce::Day
1235        );
1236        assert_eq!(
1237            TimeInForce::try_from(BitmexTimeInForce::GoodTillCancel).unwrap(),
1238            TimeInForce::Gtc
1239        );
1240        assert_eq!(
1241            TimeInForce::try_from(BitmexTimeInForce::ImmediateOrCancel).unwrap(),
1242            TimeInForce::Ioc
1243        );
1244
1245        // Unsupported BitMEX variants should fail
1246        let result = TimeInForce::try_from(BitmexTimeInForce::GoodTillCrossing);
1247        assert!(result.is_err());
1248        assert!(result.unwrap_err().to_string().contains("Unsupported"));
1249
1250        // Nautilus to BitMEX (all supported variants)
1251        assert_eq!(
1252            BitmexTimeInForce::try_from(TimeInForce::Day).unwrap(),
1253            BitmexTimeInForce::Day
1254        );
1255        assert_eq!(
1256            BitmexTimeInForce::try_from(TimeInForce::Gtc).unwrap(),
1257            BitmexTimeInForce::GoodTillCancel
1258        );
1259        assert_eq!(
1260            BitmexTimeInForce::try_from(TimeInForce::Fok).unwrap(),
1261            BitmexTimeInForce::FillOrKill
1262        );
1263    }
1264
1265    #[rstest]
1266    fn test_helper_methods() {
1267        // Test try_from_order_type helper
1268        let result = BitmexOrderType::try_from_order_type(OrderType::Limit);
1269        assert!(result.is_ok());
1270        assert_eq!(result.unwrap(), BitmexOrderType::Limit);
1271
1272        let result = BitmexOrderType::try_from_order_type(OrderType::MarketToLimit);
1273        assert!(result.is_err());
1274
1275        // Test try_from_time_in_force helper
1276        let result = BitmexTimeInForce::try_from_time_in_force(TimeInForce::Ioc);
1277        assert!(result.is_ok());
1278        assert_eq!(result.unwrap(), BitmexTimeInForce::ImmediateOrCancel);
1279    }
1280
1281    #[rstest]
1282    #[case(BitmexInstrumentState::Open, MarketStatusAction::Trading)]
1283    #[case(BitmexInstrumentState::Closed, MarketStatusAction::Close)]
1284    #[case(BitmexInstrumentState::Settled, MarketStatusAction::Close)]
1285    #[case(
1286        BitmexInstrumentState::Unlisted,
1287        MarketStatusAction::NotAvailableForTrading
1288    )]
1289    #[case(
1290        BitmexInstrumentState::Delisted,
1291        MarketStatusAction::NotAvailableForTrading
1292    )]
1293    fn test_bitmex_instrument_state_to_market_status_action(
1294        #[case] state: BitmexInstrumentState,
1295        #[case] expected: MarketStatusAction,
1296    ) {
1297        assert_eq!(MarketStatusAction::from(&state), expected);
1298    }
1299}