nautilus_bitmex/common/
enums.rs

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