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, 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}
190
191/// API key authentication type returned by `/v5/user/list-sub-apikeys`.
192#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
193#[repr(u8)]
194pub enum BybitApiKeyType {
195    /// HMAC-SHA256 signed keys (the default).
196    Hmac = 1,
197    /// RSA-signed keys.
198    Rsa = 2,
199}
200
201/// Environments supported by the Bybit API stack.
202#[derive(
203    Copy,
204    Clone,
205    Debug,
206    strum::Display,
207    PartialEq,
208    Eq,
209    Hash,
210    AsRefStr,
211    EnumIter,
212    EnumString,
213    Serialize,
214    Deserialize,
215)]
216#[serde(rename_all = "lowercase")]
217#[cfg_attr(
218    feature = "python",
219    pyo3::pyclass(
220        eq,
221        eq_int,
222        rename_all = "SCREAMING_SNAKE_CASE",
223        module = "nautilus_trader.adapters.bybit",
224        from_py_object
225    )
226)]
227#[cfg_attr(
228    feature = "python",
229    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
230)]
231pub enum BybitEnvironment {
232    /// Live trading environment.
233    Mainnet,
234    /// Demo (paper trading) environment.
235    Demo,
236    /// Testnet environment for spot/derivatives.
237    Testnet,
238}
239
240/// Product categories supported by the v5 API.
241#[derive(
242    Copy,
243    Clone,
244    Debug,
245    strum::Display,
246    Default,
247    PartialEq,
248    Eq,
249    Hash,
250    AsRefStr,
251    EnumIter,
252    EnumString,
253    Serialize,
254    Deserialize,
255)]
256#[serde(rename_all = "lowercase")]
257#[cfg_attr(
258    feature = "python",
259    pyo3::pyclass(
260        eq,
261        eq_int,
262        rename_all = "SCREAMING_SNAKE_CASE",
263        module = "nautilus_trader.adapters.bybit",
264        from_py_object
265    )
266)]
267#[cfg_attr(
268    feature = "python",
269    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
270)]
271pub enum BybitProductType {
272    #[default]
273    Spot,
274    Linear,
275    Inverse,
276    Option,
277}
278
279/// Spot margin trading enablement states.
280#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
281pub enum BybitMarginTrading {
282    #[serde(rename = "none")]
283    None,
284    #[serde(rename = "utaOnly")]
285    UtaOnly,
286    #[serde(rename = "both")]
287    Both,
288    #[serde(other)]
289    Other,
290}
291
292/// Innovation market flag for spot instruments.
293#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
294pub enum BybitInnovationFlag {
295    #[serde(rename = "0")]
296    Standard,
297    #[serde(rename = "1")]
298    Innovation,
299    #[serde(other)]
300    Other,
301}
302
303/// Instrument lifecycle status values.
304#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
305#[serde(rename_all = "PascalCase")]
306pub enum BybitInstrumentStatus {
307    PreLaunch,
308    Trading,
309    Delivering,
310    Closed,
311    #[serde(other)]
312    Other,
313}
314
315impl BybitProductType {
316    /// Returns the canonical lowercase identifier used for REST/WS routes.
317    #[must_use]
318    pub const fn as_str(self) -> &'static str {
319        match self {
320            Self::Spot => "spot",
321            Self::Linear => "linear",
322            Self::Inverse => "inverse",
323            Self::Option => "option",
324        }
325    }
326
327    /// Returns the uppercase suffix used in instrument identifiers (e.g. `-LINEAR`).
328    #[must_use]
329    pub const fn suffix(self) -> &'static str {
330        match self {
331            Self::Spot => "-SPOT",
332            Self::Linear => "-LINEAR",
333            Self::Inverse => "-INVERSE",
334            Self::Option => "-OPTION",
335        }
336    }
337
338    /// Returns the product type identified by the suffix in the symbol string.
339    #[must_use]
340    pub fn from_suffix(symbol: &str) -> Option<Self> {
341        if symbol.ends_with("-SPOT") {
342            Some(Self::Spot)
343        } else if symbol.ends_with("-LINEAR") {
344            Some(Self::Linear)
345        } else if symbol.ends_with("-INVERSE") {
346            Some(Self::Inverse)
347        } else if symbol.ends_with("-OPTION") {
348            Some(Self::Option)
349        } else {
350            None
351        }
352    }
353
354    /// Returns `true` if the product is a spot instrument.
355    #[must_use]
356    pub fn is_spot(self) -> bool {
357        matches!(self, Self::Spot)
358    }
359
360    /// Returns `true` if the product is a linear contract.
361    #[must_use]
362    pub fn is_linear(self) -> bool {
363        matches!(self, Self::Linear)
364    }
365
366    /// Returns `true` if the product is an inverse contract.
367    #[must_use]
368    pub fn is_inverse(self) -> bool {
369        matches!(self, Self::Inverse)
370    }
371
372    /// Returns `true` if the product is an option contract.
373    #[must_use]
374    pub fn is_option(self) -> bool {
375        matches!(self, Self::Option)
376    }
377}
378
379/// Contract type enumeration for linear and inverse derivatives.
380#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
381#[serde(rename_all = "PascalCase")]
382pub enum BybitContractType {
383    LinearPerpetual,
384    LinearFutures,
385    InversePerpetual,
386    InverseFutures,
387}
388
389/// Option flavour values.
390#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
391#[serde(rename_all = "PascalCase")]
392pub enum BybitOptionType {
393    Call,
394    Put,
395}
396
397/// Symbol type values for spot/linear/inverse instrument info responses.
398///
399/// New values may be added by the venue; unknown strings fall back to `Other` so deserialization
400/// remains forward-compatible.
401///
402/// # References
403/// - <https://bybit-exchange.github.io/docs/v5/enum#symboltype>
404#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
405#[serde(rename_all = "lowercase")]
406pub enum BybitSymbolType {
407    /// Innovation-zone derivatives.
408    Innovation,
409    /// Adventure-zone spot pairs.
410    Adventure,
411    /// Tokenized equities (spot xstocks).
412    Xstocks,
413    /// Commodity instruments.
414    Commodity,
415    /// Tokenized stock derivatives.
416    Stock,
417    /// Foreign exchange instruments.
418    Forex,
419    /// Exchange-traded fund derivatives.
420    #[serde(rename = "ETF")]
421    Etf,
422    /// Forward-compatible fallback for any value the venue adds later.
423    #[serde(other)]
424    Other,
425}
426
427impl BybitSymbolType {
428    /// Returns the exact recognized value used by Bybit, or `None` for an unknown value.
429    #[must_use]
430    pub(crate) const fn as_str(self) -> Option<&'static str> {
431        match self {
432            Self::Innovation => Some("innovation"),
433            Self::Adventure => Some("adventure"),
434            Self::Xstocks => Some("xstocks"),
435            Self::Commodity => Some("commodity"),
436            Self::Stock => Some("stock"),
437            Self::Forex => Some("forex"),
438            Self::Etf => Some("ETF"),
439            Self::Other => None,
440        }
441    }
442}
443
444/// Position side as represented in REST/WebSocket payloads.
445#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
446pub enum BybitPositionSide {
447    #[serde(rename = "")]
448    Flat,
449    #[serde(rename = "Buy")]
450    Buy,
451    #[serde(rename = "Sell")]
452    Sell,
453}
454
455/// WebSocket order request operations.
456#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
457pub enum BybitWsOrderRequestOp {
458    #[serde(rename = "order.create")]
459    Create,
460    #[serde(rename = "order.amend")]
461    Amend,
462    #[serde(rename = "order.cancel")]
463    Cancel,
464    #[serde(rename = "order.create-batch")]
465    CreateBatch,
466    #[serde(rename = "order.amend-batch")]
467    AmendBatch,
468    #[serde(rename = "order.cancel-batch")]
469    CancelBatch,
470}
471
472/// Available kline intervals.
473#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
474pub enum BybitKlineInterval {
475    #[serde(rename = "1")]
476    Minute1,
477    #[serde(rename = "3")]
478    Minute3,
479    #[serde(rename = "5")]
480    Minute5,
481    #[serde(rename = "15")]
482    Minute15,
483    #[serde(rename = "30")]
484    Minute30,
485    #[serde(rename = "60")]
486    Hour1,
487    #[serde(rename = "120")]
488    Hour2,
489    #[serde(rename = "240")]
490    Hour4,
491    #[serde(rename = "360")]
492    Hour6,
493    #[serde(rename = "720")]
494    Hour12,
495    #[serde(rename = "D")]
496    Day1,
497    #[serde(rename = "W")]
498    Week1,
499    #[serde(rename = "M")]
500    Month1,
501}
502
503impl BybitKlineInterval {
504    /// Returns the end time in milliseconds for a bar that starts at `start_ms`.
505    ///
506    /// For most intervals this is simply `start_ms + duration`. For monthly bars,
507    /// this calculates the actual first millisecond of the next month to handle
508    /// variable month lengths (28-31 days).
509    #[must_use]
510    pub fn bar_end_time_ms(&self, start_ms: i64) -> i64 {
511        match self {
512            Self::Month1 => {
513                let start_dt = Offset::UTC.to_datetime(
514                    Timestamp::from_millisecond(start_ms).unwrap_or(Timestamp::UNIX_EPOCH),
515                );
516                let (year, month) = if start_dt.month() == 12 {
517                    (start_dt.year() + 1, 1)
518                } else {
519                    (start_dt.year(), start_dt.month() + 1)
520                };
521                Date::new(year, month, 1)
522                    .and_then(|date| Offset::UTC.to_timestamp(date.at(0, 0, 0, 0)))
523                    .map_or(start_ms + 2_678_400_000, Timestamp::as_millisecond)
524            }
525            _ => start_ms + self.duration_ms(),
526        }
527    }
528
529    /// Returns the fixed duration of this interval in milliseconds.
530    ///
531    /// Note: For monthly bars, use [`Self::bar_end_time_ms`] instead as months have
532    /// variable lengths (28-31 days).
533    #[must_use]
534    pub const fn duration_ms(&self) -> i64 {
535        match self {
536            Self::Minute1 => 60_000,
537            Self::Minute3 => 180_000,
538            Self::Minute5 => 300_000,
539            Self::Minute15 => 900_000,
540            Self::Minute30 => 1_800_000,
541            Self::Hour1 => 3_600_000,
542            Self::Hour2 => 7_200_000,
543            Self::Hour4 => 14_400_000,
544            Self::Hour6 => 21_600_000,
545            Self::Hour12 => 43_200_000,
546            Self::Day1 => 86_400_000,
547            Self::Week1 => 604_800_000,
548            Self::Month1 => 2_678_400_000, // 31 days - use bar_end_time_ms() for accurate calculation
549        }
550    }
551}
552
553impl Display for BybitKlineInterval {
554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555        let s = match self {
556            Self::Minute1 => "1",
557            Self::Minute3 => "3",
558            Self::Minute5 => "5",
559            Self::Minute15 => "15",
560            Self::Minute30 => "30",
561            Self::Hour1 => "60",
562            Self::Hour2 => "120",
563            Self::Hour4 => "240",
564            Self::Hour6 => "360",
565            Self::Hour12 => "720",
566            Self::Day1 => "D",
567            Self::Week1 => "W",
568            Self::Month1 => "M",
569        };
570        write!(f, "{s}")
571    }
572}
573
574/// Order status values returned by Bybit.
575#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
576#[cfg_attr(
577    feature = "python",
578    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
579)]
580#[cfg_attr(
581    feature = "python",
582    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
583)]
584pub enum BybitOrderStatus {
585    #[serde(rename = "Created")]
586    Created,
587    #[serde(rename = "New")]
588    New,
589    #[serde(rename = "Rejected")]
590    Rejected,
591    #[serde(rename = "PartiallyFilled")]
592    PartiallyFilled,
593    #[serde(rename = "PartiallyFilledCanceled")]
594    PartiallyFilledCanceled,
595    #[serde(rename = "Filled")]
596    Filled,
597    #[serde(rename = "Cancelled")]
598    Canceled,
599    #[serde(rename = "Untriggered")]
600    Untriggered,
601    #[serde(rename = "Triggered")]
602    Triggered,
603    #[serde(rename = "Deactivated")]
604    Deactivated,
605}
606
607/// Order side enumeration.
608#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
609#[cfg_attr(
610    feature = "python",
611    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
612)]
613#[cfg_attr(
614    feature = "python",
615    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
616)]
617pub enum BybitOrderSide {
618    #[serde(rename = "")]
619    Unknown,
620    #[serde(rename = "Buy")]
621    Buy,
622    #[serde(rename = "Sell")]
623    Sell,
624}
625
626impl From<BybitOrderSide> for AggressorSide {
627    fn from(value: BybitOrderSide) -> Self {
628        match value {
629            BybitOrderSide::Buy => Self::Buy,
630            BybitOrderSide::Sell => Self::Sell,
631            BybitOrderSide::Unknown => Self::NoAggressor,
632        }
633    }
634}
635
636impl From<BybitOrderSide> for Option<OrderSide> {
637    fn from(value: BybitOrderSide) -> Self {
638        match value {
639            BybitOrderSide::Buy => Some(OrderSide::Buy),
640            BybitOrderSide::Sell => Some(OrderSide::Sell),
641            BybitOrderSide::Unknown => None,
642        }
643    }
644}
645
646impl TryFrom<BybitOrderSide> for OrderSide {
647    type Error = anyhow::Error;
648
649    fn try_from(value: BybitOrderSide) -> Result<Self, Self::Error> {
650        match value {
651            BybitOrderSide::Buy => Ok(Self::Buy),
652            BybitOrderSide::Sell => Ok(Self::Sell),
653            BybitOrderSide::Unknown => anyhow::bail!("Unspecified Bybit order side"),
654        }
655    }
656}
657
658impl From<OrderSide> for BybitOrderSide {
659    fn from(value: OrderSide) -> Self {
660        match value {
661            OrderSide::Buy => Self::Buy,
662            OrderSide::Sell => Self::Sell,
663        }
664    }
665}
666
667impl From<BybitTriggerType> for TriggerType {
668    fn from(value: BybitTriggerType) -> Self {
669        match value {
670            BybitTriggerType::None => Self::Default,
671            BybitTriggerType::LastPrice => Self::LastPrice,
672            BybitTriggerType::IndexPrice => Self::IndexPrice,
673            BybitTriggerType::MarkPrice => Self::MarkPrice,
674        }
675    }
676}
677
678impl From<TriggerType> for BybitTriggerType {
679    fn from(value: TriggerType) -> Self {
680        match value {
681            TriggerType::Default | TriggerType::LastPrice => Self::LastPrice,
682            TriggerType::IndexPrice => Self::IndexPrice,
683            TriggerType::MarkPrice => Self::MarkPrice,
684            _ => Self::LastPrice,
685        }
686    }
687}
688
689/// Resolves an optional Nautilus trigger type to a Bybit trigger type,
690/// defaulting to `LastPrice` when absent.
691pub fn resolve_trigger_type(trigger_type: Option<TriggerType>) -> BybitTriggerType {
692    trigger_type.map_or(BybitTriggerType::LastPrice, BybitTriggerType::from)
693}
694
695/// Order cancel reason values as returned by Bybit.
696#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
697#[serde(rename_all = "PascalCase")]
698#[cfg_attr(
699    feature = "python",
700    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
701)]
702#[cfg_attr(
703    feature = "python",
704    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
705)]
706pub enum BybitCancelType {
707    CancelByUser,
708    CancelByReduceOnly,
709    CancelByPrepareLackOfMargin,
710    CancelByPrepareOrderFilter,
711    CancelByPrepareOrderMarginCheckFailed,
712    CancelByPrepareOrderCommission,
713    CancelByPrepareOrderRms,
714    CancelByPrepareOrderOther,
715    CancelByRiskLimit,
716    CancelOnDisconnect,
717    CancelByStopOrdersExceeded,
718    CancelByPzMarketClose,
719    CancelByMarginCheckFailed,
720    CancelByPzTakeover,
721    CancelByAdmin,
722    CancelByTpSlTsClear,
723    CancelByAmendNotModified,
724    CancelByPzCancel,
725    CancelByCrossSelfMatch,
726    CancelBySelfMatchPrevention,
727    #[serde(other)]
728    Other,
729}
730
731/// Order creation origin values.
732#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
733#[serde(rename_all = "PascalCase")]
734pub enum BybitCreateType {
735    CreateByUser,
736    CreateByClosing,
737    CreateByTakeProfit,
738    CreateByStopLoss,
739    CreateByTrailingStop,
740    CreateByStopOrder,
741    CreateByPartialTakeProfit,
742    CreateByPartialStopLoss,
743    CreateByAdl,
744    CreateByLiquidate,
745    CreateByTakeover,
746    CreateByTpsl,
747    CreateByBboOrder,
748    #[serde(other)]
749    Other,
750}
751
752/// BBO side type values for Bybit order placement.
753#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
754#[serde(rename_all = "PascalCase")]
755pub enum BybitBboSideType {
756    Queue,
757    Counterparty,
758}
759
760/// Venue order type enumeration.
761#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
762#[cfg_attr(
763    feature = "python",
764    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
765)]
766#[cfg_attr(
767    feature = "python",
768    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
769)]
770pub enum BybitOrderType {
771    #[serde(rename = "Market")]
772    Market,
773    #[serde(rename = "Limit")]
774    Limit,
775    #[serde(rename = "UNKNOWN")]
776    Unknown,
777}
778
779/// Stop order type classification.
780#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
781#[cfg_attr(
782    feature = "python",
783    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
784)]
785#[cfg_attr(
786    feature = "python",
787    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
788)]
789pub enum BybitStopOrderType {
790    #[serde(rename = "")]
791    None,
792    #[serde(rename = "UNKNOWN")]
793    Unknown,
794    #[serde(rename = "TakeProfit")]
795    TakeProfit,
796    #[serde(rename = "StopLoss")]
797    StopLoss,
798    #[serde(rename = "TrailingStop")]
799    TrailingStop,
800    #[serde(rename = "Stop")]
801    Stop,
802    #[serde(rename = "PartialTakeProfit")]
803    PartialTakeProfit,
804    #[serde(rename = "PartialStopLoss")]
805    PartialStopLoss,
806    #[serde(rename = "tpslOrder")]
807    TpslOrder,
808    #[serde(rename = "OcoOrder")]
809    OcoOrder,
810    #[serde(rename = "MmRateClose")]
811    MmRateClose,
812    #[serde(rename = "BidirectionalTpslOrder")]
813    BidirectionalTpslOrder,
814}
815
816/// Trigger type configuration.
817#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
818#[cfg_attr(
819    feature = "python",
820    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
821)]
822#[cfg_attr(
823    feature = "python",
824    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
825)]
826pub enum BybitTriggerType {
827    #[serde(rename = "")]
828    None,
829    #[serde(rename = "LastPrice")]
830    LastPrice,
831    #[serde(rename = "IndexPrice")]
832    IndexPrice,
833    #[serde(rename = "MarkPrice")]
834    MarkPrice,
835}
836
837/// Trigger direction integers used by the API.
838#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
839#[repr(i32)]
840#[cfg_attr(
841    feature = "python",
842    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
843)]
844#[cfg_attr(
845    feature = "python",
846    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
847)]
848pub enum BybitTriggerDirection {
849    None = 0,
850    RisesTo = 1,
851    FallsTo = 2,
852}
853
854/// Take-profit/stop-loss mode for derivatives orders.
855#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
856#[serde(rename_all = "PascalCase")]
857#[cfg_attr(
858    feature = "python",
859    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
860)]
861#[cfg_attr(
862    feature = "python",
863    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
864)]
865pub enum BybitTpSlMode {
866    Full,
867    Partial,
868    #[serde(other)]
869    Unknown,
870}
871
872/// Time-in-force enumeration.
873#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
874#[cfg_attr(
875    feature = "python",
876    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
877)]
878#[cfg_attr(
879    feature = "python",
880    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
881)]
882pub enum BybitTimeInForce {
883    #[serde(rename = "GTC")]
884    Gtc,
885    #[serde(rename = "IOC")]
886    Ioc,
887    #[serde(rename = "FOK")]
888    Fok,
889    #[serde(rename = "PostOnly")]
890    PostOnly,
891}
892
893/// Execution type values used in execution reports.
894///
895/// Reference: <https://bybit-exchange.github.io/docs/v5/enum#exectype>.
896#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
897pub enum BybitExecType {
898    #[serde(rename = "Trade")]
899    Trade,
900    #[serde(rename = "AdlTrade")]
901    AdlTrade,
902    #[serde(rename = "Funding")]
903    Funding,
904    #[serde(rename = "BustTrade")]
905    BustTrade,
906    #[serde(rename = "Delivery")]
907    Delivery,
908    #[serde(rename = "Settle")]
909    Settle,
910    #[serde(rename = "BlockTrade")]
911    BlockTrade,
912    #[serde(rename = "MovePosition")]
913    MovePosition,
914    #[serde(rename = "CorporateAction")]
915    CorporateAction,
916    #[serde(rename = "UNKNOWN")]
917    Unknown,
918}
919
920impl BybitExecType {
921    /// Returns `true` if this execution was generated by the venue rather than the user.
922    ///
923    /// This covers auto-deleveraging (`AdlTrade`), liquidation takeovers (`BustTrade`),
924    /// scheduled deliveries (`Delivery`), settlement (`Settle`), and corporate actions
925    /// (`CorporateAction`).
926    #[must_use]
927    pub const fn is_exchange_generated(&self) -> bool {
928        matches!(
929            self,
930            Self::AdlTrade
931                | Self::BustTrade
932                | Self::Delivery
933                | Self::Settle
934                | Self::CorporateAction
935        )
936    }
937}
938
939/// Transaction types for wallet funding records.
940#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
941pub enum BybitTransactionType {
942    #[serde(rename = "TRANSFER_IN")]
943    TransferIn,
944    #[serde(rename = "TRANSFER_OUT")]
945    TransferOut,
946    #[serde(rename = "TRADE")]
947    Trade,
948    #[serde(rename = "SETTLEMENT")]
949    Settlement,
950    #[serde(rename = "DELIVERY")]
951    Delivery,
952    #[serde(rename = "LIQUIDATION")]
953    Liquidation,
954    #[serde(rename = "AIRDRP")]
955    Airdrop,
956}
957
958/// Endpoint classifications used by the Bybit API.
959#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
960#[serde(rename_all = "UPPERCASE")]
961pub enum BybitEndpointType {
962    None,
963    Asset,
964    Market,
965    Account,
966    Trade,
967    Position,
968    User,
969}
970
971/// Filter for open orders query.
972///
973/// Used with `GET /v5/order/realtime` to filter order status.
974#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
975#[repr(i32)]
976#[cfg_attr(
977    feature = "python",
978    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
979)]
980#[cfg_attr(
981    feature = "python",
982    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
983)]
984pub enum BybitOpenOnly {
985    /// Query open status orders only (New, PartiallyFilled).
986    #[default]
987    OpenOnly = 0,
988    /// Query up to 500 recent closed orders (cancelled, rejected, filled).
989    ClosedRecent = 1,
990}
991
992/// Order filter for querying specific order types.
993///
994/// Used with `GET /v5/order/realtime` to filter by order category.
995#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
996#[cfg_attr(
997    feature = "python",
998    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", eq, eq_int, from_py_object)
999)]
1000#[cfg_attr(
1001    feature = "python",
1002    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
1003)]
1004pub enum BybitOrderFilter {
1005    /// Active orders (default).
1006    #[default]
1007    Order,
1008    /// Conditional orders (futures and spot).
1009    StopOrder,
1010    /// Spot take-profit/stop-loss orders.
1011    #[serde(rename = "tpslOrder")]
1012    TpslOrder,
1013    /// Spot one-cancels-other orders.
1014    OcoOrder,
1015    /// Spot bidirectional TP/SL orders.
1016    BidirectionalTpslOrder,
1017}
1018
1019/// Margin actions for spot margin trading operations.
1020#[derive(
1021    Clone,
1022    Copy,
1023    Debug,
1024    strum::Display,
1025    Eq,
1026    PartialEq,
1027    Hash,
1028    AsRefStr,
1029    EnumIter,
1030    EnumString,
1031    Serialize,
1032    Deserialize,
1033)]
1034#[serde(rename_all = "snake_case")]
1035#[strum(serialize_all = "snake_case")]
1036#[cfg_attr(
1037    feature = "python",
1038    pyo3::pyclass(
1039        eq,
1040        eq_int,
1041        hash,
1042        frozen,
1043        rename_all = "SCREAMING_SNAKE_CASE",
1044        module = "nautilus_trader.adapters.bybit",
1045        from_py_object,
1046    )
1047)]
1048#[cfg_attr(
1049    feature = "python",
1050    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.bybit")
1051)]
1052pub enum BybitMarginAction {
1053    /// Borrow funds for margin trading.
1054    Borrow,
1055    /// Repay borrowed funds.
1056    Repay,
1057    /// Query current borrowed amount.
1058    GetBorrowAmount,
1059}
1060
1061/// Result status returned by Bybit repayment endpoints.
1062#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, strum::Display, Serialize, Deserialize)]
1063pub enum BybitRepayStatus {
1064    /// The repayment is processing.
1065    #[serde(rename = "P")]
1066    #[strum(serialize = "P")]
1067    Processing,
1068    /// The repayment succeeded.
1069    #[serde(rename = "SU")]
1070    #[strum(serialize = "SU")]
1071    Success,
1072    /// The repayment failed.
1073    #[serde(rename = "FA")]
1074    #[strum(serialize = "FA")]
1075    Failed,
1076}
1077
1078/// Position status enumeration.
1079#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1080#[serde(rename_all = "PascalCase")]
1081pub enum BybitPositionStatus {
1082    Normal,
1083    Settle,
1084    Delivering,
1085    #[serde(other)]
1086    Other,
1087}
1088
1089/// Market unit for spot market orders.
1090#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1091pub enum BybitMarketUnit {
1092    #[serde(rename = "baseCoin")]
1093    BaseCoin,
1094    #[serde(rename = "quoteCoin")]
1095    QuoteCoin,
1096}
1097
1098/// Self-match prevention type.
1099#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1100pub enum BybitSmpType {
1101    None,
1102    CancelMaker,
1103    CancelTaker,
1104    CancelBoth,
1105    #[serde(other)]
1106    Other,
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use rstest::rstest;
1112
1113    use super::*;
1114
1115    #[rstest]
1116    #[case::minute1(BybitKlineInterval::Minute1, 60_000)]
1117    #[case::minute3(BybitKlineInterval::Minute3, 180_000)]
1118    #[case::minute5(BybitKlineInterval::Minute5, 300_000)]
1119    #[case::minute15(BybitKlineInterval::Minute15, 900_000)]
1120    #[case::minute30(BybitKlineInterval::Minute30, 1_800_000)]
1121    #[case::hour1(BybitKlineInterval::Hour1, 3_600_000)]
1122    #[case::hour2(BybitKlineInterval::Hour2, 7_200_000)]
1123    #[case::hour4(BybitKlineInterval::Hour4, 14_400_000)]
1124    #[case::hour6(BybitKlineInterval::Hour6, 21_600_000)]
1125    #[case::hour12(BybitKlineInterval::Hour12, 43_200_000)]
1126    #[case::day1(BybitKlineInterval::Day1, 86_400_000)]
1127    #[case::week1(BybitKlineInterval::Week1, 604_800_000)]
1128    #[case::month1(BybitKlineInterval::Month1, 2_678_400_000)]
1129    fn test_kline_interval_duration_ms(
1130        #[case] interval: BybitKlineInterval,
1131        #[case] expected_ms: i64,
1132    ) {
1133        assert_eq!(interval.duration_ms(), expected_ms);
1134    }
1135
1136    #[rstest]
1137    fn test_bar_end_time_ms_non_monthly_adds_duration() {
1138        let interval = BybitKlineInterval::Minute1;
1139        let start_ms = 1704067200000i64;
1140        assert_eq!(interval.bar_end_time_ms(start_ms), start_ms + 60_000);
1141    }
1142
1143    #[rstest]
1144    #[case::jan_31_days(1704067200000i64, 1706745600000i64)]
1145    #[case::feb_leap_year_29_days(1706745600000i64, 1709251200000i64)]
1146    #[case::apr_30_days(1711929600000i64, 1714521600000i64)]
1147    #[case::dec_to_next_year(1733011200000i64, 1735689600000i64)]
1148    fn test_bar_end_time_ms_monthly_variable_lengths(
1149        #[case] start_ms: i64,
1150        #[case] expected_end_ms: i64,
1151    ) {
1152        let interval = BybitKlineInterval::Month1;
1153        assert_eq!(interval.bar_end_time_ms(start_ms), expected_end_ms);
1154    }
1155
1156    #[rstest]
1157    #[case(BybitSymbolType::Innovation, "innovation")]
1158    #[case(BybitSymbolType::Adventure, "adventure")]
1159    #[case(BybitSymbolType::Xstocks, "xstocks")]
1160    #[case(BybitSymbolType::Commodity, "commodity")]
1161    #[case(BybitSymbolType::Stock, "stock")]
1162    #[case(BybitSymbolType::Forex, "forex")]
1163    #[case(BybitSymbolType::Etf, "ETF")]
1164    fn test_symbol_type_round_trip(#[case] symbol_type: BybitSymbolType, #[case] wire_value: &str) {
1165        let value = serde_json::Value::String(wire_value.to_string());
1166
1167        assert_eq!(
1168            serde_json::from_value::<BybitSymbolType>(value.clone()).unwrap(),
1169            symbol_type
1170        );
1171        assert_eq!(serde_json::to_value(symbol_type).unwrap(), value);
1172        assert_eq!(symbol_type.as_str(), Some(wire_value));
1173    }
1174
1175    #[rstest]
1176    fn test_unknown_symbol_type_has_no_wire_value() {
1177        assert_eq!(BybitSymbolType::Other.as_str(), None);
1178    }
1179
1180    #[rstest]
1181    #[case(BybitExecType::Trade, false)]
1182    #[case(BybitExecType::AdlTrade, true)]
1183    #[case(BybitExecType::BustTrade, true)]
1184    #[case(BybitExecType::Delivery, true)]
1185    #[case(BybitExecType::Settle, true)]
1186    #[case(BybitExecType::Funding, false)]
1187    #[case(BybitExecType::BlockTrade, false)]
1188    #[case(BybitExecType::MovePosition, false)]
1189    #[case(BybitExecType::CorporateAction, true)]
1190    #[case(BybitExecType::Unknown, false)]
1191    fn test_exec_type_is_exchange_generated(
1192        #[case] exec_type: BybitExecType,
1193        #[case] expected: bool,
1194    ) {
1195        assert_eq!(exec_type.is_exchange_generated(), expected);
1196    }
1197}