Skip to main content

nautilus_bybit/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//! Enumerations that model Bybit string/int enums across HTTP and WebSocket payloads.
17
18use std::fmt::Display;
19
20use jiff::{Timestamp, civil::Date, tz::Offset};
21use nautilus_model::enums::{AggressorSide, AssetClass, OrderSide, TriggerType};
22use serde::{Deserialize, Serialize};
23use serde_repr::{Deserialize_repr, Serialize_repr};
24use strum::{AsRefStr, EnumIter, EnumString};
25
26/// Unified margin account status values.
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
28#[repr(i32)]
29pub enum BybitUnifiedMarginStatus {
30    /// Classic account.
31    ClassicAccount = 1,
32    /// Unified trading account 1.0.
33    UnifiedTradingAccount10 = 3,
34    /// Unified trading account 1.0 pro.
35    UnifiedTradingAccount10Pro = 4,
36    /// Unified trading account 2.0.
37    UnifiedTradingAccount20 = 5,
38    /// Unified trading account 2.0 pro.
39    UnifiedTradingAccount20Pro = 6,
40}
41
42/// Margin mode used by Bybit when switching risk profiles.
43#[derive(
44    Clone,
45    Copy,
46    Debug,
47    strum::Display,
48    Eq,
49    PartialEq,
50    Hash,
51    AsRefStr,
52    EnumIter,
53    EnumString,
54    Serialize,
55    Deserialize,
56)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
59#[cfg_attr(
60    feature = "python",
61    pyo3::pyclass(
62        eq,
63        eq_int,
64        rename_all = "SCREAMING_SNAKE_CASE",
65        module = "nautilus_trader.adapters.bybit",
66        from_py_object
67    )
68)]
69#[cfg_attr(
70    feature = "python",
71    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
72)]
73pub enum BybitMarginMode {
74    IsolatedMargin,
75    RegularMargin,
76    PortfolioMargin,
77}
78
79/// Position mode as returned by the v5 API.
80#[derive(
81    Clone,
82    Copy,
83    Debug,
84    strum::Display,
85    Eq,
86    PartialEq,
87    Hash,
88    AsRefStr,
89    EnumIter,
90    EnumString,
91    Serialize_repr,
92    Deserialize_repr,
93)]
94#[repr(i32)]
95#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
96#[cfg_attr(
97    feature = "python",
98    pyo3::pyclass(
99        eq,
100        eq_int,
101        rename_all = "SCREAMING_SNAKE_CASE",
102        module = "nautilus_trader.adapters.bybit",
103        from_py_object
104    )
105)]
106#[cfg_attr(
107    feature = "python",
108    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
109)]
110pub enum BybitPositionMode {
111    /// Merged single position mode.
112    MergedSingle = 0,
113    /// Dual-side hedged position mode.
114    BothSides = 3,
115}
116
117/// Position index values used for hedge mode payloads.
118#[derive(
119    Clone,
120    Copy,
121    Debug,
122    strum::Display,
123    Eq,
124    PartialEq,
125    Hash,
126    AsRefStr,
127    EnumIter,
128    EnumString,
129    Serialize_repr,
130    Deserialize_repr,
131)]
132#[repr(i32)]
133#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
134#[cfg_attr(
135    feature = "python",
136    pyo3::pyclass(
137        eq,
138        eq_int,
139        rename_all = "SCREAMING_SNAKE_CASE",
140        module = "nautilus_trader.adapters.bybit",
141        from_py_object
142    )
143)]
144#[cfg_attr(
145    feature = "python",
146    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
147)]
148pub enum BybitPositionIdx {
149    /// One-way mode position identifier.
150    OneWay = 0,
151    /// Buy side of a hedge-mode position.
152    BuyHedge = 1,
153    /// Sell side of a hedge-mode position.
154    SellHedge = 2,
155}
156
157/// Account type enumeration.
158#[derive(
159    Copy,
160    Clone,
161    Debug,
162    strum::Display,
163    PartialEq,
164    Eq,
165    Hash,
166    AsRefStr,
167    EnumIter,
168    EnumString,
169    Serialize,
170    Deserialize,
171)]
172#[serde(rename_all = "UPPERCASE")]
173#[cfg_attr(
174    feature = "python",
175    pyo3::pyclass(
176        eq,
177        eq_int,
178        rename_all = "SCREAMING_SNAKE_CASE",
179        module = "nautilus_trader.adapters.bybit",
180        from_py_object
181    )
182)]
183#[cfg_attr(
184    feature = "python",
185    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
186)]
187pub enum BybitAccountType {
188    Unified,
189    Fund,
190}
191
192/// API key authentication type returned by `/v5/user/list-sub-apikeys`.
193#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
194#[repr(u8)]
195pub enum BybitApiKeyType {
196    /// HMAC-SHA256 signed keys (the default).
197    Hmac = 1,
198    /// RSA-signed keys.
199    Rsa = 2,
200}
201
202/// Environments supported by the Bybit API stack.
203#[derive(
204    Copy,
205    Clone,
206    Debug,
207    strum::Display,
208    PartialEq,
209    Eq,
210    Hash,
211    AsRefStr,
212    EnumIter,
213    EnumString,
214    Serialize,
215    Deserialize,
216)]
217#[serde(rename_all = "lowercase")]
218#[cfg_attr(
219    feature = "python",
220    pyo3::pyclass(
221        eq,
222        eq_int,
223        rename_all = "SCREAMING_SNAKE_CASE",
224        module = "nautilus_trader.adapters.bybit",
225        from_py_object
226    )
227)]
228#[cfg_attr(
229    feature = "python",
230    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
231)]
232pub enum BybitEnvironment {
233    /// Live trading environment.
234    Mainnet,
235    /// Demo (paper trading) environment.
236    Demo,
237    /// Testnet environment for spot/derivatives.
238    Testnet,
239}
240
241/// Product categories supported by the v5 API.
242#[derive(
243    Copy,
244    Clone,
245    Debug,
246    strum::Display,
247    Default,
248    PartialEq,
249    Eq,
250    Hash,
251    AsRefStr,
252    EnumIter,
253    EnumString,
254    Serialize,
255    Deserialize,
256)]
257#[serde(rename_all = "lowercase")]
258#[cfg_attr(
259    feature = "python",
260    pyo3::pyclass(
261        eq,
262        eq_int,
263        rename_all = "SCREAMING_SNAKE_CASE",
264        module = "nautilus_trader.adapters.bybit",
265        from_py_object
266    )
267)]
268#[cfg_attr(
269    feature = "python",
270    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
271)]
272pub enum BybitProductType {
273    #[default]
274    Spot,
275    Linear,
276    Inverse,
277    Option,
278}
279
280/// Spot margin trading enablement states.
281#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
282pub enum BybitMarginTrading {
283    #[serde(rename = "none")]
284    None,
285    #[serde(rename = "utaOnly")]
286    UtaOnly,
287    #[serde(rename = "both")]
288    Both,
289    #[serde(rename = "normalSpotOnly")]
290    NormalSpotOnly,
291    #[serde(other)]
292    Other,
293}
294
295/// Innovation market flag for spot instruments.
296#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
297pub enum BybitInnovationFlag {
298    #[serde(rename = "0")]
299    Standard,
300    #[serde(rename = "1")]
301    Innovation,
302    #[serde(other)]
303    Other,
304}
305
306/// Instrument lifecycle status values.
307#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
308#[serde(rename_all = "PascalCase")]
309pub enum BybitInstrumentStatus {
310    PreLaunch,
311    PendingOpen,
312    Trading,
313    Delivering,
314    Closed,
315    #[serde(other)]
316    Other,
317}
318
319impl BybitProductType {
320    /// Returns the canonical lowercase identifier used for REST/WS routes.
321    #[must_use]
322    pub const fn as_str(self) -> &'static str {
323        match self {
324            Self::Spot => "spot",
325            Self::Linear => "linear",
326            Self::Inverse => "inverse",
327            Self::Option => "option",
328        }
329    }
330
331    /// Returns the uppercase suffix used in instrument identifiers (e.g. `-LINEAR`).
332    #[must_use]
333    pub const fn suffix(self) -> &'static str {
334        match self {
335            Self::Spot => "-SPOT",
336            Self::Linear => "-LINEAR",
337            Self::Inverse => "-INVERSE",
338            Self::Option => "-OPTION",
339        }
340    }
341
342    /// Returns the product type identified by the suffix in the symbol string.
343    #[must_use]
344    pub fn from_suffix(symbol: &str) -> Option<Self> {
345        if symbol.ends_with("-SPOT") {
346            Some(Self::Spot)
347        } else if symbol.ends_with("-LINEAR") {
348            Some(Self::Linear)
349        } else if symbol.ends_with("-INVERSE") {
350            Some(Self::Inverse)
351        } else if symbol.ends_with("-OPTION") {
352            Some(Self::Option)
353        } else {
354            None
355        }
356    }
357
358    /// Returns `true` if the product is a spot instrument.
359    #[must_use]
360    pub fn is_spot(self) -> bool {
361        matches!(self, Self::Spot)
362    }
363
364    /// Returns `true` if the product is a linear contract.
365    #[must_use]
366    pub fn is_linear(self) -> bool {
367        matches!(self, Self::Linear)
368    }
369
370    /// Returns `true` if the product is an inverse contract.
371    #[must_use]
372    pub fn is_inverse(self) -> bool {
373        matches!(self, Self::Inverse)
374    }
375
376    /// Returns `true` if the product is an option contract.
377    #[must_use]
378    pub fn is_option(self) -> bool {
379        matches!(self, Self::Option)
380    }
381}
382
383/// Contract type enumeration for linear and inverse derivatives.
384#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
385#[serde(rename_all = "PascalCase")]
386pub enum BybitContractType {
387    LinearPerpetual,
388    LinearFutures,
389    InversePerpetual,
390    InverseFutures,
391}
392
393/// Option flavor values.
394#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
395#[serde(rename_all = "PascalCase")]
396pub enum BybitOptionType {
397    Call,
398    Put,
399}
400
401/// Symbol type values for spot/linear/inverse instrument info responses.
402///
403/// New values may be added by the venue; unknown strings fall back to `Other` so deserialization
404/// remains forward-compatible.
405///
406/// # References
407/// - <https://bybit-exchange.github.io/docs/v5/enum#symboltype>
408#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
409#[serde(rename_all = "lowercase")]
410pub enum BybitSymbolType {
411    /// Innovation-zone derivatives.
412    Innovation,
413    /// Adventure-zone spot pairs.
414    Adventure,
415    /// Tokenized equities (spot xstocks).
416    Xstocks,
417    /// Commodity instruments.
418    Commodity,
419    /// Tokenized stock derivatives.
420    Stock,
421    /// Foreign exchange instruments.
422    Forex,
423    /// Exchange-traded fund derivatives.
424    #[serde(rename = "ETF")]
425    Etf,
426    /// Tokenized equities (spot mstocks).
427    Mstocks,
428    /// Forward-compatible fallback for any value the venue adds later.
429    #[serde(other)]
430    Other,
431}
432
433impl BybitSymbolType {
434    /// Returns the exact recognized value used by Bybit, or `None` for an unknown value.
435    #[must_use]
436    pub(crate) const fn as_str(self) -> Option<&'static str> {
437        match self {
438            Self::Innovation => Some("innovation"),
439            Self::Adventure => Some("adventure"),
440            Self::Xstocks => Some("xstocks"),
441            Self::Commodity => Some("commodity"),
442            Self::Stock => Some("stock"),
443            Self::Forex => Some("forex"),
444            Self::Etf => Some("ETF"),
445            Self::Mstocks => Some("mstocks"),
446            Self::Other => None,
447        }
448    }
449}
450
451impl From<BybitSymbolType> for AssetClass {
452    fn from(symbol_type: BybitSymbolType) -> Self {
453        match symbol_type {
454            BybitSymbolType::Innovation | BybitSymbolType::Adventure => Self::Cryptocurrency,
455            BybitSymbolType::Stock
456            | BybitSymbolType::Etf
457            | BybitSymbolType::Xstocks
458            | BybitSymbolType::Mstocks => Self::Equity,
459            BybitSymbolType::Commodity => Self::Commodity,
460            BybitSymbolType::Forex => Self::FX,
461            BybitSymbolType::Other => Self::Alternative,
462        }
463    }
464}
465
466/// Position side as represented in REST/WebSocket payloads.
467#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
468pub enum BybitPositionSide {
469    #[serde(rename = "")]
470    Flat,
471    #[serde(rename = "Buy")]
472    Buy,
473    #[serde(rename = "Sell")]
474    Sell,
475}
476
477/// WebSocket order request operations.
478#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
479pub enum BybitWsOrderRequestOp {
480    #[serde(rename = "order.create")]
481    Create,
482    #[serde(rename = "order.amend")]
483    Amend,
484    #[serde(rename = "order.cancel")]
485    Cancel,
486    #[serde(rename = "order.create-batch")]
487    CreateBatch,
488    #[serde(rename = "order.amend-batch")]
489    AmendBatch,
490    #[serde(rename = "order.cancel-batch")]
491    CancelBatch,
492}
493
494/// Available kline intervals.
495#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
496pub enum BybitKlineInterval {
497    #[serde(rename = "1")]
498    Minute1,
499    #[serde(rename = "3")]
500    Minute3,
501    #[serde(rename = "5")]
502    Minute5,
503    #[serde(rename = "15")]
504    Minute15,
505    #[serde(rename = "30")]
506    Minute30,
507    #[serde(rename = "60")]
508    Hour1,
509    #[serde(rename = "120")]
510    Hour2,
511    #[serde(rename = "240")]
512    Hour4,
513    #[serde(rename = "360")]
514    Hour6,
515    #[serde(rename = "720")]
516    Hour12,
517    #[serde(rename = "D")]
518    Day1,
519    #[serde(rename = "W")]
520    Week1,
521    #[serde(rename = "M")]
522    Month1,
523}
524
525impl BybitKlineInterval {
526    /// Returns the end time in milliseconds for a bar that starts at `start_ms`.
527    ///
528    /// For most intervals this is simply `start_ms + duration`. For monthly bars,
529    /// this calculates the actual first millisecond of the next month to handle
530    /// variable month lengths (28-31 days).
531    #[must_use]
532    pub fn bar_end_time_ms(&self, start_ms: i64) -> i64 {
533        match self {
534            Self::Month1 => {
535                let start_dt = Offset::UTC.to_datetime(
536                    Timestamp::from_millisecond(start_ms).unwrap_or(Timestamp::UNIX_EPOCH),
537                );
538                let (year, month) = if start_dt.month() == 12 {
539                    (start_dt.year() + 1, 1)
540                } else {
541                    (start_dt.year(), start_dt.month() + 1)
542                };
543                Date::new(year, month, 1)
544                    .and_then(|date| Offset::UTC.to_timestamp(date.at(0, 0, 0, 0)))
545                    .map_or(start_ms + 2_678_400_000, Timestamp::as_millisecond)
546            }
547            _ => start_ms + self.duration_ms(),
548        }
549    }
550
551    /// Returns the fixed duration of this interval in milliseconds.
552    ///
553    /// Note: For monthly bars, use [`Self::bar_end_time_ms`] instead as months have
554    /// variable lengths (28-31 days).
555    #[must_use]
556    pub const fn duration_ms(&self) -> i64 {
557        match self {
558            Self::Minute1 => 60_000,
559            Self::Minute3 => 180_000,
560            Self::Minute5 => 300_000,
561            Self::Minute15 => 900_000,
562            Self::Minute30 => 1_800_000,
563            Self::Hour1 => 3_600_000,
564            Self::Hour2 => 7_200_000,
565            Self::Hour4 => 14_400_000,
566            Self::Hour6 => 21_600_000,
567            Self::Hour12 => 43_200_000,
568            Self::Day1 => 86_400_000,
569            Self::Week1 => 604_800_000,
570            Self::Month1 => 2_678_400_000, // 31 days - use bar_end_time_ms() for accurate calculation
571        }
572    }
573}
574
575impl Display for BybitKlineInterval {
576    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577        let s = match self {
578            Self::Minute1 => "1",
579            Self::Minute3 => "3",
580            Self::Minute5 => "5",
581            Self::Minute15 => "15",
582            Self::Minute30 => "30",
583            Self::Hour1 => "60",
584            Self::Hour2 => "120",
585            Self::Hour4 => "240",
586            Self::Hour6 => "360",
587            Self::Hour12 => "720",
588            Self::Day1 => "D",
589            Self::Week1 => "W",
590            Self::Month1 => "M",
591        };
592        write!(f, "{s}")
593    }
594}
595
596/// Order status values returned by Bybit.
597#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
598#[cfg_attr(
599    feature = "python",
600    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
601)]
602#[cfg_attr(
603    feature = "python",
604    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
605)]
606pub enum BybitOrderStatus {
607    #[serde(rename = "Created")]
608    Created,
609    #[serde(rename = "New")]
610    New,
611    #[serde(rename = "Rejected")]
612    Rejected,
613    #[serde(rename = "PartiallyFilled")]
614    PartiallyFilled,
615    #[serde(rename = "PartiallyFilledCanceled")]
616    PartiallyFilledCanceled,
617    #[serde(rename = "Filled")]
618    Filled,
619    #[serde(rename = "Cancelled")]
620    Canceled,
621    #[serde(rename = "Untriggered")]
622    Untriggered,
623    #[serde(rename = "Triggered")]
624    Triggered,
625    #[serde(rename = "Deactivated")]
626    Deactivated,
627}
628
629/// Order side enumeration.
630#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
631#[cfg_attr(
632    feature = "python",
633    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
634)]
635#[cfg_attr(
636    feature = "python",
637    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
638)]
639pub enum BybitOrderSide {
640    #[serde(rename = "")]
641    Unknown,
642    #[serde(rename = "Buy")]
643    Buy,
644    #[serde(rename = "Sell")]
645    Sell,
646}
647
648impl From<BybitOrderSide> for AggressorSide {
649    fn from(value: BybitOrderSide) -> Self {
650        match value {
651            BybitOrderSide::Buy => Self::Buy,
652            BybitOrderSide::Sell => Self::Sell,
653            BybitOrderSide::Unknown => Self::NoAggressor,
654        }
655    }
656}
657
658impl From<BybitOrderSide> for Option<OrderSide> {
659    fn from(value: BybitOrderSide) -> Self {
660        match value {
661            BybitOrderSide::Buy => Some(OrderSide::Buy),
662            BybitOrderSide::Sell => Some(OrderSide::Sell),
663            BybitOrderSide::Unknown => None,
664        }
665    }
666}
667
668impl TryFrom<BybitOrderSide> for OrderSide {
669    type Error = anyhow::Error;
670
671    fn try_from(value: BybitOrderSide) -> Result<Self, Self::Error> {
672        match value {
673            BybitOrderSide::Buy => Ok(Self::Buy),
674            BybitOrderSide::Sell => Ok(Self::Sell),
675            BybitOrderSide::Unknown => anyhow::bail!("Unspecified Bybit order side"),
676        }
677    }
678}
679
680impl From<OrderSide> for BybitOrderSide {
681    fn from(value: OrderSide) -> Self {
682        match value {
683            OrderSide::Buy => Self::Buy,
684            OrderSide::Sell => Self::Sell,
685        }
686    }
687}
688
689impl From<BybitTriggerType> for TriggerType {
690    fn from(value: BybitTriggerType) -> Self {
691        match value {
692            BybitTriggerType::None => Self::Default,
693            BybitTriggerType::LastPrice => Self::LastPrice,
694            BybitTriggerType::IndexPrice => Self::IndexPrice,
695            BybitTriggerType::MarkPrice => Self::MarkPrice,
696        }
697    }
698}
699
700impl From<TriggerType> for BybitTriggerType {
701    fn from(value: TriggerType) -> Self {
702        match value {
703            TriggerType::Default | TriggerType::LastPrice => Self::LastPrice,
704            TriggerType::IndexPrice => Self::IndexPrice,
705            TriggerType::MarkPrice => Self::MarkPrice,
706            _ => Self::LastPrice,
707        }
708    }
709}
710
711/// Resolves an optional Nautilus trigger type to a Bybit trigger type,
712/// defaulting to `LastPrice` when absent.
713pub fn resolve_trigger_type(trigger_type: Option<TriggerType>) -> BybitTriggerType {
714    trigger_type.map_or(BybitTriggerType::LastPrice, BybitTriggerType::from)
715}
716
717/// Order cancel reason values as returned by Bybit.
718#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
719#[serde(rename_all = "PascalCase")]
720#[cfg_attr(
721    feature = "python",
722    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
723)]
724#[cfg_attr(
725    feature = "python",
726    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
727)]
728pub enum BybitCancelType {
729    CancelByUser,
730    CancelByReduceOnly,
731    CancelByPrepareLiq,
732    CancelAllBeforeLiq,
733    CancelByPrepareAdl,
734    CancelAllBeforeAdl,
735    CancelByPrepareLackOfMargin,
736    CancelByPrepareOrderFilter,
737    CancelByPrepareOrderMarginCheckFailed,
738    CancelByPrepareOrderCommission,
739    CancelByPrepareOrderRms,
740    CancelByPrepareOrderOther,
741    CancelByRiskLimit,
742    CancelOnDisconnect,
743    CancelByStopOrdersExceeded,
744    CancelByPzMarketClose,
745    CancelByMarginCheckFailed,
746    CancelByPzTakeover,
747    CancelByAdmin,
748    CancelBySettle,
749    CancelByTpSlTsClear,
750    CancelByAmendNotModified,
751    CancelByPzCancel,
752    CancelBySmp,
753    #[serde(rename = "CancelByDCP")]
754    CancelByDcp,
755    CancelByRebalance,
756    #[serde(rename = "CancelByOCOTpCanceledBySlTriggered")]
757    CancelByOcoTpCanceledBySlTriggered,
758    #[serde(rename = "CancelByOCOSlCanceledByTpTriggered")]
759    CancelByOcoSlCanceledByTpTriggered,
760    CancelByCannotAffordOrderCost,
761    CancelByPmTrialMmOverEquity,
762    CancelByAccountBlocking,
763    CancelByDelivery,
764    CancelByMmpTriggered,
765    CancelByCrossSelfMuch,
766    CancelByCrossReachMaxTradeNum,
767    CancelByCrossSelfMatch,
768    CancelBySelfMatchPrevention,
769    #[serde(other)]
770    Other,
771}
772
773/// Order creation origin values.
774#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
775#[serde(rename_all = "PascalCase")]
776pub enum BybitCreateType {
777    CreateByUser,
778    CreateByFutureSpread,
779    CreateByAdminClosing,
780    CreateBySettle,
781    CreateByClosing,
782    CreateByTakeProfit,
783    CreateByStopLoss,
784    CreateByTrailingStop,
785    CreateByTrailingProfit,
786    CreateByStopOrder,
787    CreateByPartialTakeProfit,
788    CreateByPartialStopLoss,
789    CreateByAdl,
790    #[serde(rename = "CreateByAdl_PassThrough")]
791    CreateByAdlPassThrough,
792    CreateByLiq,
793    CreateByLiquidate,
794    CreateByTakeover,
795    #[serde(rename = "CreateByTakeOver_PassThrough")]
796    CreateByTakeOverPassThrough,
797    #[serde(rename = "CreateByBlock_PassThrough")]
798    CreateByBlockPassThrough,
799    #[serde(rename = "CreateByBlockTradeMovePosition_PassThrough")]
800    CreateByBlockTradeMovePositionPassThrough,
801    CreateByChaseOrder,
802    #[serde(rename = "CreateByFGridBot")]
803    CreateByFGridBot,
804    #[serde(rename = "CloseByFGridBot")]
805    CloseByFGridBot,
806    #[serde(rename = "CreateByTWAP")]
807    CreateByTwap,
808    CreateByMartingaleBot,
809    CloseByMartingaleBot,
810    #[serde(rename = "CreateByIceBerg")]
811    CreateByIceBerg,
812    CreateByBboOrder,
813    #[serde(rename = "CreateByTVSignal")]
814    CreateByTvSignal,
815    CreateByMmRateClose,
816    CreateByArbitrage,
817    CreateByDdh,
818    CreateByTpsl,
819    #[serde(other)]
820    Other,
821}
822
823/// BBO side type values for Bybit order placement.
824#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
825#[serde(rename_all = "PascalCase")]
826pub enum BybitBboSideType {
827    Queue,
828    Counterparty,
829}
830
831/// Venue order type enumeration.
832#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
833#[cfg_attr(
834    feature = "python",
835    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
836)]
837#[cfg_attr(
838    feature = "python",
839    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
840)]
841pub enum BybitOrderType {
842    #[serde(rename = "Market")]
843    Market,
844    #[serde(rename = "Limit")]
845    Limit,
846    #[serde(rename = "UNKNOWN")]
847    Unknown,
848}
849
850/// Stop order type classification.
851#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
852#[cfg_attr(
853    feature = "python",
854    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
855)]
856#[cfg_attr(
857    feature = "python",
858    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
859)]
860pub enum BybitStopOrderType {
861    #[serde(rename = "")]
862    None,
863    #[serde(rename = "UNKNOWN")]
864    Unknown,
865    #[serde(rename = "TakeProfit")]
866    TakeProfit,
867    #[serde(rename = "StopLoss")]
868    StopLoss,
869    #[serde(rename = "TrailingStop")]
870    TrailingStop,
871    #[serde(rename = "Stop")]
872    Stop,
873    #[serde(rename = "PartialTakeProfit")]
874    PartialTakeProfit,
875    #[serde(rename = "PartialStopLoss")]
876    PartialStopLoss,
877    #[serde(rename = "tpslOrder")]
878    TpslOrder,
879    #[serde(rename = "OcoOrder")]
880    OcoOrder,
881    #[serde(rename = "MmRateClose")]
882    MmRateClose,
883    #[serde(rename = "BidirectionalTpslOrder")]
884    BidirectionalTpslOrder,
885}
886
887/// Trigger type configuration.
888#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
889#[cfg_attr(
890    feature = "python",
891    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
892)]
893#[cfg_attr(
894    feature = "python",
895    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
896)]
897pub enum BybitTriggerType {
898    #[serde(rename = "")]
899    None,
900    #[serde(rename = "LastPrice")]
901    LastPrice,
902    #[serde(rename = "IndexPrice")]
903    IndexPrice,
904    #[serde(rename = "MarkPrice")]
905    MarkPrice,
906}
907
908/// Trigger direction integers used by the API.
909#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
910#[repr(i32)]
911#[cfg_attr(
912    feature = "python",
913    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
914)]
915#[cfg_attr(
916    feature = "python",
917    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
918)]
919pub enum BybitTriggerDirection {
920    None = 0,
921    RisesTo = 1,
922    FallsTo = 2,
923}
924
925/// Take-profit/stop-loss mode for derivatives orders.
926#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
927#[serde(rename_all = "PascalCase")]
928#[cfg_attr(
929    feature = "python",
930    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
931)]
932#[cfg_attr(
933    feature = "python",
934    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
935)]
936pub enum BybitTpSlMode {
937    Full,
938    Partial,
939    #[serde(other)]
940    Unknown,
941}
942
943/// Time-in-force enumeration.
944#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
945#[cfg_attr(
946    feature = "python",
947    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
948)]
949#[cfg_attr(
950    feature = "python",
951    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
952)]
953pub enum BybitTimeInForce {
954    #[serde(rename = "GTC")]
955    Gtc,
956    #[serde(rename = "IOC")]
957    Ioc,
958    #[serde(rename = "FOK")]
959    Fok,
960    #[serde(rename = "PostOnly")]
961    PostOnly,
962    #[serde(rename = "RPI")]
963    Rpi,
964}
965
966/// Execution type values used in execution reports.
967///
968/// Reference: <https://bybit-exchange.github.io/docs/v5/enum#exectype>.
969#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
970pub enum BybitExecType {
971    #[serde(rename = "Trade")]
972    Trade,
973    #[serde(rename = "AdlTrade")]
974    AdlTrade,
975    #[serde(rename = "Funding")]
976    Funding,
977    #[serde(rename = "BustTrade")]
978    BustTrade,
979    #[serde(rename = "Delivery")]
980    Delivery,
981    #[serde(rename = "Settle")]
982    Settle,
983    #[serde(rename = "BlockTrade")]
984    BlockTrade,
985    #[serde(rename = "MovePosition")]
986    MovePosition,
987    #[serde(rename = "FutureSpread")]
988    FutureSpread,
989    /// Retained for execution history recorded while the venue issued this type
990    /// (2026-07-23 to 2026-08-20), before its replacement by the split settlement
991    /// and dividend types.
992    #[serde(rename = "CorporateAction")]
993    CorporateAction,
994    #[serde(rename = "ForwardSplitSettle")]
995    ForwardSplitSettle,
996    #[serde(rename = "ReverseSplitSettle")]
997    ReverseSplitSettle,
998    #[serde(rename = "Dividend")]
999    Dividend,
1000    #[serde(other)]
1001    #[serde(rename = "UNKNOWN")]
1002    Unknown,
1003}
1004
1005impl BybitExecType {
1006    /// Returns `true` if this execution was generated by the venue rather than the user.
1007    ///
1008    /// This covers auto-deleveraging (`AdlTrade`), liquidation takeovers (`BustTrade`),
1009    /// scheduled deliveries (`Delivery`), settlement (`Settle`), and corporate actions
1010    /// (`CorporateAction`, `ForwardSplitSettle`, `ReverseSplitSettle`, `Dividend`).
1011    #[must_use]
1012    pub const fn is_exchange_generated(&self) -> bool {
1013        matches!(
1014            self,
1015            Self::AdlTrade
1016                | Self::BustTrade
1017                | Self::Delivery
1018                | Self::Settle
1019                | Self::CorporateAction
1020                | Self::ForwardSplitSettle
1021                | Self::ReverseSplitSettle
1022                | Self::Dividend
1023        )
1024    }
1025}
1026
1027/// Transaction types for wallet funding records.
1028#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1029pub enum BybitTransactionType {
1030    #[serde(rename = "TRANSFER_IN")]
1031    TransferIn,
1032    #[serde(rename = "TRANSFER_OUT")]
1033    TransferOut,
1034    #[serde(rename = "TRADE")]
1035    Trade,
1036    #[serde(rename = "SETTLEMENT")]
1037    Settlement,
1038    #[serde(rename = "DELIVERY")]
1039    Delivery,
1040    #[serde(rename = "LIQUIDATION")]
1041    Liquidation,
1042    #[serde(rename = "AIRDRP")]
1043    Airdrop,
1044    #[serde(rename = "ADL")]
1045    Adl,
1046    #[serde(rename = "DIVIDEND_SETTLEMENT")]
1047    DividendSettlement,
1048    #[serde(rename = "FORWARD_SPLIT_SETTLE")]
1049    ForwardSplitSettle,
1050    #[serde(rename = "REVERSE_SPLIT_SETTLE")]
1051    ReverseSplitSettle,
1052    #[serde(other)]
1053    Other,
1054}
1055
1056/// Endpoint classifications used by the Bybit API.
1057#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1058#[serde(rename_all = "UPPERCASE")]
1059pub enum BybitEndpointType {
1060    None,
1061    Asset,
1062    Market,
1063    Account,
1064    Trade,
1065    Position,
1066    User,
1067}
1068
1069/// Filter for open orders query.
1070///
1071/// Used with `GET /v5/order/realtime` to filter order status.
1072#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
1073#[repr(i32)]
1074#[cfg_attr(
1075    feature = "python",
1076    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
1077)]
1078#[cfg_attr(
1079    feature = "python",
1080    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
1081)]
1082pub enum BybitOpenOnly {
1083    /// Query open status orders only (New, PartiallyFilled).
1084    #[default]
1085    OpenOnly = 0,
1086    /// Query up to 500 recent closed orders (cancelled, rejected, filled).
1087    ClosedRecent = 1,
1088}
1089
1090/// Order filter for querying specific order types.
1091///
1092/// Used with `GET /v5/order/realtime` to filter by order category.
1093#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
1094#[cfg_attr(
1095    feature = "python",
1096    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
1097)]
1098#[cfg_attr(
1099    feature = "python",
1100    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
1101)]
1102pub enum BybitOrderFilter {
1103    /// Active orders (default).
1104    #[default]
1105    Order,
1106    /// Conditional orders (futures and spot).
1107    StopOrder,
1108    /// Spot take-profit/stop-loss orders.
1109    #[serde(rename = "tpslOrder")]
1110    TpslOrder,
1111    /// Spot one-cancels-other orders.
1112    OcoOrder,
1113    /// Spot bidirectional TP/SL orders.
1114    BidirectionalTpslOrder,
1115}
1116
1117/// Margin actions for spot margin trading operations.
1118#[derive(
1119    Clone,
1120    Copy,
1121    Debug,
1122    strum::Display,
1123    Eq,
1124    PartialEq,
1125    Hash,
1126    AsRefStr,
1127    EnumIter,
1128    EnumString,
1129    Serialize,
1130    Deserialize,
1131)]
1132#[serde(rename_all = "snake_case")]
1133#[strum(serialize_all = "snake_case")]
1134#[cfg_attr(
1135    feature = "python",
1136    pyo3::pyclass(
1137        eq,
1138        eq_int,
1139        hash,
1140        frozen,
1141        rename_all = "SCREAMING_SNAKE_CASE",
1142        module = "nautilus_trader.adapters.bybit",
1143        from_py_object,
1144    )
1145)]
1146#[cfg_attr(
1147    feature = "python",
1148    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
1149)]
1150pub enum BybitMarginAction {
1151    /// Borrow funds for margin trading.
1152    Borrow,
1153    /// Repay borrowed funds.
1154    Repay,
1155    /// Query current borrowed amount.
1156    GetBorrowAmount,
1157}
1158
1159/// Result status returned by Bybit repayment endpoints.
1160#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, strum::Display, Serialize, Deserialize)]
1161pub enum BybitRepayStatus {
1162    /// The repayment is processing.
1163    #[serde(rename = "P")]
1164    #[strum(serialize = "P")]
1165    Processing,
1166    /// The repayment succeeded.
1167    #[serde(rename = "SU")]
1168    #[strum(serialize = "SU")]
1169    Success,
1170    /// The repayment failed.
1171    #[serde(rename = "FA")]
1172    #[strum(serialize = "FA")]
1173    Failed,
1174}
1175
1176/// Position status enumeration.
1177#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1178#[serde(rename_all = "PascalCase")]
1179pub enum BybitPositionStatus {
1180    Normal,
1181    Liq,
1182    Adl,
1183    Settle,
1184    Delivering,
1185    #[serde(other)]
1186    Other,
1187}
1188
1189/// Market unit for spot market orders.
1190#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1191pub enum BybitMarketUnit {
1192    #[serde(rename = "baseCoin")]
1193    BaseCoin,
1194    #[serde(rename = "quoteCoin")]
1195    QuoteCoin,
1196}
1197
1198/// Self-match prevention type reported on an order or execution.
1199///
1200/// `Other` is a catch-all for values Bybit adds after this enum was written. Requests use
1201/// [`BybitOrderSmpType`], which has no catch-all, so an unknown value cannot reach the venue.
1202#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1203pub enum BybitSmpType {
1204    None,
1205    CancelMaker,
1206    CancelTaker,
1207    CancelBoth,
1208    #[serde(other)]
1209    Other,
1210}
1211
1212/// Self-match prevention type accepted on an order request.
1213///
1214/// Mirrors [`BybitSmpType`] without its inbound catch-all, so every value this type can hold is one
1215/// Bybit accepts on an order.
1216#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, AsRefStr, Serialize, Deserialize)]
1217pub enum BybitOrderSmpType {
1218    None,
1219    CancelMaker,
1220    CancelTaker,
1221    CancelBoth,
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226    use rstest::rstest;
1227
1228    use super::*;
1229
1230    #[rstest]
1231    #[case::minute1(BybitKlineInterval::Minute1, 60_000)]
1232    #[case::minute3(BybitKlineInterval::Minute3, 180_000)]
1233    #[case::minute5(BybitKlineInterval::Minute5, 300_000)]
1234    #[case::minute15(BybitKlineInterval::Minute15, 900_000)]
1235    #[case::minute30(BybitKlineInterval::Minute30, 1_800_000)]
1236    #[case::hour1(BybitKlineInterval::Hour1, 3_600_000)]
1237    #[case::hour2(BybitKlineInterval::Hour2, 7_200_000)]
1238    #[case::hour4(BybitKlineInterval::Hour4, 14_400_000)]
1239    #[case::hour6(BybitKlineInterval::Hour6, 21_600_000)]
1240    #[case::hour12(BybitKlineInterval::Hour12, 43_200_000)]
1241    #[case::day1(BybitKlineInterval::Day1, 86_400_000)]
1242    #[case::week1(BybitKlineInterval::Week1, 604_800_000)]
1243    #[case::month1(BybitKlineInterval::Month1, 2_678_400_000)]
1244    fn test_kline_interval_duration_ms(
1245        #[case] interval: BybitKlineInterval,
1246        #[case] expected_ms: i64,
1247    ) {
1248        assert_eq!(interval.duration_ms(), expected_ms);
1249    }
1250
1251    #[rstest]
1252    fn test_bar_end_time_ms_non_monthly_adds_duration() {
1253        let interval = BybitKlineInterval::Minute1;
1254        let start_ms = 1704067200000i64;
1255        assert_eq!(interval.bar_end_time_ms(start_ms), start_ms + 60_000);
1256    }
1257
1258    #[rstest]
1259    #[case::jan_31_days(1704067200000i64, 1706745600000i64)]
1260    #[case::feb_leap_year_29_days(1706745600000i64, 1709251200000i64)]
1261    #[case::apr_30_days(1711929600000i64, 1714521600000i64)]
1262    #[case::dec_to_next_year(1733011200000i64, 1735689600000i64)]
1263    fn test_bar_end_time_ms_monthly_variable_lengths(
1264        #[case] start_ms: i64,
1265        #[case] expected_end_ms: i64,
1266    ) {
1267        let interval = BybitKlineInterval::Month1;
1268        assert_eq!(interval.bar_end_time_ms(start_ms), expected_end_ms);
1269    }
1270
1271    #[rstest]
1272    #[case(BybitSymbolType::Innovation, "innovation")]
1273    #[case(BybitSymbolType::Adventure, "adventure")]
1274    #[case(BybitSymbolType::Xstocks, "xstocks")]
1275    #[case(BybitSymbolType::Commodity, "commodity")]
1276    #[case(BybitSymbolType::Stock, "stock")]
1277    #[case(BybitSymbolType::Forex, "forex")]
1278    #[case(BybitSymbolType::Etf, "ETF")]
1279    #[case(BybitSymbolType::Mstocks, "mstocks")]
1280    fn test_symbol_type_round_trip(#[case] symbol_type: BybitSymbolType, #[case] wire_value: &str) {
1281        let value = serde_json::Value::String(wire_value.to_string());
1282
1283        assert_eq!(
1284            serde_json::from_value::<BybitSymbolType>(value.clone()).unwrap(),
1285            symbol_type
1286        );
1287        assert_eq!(serde_json::to_value(symbol_type).unwrap(), value);
1288        assert_eq!(symbol_type.as_str(), Some(wire_value));
1289    }
1290
1291    #[rstest]
1292    fn test_unknown_symbol_type_has_no_wire_value() {
1293        assert_eq!(BybitSymbolType::Other.as_str(), None);
1294    }
1295
1296    #[rstest]
1297    #[case(BybitSymbolType::Innovation, AssetClass::Cryptocurrency)]
1298    #[case(BybitSymbolType::Adventure, AssetClass::Cryptocurrency)]
1299    #[case(BybitSymbolType::Stock, AssetClass::Equity)]
1300    #[case(BybitSymbolType::Etf, AssetClass::Equity)]
1301    #[case(BybitSymbolType::Xstocks, AssetClass::Equity)]
1302    #[case(BybitSymbolType::Mstocks, AssetClass::Equity)]
1303    #[case(BybitSymbolType::Commodity, AssetClass::Commodity)]
1304    #[case(BybitSymbolType::Forex, AssetClass::FX)]
1305    #[case(BybitSymbolType::Other, AssetClass::Alternative)]
1306    fn test_symbol_type_to_asset_class(
1307        #[case] symbol_type: BybitSymbolType,
1308        #[case] expected: AssetClass,
1309    ) {
1310        assert_eq!(AssetClass::from(symbol_type), expected);
1311    }
1312
1313    #[rstest]
1314    #[case(BybitExecType::Trade, false)]
1315    #[case(BybitExecType::AdlTrade, true)]
1316    #[case(BybitExecType::BustTrade, true)]
1317    #[case(BybitExecType::Delivery, true)]
1318    #[case(BybitExecType::Settle, true)]
1319    #[case(BybitExecType::Funding, false)]
1320    #[case(BybitExecType::BlockTrade, false)]
1321    #[case(BybitExecType::MovePosition, false)]
1322    #[case(BybitExecType::FutureSpread, false)]
1323    #[case(BybitExecType::CorporateAction, true)]
1324    #[case(BybitExecType::ForwardSplitSettle, true)]
1325    #[case(BybitExecType::ReverseSplitSettle, true)]
1326    #[case(BybitExecType::Dividend, true)]
1327    #[case(BybitExecType::Unknown, false)]
1328    fn test_exec_type_is_exchange_generated(
1329        #[case] exec_type: BybitExecType,
1330        #[case] expected: bool,
1331    ) {
1332        assert_eq!(exec_type.is_exchange_generated(), expected);
1333    }
1334
1335    #[rstest]
1336    #[case(BybitExecType::Trade, "Trade")]
1337    #[case(BybitExecType::AdlTrade, "AdlTrade")]
1338    #[case(BybitExecType::Funding, "Funding")]
1339    #[case(BybitExecType::BustTrade, "BustTrade")]
1340    #[case(BybitExecType::Delivery, "Delivery")]
1341    #[case(BybitExecType::Settle, "Settle")]
1342    #[case(BybitExecType::BlockTrade, "BlockTrade")]
1343    #[case(BybitExecType::MovePosition, "MovePosition")]
1344    #[case(BybitExecType::FutureSpread, "FutureSpread")]
1345    #[case(BybitExecType::CorporateAction, "CorporateAction")]
1346    #[case(BybitExecType::ForwardSplitSettle, "ForwardSplitSettle")]
1347    #[case(BybitExecType::ReverseSplitSettle, "ReverseSplitSettle")]
1348    #[case(BybitExecType::Dividend, "Dividend")]
1349    #[case(BybitExecType::Unknown, "UNKNOWN")]
1350    fn test_exec_type_round_trip(#[case] exec_type: BybitExecType, #[case] wire_value: &str) {
1351        let value = serde_json::Value::String(wire_value.to_string());
1352
1353        assert_eq!(
1354            serde_json::from_value::<BybitExecType>(value.clone()).unwrap(),
1355            exec_type
1356        );
1357        assert_eq!(serde_json::to_value(exec_type).unwrap(), value);
1358    }
1359
1360    #[rstest]
1361    fn test_exec_type_unrecognized_value_deserializes_to_unknown() {
1362        let value = serde_json::Value::String("StockMerger".to_string());
1363
1364        assert_eq!(
1365            serde_json::from_value::<BybitExecType>(value).unwrap(),
1366            BybitExecType::Unknown
1367        );
1368    }
1369
1370    #[rstest]
1371    #[case(BybitAccountType::Unified, "UNIFIED")]
1372    #[case(BybitAccountType::Fund, "FUND")]
1373    fn test_account_type_round_trip(
1374        #[case] account_type: BybitAccountType,
1375        #[case] wire_value: &str,
1376    ) {
1377        let value = serde_json::Value::String(wire_value.to_string());
1378
1379        assert_eq!(
1380            serde_json::from_value::<BybitAccountType>(value.clone()).unwrap(),
1381            account_type
1382        );
1383        assert_eq!(serde_json::to_value(account_type).unwrap(), value);
1384    }
1385
1386    #[rstest]
1387    #[case(BybitTimeInForce::Gtc, "GTC")]
1388    #[case(BybitTimeInForce::Ioc, "IOC")]
1389    #[case(BybitTimeInForce::Fok, "FOK")]
1390    #[case(BybitTimeInForce::PostOnly, "PostOnly")]
1391    #[case(BybitTimeInForce::Rpi, "RPI")]
1392    fn test_time_in_force_round_trip(#[case] tif: BybitTimeInForce, #[case] wire_value: &str) {
1393        let value = serde_json::Value::String(wire_value.to_string());
1394
1395        assert_eq!(
1396            serde_json::from_value::<BybitTimeInForce>(value.clone()).unwrap(),
1397            tif
1398        );
1399        assert_eq!(serde_json::to_value(tif).unwrap(), value);
1400    }
1401
1402    #[rstest]
1403    #[case("CreateByAdl_PassThrough", BybitCreateType::CreateByAdlPassThrough)]
1404    #[case("CreateByLiq", BybitCreateType::CreateByLiq)]
1405    #[case(
1406        "CreateByTakeOver_PassThrough",
1407        BybitCreateType::CreateByTakeOverPassThrough
1408    )]
1409    #[case("CreateByTWAP", BybitCreateType::CreateByTwap)]
1410    #[case("CreateByFutureSpread", BybitCreateType::CreateByFutureSpread)]
1411    fn test_create_type_venue_wire_values(
1412        #[case] wire_value: &str,
1413        #[case] expected: BybitCreateType,
1414    ) {
1415        let value = serde_json::Value::String(wire_value.to_string());
1416
1417        assert_eq!(
1418            serde_json::from_value::<BybitCreateType>(value.clone()).unwrap(),
1419            expected
1420        );
1421        assert_eq!(serde_json::to_value(expected).unwrap(), value);
1422    }
1423
1424    #[rstest]
1425    #[case("CancelBySettle", BybitCancelType::CancelBySettle)]
1426    #[case("CancelBySmp", BybitCancelType::CancelBySmp)]
1427    #[case("CancelByDCP", BybitCancelType::CancelByDcp)]
1428    #[case("CancelAllBeforeLiq", BybitCancelType::CancelAllBeforeLiq)]
1429    #[case(
1430        "CancelByOCOTpCanceledBySlTriggered",
1431        BybitCancelType::CancelByOcoTpCanceledBySlTriggered
1432    )]
1433    fn test_cancel_type_venue_wire_values(
1434        #[case] wire_value: &str,
1435        #[case] expected: BybitCancelType,
1436    ) {
1437        let value = serde_json::Value::String(wire_value.to_string());
1438
1439        assert_eq!(
1440            serde_json::from_value::<BybitCancelType>(value.clone()).unwrap(),
1441            expected
1442        );
1443        assert_eq!(serde_json::to_value(expected).unwrap(), value);
1444    }
1445
1446    #[rstest]
1447    fn test_pending_open_instrument_status_round_trip() {
1448        let value = serde_json::Value::String("PendingOpen".to_string());
1449
1450        assert_eq!(
1451            serde_json::from_value::<BybitInstrumentStatus>(value.clone()).unwrap(),
1452            BybitInstrumentStatus::PendingOpen
1453        );
1454        assert_eq!(
1455            serde_json::to_value(BybitInstrumentStatus::PendingOpen).unwrap(),
1456            value
1457        );
1458    }
1459
1460    #[rstest]
1461    fn test_normal_spot_only_margin_trading_round_trip() {
1462        let value = serde_json::Value::String("normalSpotOnly".to_string());
1463
1464        assert_eq!(
1465            serde_json::from_value::<BybitMarginTrading>(value.clone()).unwrap(),
1466            BybitMarginTrading::NormalSpotOnly
1467        );
1468        assert_eq!(
1469            serde_json::to_value(BybitMarginTrading::NormalSpotOnly).unwrap(),
1470            value
1471        );
1472    }
1473
1474    #[rstest]
1475    #[case(BybitPositionStatus::Liq, "Liq")]
1476    #[case(BybitPositionStatus::Adl, "Adl")]
1477    fn test_position_status_liq_adl_round_trip(
1478        #[case] status: BybitPositionStatus,
1479        #[case] wire_value: &str,
1480    ) {
1481        let value = serde_json::Value::String(wire_value.to_string());
1482
1483        assert_eq!(
1484            serde_json::from_value::<BybitPositionStatus>(value.clone()).unwrap(),
1485            status
1486        );
1487        assert_eq!(serde_json::to_value(status).unwrap(), value);
1488    }
1489}