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